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.
Files changed (375) hide show
  1. package/THIRD_PARTY_NOTICES.md +63 -63
  2. package/bin/codeply.js +129 -6
  3. package/lib/agent.mjs +2139 -2139
  4. package/lib/ai.mjs +6 -6
  5. package/lib/apply-patch.js +217 -217
  6. package/lib/arity-table.json +138 -138
  7. package/lib/arity.js +70 -70
  8. package/lib/auth.mjs +6 -6
  9. package/lib/cloud.mjs +708 -0
  10. package/lib/commands.js +103 -103
  11. package/lib/design-library/assemble-artifact.mjs +21 -21
  12. package/lib/design-library/cleanup.mjs +45 -45
  13. package/lib/design-library/index.json +22379 -22379
  14. package/lib/design-library/make-table-data.mjs +39 -39
  15. package/lib/diagnostics.js +192 -192
  16. package/lib/edit-engine.js +364 -364
  17. package/lib/github-agent.mjs +311 -290
  18. package/lib/history.js +75 -75
  19. package/lib/mcp.js +258 -258
  20. package/lib/native-tools.mjs +189 -189
  21. package/lib/oauth-connectors.js +2 -1
  22. package/lib/permissions.js +109 -109
  23. package/lib/plugins.js +353 -353
  24. package/lib/server.mjs +457 -457
  25. package/lib/session-db.js +220 -220
  26. package/lib/share.js +168 -168
  27. package/lib/skills.js +416 -416
  28. package/lib/snapshot.js +146 -146
  29. package/lib/terminal-hints.js +154 -154
  30. package/lib/tools.mjs +2012 -2012
  31. package/lib/typecheck-worker.js +147 -147
  32. package/lib/web-tools.js +158 -158
  33. package/package.json +49 -47
  34. package/skills/SOURCE.md +58 -58
  35. package/skills/accessibility/SKILL.md +147 -147
  36. package/skills/agent-architecture-audit/SKILL.md +257 -257
  37. package/skills/agent-eval/SKILL.md +147 -147
  38. package/skills/agent-harness-construction/SKILL.md +74 -74
  39. package/skills/agent-introspection-debugging/SKILL.md +154 -154
  40. package/skills/agent-payment-x402/SKILL.md +225 -225
  41. package/skills/agent-self-evaluation/SKILL.md +182 -182
  42. package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -71
  43. package/skills/agent-self-evaluation/references/hook-integration.md +64 -64
  44. package/skills/agent-sort/SKILL.md +216 -216
  45. package/skills/agentic-engineering/SKILL.md +64 -64
  46. package/skills/agentic-os/SKILL.md +388 -388
  47. package/skills/ai-first-engineering/SKILL.md +52 -52
  48. package/skills/ai-regression-testing/SKILL.md +386 -386
  49. package/skills/android-clean-architecture/SKILL.md +340 -340
  50. package/skills/angular-developer/SKILL.md +155 -155
  51. package/skills/angular-developer/references/angular-animations.md +160 -160
  52. package/skills/angular-developer/references/angular-aria.md +410 -410
  53. package/skills/angular-developer/references/cli.md +86 -86
  54. package/skills/angular-developer/references/component-harnesses.md +59 -59
  55. package/skills/angular-developer/references/component-styling.md +91 -91
  56. package/skills/angular-developer/references/components.md +117 -117
  57. package/skills/angular-developer/references/creating-services.md +97 -97
  58. package/skills/angular-developer/references/data-resolvers.md +69 -69
  59. package/skills/angular-developer/references/define-routes.md +67 -67
  60. package/skills/angular-developer/references/defining-providers.md +72 -72
  61. package/skills/angular-developer/references/di-fundamentals.md +120 -120
  62. package/skills/angular-developer/references/e2e-testing.md +56 -56
  63. package/skills/angular-developer/references/effects.md +83 -83
  64. package/skills/angular-developer/references/hierarchical-injectors.md +43 -43
  65. package/skills/angular-developer/references/host-elements.md +80 -80
  66. package/skills/angular-developer/references/injection-context.md +63 -63
  67. package/skills/angular-developer/references/inputs.md +101 -101
  68. package/skills/angular-developer/references/linked-signal.md +59 -59
  69. package/skills/angular-developer/references/loading-strategies.md +61 -61
  70. package/skills/angular-developer/references/mcp.md +108 -108
  71. package/skills/angular-developer/references/navigate-to-routes.md +69 -69
  72. package/skills/angular-developer/references/outputs.md +86 -86
  73. package/skills/angular-developer/references/reactive-forms.md +122 -122
  74. package/skills/angular-developer/references/rendering-strategies.md +44 -44
  75. package/skills/angular-developer/references/resource.md +77 -77
  76. package/skills/angular-developer/references/route-animations.md +56 -56
  77. package/skills/angular-developer/references/route-guards.md +52 -52
  78. package/skills/angular-developer/references/router-lifecycle.md +45 -45
  79. package/skills/angular-developer/references/router-testing.md +87 -87
  80. package/skills/angular-developer/references/show-routes-with-outlets.md +68 -68
  81. package/skills/angular-developer/references/signal-forms.md +795 -795
  82. package/skills/angular-developer/references/signals-overview.md +94 -94
  83. package/skills/angular-developer/references/tailwind-css.md +69 -69
  84. package/skills/angular-developer/references/template-driven-forms.md +114 -114
  85. package/skills/angular-developer/references/testing-fundamentals.md +65 -65
  86. package/skills/api-connector-builder/SKILL.md +121 -121
  87. package/skills/api-design/SKILL.md +524 -524
  88. package/skills/architecture-decision-records/SKILL.md +180 -180
  89. package/skills/article-writing/SKILL.md +80 -80
  90. package/skills/automation-audit-ops/SKILL.md +143 -143
  91. package/skills/autonomous-agent-harness/SKILL.md +274 -274
  92. package/skills/autonomous-loops/SKILL.md +611 -611
  93. package/skills/backend-patterns/SKILL.md +562 -562
  94. package/skills/benchmark/SKILL.md +95 -95
  95. package/skills/benchmark-methodology/SKILL.md +191 -191
  96. package/skills/benchmark-optimization-loop/SKILL.md +71 -71
  97. package/skills/blender-motion-state-inspection/SKILL.md +165 -165
  98. package/skills/blueprint/SKILL.md +106 -106
  99. package/skills/brand-discovery/SKILL.md +145 -145
  100. package/skills/brand-discovery/references/10_purpose-why.md +40 -40
  101. package/skills/brand-discovery/references/20_positioning.md +44 -44
  102. package/skills/brand-discovery/references/30_audience-niche.md +52 -52
  103. package/skills/brand-discovery/references/40_personality-archetype.md +57 -57
  104. package/skills/brand-discovery/references/50_voice-tone.md +59 -59
  105. package/skills/brand-discovery/references/60_narrative-story.md +50 -50
  106. package/skills/brand-discovery/references/70_founder-tension.md +49 -49
  107. package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -133
  108. package/skills/brand-voice/SKILL.md +98 -98
  109. package/skills/brand-voice/references/voice-profile-schema.md +55 -55
  110. package/skills/browser-qa/SKILL.md +105 -105
  111. package/skills/bun-runtime/SKILL.md +85 -85
  112. package/skills/canary-watch/SKILL.md +108 -108
  113. package/skills/carrier-relationship-management/SKILL.md +212 -212
  114. package/skills/cisco-ios-patterns/SKILL.md +164 -164
  115. package/skills/ck/SKILL.md +148 -148
  116. package/skills/claude-devfleet/SKILL.md +112 -112
  117. package/skills/click-path-audit/SKILL.md +245 -245
  118. package/skills/clickhouse-io/SKILL.md +445 -445
  119. package/skills/code-tour/SKILL.md +254 -254
  120. package/skills/codebase-onboarding/SKILL.md +234 -234
  121. package/skills/codehealth-mcp/SKILL.md +167 -167
  122. package/skills/coding-standards/SKILL.md +551 -551
  123. package/skills/competitive-platform-analysis/SKILL.md +214 -214
  124. package/skills/competitive-report-structure/SKILL.md +162 -162
  125. package/skills/compose-multiplatform-patterns/SKILL.md +300 -300
  126. package/skills/config-gc/SKILL.md +120 -120
  127. package/skills/configure-ecc/SKILL.md +385 -385
  128. package/skills/connections-optimizer/SKILL.md +190 -190
  129. package/skills/content-engine/SKILL.md +132 -132
  130. package/skills/content-hash-cache-pattern/SKILL.md +162 -162
  131. package/skills/context-budget/SKILL.md +136 -136
  132. package/skills/continuous-agent-loop/SKILL.md +46 -46
  133. package/skills/continuous-learning/SKILL.md +132 -132
  134. package/skills/continuous-learning-v2/SKILL.md +361 -361
  135. package/skills/contract-first/SKILL.md +287 -287
  136. package/skills/cost-aware-llm-pipeline/SKILL.md +184 -184
  137. package/skills/cost-tracking/SKILL.md +97 -97
  138. package/skills/council/SKILL.md +204 -204
  139. package/skills/cpp-coding-standards/SKILL.md +724 -724
  140. package/skills/cpp-testing/SKILL.md +325 -325
  141. package/skills/crosspost/SKILL.md +112 -112
  142. package/skills/csharp-testing/SKILL.md +322 -322
  143. package/skills/customer-billing-ops/SKILL.md +141 -141
  144. package/skills/customs-trade-compliance/SKILL.md +263 -263
  145. package/skills/dart-flutter-patterns/SKILL.md +564 -564
  146. package/skills/dashboard-builder/SKILL.md +109 -109
  147. package/skills/data-scraper-agent/SKILL.md +765 -765
  148. package/skills/data-throughput-accelerator/SKILL.md +74 -74
  149. package/skills/database-migrations/SKILL.md +430 -430
  150. package/skills/deep-research/SKILL.md +160 -160
  151. package/skills/defi-amm-security/SKILL.md +167 -167
  152. package/skills/delivery-gate/SKILL.md +126 -126
  153. package/skills/deployment-patterns/SKILL.md +428 -428
  154. package/skills/design-system/SKILL.md +83 -83
  155. package/skills/django-celery/SKILL.md +458 -458
  156. package/skills/django-patterns/SKILL.md +735 -735
  157. package/skills/django-security/SKILL.md +644 -644
  158. package/skills/django-tdd/SKILL.md +730 -730
  159. package/skills/django-verification/SKILL.md +470 -470
  160. package/skills/dmux-workflows/SKILL.md +192 -192
  161. package/skills/docker-patterns/SKILL.md +365 -365
  162. package/skills/documentation-lookup/SKILL.md +91 -91
  163. package/skills/dotnet-patterns/SKILL.md +322 -322
  164. package/skills/dynamic-workflow-mode/SKILL.md +124 -124
  165. package/skills/e2e-testing/SKILL.md +327 -327
  166. package/skills/ecc-guide/SKILL.md +190 -190
  167. package/skills/ecc-recipes/SKILL.md +149 -149
  168. package/skills/ecc-tools-cost-audit/SKILL.md +161 -161
  169. package/skills/email-ops/SKILL.md +122 -122
  170. package/skills/energy-procurement/SKILL.md +228 -228
  171. package/skills/enterprise-agent-ops/SKILL.md +51 -51
  172. package/skills/error-handling/SKILL.md +377 -377
  173. package/skills/eval-harness/SKILL.md +271 -271
  174. package/skills/evm-token-decimals/SKILL.md +131 -131
  175. package/skills/exa-search/SKILL.md +108 -108
  176. package/skills/fal-ai-media/SKILL.md +289 -289
  177. package/skills/fastapi-patterns/SKILL.md +514 -514
  178. package/skills/finance-billing-ops/SKILL.md +128 -128
  179. package/skills/flox-environments/SKILL.md +497 -497
  180. package/skills/flutter-dart-code-review/SKILL.md +436 -436
  181. package/skills/foundation-models-on-device/SKILL.md +243 -243
  182. package/skills/frontend-a11y/SKILL.md +446 -446
  183. package/skills/frontend-design-direction/SKILL.md +93 -93
  184. package/skills/frontend-patterns/SKILL.md +657 -657
  185. package/skills/frontend-slides/SKILL.md +185 -185
  186. package/skills/fsharp-testing/SKILL.md +281 -281
  187. package/skills/gan-style-harness/SKILL.md +279 -279
  188. package/skills/gateguard/SKILL.md +133 -133
  189. package/skills/generating-python-installer/SKILL.md +820 -820
  190. package/skills/git-workflow/SKILL.md +716 -716
  191. package/skills/github-ops/SKILL.md +145 -145
  192. package/skills/golang-patterns/SKILL.md +676 -676
  193. package/skills/golang-testing/SKILL.md +721 -721
  194. package/skills/google-workspace-ops/SKILL.md +96 -96
  195. package/skills/growth-log/SKILL.md +128 -128
  196. package/skills/healthcare-cdss-patterns/SKILL.md +246 -246
  197. package/skills/healthcare-emr-patterns/SKILL.md +160 -160
  198. package/skills/healthcare-eval-harness/SKILL.md +208 -208
  199. package/skills/healthcare-phi-compliance/SKILL.md +146 -146
  200. package/skills/hermes-imports/SKILL.md +89 -89
  201. package/skills/hexagonal-architecture/SKILL.md +277 -277
  202. package/skills/hipaa-compliance/SKILL.md +79 -79
  203. package/skills/homelab-network-readiness/SKILL.md +170 -170
  204. package/skills/homelab-network-setup/SKILL.md +130 -130
  205. package/skills/homelab-pihole-dns/SKILL.md +275 -275
  206. package/skills/homelab-vlan-segmentation/SKILL.md +312 -312
  207. package/skills/homelab-wireguard-vpn/SKILL.md +306 -306
  208. package/skills/hookify-rules/SKILL.md +128 -128
  209. package/skills/inherit-legacy-style/SKILL.md +157 -157
  210. package/skills/intent-driven-development/SKILL.md +360 -360
  211. package/skills/inventory-demand-planning/SKILL.md +247 -247
  212. package/skills/investor-materials/SKILL.md +97 -97
  213. package/skills/investor-outreach/SKILL.md +92 -92
  214. package/skills/ios-icon-gen/SKILL.md +158 -158
  215. package/skills/iterative-retrieval/SKILL.md +212 -212
  216. package/skills/ito-basket-compare/SKILL.md +64 -64
  217. package/skills/ito-compute/SKILL.md +129 -129
  218. package/skills/ito-data-atlas-agent/SKILL.md +64 -64
  219. package/skills/ito-market-intelligence/SKILL.md +61 -61
  220. package/skills/ito-trade-planner/SKILL.md +68 -68
  221. package/skills/java-coding-standards/SKILL.md +384 -384
  222. package/skills/jira-integration/SKILL.md +303 -303
  223. package/skills/jpa-patterns/SKILL.md +152 -152
  224. package/skills/knowledge-ops/SKILL.md +155 -155
  225. package/skills/kotlin-coroutines-flows/SKILL.md +285 -285
  226. package/skills/kotlin-exposed-patterns/SKILL.md +720 -720
  227. package/skills/kotlin-ktor-patterns/SKILL.md +690 -690
  228. package/skills/kotlin-patterns/SKILL.md +712 -712
  229. package/skills/kotlin-testing/SKILL.md +825 -825
  230. package/skills/kubernetes-patterns/SKILL.md +756 -756
  231. package/skills/laravel-patterns/SKILL.md +416 -416
  232. package/skills/laravel-plugin-discovery/SKILL.md +230 -230
  233. package/skills/laravel-security/SKILL.md +948 -948
  234. package/skills/laravel-tdd/SKILL.md +675 -675
  235. package/skills/laravel-verification/SKILL.md +180 -180
  236. package/skills/latency-critical-systems/SKILL.md +75 -75
  237. package/skills/lead-intelligence/SKILL.md +322 -322
  238. package/skills/liquid-glass-design/SKILL.md +279 -279
  239. package/skills/llm-trading-agent-security/SKILL.md +147 -147
  240. package/skills/logistics-exception-management/SKILL.md +222 -222
  241. package/skills/loop-design-check/SKILL.md +143 -143
  242. package/skills/mailtrap-email-integration/SKILL.md +77 -77
  243. package/skills/make-interfaces-feel-better/SKILL.md +152 -152
  244. package/skills/manim-video/SKILL.md +90 -90
  245. package/skills/market-research/SKILL.md +76 -76
  246. package/skills/marketing-campaign/SKILL.md +114 -114
  247. package/skills/mcp-server-patterns/SKILL.md +70 -70
  248. package/skills/messages-ops/SKILL.md +105 -105
  249. package/skills/ml-adoption-playbook/SKILL.md +57 -57
  250. package/skills/mle-workflow/SKILL.md +348 -348
  251. package/skills/motion-advanced/SKILL.md +596 -596
  252. package/skills/motion-foundations/SKILL.md +299 -299
  253. package/skills/motion-patterns/SKILL.md +434 -434
  254. package/skills/motion-ui/SKILL.md +576 -576
  255. package/skills/mysql-patterns/SKILL.md +413 -413
  256. package/skills/nanoclaw-repl/SKILL.md +34 -34
  257. package/skills/nestjs-patterns/SKILL.md +231 -231
  258. package/skills/netmiko-ssh-automation/SKILL.md +174 -174
  259. package/skills/network-bgp-diagnostics/SKILL.md +168 -168
  260. package/skills/network-config-validation/SKILL.md +211 -211
  261. package/skills/network-interface-health/SKILL.md +153 -153
  262. package/skills/nextjs-turbopack/SKILL.md +58 -58
  263. package/skills/nodejs-keccak256/SKILL.md +103 -103
  264. package/skills/nutrient-document-processing/SKILL.md +168 -168
  265. package/skills/nuxt4-patterns/SKILL.md +101 -101
  266. package/skills/openclaw-persona-forge/SKILL.md +289 -289
  267. package/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
  268. package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
  269. package/skills/openclaw-persona-forge/references/error-handling.md +53 -53
  270. package/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
  271. package/skills/openclaw-persona-forge/references/naming-system.md +39 -39
  272. package/skills/openclaw-persona-forge/references/output-template.md +166 -166
  273. package/skills/opensource-pipeline/SKILL.md +256 -256
  274. package/skills/orch-add-feature/SKILL.md +45 -45
  275. package/skills/orch-build-mvp/SKILL.md +49 -49
  276. package/skills/orch-change-feature/SKILL.md +43 -43
  277. package/skills/orch-fix-defect/SKILL.md +43 -43
  278. package/skills/orch-pipeline/SKILL.md +121 -121
  279. package/skills/orch-refine-code/SKILL.md +44 -44
  280. package/skills/parallel-execution-optimizer/SKILL.md +74 -74
  281. package/skills/perl-patterns/SKILL.md +505 -505
  282. package/skills/perl-security/SKILL.md +504 -504
  283. package/skills/perl-testing/SKILL.md +476 -476
  284. package/skills/plan-canvas/SKILL.md +153 -153
  285. package/skills/plan-orchestrate/SKILL.md +263 -263
  286. package/skills/plankton-code-quality/SKILL.md +237 -237
  287. package/skills/postgres-patterns/SKILL.md +148 -148
  288. package/skills/prediction-market-oracle-research/SKILL.md +64 -64
  289. package/skills/prediction-market-risk-review/SKILL.md +61 -61
  290. package/skills/premium-web-design/SKILL.md +154 -154
  291. package/skills/prisma-patterns/SKILL.md +401 -401
  292. package/skills/product-capability/SKILL.md +142 -142
  293. package/skills/product-lens/SKILL.md +93 -93
  294. package/skills/production-audit/SKILL.md +207 -207
  295. package/skills/production-scheduling/SKILL.md +238 -238
  296. package/skills/project-flow-ops/SKILL.md +112 -112
  297. package/skills/prompt-optimizer/SKILL.md +398 -398
  298. package/skills/python-patterns/SKILL.md +751 -751
  299. package/skills/python-testing/SKILL.md +817 -817
  300. package/skills/pytorch-patterns/SKILL.md +397 -397
  301. package/skills/quality-nonconformance/SKILL.md +260 -260
  302. package/skills/quarkus-patterns/SKILL.md +723 -723
  303. package/skills/quarkus-security/SKILL.md +468 -468
  304. package/skills/quarkus-tdd/SKILL.md +812 -812
  305. package/skills/quarkus-verification/SKILL.md +480 -480
  306. package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -68
  307. package/skills/react-native-patterns/SKILL.md +326 -326
  308. package/skills/react-patterns/SKILL.md +342 -342
  309. package/skills/react-performance/SKILL.md +575 -575
  310. package/skills/react-testing/SKILL.md +424 -424
  311. package/skills/recsys-pipeline-architect/SKILL.md +115 -115
  312. package/skills/recursive-decision-ledger/SKILL.md +81 -81
  313. package/skills/redis-patterns/SKILL.md +404 -404
  314. package/skills/regex-vs-llm-structured-text/SKILL.md +221 -221
  315. package/skills/remotion-video-creation/SKILL.md +43 -43
  316. package/skills/repo-scan/SKILL.md +79 -79
  317. package/skills/research-ops/SKILL.md +113 -113
  318. package/skills/returns-reverse-logistics/SKILL.md +240 -240
  319. package/skills/rules-distill/SKILL.md +265 -265
  320. package/skills/rust-patterns/SKILL.md +500 -500
  321. package/skills/rust-testing/SKILL.md +501 -501
  322. package/skills/safety-guard/SKILL.md +76 -76
  323. package/skills/santa-method/SKILL.md +307 -307
  324. package/skills/scientific-db-pubmed-database/SKILL.md +176 -176
  325. package/skills/scientific-db-uspto-database/SKILL.md +178 -178
  326. package/skills/scientific-pkg-gget/SKILL.md +167 -167
  327. package/skills/scientific-thinking-literature-review/SKILL.md +193 -193
  328. package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -161
  329. package/skills/search-first/SKILL.md +183 -183
  330. package/skills/security-bounty-hunter/SKILL.md +100 -100
  331. package/skills/security-review/SKILL.md +504 -504
  332. package/skills/security-scan/SKILL.md +166 -166
  333. package/skills/seo/SKILL.md +155 -155
  334. package/skills/skill-comply/SKILL.md +59 -59
  335. package/skills/skill-scout/SKILL.md +141 -141
  336. package/skills/skill-stocktake/SKILL.md +195 -195
  337. package/skills/social-graph-ranker/SKILL.md +155 -155
  338. package/skills/social-publisher/SKILL.md +130 -130
  339. package/skills/springboot-patterns/SKILL.md +315 -315
  340. package/skills/springboot-security/SKILL.md +273 -273
  341. package/skills/springboot-tdd/SKILL.md +159 -159
  342. package/skills/springboot-verification/SKILL.md +232 -232
  343. package/skills/strategic-compact/SKILL.md +142 -142
  344. package/skills/swift-actor-persistence/SKILL.md +144 -144
  345. package/skills/swift-concurrency-6-2/SKILL.md +216 -216
  346. package/skills/swift-protocol-di-testing/SKILL.md +191 -191
  347. package/skills/swiftui-patterns/SKILL.md +259 -259
  348. package/skills/taste/SKILL.md +264 -264
  349. package/skills/taste/references/genre-taxonomy.md +87 -87
  350. package/skills/tdd-workflow/SKILL.md +583 -583
  351. package/skills/team-agent-orchestration/SKILL.md +111 -111
  352. package/skills/team-builder/SKILL.md +169 -169
  353. package/skills/terminal-ops/SKILL.md +110 -110
  354. package/skills/tinystruct-patterns/SKILL.md +279 -279
  355. package/skills/tinystruct-patterns/references/architecture.md +90 -90
  356. package/skills/tinystruct-patterns/references/data-handling.md +60 -60
  357. package/skills/tinystruct-patterns/references/database.md +99 -99
  358. package/skills/tinystruct-patterns/references/routing.md +64 -64
  359. package/skills/tinystruct-patterns/references/system-usage.md +97 -97
  360. package/skills/tinystruct-patterns/references/testing.md +72 -72
  361. package/skills/token-budget-advisor/SKILL.md +134 -134
  362. package/skills/ui-demo/SKILL.md +466 -466
  363. package/skills/ui-to-vue/SKILL.md +135 -135
  364. package/skills/uncloud/SKILL.md +344 -344
  365. package/skills/unified-memory/SKILL.md +170 -170
  366. package/skills/unified-notifications-ops/SKILL.md +188 -188
  367. package/skills/verification-loop/SKILL.md +129 -129
  368. package/skills/video-editing/SKILL.md +311 -311
  369. package/skills/videodb/SKILL.md +375 -375
  370. package/skills/visa-doc-translate/SKILL.md +117 -117
  371. package/skills/vite-patterns/SKILL.md +450 -450
  372. package/skills/vue-patterns/SKILL.md +471 -471
  373. package/skills/windows-desktop-e2e/SKILL.md +888 -888
  374. package/skills/workspace-surface-audit/SKILL.md +126 -126
  375. 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
+ }