codeply-cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +63 -63
- package/bin/codeply.js +129 -6
- package/lib/agent.mjs +2139 -2139
- package/lib/ai.mjs +6 -6
- package/lib/apply-patch.js +217 -217
- package/lib/arity-table.json +138 -138
- package/lib/arity.js +70 -70
- package/lib/auth.mjs +6 -6
- package/lib/cloud.mjs +708 -0
- package/lib/commands.js +103 -103
- package/lib/design-library/assemble-artifact.mjs +21 -21
- package/lib/design-library/cleanup.mjs +45 -45
- package/lib/design-library/index.json +22379 -22379
- package/lib/design-library/make-table-data.mjs +39 -39
- package/lib/diagnostics.js +192 -192
- package/lib/edit-engine.js +364 -364
- package/lib/github-agent.mjs +311 -290
- package/lib/history.js +75 -75
- package/lib/mcp.js +258 -258
- package/lib/native-tools.mjs +189 -189
- package/lib/oauth-connectors.js +2 -1
- package/lib/permissions.js +109 -109
- package/lib/plugins.js +353 -353
- package/lib/server.mjs +457 -457
- package/lib/session-db.js +220 -220
- package/lib/share.js +168 -168
- package/lib/skills.js +416 -416
- package/lib/snapshot.js +146 -146
- package/lib/terminal-hints.js +154 -154
- package/lib/tools.mjs +2012 -2012
- package/lib/typecheck-worker.js +147 -147
- package/lib/web-tools.js +158 -158
- package/package.json +49 -47
- package/skills/SOURCE.md +58 -58
- package/skills/accessibility/SKILL.md +147 -147
- package/skills/agent-architecture-audit/SKILL.md +257 -257
- package/skills/agent-eval/SKILL.md +147 -147
- package/skills/agent-harness-construction/SKILL.md +74 -74
- package/skills/agent-introspection-debugging/SKILL.md +154 -154
- package/skills/agent-payment-x402/SKILL.md +225 -225
- package/skills/agent-self-evaluation/SKILL.md +182 -182
- package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -71
- package/skills/agent-self-evaluation/references/hook-integration.md +64 -64
- package/skills/agent-sort/SKILL.md +216 -216
- package/skills/agentic-engineering/SKILL.md +64 -64
- package/skills/agentic-os/SKILL.md +388 -388
- package/skills/ai-first-engineering/SKILL.md +52 -52
- package/skills/ai-regression-testing/SKILL.md +386 -386
- package/skills/android-clean-architecture/SKILL.md +340 -340
- package/skills/angular-developer/SKILL.md +155 -155
- package/skills/angular-developer/references/angular-animations.md +160 -160
- package/skills/angular-developer/references/angular-aria.md +410 -410
- package/skills/angular-developer/references/cli.md +86 -86
- package/skills/angular-developer/references/component-harnesses.md +59 -59
- package/skills/angular-developer/references/component-styling.md +91 -91
- package/skills/angular-developer/references/components.md +117 -117
- package/skills/angular-developer/references/creating-services.md +97 -97
- package/skills/angular-developer/references/data-resolvers.md +69 -69
- package/skills/angular-developer/references/define-routes.md +67 -67
- package/skills/angular-developer/references/defining-providers.md +72 -72
- package/skills/angular-developer/references/di-fundamentals.md +120 -120
- package/skills/angular-developer/references/e2e-testing.md +56 -56
- package/skills/angular-developer/references/effects.md +83 -83
- package/skills/angular-developer/references/hierarchical-injectors.md +43 -43
- package/skills/angular-developer/references/host-elements.md +80 -80
- package/skills/angular-developer/references/injection-context.md +63 -63
- package/skills/angular-developer/references/inputs.md +101 -101
- package/skills/angular-developer/references/linked-signal.md +59 -59
- package/skills/angular-developer/references/loading-strategies.md +61 -61
- package/skills/angular-developer/references/mcp.md +108 -108
- package/skills/angular-developer/references/navigate-to-routes.md +69 -69
- package/skills/angular-developer/references/outputs.md +86 -86
- package/skills/angular-developer/references/reactive-forms.md +122 -122
- package/skills/angular-developer/references/rendering-strategies.md +44 -44
- package/skills/angular-developer/references/resource.md +77 -77
- package/skills/angular-developer/references/route-animations.md +56 -56
- package/skills/angular-developer/references/route-guards.md +52 -52
- package/skills/angular-developer/references/router-lifecycle.md +45 -45
- package/skills/angular-developer/references/router-testing.md +87 -87
- package/skills/angular-developer/references/show-routes-with-outlets.md +68 -68
- package/skills/angular-developer/references/signal-forms.md +795 -795
- package/skills/angular-developer/references/signals-overview.md +94 -94
- package/skills/angular-developer/references/tailwind-css.md +69 -69
- package/skills/angular-developer/references/template-driven-forms.md +114 -114
- package/skills/angular-developer/references/testing-fundamentals.md +65 -65
- package/skills/api-connector-builder/SKILL.md +121 -121
- package/skills/api-design/SKILL.md +524 -524
- package/skills/architecture-decision-records/SKILL.md +180 -180
- package/skills/article-writing/SKILL.md +80 -80
- package/skills/automation-audit-ops/SKILL.md +143 -143
- package/skills/autonomous-agent-harness/SKILL.md +274 -274
- package/skills/autonomous-loops/SKILL.md +611 -611
- package/skills/backend-patterns/SKILL.md +562 -562
- package/skills/benchmark/SKILL.md +95 -95
- package/skills/benchmark-methodology/SKILL.md +191 -191
- package/skills/benchmark-optimization-loop/SKILL.md +71 -71
- package/skills/blender-motion-state-inspection/SKILL.md +165 -165
- package/skills/blueprint/SKILL.md +106 -106
- package/skills/brand-discovery/SKILL.md +145 -145
- package/skills/brand-discovery/references/10_purpose-why.md +40 -40
- package/skills/brand-discovery/references/20_positioning.md +44 -44
- package/skills/brand-discovery/references/30_audience-niche.md +52 -52
- package/skills/brand-discovery/references/40_personality-archetype.md +57 -57
- package/skills/brand-discovery/references/50_voice-tone.md +59 -59
- package/skills/brand-discovery/references/60_narrative-story.md +50 -50
- package/skills/brand-discovery/references/70_founder-tension.md +49 -49
- package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -133
- package/skills/brand-voice/SKILL.md +98 -98
- package/skills/brand-voice/references/voice-profile-schema.md +55 -55
- package/skills/browser-qa/SKILL.md +105 -105
- package/skills/bun-runtime/SKILL.md +85 -85
- package/skills/canary-watch/SKILL.md +108 -108
- package/skills/carrier-relationship-management/SKILL.md +212 -212
- package/skills/cisco-ios-patterns/SKILL.md +164 -164
- package/skills/ck/SKILL.md +148 -148
- package/skills/claude-devfleet/SKILL.md +112 -112
- package/skills/click-path-audit/SKILL.md +245 -245
- package/skills/clickhouse-io/SKILL.md +445 -445
- package/skills/code-tour/SKILL.md +254 -254
- package/skills/codebase-onboarding/SKILL.md +234 -234
- package/skills/codehealth-mcp/SKILL.md +167 -167
- package/skills/coding-standards/SKILL.md +551 -551
- package/skills/competitive-platform-analysis/SKILL.md +214 -214
- package/skills/competitive-report-structure/SKILL.md +162 -162
- package/skills/compose-multiplatform-patterns/SKILL.md +300 -300
- package/skills/config-gc/SKILL.md +120 -120
- package/skills/configure-ecc/SKILL.md +385 -385
- package/skills/connections-optimizer/SKILL.md +190 -190
- package/skills/content-engine/SKILL.md +132 -132
- package/skills/content-hash-cache-pattern/SKILL.md +162 -162
- package/skills/context-budget/SKILL.md +136 -136
- package/skills/continuous-agent-loop/SKILL.md +46 -46
- package/skills/continuous-learning/SKILL.md +132 -132
- package/skills/continuous-learning-v2/SKILL.md +361 -361
- package/skills/contract-first/SKILL.md +287 -287
- package/skills/cost-aware-llm-pipeline/SKILL.md +184 -184
- package/skills/cost-tracking/SKILL.md +97 -97
- package/skills/council/SKILL.md +204 -204
- package/skills/cpp-coding-standards/SKILL.md +724 -724
- package/skills/cpp-testing/SKILL.md +325 -325
- package/skills/crosspost/SKILL.md +112 -112
- package/skills/csharp-testing/SKILL.md +322 -322
- package/skills/customer-billing-ops/SKILL.md +141 -141
- package/skills/customs-trade-compliance/SKILL.md +263 -263
- package/skills/dart-flutter-patterns/SKILL.md +564 -564
- package/skills/dashboard-builder/SKILL.md +109 -109
- package/skills/data-scraper-agent/SKILL.md +765 -765
- package/skills/data-throughput-accelerator/SKILL.md +74 -74
- package/skills/database-migrations/SKILL.md +430 -430
- package/skills/deep-research/SKILL.md +160 -160
- package/skills/defi-amm-security/SKILL.md +167 -167
- package/skills/delivery-gate/SKILL.md +126 -126
- package/skills/deployment-patterns/SKILL.md +428 -428
- package/skills/design-system/SKILL.md +83 -83
- package/skills/django-celery/SKILL.md +458 -458
- package/skills/django-patterns/SKILL.md +735 -735
- package/skills/django-security/SKILL.md +644 -644
- package/skills/django-tdd/SKILL.md +730 -730
- package/skills/django-verification/SKILL.md +470 -470
- package/skills/dmux-workflows/SKILL.md +192 -192
- package/skills/docker-patterns/SKILL.md +365 -365
- package/skills/documentation-lookup/SKILL.md +91 -91
- package/skills/dotnet-patterns/SKILL.md +322 -322
- package/skills/dynamic-workflow-mode/SKILL.md +124 -124
- package/skills/e2e-testing/SKILL.md +327 -327
- package/skills/ecc-guide/SKILL.md +190 -190
- package/skills/ecc-recipes/SKILL.md +149 -149
- package/skills/ecc-tools-cost-audit/SKILL.md +161 -161
- package/skills/email-ops/SKILL.md +122 -122
- package/skills/energy-procurement/SKILL.md +228 -228
- package/skills/enterprise-agent-ops/SKILL.md +51 -51
- package/skills/error-handling/SKILL.md +377 -377
- package/skills/eval-harness/SKILL.md +271 -271
- package/skills/evm-token-decimals/SKILL.md +131 -131
- package/skills/exa-search/SKILL.md +108 -108
- package/skills/fal-ai-media/SKILL.md +289 -289
- package/skills/fastapi-patterns/SKILL.md +514 -514
- package/skills/finance-billing-ops/SKILL.md +128 -128
- package/skills/flox-environments/SKILL.md +497 -497
- package/skills/flutter-dart-code-review/SKILL.md +436 -436
- package/skills/foundation-models-on-device/SKILL.md +243 -243
- package/skills/frontend-a11y/SKILL.md +446 -446
- package/skills/frontend-design-direction/SKILL.md +93 -93
- package/skills/frontend-patterns/SKILL.md +657 -657
- package/skills/frontend-slides/SKILL.md +185 -185
- package/skills/fsharp-testing/SKILL.md +281 -281
- package/skills/gan-style-harness/SKILL.md +279 -279
- package/skills/gateguard/SKILL.md +133 -133
- package/skills/generating-python-installer/SKILL.md +820 -820
- package/skills/git-workflow/SKILL.md +716 -716
- package/skills/github-ops/SKILL.md +145 -145
- package/skills/golang-patterns/SKILL.md +676 -676
- package/skills/golang-testing/SKILL.md +721 -721
- package/skills/google-workspace-ops/SKILL.md +96 -96
- package/skills/growth-log/SKILL.md +128 -128
- package/skills/healthcare-cdss-patterns/SKILL.md +246 -246
- package/skills/healthcare-emr-patterns/SKILL.md +160 -160
- package/skills/healthcare-eval-harness/SKILL.md +208 -208
- package/skills/healthcare-phi-compliance/SKILL.md +146 -146
- package/skills/hermes-imports/SKILL.md +89 -89
- package/skills/hexagonal-architecture/SKILL.md +277 -277
- package/skills/hipaa-compliance/SKILL.md +79 -79
- package/skills/homelab-network-readiness/SKILL.md +170 -170
- package/skills/homelab-network-setup/SKILL.md +130 -130
- package/skills/homelab-pihole-dns/SKILL.md +275 -275
- package/skills/homelab-vlan-segmentation/SKILL.md +312 -312
- package/skills/homelab-wireguard-vpn/SKILL.md +306 -306
- package/skills/hookify-rules/SKILL.md +128 -128
- package/skills/inherit-legacy-style/SKILL.md +157 -157
- package/skills/intent-driven-development/SKILL.md +360 -360
- package/skills/inventory-demand-planning/SKILL.md +247 -247
- package/skills/investor-materials/SKILL.md +97 -97
- package/skills/investor-outreach/SKILL.md +92 -92
- package/skills/ios-icon-gen/SKILL.md +158 -158
- package/skills/iterative-retrieval/SKILL.md +212 -212
- package/skills/ito-basket-compare/SKILL.md +64 -64
- package/skills/ito-compute/SKILL.md +129 -129
- package/skills/ito-data-atlas-agent/SKILL.md +64 -64
- package/skills/ito-market-intelligence/SKILL.md +61 -61
- package/skills/ito-trade-planner/SKILL.md +68 -68
- package/skills/java-coding-standards/SKILL.md +384 -384
- package/skills/jira-integration/SKILL.md +303 -303
- package/skills/jpa-patterns/SKILL.md +152 -152
- package/skills/knowledge-ops/SKILL.md +155 -155
- package/skills/kotlin-coroutines-flows/SKILL.md +285 -285
- package/skills/kotlin-exposed-patterns/SKILL.md +720 -720
- package/skills/kotlin-ktor-patterns/SKILL.md +690 -690
- package/skills/kotlin-patterns/SKILL.md +712 -712
- package/skills/kotlin-testing/SKILL.md +825 -825
- package/skills/kubernetes-patterns/SKILL.md +756 -756
- package/skills/laravel-patterns/SKILL.md +416 -416
- package/skills/laravel-plugin-discovery/SKILL.md +230 -230
- package/skills/laravel-security/SKILL.md +948 -948
- package/skills/laravel-tdd/SKILL.md +675 -675
- package/skills/laravel-verification/SKILL.md +180 -180
- package/skills/latency-critical-systems/SKILL.md +75 -75
- package/skills/lead-intelligence/SKILL.md +322 -322
- package/skills/liquid-glass-design/SKILL.md +279 -279
- package/skills/llm-trading-agent-security/SKILL.md +147 -147
- package/skills/logistics-exception-management/SKILL.md +222 -222
- package/skills/loop-design-check/SKILL.md +143 -143
- package/skills/mailtrap-email-integration/SKILL.md +77 -77
- package/skills/make-interfaces-feel-better/SKILL.md +152 -152
- package/skills/manim-video/SKILL.md +90 -90
- package/skills/market-research/SKILL.md +76 -76
- package/skills/marketing-campaign/SKILL.md +114 -114
- package/skills/mcp-server-patterns/SKILL.md +70 -70
- package/skills/messages-ops/SKILL.md +105 -105
- package/skills/ml-adoption-playbook/SKILL.md +57 -57
- package/skills/mle-workflow/SKILL.md +348 -348
- package/skills/motion-advanced/SKILL.md +596 -596
- package/skills/motion-foundations/SKILL.md +299 -299
- package/skills/motion-patterns/SKILL.md +434 -434
- package/skills/motion-ui/SKILL.md +576 -576
- package/skills/mysql-patterns/SKILL.md +413 -413
- package/skills/nanoclaw-repl/SKILL.md +34 -34
- package/skills/nestjs-patterns/SKILL.md +231 -231
- package/skills/netmiko-ssh-automation/SKILL.md +174 -174
- package/skills/network-bgp-diagnostics/SKILL.md +168 -168
- package/skills/network-config-validation/SKILL.md +211 -211
- package/skills/network-interface-health/SKILL.md +153 -153
- package/skills/nextjs-turbopack/SKILL.md +58 -58
- package/skills/nodejs-keccak256/SKILL.md +103 -103
- package/skills/nutrient-document-processing/SKILL.md +168 -168
- package/skills/nuxt4-patterns/SKILL.md +101 -101
- package/skills/openclaw-persona-forge/SKILL.md +289 -289
- package/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
- package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
- package/skills/openclaw-persona-forge/references/error-handling.md +53 -53
- package/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
- package/skills/openclaw-persona-forge/references/naming-system.md +39 -39
- package/skills/openclaw-persona-forge/references/output-template.md +166 -166
- package/skills/opensource-pipeline/SKILL.md +256 -256
- package/skills/orch-add-feature/SKILL.md +45 -45
- package/skills/orch-build-mvp/SKILL.md +49 -49
- package/skills/orch-change-feature/SKILL.md +43 -43
- package/skills/orch-fix-defect/SKILL.md +43 -43
- package/skills/orch-pipeline/SKILL.md +121 -121
- package/skills/orch-refine-code/SKILL.md +44 -44
- package/skills/parallel-execution-optimizer/SKILL.md +74 -74
- package/skills/perl-patterns/SKILL.md +505 -505
- package/skills/perl-security/SKILL.md +504 -504
- package/skills/perl-testing/SKILL.md +476 -476
- package/skills/plan-canvas/SKILL.md +153 -153
- package/skills/plan-orchestrate/SKILL.md +263 -263
- package/skills/plankton-code-quality/SKILL.md +237 -237
- package/skills/postgres-patterns/SKILL.md +148 -148
- package/skills/prediction-market-oracle-research/SKILL.md +64 -64
- package/skills/prediction-market-risk-review/SKILL.md +61 -61
- package/skills/premium-web-design/SKILL.md +154 -154
- package/skills/prisma-patterns/SKILL.md +401 -401
- package/skills/product-capability/SKILL.md +142 -142
- package/skills/product-lens/SKILL.md +93 -93
- package/skills/production-audit/SKILL.md +207 -207
- package/skills/production-scheduling/SKILL.md +238 -238
- package/skills/project-flow-ops/SKILL.md +112 -112
- package/skills/prompt-optimizer/SKILL.md +398 -398
- package/skills/python-patterns/SKILL.md +751 -751
- package/skills/python-testing/SKILL.md +817 -817
- package/skills/pytorch-patterns/SKILL.md +397 -397
- package/skills/quality-nonconformance/SKILL.md +260 -260
- package/skills/quarkus-patterns/SKILL.md +723 -723
- package/skills/quarkus-security/SKILL.md +468 -468
- package/skills/quarkus-tdd/SKILL.md +812 -812
- package/skills/quarkus-verification/SKILL.md +480 -480
- package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -68
- package/skills/react-native-patterns/SKILL.md +326 -326
- package/skills/react-patterns/SKILL.md +342 -342
- package/skills/react-performance/SKILL.md +575 -575
- package/skills/react-testing/SKILL.md +424 -424
- package/skills/recsys-pipeline-architect/SKILL.md +115 -115
- package/skills/recursive-decision-ledger/SKILL.md +81 -81
- package/skills/redis-patterns/SKILL.md +404 -404
- package/skills/regex-vs-llm-structured-text/SKILL.md +221 -221
- package/skills/remotion-video-creation/SKILL.md +43 -43
- package/skills/repo-scan/SKILL.md +79 -79
- package/skills/research-ops/SKILL.md +113 -113
- package/skills/returns-reverse-logistics/SKILL.md +240 -240
- package/skills/rules-distill/SKILL.md +265 -265
- package/skills/rust-patterns/SKILL.md +500 -500
- package/skills/rust-testing/SKILL.md +501 -501
- package/skills/safety-guard/SKILL.md +76 -76
- package/skills/santa-method/SKILL.md +307 -307
- package/skills/scientific-db-pubmed-database/SKILL.md +176 -176
- package/skills/scientific-db-uspto-database/SKILL.md +178 -178
- package/skills/scientific-pkg-gget/SKILL.md +167 -167
- package/skills/scientific-thinking-literature-review/SKILL.md +193 -193
- package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -161
- package/skills/search-first/SKILL.md +183 -183
- package/skills/security-bounty-hunter/SKILL.md +100 -100
- package/skills/security-review/SKILL.md +504 -504
- package/skills/security-scan/SKILL.md +166 -166
- package/skills/seo/SKILL.md +155 -155
- package/skills/skill-comply/SKILL.md +59 -59
- package/skills/skill-scout/SKILL.md +141 -141
- package/skills/skill-stocktake/SKILL.md +195 -195
- package/skills/social-graph-ranker/SKILL.md +155 -155
- package/skills/social-publisher/SKILL.md +130 -130
- package/skills/springboot-patterns/SKILL.md +315 -315
- package/skills/springboot-security/SKILL.md +273 -273
- package/skills/springboot-tdd/SKILL.md +159 -159
- package/skills/springboot-verification/SKILL.md +232 -232
- package/skills/strategic-compact/SKILL.md +142 -142
- package/skills/swift-actor-persistence/SKILL.md +144 -144
- package/skills/swift-concurrency-6-2/SKILL.md +216 -216
- package/skills/swift-protocol-di-testing/SKILL.md +191 -191
- package/skills/swiftui-patterns/SKILL.md +259 -259
- package/skills/taste/SKILL.md +264 -264
- package/skills/taste/references/genre-taxonomy.md +87 -87
- package/skills/tdd-workflow/SKILL.md +583 -583
- package/skills/team-agent-orchestration/SKILL.md +111 -111
- package/skills/team-builder/SKILL.md +169 -169
- package/skills/terminal-ops/SKILL.md +110 -110
- package/skills/tinystruct-patterns/SKILL.md +279 -279
- package/skills/tinystruct-patterns/references/architecture.md +90 -90
- package/skills/tinystruct-patterns/references/data-handling.md +60 -60
- package/skills/tinystruct-patterns/references/database.md +99 -99
- package/skills/tinystruct-patterns/references/routing.md +64 -64
- package/skills/tinystruct-patterns/references/system-usage.md +97 -97
- package/skills/tinystruct-patterns/references/testing.md +72 -72
- package/skills/token-budget-advisor/SKILL.md +134 -134
- package/skills/ui-demo/SKILL.md +466 -466
- package/skills/ui-to-vue/SKILL.md +135 -135
- package/skills/uncloud/SKILL.md +344 -344
- package/skills/unified-memory/SKILL.md +170 -170
- package/skills/unified-notifications-ops/SKILL.md +188 -188
- package/skills/verification-loop/SKILL.md +129 -129
- package/skills/video-editing/SKILL.md +311 -311
- package/skills/videodb/SKILL.md +375 -375
- package/skills/visa-doc-translate/SKILL.md +117 -117
- package/skills/vite-patterns/SKILL.md +450 -450
- package/skills/vue-patterns/SKILL.md +471 -471
- package/skills/windows-desktop-e2e/SKILL.md +888 -888
- package/skills/workspace-surface-audit/SKILL.md +126 -126
- package/skills/x-api/SKILL.md +235 -235
package/lib/cloud.mjs
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Craft Cloud: the agent keeps working while this PC is off, on GitHub's
|
|
3
|
+
* machines, in the user's own account. Nothing runs on Codeply servers.
|
|
4
|
+
*
|
|
5
|
+
* mirror A private repo (craft-workspace-<project>) that holds snapshots
|
|
6
|
+
* of the project. It is separate from the user's real repo, so a
|
|
7
|
+
* cloud run never touches production code.
|
|
8
|
+
* snapshot The working tree, pushed to the mirror's main branch through a
|
|
9
|
+
* git folder of Craft's own (~/.codeply/cloud/<id>.git). The
|
|
10
|
+
* project's own .git is never read or written. .gitignore is
|
|
11
|
+
* respected, .env files and keys are always left out, and files
|
|
12
|
+
* holding key-like strings are skipped.
|
|
13
|
+
* run A workflow_dispatch of craft-cloud.yml. The runner reports
|
|
14
|
+
* progress in a check run, pushes code changes to craft/task-<id>,
|
|
15
|
+
* and keeps the chat on the craft-sessions branch so the next run
|
|
16
|
+
* continues the same conversation.
|
|
17
|
+
* pull The task branch's changes, applied back onto the local project
|
|
18
|
+
* with a 3-way merge, so edits made meanwhile survive.
|
|
19
|
+
*
|
|
20
|
+
* The model key lives only in the mirror's encrypted Actions secrets.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import os from 'os';
|
|
24
|
+
import path from 'path';
|
|
25
|
+
import crypto from 'crypto';
|
|
26
|
+
import { createRequire } from 'module';
|
|
27
|
+
import { runAgent } from './agent.mjs';
|
|
28
|
+
import { git, ciApprove, routeFromEnv } from './github-agent.mjs';
|
|
29
|
+
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const nacl = require('tweetnacl');
|
|
32
|
+
const blake = require('blakejs');
|
|
33
|
+
|
|
34
|
+
export const WORKFLOW_FILE = 'craft-cloud.yml';
|
|
35
|
+
export const WORKFLOW_PATH = `.github/workflows/${WORKFLOW_FILE}`;
|
|
36
|
+
export const MIRROR_PREFIX = 'craft-workspace-';
|
|
37
|
+
export const MIRROR_DESCRIPTION = 'Craft workspace mirror: a private backup that Craft cloud runs work in.';
|
|
38
|
+
export const SESSIONS_BRANCH = 'craft-sessions';
|
|
39
|
+
export const ENGINE = 'codeply-cli@0.4';
|
|
40
|
+
export const taskBranch = (id) => `craft/task-${id}`;
|
|
41
|
+
export const checkName = (id) => `craft ${id}`;
|
|
42
|
+
|
|
43
|
+
const MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
44
|
+
const MAX_SCAN_BYTES = 2 * 1024 * 1024;
|
|
45
|
+
const MAX_PROMPT = 20000;
|
|
46
|
+
const MAX_OUTPUT = 60000;
|
|
47
|
+
const HISTORY_TURNS = 40;
|
|
48
|
+
|
|
49
|
+
export const CLOUD_WORKFLOW = `# Written by Codeply Craft. Starts only from Craft (workflow_dispatch), which
|
|
50
|
+
# needs write access to this repo, so nobody else can spend its minutes.
|
|
51
|
+
name: craft-cloud
|
|
52
|
+
run-name: craft \${{ inputs.task_id }}
|
|
53
|
+
|
|
54
|
+
on:
|
|
55
|
+
workflow_dispatch:
|
|
56
|
+
inputs:
|
|
57
|
+
prompt:
|
|
58
|
+
description: What to do
|
|
59
|
+
required: true
|
|
60
|
+
type: string
|
|
61
|
+
mode:
|
|
62
|
+
description: Build, Plan or Ask
|
|
63
|
+
required: false
|
|
64
|
+
default: Build
|
|
65
|
+
type: string
|
|
66
|
+
task_id:
|
|
67
|
+
required: true
|
|
68
|
+
type: string
|
|
69
|
+
session_id:
|
|
70
|
+
required: false
|
|
71
|
+
default: ''
|
|
72
|
+
type: string
|
|
73
|
+
|
|
74
|
+
permissions:
|
|
75
|
+
contents: write
|
|
76
|
+
checks: write
|
|
77
|
+
|
|
78
|
+
concurrency:
|
|
79
|
+
group: craft-\${{ inputs.session_id || inputs.task_id }}
|
|
80
|
+
cancel-in-progress: false
|
|
81
|
+
|
|
82
|
+
jobs:
|
|
83
|
+
craft:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
timeout-minutes: 60
|
|
86
|
+
steps:
|
|
87
|
+
- uses: actions/checkout@v4
|
|
88
|
+
with:
|
|
89
|
+
fetch-depth: 1
|
|
90
|
+
persist-credentials: false
|
|
91
|
+
- uses: actions/setup-node@v4
|
|
92
|
+
with:
|
|
93
|
+
node-version: 22
|
|
94
|
+
- name: Run Craft
|
|
95
|
+
run: npx -y --package="\${CRAFT_ENGINE:-${ENGINE}}" codeply cloud runner
|
|
96
|
+
env:
|
|
97
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
98
|
+
CODEPLY_API_KEY: \${{ secrets.CODEPLY_API_KEY }}
|
|
99
|
+
CODEPLY_MODEL_KIND: \${{ vars.CODEPLY_MODEL_KIND }}
|
|
100
|
+
CODEPLY_MODEL: \${{ vars.CODEPLY_MODEL }}
|
|
101
|
+
CODEPLY_BASE_URL: \${{ vars.CODEPLY_BASE_URL }}
|
|
102
|
+
CRAFT_ENGINE: \${{ vars.CRAFT_ENGINE }}
|
|
103
|
+
CRAFT_PROMPT: \${{ inputs.prompt }}
|
|
104
|
+
CRAFT_MODE: \${{ inputs.mode }}
|
|
105
|
+
CRAFT_TASK_ID: \${{ inputs.task_id }}
|
|
106
|
+
CRAFT_SESSION_ID: \${{ inputs.session_id }}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
// Never mirrored, whatever .gitignore says.
|
|
110
|
+
export const ALWAYS_EXCLUDE = [
|
|
111
|
+
'.env', '.env.*', '!.env.example', '!.env.sample', '!.env.template',
|
|
112
|
+
'*.pem', '*.key', '*.p12', '*.pfx', '*.keystore', '*.jks', '*.mobileprovision',
|
|
113
|
+
'id_rsa*', 'id_dsa*', 'id_ecdsa*', 'id_ed25519*', '.netrc', '.pgpass',
|
|
114
|
+
'node_modules/', '.codeply/', '*.log',
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// High-confidence key formats only: a false alarm leaves a real source file out of the mirror.
|
|
118
|
+
const SECRET_PATTERNS = [
|
|
119
|
+
['private key', /-----BEGIN [A-Z ]*PRIVATE KEY-----/],
|
|
120
|
+
['Anthropic key', /\bsk-ant-[A-Za-z0-9_-]{20,}/],
|
|
121
|
+
['OpenAI key', /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{32,}/],
|
|
122
|
+
['GitHub token', /\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,})/],
|
|
123
|
+
['Slack token', /\bxox[abprs]-[A-Za-z0-9-]{10,}/],
|
|
124
|
+
['AWS key', /\bAKIA[0-9A-Z]{16}\b/],
|
|
125
|
+
['Google key', /\bAIza[0-9A-Za-z_-]{35}\b/],
|
|
126
|
+
['Stripe key', /\b[sr]k_live_[A-Za-z0-9]{20,}/],
|
|
127
|
+
['npm token', /\bnpm_[A-Za-z0-9]{36}\b/],
|
|
128
|
+
['Groq key', /\bgsk_[A-Za-z0-9]{40,}/],
|
|
129
|
+
['OpenRouter key', /\bsk-or-v1-[a-f0-9]{40,}/],
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
/** The first key-like string in a file's text, or null. Supabase anon keys are public; service_role keys are not. */
|
|
133
|
+
export function findSecret(text) {
|
|
134
|
+
for (const [label, re] of SECRET_PATTERNS) if (re.test(text)) return label;
|
|
135
|
+
const jwts = text.match(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g) || [];
|
|
136
|
+
for (const t of jwts) {
|
|
137
|
+
try { if (/service_role/.test(Buffer.from(t.split('.')[1], 'base64url').toString('utf8'))) return 'Supabase service_role key'; } catch {}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Encrypted secrets (libsodium sealed box, what GitHub expects) ─────────
|
|
143
|
+
|
|
144
|
+
export function sealSecret(publicKeyB64, value) {
|
|
145
|
+
const pk = new Uint8Array(Buffer.from(publicKeyB64, 'base64'));
|
|
146
|
+
const eph = nacl.box.keyPair();
|
|
147
|
+
const nonceIn = new Uint8Array(eph.publicKey.length + pk.length);
|
|
148
|
+
nonceIn.set(eph.publicKey); nonceIn.set(pk, eph.publicKey.length);
|
|
149
|
+
const nonce = blake.blake2b(nonceIn, undefined, nacl.box.nonceLength);
|
|
150
|
+
const boxed = nacl.box(new Uint8Array(Buffer.from(String(value), 'utf8')), nonce, pk, eph.secretKey);
|
|
151
|
+
return Buffer.concat([Buffer.from(eph.publicKey), Buffer.from(boxed)]).toString('base64');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Inverse of sealSecret, for tests. */
|
|
155
|
+
export function openSealed(sealedB64, publicKey, secretKey) {
|
|
156
|
+
const all = new Uint8Array(Buffer.from(sealedB64, 'base64'));
|
|
157
|
+
const epk = all.slice(0, 32);
|
|
158
|
+
const nonceIn = new Uint8Array(64); nonceIn.set(epk); nonceIn.set(publicKey, 32);
|
|
159
|
+
const nonce = blake.blake2b(nonceIn, undefined, nacl.box.nonceLength);
|
|
160
|
+
const out = nacl.box.open(all.slice(32), nonce, epk, secretKey);
|
|
161
|
+
return out ? Buffer.from(out).toString('utf8') : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── GitHub API ──────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
export function githubApi({ token, apiUrl = 'https://api.github.com', fetchImpl = fetch }) {
|
|
167
|
+
const call = async (method, route, body, { allow = [] } = {}) => {
|
|
168
|
+
const res = await fetchImpl(`${apiUrl.replace(/\/$/, '')}${route}`, {
|
|
169
|
+
method,
|
|
170
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': 'codeply-craft', ...(body ? { 'Content-Type': 'application/json' } : {}) },
|
|
171
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
172
|
+
});
|
|
173
|
+
const text = await res.text();
|
|
174
|
+
let json = null;
|
|
175
|
+
try { json = text ? JSON.parse(text) : null; } catch {}
|
|
176
|
+
if (!res.ok && !allow.includes(res.status)) {
|
|
177
|
+
const e = new Error(`GitHub ${method} ${route.split('?')[0]}: ${res.status} ${(json && json.message) || text.slice(0, 200)}`);
|
|
178
|
+
e.status = res.status;
|
|
179
|
+
throw e;
|
|
180
|
+
}
|
|
181
|
+
return { status: res.status, json, text, scopes: res.headers && res.headers.get ? res.headers.get('x-oauth-scopes') : null };
|
|
182
|
+
};
|
|
183
|
+
return call;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Why a run died before Craft could report back (install failed, time limit,
|
|
188
|
+
* permissions), from the job's steps and log. Plain words, or null.
|
|
189
|
+
*/
|
|
190
|
+
export async function diagnoseRun(api, repo, runId) {
|
|
191
|
+
try {
|
|
192
|
+
const jobs = ((await api('GET', `/repos/${repo}/actions/runs/${runId}/jobs`)).json || {}).jobs || [];
|
|
193
|
+
const job = jobs.find((j) => j.conclusion === 'failure' || j.conclusion === 'timed_out') || jobs[0];
|
|
194
|
+
if (!job) return null;
|
|
195
|
+
if (job.conclusion === 'timed_out') return 'The run hit its 60-minute limit and was stopped.';
|
|
196
|
+
const step = (job.steps || []).find((s) => s.conclusion === 'failure');
|
|
197
|
+
let log = '';
|
|
198
|
+
try { log = (await api('GET', `/repos/${repo}/actions/jobs/${job.id}/logs`)).text || ''; } catch {}
|
|
199
|
+
if (/No matching version found for codeply-cli|notarget[\s\S]{0,200}codeply-cli/i.test(log)) {
|
|
200
|
+
return `GitHub couldn't install Craft's cloud engine (${ENGINE}): that version isn't on npm yet. Publish it, then run again.`;
|
|
201
|
+
}
|
|
202
|
+
if (/Resource not accessible by integration/i.test(log)) return 'GitHub refused the run permission to save its work. Check Settings > Actions > General > Workflow permissions on the mirror repo.';
|
|
203
|
+
if (/ENOTFOUND|ECONNRESET|ETIMEDOUT|network/i.test(log) && /npm (error|ERR)/i.test(log)) return 'GitHub\'s runner could not download Craft\'s engine (a network hiccup on GitHub\'s side). Run it again.';
|
|
204
|
+
const lastError = log.split('\n').map((l) => l.replace(/^\S+Z\s/, '').replace(/\x1b\[[0-9;]*m/g, '').trim())
|
|
205
|
+
.filter((l) => /error|failed|fatal/i.test(l) && !/^##\[group\]/.test(l)).slice(-1)[0];
|
|
206
|
+
if (step && step.name === 'Run Craft') return `Craft stopped before it could report back${lastError ? `: ${lastError.replace(/^##\[error\]/, '').slice(0, 240)}` : '.'}`;
|
|
207
|
+
if (step) return `The "${step.name}" step failed on GitHub${lastError ? `: ${lastError.replace(/^##\[error\]/, '').slice(0, 240)}` : '.'}`;
|
|
208
|
+
return null;
|
|
209
|
+
} catch { return null; }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Local state ─────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
const defaultHome = () => path.join(os.homedir(), '.codeply');
|
|
215
|
+
const projectKey = (cwd) => path.resolve(cwd).replace(/\\/g, '/').replace(/\/$/, '').toLowerCase();
|
|
216
|
+
const projectId = (cwd) => crypto.createHash('sha1').update(projectKey(cwd)).digest('hex').slice(0, 16);
|
|
217
|
+
|
|
218
|
+
function stateFile(home) { return path.join(home, 'cloud.json'); }
|
|
219
|
+
export function readState(home = defaultHome()) {
|
|
220
|
+
try { const s = JSON.parse(fs.readFileSync(stateFile(home), 'utf8')); return s && typeof s === 'object' && s.projects ? s : { projects: {} }; } catch { return { projects: {} }; }
|
|
221
|
+
}
|
|
222
|
+
function writeState(home, state) {
|
|
223
|
+
fs.mkdirSync(home, { recursive: true });
|
|
224
|
+
const tmp = `${stateFile(home)}.tmp`;
|
|
225
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
226
|
+
fs.renameSync(tmp, stateFile(home));
|
|
227
|
+
}
|
|
228
|
+
export function getProject(cwd, home = defaultHome()) { return readState(home).projects[projectKey(cwd)] || null; }
|
|
229
|
+
function updateProject(cwd, home, fn) {
|
|
230
|
+
const state = readState(home);
|
|
231
|
+
const key = projectKey(cwd);
|
|
232
|
+
state.projects[key] = fn(state.projects[key] || { cwd: path.resolve(cwd), tasks: [] });
|
|
233
|
+
writeState(home, state);
|
|
234
|
+
return state.projects[key];
|
|
235
|
+
}
|
|
236
|
+
function saveTask(cwd, home, task) {
|
|
237
|
+
return updateProject(cwd, home, (p) => {
|
|
238
|
+
const tasks = (p.tasks || []).filter((t) => t.id !== task.id);
|
|
239
|
+
tasks.unshift(task);
|
|
240
|
+
return { ...p, tasks: tasks.slice(0, 100) };
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/** Per-project switches the app keeps: cloudOn (send new messages to the cloud), autoBackup. */
|
|
244
|
+
export function setProjectOptions(cwd, opts, home = defaultHome()) {
|
|
245
|
+
const allowed = {};
|
|
246
|
+
for (const k of ['cloudOn', 'autoBackup']) if (typeof opts[k] === 'boolean') allowed[k] = opts[k];
|
|
247
|
+
return updateProject(cwd, home, (p) => ({ ...p, ...allowed }));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function getTask(cwd, id, home = defaultHome()) {
|
|
251
|
+
const p = getProject(cwd, home);
|
|
252
|
+
return p && (p.tasks || []).find((t) => t.id === id) || null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
const BOT = ['-c', 'user.name=Codeply Craft', '-c', 'user.email=craft-bot@users.noreply.github.com'];
|
|
258
|
+
|
|
259
|
+
function mirrorGit(cwd, home) {
|
|
260
|
+
const gitDir = path.join(home, 'cloud', `${projectId(cwd)}.git`);
|
|
261
|
+
const G = (args, opts) => git(cwd, ['--git-dir', gitDir, '--work-tree', cwd, ...args], opts);
|
|
262
|
+
return { gitDir, G };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function initMirrorGit(cwd, home) {
|
|
266
|
+
const m = mirrorGit(cwd, home);
|
|
267
|
+
if (!fs.existsSync(path.join(m.gitDir, 'HEAD'))) {
|
|
268
|
+
fs.mkdirSync(m.gitDir, { recursive: true });
|
|
269
|
+
await git(cwd, ['init', '-q', '--bare', m.gitDir]);
|
|
270
|
+
await m.G(['config', 'core.bare', 'false']);
|
|
271
|
+
await m.G(['config', 'core.autocrlf', 'false']);
|
|
272
|
+
await m.G(['config', 'core.quotepath', 'false']);
|
|
273
|
+
await m.G(['symbolic-ref', 'HEAD', 'refs/heads/main']);
|
|
274
|
+
}
|
|
275
|
+
fs.mkdirSync(path.join(m.gitDir, 'info'), { recursive: true });
|
|
276
|
+
fs.writeFileSync(path.join(m.gitDir, 'info', 'exclude'), `${ALWAYS_EXCLUDE.join('\n')}\n`);
|
|
277
|
+
return m;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Stage the working tree into Craft's own index: .gitignore and ALWAYS_EXCLUDE
|
|
282
|
+
* respected, the project's own workflows left out (they must not run on the
|
|
283
|
+
* mirror), big files and files holding keys dropped, craft-cloud.yml added.
|
|
284
|
+
*/
|
|
285
|
+
async function stageWorkingTree(cwd, home) {
|
|
286
|
+
const m = await initMirrorGit(cwd, home);
|
|
287
|
+
const { G } = m;
|
|
288
|
+
await G(['add', '-A', '--', '.']);
|
|
289
|
+
await G(['rm', '-r', '-q', '--cached', '--ignore-unmatch', '--', '.github/workflows']);
|
|
290
|
+
const hasHead = await G(['rev-parse', '-q', '--verify', 'refs/heads/main']).then(() => true, () => false);
|
|
291
|
+
const listed = hasHead
|
|
292
|
+
? await G(['diff', '--cached', '--name-only', '--diff-filter=AM', '-z', 'refs/heads/main'])
|
|
293
|
+
: await G(['ls-files', '-z', '--cached']);
|
|
294
|
+
const skipped = [];
|
|
295
|
+
for (const rel of listed.split('\0').filter(Boolean)) {
|
|
296
|
+
const full = path.join(cwd, rel);
|
|
297
|
+
let reason = null;
|
|
298
|
+
try {
|
|
299
|
+
const st = fs.statSync(full);
|
|
300
|
+
if (st.size > MAX_FILE_BYTES) reason = 'over 50 MB';
|
|
301
|
+
else if (st.size <= MAX_SCAN_BYTES) {
|
|
302
|
+
const buf = fs.readFileSync(full);
|
|
303
|
+
if (!buf.subarray(0, 8000).includes(0)) {
|
|
304
|
+
const found = findSecret(buf.toString('utf8'));
|
|
305
|
+
if (found) reason = `holds a ${found}`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch { continue; }
|
|
309
|
+
if (reason) {
|
|
310
|
+
await G(['rm', '-q', '--cached', '--', rel]);
|
|
311
|
+
skipped.push({ path: rel.replace(/\\/g, '/'), reason });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const blob = await git(cwd, ['--git-dir', m.gitDir, 'hash-object', '-w', '--stdin'], { input: CLOUD_WORKFLOW });
|
|
315
|
+
await G(['update-index', '--add', '--cacheinfo', `100644,${blob},${WORKFLOW_PATH}`]);
|
|
316
|
+
return { ...m, hasHead, skipped };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Commit the working tree to the local mirror git (no network). */
|
|
320
|
+
export async function snapshot({ cwd, home = defaultHome() }) {
|
|
321
|
+
const s = await stageWorkingTree(cwd, home);
|
|
322
|
+
const tree = await s.G(['write-tree']);
|
|
323
|
+
const head = s.hasHead ? await s.G(['rev-parse', 'refs/heads/main']) : null;
|
|
324
|
+
if (head && (await s.G(['rev-parse', `${head}^{tree}`])) === tree) return { gitDir: s.gitDir, sha: head, changed: false, skipped: s.skipped };
|
|
325
|
+
const sha = await git(cwd, ['--git-dir', s.gitDir, ...BOT, 'commit-tree', tree, ...(head ? ['-p', head] : []), '-m', `Snapshot from ${os.hostname()} at ${new Date().toISOString()}`]);
|
|
326
|
+
await s.G(['update-ref', 'refs/heads/main', sha]);
|
|
327
|
+
return { gitDir: s.gitDir, sha, changed: true, skipped: s.skipped };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const authFor = (token, serverUrl = 'https://github.com') => ({ token, server: serverUrl.replace(/\/$/, '') });
|
|
331
|
+
|
|
332
|
+
/** Snapshot, then push it to the mirror. The mirror's main is Craft's, so it is force-pushed. */
|
|
333
|
+
export async function pushSnapshot({ cwd, token, home = defaultHome(), serverUrl = 'https://github.com', remoteUrl }) {
|
|
334
|
+
const p = getProject(cwd, home);
|
|
335
|
+
if (!p || !p.repo) throw new Error('Cloud is not set up for this project yet.');
|
|
336
|
+
const snap = await snapshot({ cwd, home });
|
|
337
|
+
if (snap.changed || !p.lastPush || p.lastPush.sha !== snap.sha) {
|
|
338
|
+
const url = remoteUrl || `${serverUrl.replace(/\/$/, '')}/${p.repo}.git`;
|
|
339
|
+
await git(cwd, ['--git-dir', snap.gitDir, 'push', '-q', '--force', url, 'refs/heads/main:refs/heads/main'], { auth: authFor(token, serverUrl) });
|
|
340
|
+
updateProject(cwd, home, (q) => ({ ...q, lastPush: { sha: snap.sha, at: Date.now(), skipped: snap.skipped } }));
|
|
341
|
+
return { ...snap, pushed: true };
|
|
342
|
+
}
|
|
343
|
+
return { ...snap, pushed: false };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Setup ───────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
const slug = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, '').slice(0, 60) || 'project';
|
|
349
|
+
|
|
350
|
+
/** Find or create the private mirror repo for a project. */
|
|
351
|
+
export async function ensureMirror({ api, cwd, home = defaultHome() }) {
|
|
352
|
+
const existing = getProject(cwd, home);
|
|
353
|
+
if (existing && existing.repo) {
|
|
354
|
+
const r = await api('GET', `/repos/${existing.repo}`, null, { allow: [404] });
|
|
355
|
+
if (r.status === 200) return { repo: existing.repo, created: false };
|
|
356
|
+
}
|
|
357
|
+
const me = (await api('GET', '/user')).json;
|
|
358
|
+
const base = `${MIRROR_PREFIX}${slug(path.basename(path.resolve(cwd)))}`;
|
|
359
|
+
for (let i = 1; i <= 9; i++) {
|
|
360
|
+
const name = i === 1 ? base : `${base}-${i}`;
|
|
361
|
+
const r = await api('GET', `/repos/${me.login}/${name}`, null, { allow: [404] });
|
|
362
|
+
if (r.status === 200 && r.json.description === MIRROR_DESCRIPTION && r.json.private) {
|
|
363
|
+
updateProject(cwd, home, (p) => ({ ...p, repo: r.json.full_name }));
|
|
364
|
+
return { repo: r.json.full_name, created: false };
|
|
365
|
+
}
|
|
366
|
+
if (r.status === 404) {
|
|
367
|
+
const made = await api('POST', '/user/repos', { name, private: true, description: MIRROR_DESCRIPTION, auto_init: false, has_issues: false, has_wiki: false, has_projects: false });
|
|
368
|
+
updateProject(cwd, home, (p) => ({ ...p, repo: made.json.full_name, createdAt: Date.now() }));
|
|
369
|
+
return { repo: made.json.full_name, created: true };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
throw new Error(`Could not find a free name for the mirror (tried ${base} to ${base}-9).`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Which model the runner uses. Local models can't be reached from GitHub. */
|
|
376
|
+
export function cloudModelConfig(model) {
|
|
377
|
+
if (!model || !model.model) return { error: 'Pick one of your own models first. Cloud runs use your key, not Auto.' };
|
|
378
|
+
const kind = model.kind === 'ollama' ? 'ollama' : 'openai';
|
|
379
|
+
const baseUrl = kind === 'ollama' ? String(model.baseUrl || 'https://ollama.com').replace(/\/+$/, '').replace(/\/(api|v1)$/i, '') : String(model.baseUrl || '').trim();
|
|
380
|
+
if (!baseUrl) return { error: 'That model has no base URL.' };
|
|
381
|
+
if (/^https?:\/\/(localhost|127\.|0\.0\.0\.0|\[::1\]|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/i.test(baseUrl)) return { error: `${model.name || model.model} runs on this PC, so GitHub can't reach it. Pick a cloud model for cloud runs.` };
|
|
382
|
+
if (!model.apiKey) return { error: `${model.name || model.model} has no API key.` };
|
|
383
|
+
return { kind, baseUrl, model: model.model, apiKey: model.apiKey, sig: crypto.createHash('sha256').update([kind, baseUrl, model.model, model.apiKey].join('\n')).digest('hex').slice(0, 16) };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function setVariable(api, repo, name, value) {
|
|
387
|
+
const r = await api('PATCH', `/repos/${repo}/actions/variables/${name}`, { name, value }, { allow: [404] });
|
|
388
|
+
if (r.status === 404) await api('POST', `/repos/${repo}/actions/variables`, { name, value });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Put the model key in the mirror's encrypted secrets and the rest in variables. */
|
|
392
|
+
export async function configureModel({ api, repo, model }) {
|
|
393
|
+
const cfg = cloudModelConfig(model);
|
|
394
|
+
if (cfg.error) throw new Error(cfg.error);
|
|
395
|
+
const pk = (await api('GET', `/repos/${repo}/actions/secrets/public-key`)).json;
|
|
396
|
+
await api('PUT', `/repos/${repo}/actions/secrets/CODEPLY_API_KEY`, { encrypted_value: sealSecret(pk.key, cfg.apiKey), key_id: pk.key_id });
|
|
397
|
+
await setVariable(api, repo, 'CODEPLY_MODEL_KIND', cfg.kind);
|
|
398
|
+
await setVariable(api, repo, 'CODEPLY_BASE_URL', cfg.baseUrl);
|
|
399
|
+
await setVariable(api, repo, 'CODEPLY_MODEL', cfg.model);
|
|
400
|
+
return cfg;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Check the token can create repos and push workflow files. */
|
|
404
|
+
export async function checkToken(api) {
|
|
405
|
+
const r = await api('GET', '/user');
|
|
406
|
+
const scopes = String(r.scopes || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
407
|
+
// Fine-grained and app tokens send no scope header; let GitHub decide on those.
|
|
408
|
+
if (r.scopes != null && scopes.length && !(scopes.includes('repo') && scopes.includes('workflow'))) {
|
|
409
|
+
return { ok: false, login: r.json.login, error: 'GitHub needs to be reconnected so Craft may create private repos and workflow files (repo and workflow access).' };
|
|
410
|
+
}
|
|
411
|
+
return { ok: true, login: r.json.login };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** One-click setup: mirror repo, first snapshot with the workflow, model key. */
|
|
415
|
+
export async function setupCloud({ cwd, token, model, home = defaultHome(), apiUrl, serverUrl, fetchImpl, remoteUrl }) {
|
|
416
|
+
const api = githubApi({ token, apiUrl, fetchImpl });
|
|
417
|
+
const t = await checkToken(api);
|
|
418
|
+
if (!t.ok) throw new Error(t.error);
|
|
419
|
+
const cfg = cloudModelConfig(model);
|
|
420
|
+
if (cfg.error) throw new Error(cfg.error);
|
|
421
|
+
const { repo, created } = await ensureMirror({ api, cwd, home });
|
|
422
|
+
const pushed = await pushSnapshot({ cwd, token, home, serverUrl, remoteUrl });
|
|
423
|
+
await configureModel({ api, repo, model });
|
|
424
|
+
updateProject(cwd, home, (p) => ({ ...p, modelSig: cfg.sig, modelName: model.name || model.model, enabled: true }));
|
|
425
|
+
return { repo, created, url: `${(serverUrl || 'https://github.com').replace(/\/$/, '')}/${repo}`, skipped: pushed.skipped };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ─── Runs ────────────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
const newTaskId = () => `${Date.now().toString(36)}${crypto.randomBytes(3).toString('hex')}`;
|
|
431
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Push the latest snapshot and start a cloud run.
|
|
435
|
+
* @returns {Promise<object>} the task record (also saved in ~/.codeply/cloud.json)
|
|
436
|
+
*/
|
|
437
|
+
export async function startCloudRun({ cwd, token, prompt, mode = 'Build', sessionId = '', model, home = defaultHome(), apiUrl, serverUrl, fetchImpl, remoteUrl, retryMs = 3000, startedAt }) {
|
|
438
|
+
const p = getProject(cwd, home);
|
|
439
|
+
if (!p || !p.repo) throw new Error('Set up cloud runs for this project first.');
|
|
440
|
+
prompt = String(prompt || '').trim();
|
|
441
|
+
if (!prompt) throw new Error('Write a task first.');
|
|
442
|
+
if (prompt.length > MAX_PROMPT) throw new Error(`That task is too long for a cloud run (${prompt.length} characters, the limit is ${MAX_PROMPT}).`);
|
|
443
|
+
mode = ['Build', 'Plan', 'Ask'].includes(mode) ? mode : 'Build';
|
|
444
|
+
const api = githubApi({ token, apiUrl, fetchImpl });
|
|
445
|
+
// A different model (or key) than last time goes to the mirror first.
|
|
446
|
+
if (model) {
|
|
447
|
+
const cfg = cloudModelConfig(model);
|
|
448
|
+
if (cfg.error) throw new Error(cfg.error);
|
|
449
|
+
if (cfg.sig !== p.modelSig) {
|
|
450
|
+
await configureModel({ api, repo: p.repo, model });
|
|
451
|
+
updateProject(cwd, home, (q) => ({ ...q, modelSig: cfg.sig, modelName: model.name || model.model }));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const snap = await pushSnapshot({ cwd, token, home, serverUrl, remoteUrl });
|
|
455
|
+
const task = { id: newTaskId(), prompt, mode, sessionId: String(sessionId || ''), baseSha: snap.sha, repo: p.repo, status: 'starting', startedAt: startedAt || Date.now() };
|
|
456
|
+
// Right after the workflow file first lands, GitHub can take a few seconds to know it.
|
|
457
|
+
let lastErr = null;
|
|
458
|
+
for (let i = 0; i < 10; i++) {
|
|
459
|
+
try {
|
|
460
|
+
await api('POST', `/repos/${p.repo}/actions/workflows/${WORKFLOW_FILE}/dispatches`, { ref: 'main', inputs: { prompt, mode, task_id: task.id, session_id: task.sessionId } });
|
|
461
|
+
lastErr = null;
|
|
462
|
+
break;
|
|
463
|
+
} catch (e) {
|
|
464
|
+
lastErr = e;
|
|
465
|
+
if (e.status !== 404 && e.status !== 422) break;
|
|
466
|
+
await sleep(retryMs);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (lastErr) throw lastErr;
|
|
470
|
+
saveTask(cwd, home, task);
|
|
471
|
+
return task;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Where a cloud run is, from the workflow run and the runner's check run. */
|
|
475
|
+
export async function cloudRunStatus({ cwd, taskId, token, home = defaultHome(), apiUrl, fetchImpl }) {
|
|
476
|
+
const task = getTask(cwd, taskId, home);
|
|
477
|
+
if (!task) throw new Error('Unknown cloud task.');
|
|
478
|
+
if (task.status === 'done' || task.status === 'failed' || task.status === 'cancelled') return task;
|
|
479
|
+
const api = githubApi({ token, apiUrl, fetchImpl });
|
|
480
|
+
let run = null;
|
|
481
|
+
if (task.runId) run = (await api('GET', `/repos/${task.repo}/actions/runs/${task.runId}`)).json;
|
|
482
|
+
else {
|
|
483
|
+
const list = (await api('GET', `/repos/${task.repo}/actions/workflows/${WORKFLOW_FILE}/runs?event=workflow_dispatch&per_page=30`)).json;
|
|
484
|
+
run = (list.workflow_runs || []).find((r) => r.display_title === checkName(task.id)) || null;
|
|
485
|
+
}
|
|
486
|
+
const next = { ...task };
|
|
487
|
+
if (!run) {
|
|
488
|
+
// GitHub drops a dispatch it can't start; don't wait forever.
|
|
489
|
+
if (Date.now() - task.startedAt > 5 * 60 * 1000) Object.assign(next, { status: 'failed', error: 'GitHub never started the run. Check that Actions is enabled on the mirror repo.' });
|
|
490
|
+
saveTask(cwd, home, next);
|
|
491
|
+
return next;
|
|
492
|
+
}
|
|
493
|
+
next.runId = run.id;
|
|
494
|
+
next.runUrl = run.html_url;
|
|
495
|
+
const checks = (await api('GET', `/repos/${task.repo}/commits/${run.head_sha}/check-runs?check_name=${encodeURIComponent(checkName(task.id))}`)).json;
|
|
496
|
+
const check = (checks.check_runs || [])[0];
|
|
497
|
+
if (check && check.output) next.progress = check.output.summary || '';
|
|
498
|
+
if (check && check.status === 'completed') {
|
|
499
|
+
let result = {};
|
|
500
|
+
try { result = JSON.parse(check.output.text || '{}'); } catch {}
|
|
501
|
+
Object.assign(next, { status: check.conclusion === 'success' ? 'done' : 'failed', result, error: result.error || null, finishedAt: Date.now() });
|
|
502
|
+
} else if (run.status === 'completed') {
|
|
503
|
+
const why = run.conclusion === 'cancelled' ? 'The run was cancelled.' : (await diagnoseRun(api, task.repo, run.id)) || `The run ended (${run.conclusion}) before Craft could report back. The run log has details.`;
|
|
504
|
+
Object.assign(next, { status: run.conclusion === 'cancelled' ? 'cancelled' : 'failed', error: why, finishedAt: Date.now() });
|
|
505
|
+
} else {
|
|
506
|
+
next.status = run.status === 'queued' || run.status === 'waiting' || run.status === 'pending' ? 'queued' : 'running';
|
|
507
|
+
}
|
|
508
|
+
saveTask(cwd, home, next);
|
|
509
|
+
return next;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Runs started somewhere else (the phone, while this PC was off) become local
|
|
514
|
+
* tasks, so their changes can be pulled here. Only finished runs are imported.
|
|
515
|
+
*/
|
|
516
|
+
export async function importRemoteTasks({ cwd, token, home = defaultHome(), apiUrl, fetchImpl, limit = 20 }) {
|
|
517
|
+
const p = getProject(cwd, home);
|
|
518
|
+
if (!p || !p.repo) return [];
|
|
519
|
+
const api = githubApi({ token, apiUrl, fetchImpl });
|
|
520
|
+
const known = new Set((p.tasks || []).map((t) => t.id));
|
|
521
|
+
const list = (await api('GET', `/repos/${p.repo}/actions/workflows/${WORKFLOW_FILE}/runs?event=workflow_dispatch&status=completed&per_page=${limit}`, null, { allow: [404] })).json;
|
|
522
|
+
const added = [];
|
|
523
|
+
for (const run of (list && list.workflow_runs) || []) {
|
|
524
|
+
const id = String(run.display_title || '').replace(/^craft /, '');
|
|
525
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id) || known.has(id)) continue;
|
|
526
|
+
const checks = (await api('GET', `/repos/${p.repo}/commits/${run.head_sha}/check-runs?check_name=${encodeURIComponent(checkName(id))}`)).json;
|
|
527
|
+
const check = (checks.check_runs || [])[0];
|
|
528
|
+
if (!check || check.status !== 'completed') continue;
|
|
529
|
+
let result = {};
|
|
530
|
+
try { result = JSON.parse(check.output.text || '{}'); } catch {}
|
|
531
|
+
const task = {
|
|
532
|
+
id, prompt: result.prompt || '(started elsewhere)', mode: result.mode || 'Build', sessionId: result.sessionId || '', baseSha: result.baseSha || run.head_sha,
|
|
533
|
+
repo: p.repo, runId: run.id, runUrl: run.html_url, status: check.conclusion === 'success' ? 'done' : 'failed', result, error: result.error || null,
|
|
534
|
+
startedAt: Date.parse(run.created_at) || Date.now(), finishedAt: Date.parse(run.updated_at) || Date.now(), remote: true,
|
|
535
|
+
};
|
|
536
|
+
saveTask(cwd, home, task);
|
|
537
|
+
added.push(task);
|
|
538
|
+
}
|
|
539
|
+
return added;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Stop a cloud run. */
|
|
543
|
+
export async function cancelCloudRun({ cwd, taskId, token, home = defaultHome(), apiUrl, fetchImpl }) {
|
|
544
|
+
const task = await cloudRunStatus({ cwd, taskId, token, home, apiUrl, fetchImpl });
|
|
545
|
+
if (!task.runId || ['done', 'failed', 'cancelled'].includes(task.status)) return task;
|
|
546
|
+
const api = githubApi({ token, apiUrl, fetchImpl });
|
|
547
|
+
await api('POST', `/repos/${task.repo}/actions/runs/${task.runId}/cancel`, null, { allow: [409] });
|
|
548
|
+
return task;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Bring a finished run's code changes into the local project. Stages the
|
|
553
|
+
* current working tree first, so the 3-way merge keeps edits made since the
|
|
554
|
+
* run started; overlapping edits come back as conflict markers.
|
|
555
|
+
*/
|
|
556
|
+
export async function pullCloudRun({ cwd, taskId, token, home = defaultHome(), serverUrl = 'https://github.com', remoteUrl }) {
|
|
557
|
+
const task = getTask(cwd, taskId, home);
|
|
558
|
+
if (!task) throw new Error('Unknown cloud task.');
|
|
559
|
+
if (task.status !== 'done') throw new Error('That cloud run has not finished.');
|
|
560
|
+
if (!task.result || !task.result.branch) return { files: [], conflicts: [], applied: false, message: 'That run did not change any files.' };
|
|
561
|
+
if (task.pulledAt) return { files: task.result.files || [], conflicts: [], applied: false, message: 'Already applied.' };
|
|
562
|
+
const m = await initMirrorGit(cwd, home);
|
|
563
|
+
const url = remoteUrl || `${serverUrl.replace(/\/$/, '')}/${task.repo}.git`;
|
|
564
|
+
const ref = `refs/craft/tasks/${task.id}`;
|
|
565
|
+
await git(cwd, ['--git-dir', m.gitDir, 'fetch', '-q', '--no-tags', url, `+refs/heads/${task.result.branch}:${ref}`], { auth: authFor(token, serverUrl) });
|
|
566
|
+
const patch = await git(cwd, ['--git-dir', m.gitDir, 'diff', '--binary', '--full-index', task.baseSha, ref, '--', '.', `:(exclude)${WORKFLOW_PATH}`, ':(exclude).codeply']);
|
|
567
|
+
if (!patch.trim()) return { files: [], conflicts: [], applied: false, message: 'That run did not change any files.' };
|
|
568
|
+
const files = (await git(cwd, ['--git-dir', m.gitDir, 'diff', '--name-only', task.baseSha, ref, '--', '.', `:(exclude)${WORKFLOW_PATH}`, ':(exclude).codeply'])).split('\n').filter(Boolean);
|
|
569
|
+
await stageWorkingTree(cwd, home);
|
|
570
|
+
let conflicts = [];
|
|
571
|
+
try {
|
|
572
|
+
await m.G(['apply', '--3way', '--whitespace=nowarn', '-'], { input: `${patch}\n` });
|
|
573
|
+
} catch (e) {
|
|
574
|
+
const unmerged = await m.G(['diff', '--name-only', '--diff-filter=U']).catch(() => '');
|
|
575
|
+
conflicts = unmerged.split('\n').filter(Boolean);
|
|
576
|
+
if (!conflicts.length) throw new Error(`Could not apply the changes: ${e.message}`);
|
|
577
|
+
}
|
|
578
|
+
saveTask(cwd, home, { ...task, pulledAt: Date.now(), conflicts });
|
|
579
|
+
return { files, conflicts, applied: true };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ─── The runner (inside GitHub Actions) ─────────────────────────────────
|
|
583
|
+
|
|
584
|
+
function describeTool(ev) {
|
|
585
|
+
const a = ev.args || {};
|
|
586
|
+
const what = a.path || a.file || a.command || a.pattern || a.query || a.url || '';
|
|
587
|
+
return `- ${ev.ok === false ? '(failed) ' : ''}${ev.name}${what ? ` ${String(what).split('\n')[0].slice(0, 100)}` : ''}`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function renderProgress(lines, note) {
|
|
591
|
+
const shown = lines.length > 60 ? [`- ... ${lines.length - 60} earlier steps`, ...lines.slice(-60)] : lines;
|
|
592
|
+
return `${note}\n\n${shown.join('\n')}`.slice(0, MAX_OUTPUT);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function loadHistory(api, repo, sessionId) {
|
|
596
|
+
if (!sessionId) return { messages: [], sha: null };
|
|
597
|
+
const r = await api('GET', `/repos/${repo}/contents/sessions/${encodeURIComponent(sessionId)}.json?ref=${SESSIONS_BRANCH}`, null, { allow: [404] });
|
|
598
|
+
if (r.status !== 200) return { messages: [], sha: null };
|
|
599
|
+
try {
|
|
600
|
+
const data = JSON.parse(Buffer.from(r.json.content, 'base64').toString('utf8'));
|
|
601
|
+
return { messages: Array.isArray(data.messages) ? data.messages : [], sha: r.json.sha };
|
|
602
|
+
} catch { return { messages: [], sha: r.json.sha }; }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function saveHistory(api, repo, sessionId, messages, baseSha) {
|
|
606
|
+
if (!sessionId) return;
|
|
607
|
+
const ref = await api('GET', `/repos/${repo}/git/ref/heads/${SESSIONS_BRANCH}`, null, { allow: [404] });
|
|
608
|
+
if (ref.status === 404) await api('POST', `/repos/${repo}/git/refs`, { ref: `refs/heads/${SESSIONS_BRANCH}`, sha: baseSha }, { allow: [422] });
|
|
609
|
+
const file = `sessions/${encodeURIComponent(sessionId)}.json`;
|
|
610
|
+
const body = (sha) => ({ message: `Chat ${sessionId}`, branch: SESSIONS_BRANCH, content: Buffer.from(JSON.stringify({ sessionId, updatedAt: new Date().toISOString(), messages }, null, 1)).toString('base64'), ...(sha ? { sha } : {}) });
|
|
611
|
+
for (let i = 0; i < 3; i++) {
|
|
612
|
+
const cur = await api('GET', `/repos/${repo}/contents/${file}?ref=${SESSIONS_BRANCH}`, null, { allow: [404] });
|
|
613
|
+
const r = await api('PUT', `/repos/${repo}/contents/${file}`, body(cur.status === 200 ? cur.json.sha : null), { allow: [409, 422] });
|
|
614
|
+
if (r.status < 300) return;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* `codeply cloud runner`: one cloud task, end to end, inside GitHub Actions.
|
|
620
|
+
* @returns {Promise<{status:'done'|'failed', result:object}>}
|
|
621
|
+
*/
|
|
622
|
+
export async function runCloudRunner({ env = process.env, cwd = process.cwd(), token, route, setupError = null, fetchImpl = fetch, runAgentImpl = runAgent, log = () => {}, maxSteps = 80, updateMs = 5000 }) {
|
|
623
|
+
const repo = env.GITHUB_REPOSITORY;
|
|
624
|
+
const taskId = String(env.CRAFT_TASK_ID || '').replace(/[^A-Za-z0-9_-]/g, '');
|
|
625
|
+
const mode = ['Build', 'Plan', 'Ask'].includes(env.CRAFT_MODE) ? env.CRAFT_MODE : 'Build';
|
|
626
|
+
const prompt = String(env.CRAFT_PROMPT || '').slice(0, MAX_PROMPT);
|
|
627
|
+
const sessionId = String(env.CRAFT_SESSION_ID || '').replace(/[^A-Za-z0-9_-]/g, '');
|
|
628
|
+
const headSha = env.GITHUB_SHA;
|
|
629
|
+
if (!repo || !token || !taskId || !headSha) return { status: 'failed', result: { error: 'Missing GITHUB_REPOSITORY, GITHUB_TOKEN, GITHUB_SHA or CRAFT_TASK_ID.' } };
|
|
630
|
+
const apiUrl = env.GITHUB_API_URL || 'https://api.github.com';
|
|
631
|
+
const serverUrl = (env.GITHUB_SERVER_URL || 'https://github.com').replace(/\/$/, '');
|
|
632
|
+
const api = githubApi({ token, apiUrl, fetchImpl });
|
|
633
|
+
|
|
634
|
+
const check = (await api('POST', `/repos/${repo}/check-runs`, {
|
|
635
|
+
name: checkName(taskId), head_sha: headSha, status: 'in_progress', started_at: new Date().toISOString(),
|
|
636
|
+
output: { title: 'Starting', summary: 'Starting...' },
|
|
637
|
+
})).json;
|
|
638
|
+
const finish = async (ok, title, summary, result) => {
|
|
639
|
+
await api('PATCH', `/repos/${repo}/check-runs/${check.id}`, {
|
|
640
|
+
status: 'completed', conclusion: ok ? 'success' : 'failure', completed_at: new Date().toISOString(),
|
|
641
|
+
output: { title, summary: String(summary || title).slice(0, MAX_OUTPUT), text: JSON.stringify(result).slice(0, MAX_OUTPUT) },
|
|
642
|
+
}).catch((e) => log(`Could not finish the check run: ${e.message}`));
|
|
643
|
+
return { status: ok ? 'done' : 'failed', result };
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
if (setupError) return finish(false, 'Could not start', setupError, { error: setupError, mode });
|
|
647
|
+
if (!prompt) return finish(false, 'Could not start', 'The task was empty.', { error: 'The task was empty.', mode });
|
|
648
|
+
|
|
649
|
+
const lines = [];
|
|
650
|
+
let lastPush = 0; let pending = null;
|
|
651
|
+
const pushProgress = async (note, force = false) => {
|
|
652
|
+
if (!force && Date.now() - lastPush < updateMs) return;
|
|
653
|
+
lastPush = Date.now();
|
|
654
|
+
pending = api('PATCH', `/repos/${repo}/check-runs/${check.id}`, { output: { title: 'Working', summary: renderProgress(lines, note) } }).catch(() => {});
|
|
655
|
+
await pending;
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
try {
|
|
659
|
+
const hist = await loadHistory(api, repo, sessionId);
|
|
660
|
+
await pushProgress(hist.messages.length ? `Continuing the chat (${hist.messages.length / 2} earlier turns).` : 'Working on it.', true);
|
|
661
|
+
const approve = ciApprove(cwd);
|
|
662
|
+
let answer = ''; let steps = 0; let failure = '';
|
|
663
|
+
const userMessage = `${prompt}\n\n(You are running unattended in Craft Cloud, on a copy of the project. Nobody can answer questions mid-run, so make sensible choices and say what you assumed. Do not commit or push; Craft does that when you finish.)`;
|
|
664
|
+
for await (const ev of runAgentImpl({ userMessage, history: hist.messages.slice(-HISTORY_TURNS), mode, cwd, approve, signal: new AbortController().signal, route, maxSteps })) {
|
|
665
|
+
if (ev.type === 'text' && !ev.interim) answer = ev.text;
|
|
666
|
+
else if (ev.type === 'tool_end') { steps++; lines.push(describeTool(ev)); await pushProgress(`Working: ${steps} step${steps === 1 ? '' : 's'} so far.`); }
|
|
667
|
+
else if (ev.type === 'error') failure = ev.error;
|
|
668
|
+
if (ev.type === 'done' || ev.type === 'error' || ev.type === 'aborted') break;
|
|
669
|
+
}
|
|
670
|
+
if (pending) await pending;
|
|
671
|
+
if (failure && !answer) throw new Error(failure);
|
|
672
|
+
|
|
673
|
+
if (mode === 'Plan') {
|
|
674
|
+
const dir = path.join(cwd, '.codeply', 'plans');
|
|
675
|
+
const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith('.md')).map((f) => ({ f, t: fs.statSync(path.join(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t) : [];
|
|
676
|
+
if (files.length) answer = fs.readFileSync(path.join(dir, files[0].f), 'utf8');
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let branch = null; let sha = null; let files = [];
|
|
680
|
+
if (mode === 'Build') {
|
|
681
|
+
await git(cwd, ['add', '-A', '--', '.', ':(exclude).codeply', `:(exclude)${WORKFLOW_PATH}`]);
|
|
682
|
+
const staged = await git(cwd, ['diff', '--cached', '--name-only']);
|
|
683
|
+
if (staged) {
|
|
684
|
+
files = staged.split('\n').filter(Boolean);
|
|
685
|
+
branch = taskBranch(taskId);
|
|
686
|
+
await git(cwd, [...BOT, 'commit', '-q', '-m', `${prompt.split('\n')[0].slice(0, 60)}\n\nCraft cloud task ${taskId}.`]);
|
|
687
|
+
sha = await git(cwd, ['rev-parse', 'HEAD']);
|
|
688
|
+
await git(cwd, ['push', '-q', 'origin', `HEAD:refs/heads/${branch}`], { auth: authFor(token, serverUrl) });
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
await saveHistory(api, repo, sessionId, [...hist.messages, { role: 'user', content: prompt }, { role: 'assistant', content: answer || '(no reply)' }].slice(-HISTORY_TURNS * 2), headSha)
|
|
693
|
+
.catch((e) => log(`Could not save the chat: ${e.message}`));
|
|
694
|
+
const result = { mode, prompt: prompt.slice(0, 4000), sessionId, answer: answer.slice(0, 40000), branch, sha, files, steps, baseSha: headSha };
|
|
695
|
+
const title = files.length ? `Changed ${files.length} file${files.length === 1 ? '' : 's'}` : mode === 'Build' ? 'No files changed' : 'Answered';
|
|
696
|
+
return finish(true, title, answer || title, result);
|
|
697
|
+
} catch (e) {
|
|
698
|
+
return finish(false, 'Failed', `Something went wrong: ${e.message}`, { error: e.message, mode, steps: lines.length });
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** Route and token from the runner environment, with secrets removed from process.env. */
|
|
703
|
+
export function runnerSetup(env = process.env) {
|
|
704
|
+
const cfg = { ...env };
|
|
705
|
+
for (const k of ['GITHUB_TOKEN', 'GH_TOKEN', 'CODEPLY_API_KEY', 'ACTIONS_RUNTIME_TOKEN', 'ACTIONS_ID_TOKEN_REQUEST_TOKEN']) delete env[k];
|
|
706
|
+
const r = routeFromEnv(cfg);
|
|
707
|
+
return { cfg, token: cfg.GITHUB_TOKEN || cfg.GH_TOKEN, route: r.route, setupError: r.error || null };
|
|
708
|
+
}
|