codeply-cli 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +65 -63
- package/bin/codeply.js +1 -1
- package/lib/agent.mjs +133 -24
- package/lib/ai.js +268 -9
- package/lib/ai.mjs +6 -6
- package/lib/apply-patch.js +217 -217
- package/lib/arity-table.json +138 -138
- package/lib/arity.js +70 -70
- package/lib/auth.mjs +6 -6
- package/lib/bots.js +82 -17
- package/lib/chatgpt.js +282 -0
- package/lib/cloud-browser.mjs +143 -143
- package/lib/cloud.mjs +1186 -1144
- package/lib/commands.js +103 -103
- package/lib/config.js +83 -3
- package/lib/design-library/assemble-artifact.mjs +21 -21
- package/lib/design-library/cleanup.mjs +45 -45
- package/lib/design-library/index.json +22379 -22379
- package/lib/design-library/make-table-data.mjs +39 -39
- package/lib/diagnostics.js +192 -192
- package/lib/edit-engine.js +364 -364
- package/lib/github-agent.mjs +311 -311
- package/lib/history.js +75 -75
- package/lib/mail-watch.js +504 -0
- package/lib/mcp.js +258 -258
- package/lib/model-sync.js +114 -0
- package/lib/native-tools.mjs +16 -1
- package/lib/oauth-connectors.js +146 -24
- package/lib/permissions.js +109 -109
- package/lib/plugins.js +353 -353
- package/lib/publish.js +916 -0
- package/lib/reach.js +67 -0
- package/lib/server.mjs +457 -457
- package/lib/session-db.js +220 -220
- package/lib/share.js +168 -168
- package/lib/skills.js +2 -0
- package/lib/snapshot.js +146 -146
- package/lib/telemetry.js +102 -0
- package/lib/terminal-hints.js +154 -154
- package/lib/tools.mjs +316 -14
- package/lib/typecheck-worker.js +147 -147
- package/lib/web-tools.js +209 -158
- package/package.json +49 -49
- package/skills/SOURCE.md +58 -58
- package/skills/accessibility/SKILL.md +147 -147
- package/skills/agent-architecture-audit/SKILL.md +257 -257
- package/skills/agent-eval/SKILL.md +147 -147
- package/skills/agent-harness-construction/SKILL.md +74 -74
- package/skills/agent-introspection-debugging/SKILL.md +154 -154
- package/skills/agent-payment-x402/SKILL.md +225 -225
- package/skills/agent-self-evaluation/SKILL.md +182 -182
- package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -71
- package/skills/agent-self-evaluation/references/hook-integration.md +64 -64
- package/skills/agent-sort/SKILL.md +216 -216
- package/skills/agentic-engineering/SKILL.md +64 -64
- package/skills/agentic-os/SKILL.md +388 -388
- package/skills/ai-first-engineering/SKILL.md +52 -52
- package/skills/ai-regression-testing/SKILL.md +386 -386
- package/skills/android-clean-architecture/SKILL.md +340 -340
- package/skills/angular-developer/SKILL.md +155 -155
- package/skills/angular-developer/references/angular-animations.md +160 -160
- package/skills/angular-developer/references/angular-aria.md +410 -410
- package/skills/angular-developer/references/cli.md +86 -86
- package/skills/angular-developer/references/component-harnesses.md +59 -59
- package/skills/angular-developer/references/component-styling.md +91 -91
- package/skills/angular-developer/references/components.md +117 -117
- package/skills/angular-developer/references/creating-services.md +97 -97
- package/skills/angular-developer/references/data-resolvers.md +69 -69
- package/skills/angular-developer/references/define-routes.md +67 -67
- package/skills/angular-developer/references/defining-providers.md +72 -72
- package/skills/angular-developer/references/di-fundamentals.md +120 -120
- package/skills/angular-developer/references/e2e-testing.md +56 -56
- package/skills/angular-developer/references/effects.md +83 -83
- package/skills/angular-developer/references/hierarchical-injectors.md +43 -43
- package/skills/angular-developer/references/host-elements.md +80 -80
- package/skills/angular-developer/references/injection-context.md +63 -63
- package/skills/angular-developer/references/inputs.md +101 -101
- package/skills/angular-developer/references/linked-signal.md +59 -59
- package/skills/angular-developer/references/loading-strategies.md +61 -61
- package/skills/angular-developer/references/mcp.md +108 -108
- package/skills/angular-developer/references/navigate-to-routes.md +69 -69
- package/skills/angular-developer/references/outputs.md +86 -86
- package/skills/angular-developer/references/reactive-forms.md +122 -122
- package/skills/angular-developer/references/rendering-strategies.md +44 -44
- package/skills/angular-developer/references/resource.md +77 -77
- package/skills/angular-developer/references/route-animations.md +56 -56
- package/skills/angular-developer/references/route-guards.md +52 -52
- package/skills/angular-developer/references/router-lifecycle.md +45 -45
- package/skills/angular-developer/references/router-testing.md +87 -87
- package/skills/angular-developer/references/show-routes-with-outlets.md +68 -68
- package/skills/angular-developer/references/signal-forms.md +795 -795
- package/skills/angular-developer/references/signals-overview.md +94 -94
- package/skills/angular-developer/references/tailwind-css.md +69 -69
- package/skills/angular-developer/references/template-driven-forms.md +114 -114
- package/skills/angular-developer/references/testing-fundamentals.md +65 -65
- package/skills/api-connector-builder/SKILL.md +121 -121
- package/skills/api-design/SKILL.md +524 -524
- package/skills/architecture-decision-records/SKILL.md +180 -180
- package/skills/article-writing/SKILL.md +80 -80
- package/skills/automation-audit-ops/SKILL.md +143 -143
- package/skills/autonomous-agent-harness/SKILL.md +274 -274
- package/skills/autonomous-loops/SKILL.md +611 -611
- package/skills/backend-patterns/SKILL.md +562 -562
- package/skills/benchmark/SKILL.md +95 -95
- package/skills/benchmark-methodology/SKILL.md +191 -191
- package/skills/benchmark-optimization-loop/SKILL.md +71 -71
- package/skills/blender-motion-state-inspection/SKILL.md +165 -165
- package/skills/blueprint/SKILL.md +106 -106
- package/skills/brand-discovery/SKILL.md +145 -145
- package/skills/brand-discovery/references/10_purpose-why.md +40 -40
- package/skills/brand-discovery/references/20_positioning.md +44 -44
- package/skills/brand-discovery/references/30_audience-niche.md +52 -52
- package/skills/brand-discovery/references/40_personality-archetype.md +57 -57
- package/skills/brand-discovery/references/50_voice-tone.md +59 -59
- package/skills/brand-discovery/references/60_narrative-story.md +50 -50
- package/skills/brand-discovery/references/70_founder-tension.md +49 -49
- package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -133
- package/skills/brand-voice/SKILL.md +98 -98
- package/skills/brand-voice/references/voice-profile-schema.md +55 -55
- package/skills/browser-qa/SKILL.md +105 -105
- package/skills/bun-runtime/SKILL.md +85 -85
- package/skills/canary-watch/SKILL.md +108 -108
- package/skills/carrier-relationship-management/SKILL.md +212 -212
- package/skills/cisco-ios-patterns/SKILL.md +164 -164
- package/skills/ck/SKILL.md +148 -148
- package/skills/claude-devfleet/SKILL.md +112 -112
- package/skills/click-path-audit/SKILL.md +245 -245
- package/skills/clickhouse-io/SKILL.md +445 -445
- package/skills/code-tour/SKILL.md +254 -254
- package/skills/codebase-onboarding/SKILL.md +234 -234
- package/skills/codehealth-mcp/SKILL.md +167 -167
- package/skills/coding-standards/SKILL.md +551 -551
- package/skills/competitive-platform-analysis/SKILL.md +214 -214
- package/skills/competitive-report-structure/SKILL.md +162 -162
- package/skills/compose-multiplatform-patterns/SKILL.md +300 -300
- package/skills/config-gc/SKILL.md +120 -120
- package/skills/configure-ecc/SKILL.md +385 -385
- package/skills/connections-optimizer/SKILL.md +190 -190
- package/skills/content-engine/SKILL.md +132 -132
- package/skills/content-hash-cache-pattern/SKILL.md +162 -162
- package/skills/context-budget/SKILL.md +136 -136
- package/skills/continuous-agent-loop/SKILL.md +46 -46
- package/skills/continuous-learning/SKILL.md +132 -132
- package/skills/continuous-learning-v2/SKILL.md +361 -361
- package/skills/contract-first/SKILL.md +287 -287
- package/skills/cost-aware-llm-pipeline/SKILL.md +184 -184
- package/skills/cost-tracking/SKILL.md +97 -97
- package/skills/council/SKILL.md +204 -204
- package/skills/cpp-coding-standards/SKILL.md +724 -724
- package/skills/cpp-testing/SKILL.md +325 -325
- package/skills/crosspost/SKILL.md +112 -112
- package/skills/csharp-testing/SKILL.md +322 -322
- package/skills/customer-billing-ops/SKILL.md +141 -141
- package/skills/customs-trade-compliance/SKILL.md +263 -263
- package/skills/dart-flutter-patterns/SKILL.md +564 -564
- package/skills/dashboard-builder/SKILL.md +109 -109
- package/skills/data-scraper-agent/SKILL.md +765 -765
- package/skills/data-throughput-accelerator/SKILL.md +74 -74
- package/skills/database-migrations/SKILL.md +430 -430
- package/skills/deep-research/SKILL.md +160 -160
- package/skills/defi-amm-security/SKILL.md +167 -167
- package/skills/delivery-gate/SKILL.md +126 -126
- package/skills/deployment-patterns/SKILL.md +428 -428
- package/skills/design-system/SKILL.md +83 -83
- package/skills/django-celery/SKILL.md +458 -458
- package/skills/django-patterns/SKILL.md +735 -735
- package/skills/django-security/SKILL.md +644 -644
- package/skills/django-tdd/SKILL.md +730 -730
- package/skills/django-verification/SKILL.md +470 -470
- package/skills/dmux-workflows/SKILL.md +192 -192
- package/skills/docker-patterns/SKILL.md +365 -365
- package/skills/documentation-lookup/SKILL.md +91 -91
- package/skills/dotnet-patterns/SKILL.md +322 -322
- package/skills/dynamic-workflow-mode/SKILL.md +124 -124
- package/skills/e2e-testing/SKILL.md +327 -327
- package/skills/ecc-guide/SKILL.md +190 -190
- package/skills/ecc-recipes/SKILL.md +149 -149
- package/skills/ecc-tools-cost-audit/SKILL.md +161 -161
- package/skills/email-ops/SKILL.md +122 -122
- package/skills/energy-procurement/SKILL.md +228 -228
- package/skills/enterprise-agent-ops/SKILL.md +51 -51
- package/skills/error-handling/SKILL.md +377 -377
- package/skills/eval-harness/SKILL.md +271 -271
- package/skills/evm-token-decimals/SKILL.md +131 -131
- package/skills/exa-search/SKILL.md +108 -108
- package/skills/fal-ai-media/SKILL.md +289 -289
- package/skills/fastapi-patterns/SKILL.md +514 -514
- package/skills/finance-billing-ops/SKILL.md +128 -128
- package/skills/flox-environments/SKILL.md +497 -497
- package/skills/flutter-dart-code-review/SKILL.md +436 -436
- package/skills/foundation-models-on-device/SKILL.md +243 -243
- package/skills/frontend-a11y/SKILL.md +446 -446
- package/skills/frontend-design-direction/SKILL.md +93 -93
- package/skills/frontend-patterns/SKILL.md +657 -657
- package/skills/frontend-slides/SKILL.md +185 -185
- package/skills/fsharp-testing/SKILL.md +281 -281
- package/skills/gan-style-harness/SKILL.md +279 -279
- package/skills/gateguard/SKILL.md +133 -133
- package/skills/generating-python-installer/SKILL.md +820 -820
- package/skills/git-workflow/SKILL.md +716 -716
- package/skills/github-ops/SKILL.md +145 -145
- package/skills/golang-patterns/SKILL.md +676 -676
- package/skills/golang-testing/SKILL.md +721 -721
- package/skills/google-workspace-ops/SKILL.md +96 -96
- package/skills/growth-log/SKILL.md +128 -128
- package/skills/healthcare-cdss-patterns/SKILL.md +246 -246
- package/skills/healthcare-emr-patterns/SKILL.md +160 -160
- package/skills/healthcare-eval-harness/SKILL.md +208 -208
- package/skills/healthcare-phi-compliance/SKILL.md +146 -146
- package/skills/hermes-imports/SKILL.md +89 -89
- package/skills/hexagonal-architecture/SKILL.md +277 -277
- package/skills/hipaa-compliance/SKILL.md +79 -79
- package/skills/homelab-network-readiness/SKILL.md +170 -170
- package/skills/homelab-network-setup/SKILL.md +130 -130
- package/skills/homelab-pihole-dns/SKILL.md +275 -275
- package/skills/homelab-vlan-segmentation/SKILL.md +312 -312
- package/skills/homelab-wireguard-vpn/SKILL.md +306 -306
- package/skills/hookify-rules/SKILL.md +128 -128
- package/skills/inherit-legacy-style/SKILL.md +157 -157
- package/skills/intent-driven-development/SKILL.md +360 -360
- package/skills/inventory-demand-planning/SKILL.md +247 -247
- package/skills/investor-materials/SKILL.md +97 -97
- package/skills/investor-outreach/SKILL.md +92 -92
- package/skills/ios-icon-gen/SKILL.md +158 -158
- package/skills/iterative-retrieval/SKILL.md +212 -212
- package/skills/ito-basket-compare/SKILL.md +64 -64
- package/skills/ito-compute/SKILL.md +129 -129
- package/skills/ito-data-atlas-agent/SKILL.md +64 -64
- package/skills/ito-market-intelligence/SKILL.md +61 -61
- package/skills/ito-trade-planner/SKILL.md +68 -68
- package/skills/java-coding-standards/SKILL.md +384 -384
- package/skills/jira-integration/SKILL.md +303 -303
- package/skills/jpa-patterns/SKILL.md +152 -152
- package/skills/knowledge-ops/SKILL.md +155 -155
- package/skills/kotlin-coroutines-flows/SKILL.md +285 -285
- package/skills/kotlin-exposed-patterns/SKILL.md +720 -720
- package/skills/kotlin-ktor-patterns/SKILL.md +690 -690
- package/skills/kotlin-patterns/SKILL.md +712 -712
- package/skills/kotlin-testing/SKILL.md +825 -825
- package/skills/kubernetes-patterns/SKILL.md +756 -756
- package/skills/laravel-patterns/SKILL.md +416 -416
- package/skills/laravel-plugin-discovery/SKILL.md +230 -230
- package/skills/laravel-security/SKILL.md +948 -948
- package/skills/laravel-tdd/SKILL.md +675 -675
- package/skills/laravel-verification/SKILL.md +180 -180
- package/skills/latency-critical-systems/SKILL.md +75 -75
- package/skills/lead-intelligence/SKILL.md +322 -322
- package/skills/liquid-glass-design/SKILL.md +279 -279
- package/skills/llm-trading-agent-security/SKILL.md +147 -147
- package/skills/logistics-exception-management/SKILL.md +222 -222
- package/skills/loop-design-check/SKILL.md +143 -143
- package/skills/mailtrap-email-integration/SKILL.md +77 -77
- package/skills/make-interfaces-feel-better/SKILL.md +152 -152
- package/skills/manim-video/SKILL.md +90 -90
- package/skills/market-research/SKILL.md +76 -76
- package/skills/marketing-campaign/SKILL.md +114 -114
- package/skills/mcp-server-patterns/SKILL.md +70 -70
- package/skills/messages-ops/SKILL.md +105 -105
- package/skills/ml-adoption-playbook/SKILL.md +57 -57
- package/skills/mle-workflow/SKILL.md +348 -348
- package/skills/motion-advanced/SKILL.md +596 -596
- package/skills/motion-foundations/SKILL.md +299 -299
- package/skills/motion-patterns/SKILL.md +434 -434
- package/skills/motion-ui/SKILL.md +576 -576
- package/skills/mysql-patterns/SKILL.md +413 -413
- package/skills/nanoclaw-repl/SKILL.md +34 -34
- package/skills/nestjs-patterns/SKILL.md +231 -231
- package/skills/netmiko-ssh-automation/SKILL.md +174 -174
- package/skills/network-bgp-diagnostics/SKILL.md +168 -168
- package/skills/network-config-validation/SKILL.md +211 -211
- package/skills/network-interface-health/SKILL.md +153 -153
- package/skills/nextjs-turbopack/SKILL.md +58 -58
- package/skills/nodejs-keccak256/SKILL.md +103 -103
- package/skills/nutrient-document-processing/SKILL.md +168 -168
- package/skills/nuxt4-patterns/SKILL.md +101 -101
- package/skills/openclaw-persona-forge/SKILL.md +289 -289
- package/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
- package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
- package/skills/openclaw-persona-forge/references/error-handling.md +53 -53
- package/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
- package/skills/openclaw-persona-forge/references/naming-system.md +39 -39
- package/skills/openclaw-persona-forge/references/output-template.md +166 -166
- package/skills/opensource-pipeline/SKILL.md +256 -256
- package/skills/orch-add-feature/SKILL.md +45 -45
- package/skills/orch-build-mvp/SKILL.md +49 -49
- package/skills/orch-change-feature/SKILL.md +43 -43
- package/skills/orch-fix-defect/SKILL.md +43 -43
- package/skills/orch-pipeline/SKILL.md +121 -121
- package/skills/orch-refine-code/SKILL.md +44 -44
- package/skills/parallel-execution-optimizer/SKILL.md +74 -74
- package/skills/perl-patterns/SKILL.md +505 -505
- package/skills/perl-security/SKILL.md +504 -504
- package/skills/perl-testing/SKILL.md +476 -476
- package/skills/plan-canvas/SKILL.md +153 -153
- package/skills/plan-orchestrate/SKILL.md +263 -263
- package/skills/plankton-code-quality/SKILL.md +237 -237
- package/skills/postgres-patterns/SKILL.md +148 -148
- package/skills/prediction-market-oracle-research/SKILL.md +64 -64
- package/skills/prediction-market-risk-review/SKILL.md +61 -61
- package/skills/premium-web-design/SKILL.md +154 -154
- package/skills/prisma-patterns/SKILL.md +401 -401
- package/skills/product-capability/SKILL.md +142 -142
- package/skills/product-lens/SKILL.md +93 -93
- package/skills/production-audit/SKILL.md +207 -207
- package/skills/production-scheduling/SKILL.md +238 -238
- package/skills/project-flow-ops/SKILL.md +112 -112
- package/skills/prompt-optimizer/SKILL.md +398 -398
- package/skills/publish-website/SKILL.md +76 -0
- package/skills/python-patterns/SKILL.md +751 -751
- package/skills/python-testing/SKILL.md +817 -817
- package/skills/pytorch-patterns/SKILL.md +397 -397
- package/skills/quality-nonconformance/SKILL.md +260 -260
- package/skills/quarkus-patterns/SKILL.md +723 -723
- package/skills/quarkus-security/SKILL.md +468 -468
- package/skills/quarkus-tdd/SKILL.md +812 -812
- package/skills/quarkus-verification/SKILL.md +480 -480
- package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -68
- package/skills/react-native-patterns/SKILL.md +326 -326
- package/skills/react-patterns/SKILL.md +342 -342
- package/skills/react-performance/SKILL.md +575 -575
- package/skills/react-testing/SKILL.md +424 -424
- package/skills/recsys-pipeline-architect/SKILL.md +115 -115
- package/skills/recursive-decision-ledger/SKILL.md +81 -81
- package/skills/redis-patterns/SKILL.md +404 -404
- package/skills/regex-vs-llm-structured-text/SKILL.md +221 -221
- package/skills/remotion-video-creation/SKILL.md +43 -43
- package/skills/repo-scan/SKILL.md +79 -79
- package/skills/research-ops/SKILL.md +113 -113
- package/skills/returns-reverse-logistics/SKILL.md +240 -240
- package/skills/rules-distill/SKILL.md +265 -265
- package/skills/rust-patterns/SKILL.md +500 -500
- package/skills/rust-testing/SKILL.md +501 -501
- package/skills/safety-guard/SKILL.md +76 -76
- package/skills/santa-method/SKILL.md +307 -307
- package/skills/scientific-db-pubmed-database/SKILL.md +176 -176
- package/skills/scientific-db-uspto-database/SKILL.md +178 -178
- package/skills/scientific-pkg-gget/SKILL.md +167 -167
- package/skills/scientific-thinking-literature-review/SKILL.md +193 -193
- package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -161
- package/skills/search-first/SKILL.md +183 -183
- package/skills/security-bounty-hunter/SKILL.md +100 -100
- package/skills/security-review/SKILL.md +504 -504
- package/skills/security-scan/SKILL.md +166 -166
- package/skills/seo/SKILL.md +155 -155
- package/skills/skill-comply/SKILL.md +59 -59
- package/skills/skill-scout/SKILL.md +141 -141
- package/skills/skill-stocktake/SKILL.md +195 -195
- package/skills/social-graph-ranker/SKILL.md +155 -155
- package/skills/social-publisher/SKILL.md +130 -130
- package/skills/springboot-patterns/SKILL.md +315 -315
- package/skills/springboot-security/SKILL.md +273 -273
- package/skills/springboot-tdd/SKILL.md +159 -159
- package/skills/springboot-verification/SKILL.md +232 -232
- package/skills/strategic-compact/SKILL.md +142 -142
- package/skills/swift-actor-persistence/SKILL.md +144 -144
- package/skills/swift-concurrency-6-2/SKILL.md +216 -216
- package/skills/swift-protocol-di-testing/SKILL.md +191 -191
- package/skills/swiftui-patterns/SKILL.md +259 -259
- package/skills/taste/SKILL.md +264 -264
- package/skills/taste/references/genre-taxonomy.md +87 -87
- package/skills/tdd-workflow/SKILL.md +583 -583
- package/skills/team-agent-orchestration/SKILL.md +111 -111
- package/skills/team-builder/SKILL.md +169 -169
- package/skills/terminal-ops/SKILL.md +110 -110
- package/skills/tinystruct-patterns/SKILL.md +279 -279
- package/skills/tinystruct-patterns/references/architecture.md +90 -90
- package/skills/tinystruct-patterns/references/data-handling.md +60 -60
- package/skills/tinystruct-patterns/references/database.md +99 -99
- package/skills/tinystruct-patterns/references/routing.md +64 -64
- package/skills/tinystruct-patterns/references/system-usage.md +97 -97
- package/skills/tinystruct-patterns/references/testing.md +72 -72
- package/skills/token-budget-advisor/SKILL.md +134 -134
- package/skills/ui-demo/SKILL.md +466 -466
- package/skills/ui-to-vue/SKILL.md +135 -135
- package/skills/uncloud/SKILL.md +344 -344
- package/skills/unified-memory/SKILL.md +170 -170
- package/skills/unified-notifications-ops/SKILL.md +188 -188
- package/skills/verification-loop/SKILL.md +129 -129
- package/skills/video-editing/SKILL.md +311 -311
- package/skills/videodb/SKILL.md +375 -375
- package/skills/visa-doc-translate/SKILL.md +117 -117
- package/skills/vite-patterns/SKILL.md +450 -450
- package/skills/vue-patterns/SKILL.md +471 -471
- package/skills/windows-desktop-e2e/SKILL.md +888 -888
- package/skills/workspace-surface-audit/SKILL.md +126 -126
- package/skills/x-api/SKILL.md +235 -235
package/lib/edit-engine.js
CHANGED
|
@@ -1,364 +1,364 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codeply CLI - surgical SEARCH/REPLACE edit engine
|
|
3
|
-
*
|
|
4
|
-
* Adapted from Codeply-App/main.js's computeInstructionEdits/
|
|
5
|
-
* applySearchReplace/applyEditsToContent - same approach (small verified
|
|
6
|
-
* diffs instead of a full-file rewrite), trimmed for a stateless single-shot
|
|
7
|
-
* CLI: no local usage-history file, no .codeply/ response cache, no
|
|
8
|
-
* multi-turn conversation context (each CLI invocation is its own process).
|
|
9
|
-
*/
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const ai = require('./ai');
|
|
13
|
-
|
|
14
|
-
// Apply ONE search/replace edit to a string. Exact match first, then
|
|
15
|
-
// whitespace-tolerant (ignore leading/trailing space, then collapse internal
|
|
16
|
-
// runs), skipping blank lines on both sides. Re-indents the replacement to fit.
|
|
17
|
-
// Returns { ok, content } or { ok:false, error:'notfound'|'multiple'|'empty' }.
|
|
18
|
-
function applySearchReplace(fileContent, searchBlock, replaceBlock) {
|
|
19
|
-
// Matching works on LF text; a CRLF file is written back as CRLF so an edit
|
|
20
|
-
// never rewrites every line ending in the file (a whole-file diff, and a
|
|
21
|
-
// mixed-ending file once a later write adds LF lines).
|
|
22
|
-
const res = applySearchReplaceLF(fileContent, searchBlock, replaceBlock);
|
|
23
|
-
if (res.ok && /\r\n/.test(fileContent || '')) res.content = res.content.replace(/\r?\n/g, '\r\n');
|
|
24
|
-
return res;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function applySearchReplaceLF(fileContent, searchBlock, replaceBlock) {
|
|
28
|
-
const nl = (s) => (s || '').replace(/\r\n/g, '\n');
|
|
29
|
-
fileContent = nl(fileContent); searchBlock = nl(searchBlock); replaceBlock = nl(replaceBlock);
|
|
30
|
-
if (!searchBlock.trim()) return { ok: false, error: 'empty' };
|
|
31
|
-
|
|
32
|
-
const fileLines = fileContent.split('\n');
|
|
33
|
-
const searchLines = searchBlock.split('\n');
|
|
34
|
-
|
|
35
|
-
const lineStartOffset = (idx) => { let o = 0; for (let k = 0; k < idx; k++) o += fileLines[k].length + 1; return o; };
|
|
36
|
-
|
|
37
|
-
const reindentBlock = (block, indent) => {
|
|
38
|
-
const ls = block.split('\n');
|
|
39
|
-
const firstNonEmpty = ls.find(l => l.trim() !== '') || '';
|
|
40
|
-
const base = (firstNonEmpty.match(/^[ \t]*/) || [''])[0];
|
|
41
|
-
return ls.map(l => {
|
|
42
|
-
if (l.trim() === '') return '';
|
|
43
|
-
const stripped = l.startsWith(base) ? l.slice(base.length) : l.replace(/^[ \t]*/, '');
|
|
44
|
-
return indent + stripped;
|
|
45
|
-
}).join('\n');
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const locate = (normalize) => {
|
|
49
|
-
const s = searchLines.map(normalize).filter(x => x !== '');
|
|
50
|
-
if (!s.length) return [];
|
|
51
|
-
const found = [];
|
|
52
|
-
for (let start = 0; start < fileLines.length; start++) {
|
|
53
|
-
if (normalize(fileLines[start]) !== s[0]) continue;
|
|
54
|
-
let fi = start, si = 0, end = start;
|
|
55
|
-
while (si < s.length && fi < fileLines.length) {
|
|
56
|
-
const nf = normalize(fileLines[fi]);
|
|
57
|
-
if (nf === '') { fi++; continue; } // skip blank lines in the file
|
|
58
|
-
if (nf !== s[si]) break;
|
|
59
|
-
end = fi; fi++; si++;
|
|
60
|
-
}
|
|
61
|
-
if (si === s.length) found.push([start, end]);
|
|
62
|
-
}
|
|
63
|
-
return found;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const exact = fileContent.indexOf(searchBlock);
|
|
67
|
-
if (exact !== -1) {
|
|
68
|
-
if (exact !== fileContent.lastIndexOf(searchBlock)) return { ok: false, error: 'multiple' };
|
|
69
|
-
return { ok: true, content: fileContent.slice(0, exact) + replaceBlock + fileContent.slice(exact + searchBlock.length) };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let matches = locate(l => l.trim());
|
|
73
|
-
if (matches.length === 0) matches = locate(l => l.trim().replace(/\s+/g, ' '));
|
|
74
|
-
|
|
75
|
-
if (matches.length === 0) {
|
|
76
|
-
// Last resort: anchor on the first AND last non-blank lines of the search
|
|
77
|
-
// block (recovers when the AI dropped/added a line in the middle). Kept
|
|
78
|
-
// conservative - both anchors must be unique and the span must be modest.
|
|
79
|
-
const sNon = searchLines.map(l => l.trim()).filter(x => x !== '');
|
|
80
|
-
if (sNon.length >= 2) {
|
|
81
|
-
const firstHits = [], lastHits = [];
|
|
82
|
-
fileLines.forEach((l, i) => {
|
|
83
|
-
const t = l.trim();
|
|
84
|
-
if (t === sNon[0]) firstHits.push(i);
|
|
85
|
-
if (t === sNon[sNon.length - 1]) lastHits.push(i);
|
|
86
|
-
});
|
|
87
|
-
if (firstHits.length === 1 && lastHits.length === 1 && lastHits[0] >= firstHits[0]) {
|
|
88
|
-
const sL = firstHits[0], eL = lastHits[0];
|
|
89
|
-
if (eL - sL + 1 <= sNon.length * 3 + 5) { // don't grab a huge region
|
|
90
|
-
const indent = (fileLines[sL].match(/^[ \t]*/) || [''])[0];
|
|
91
|
-
const reindented = reindentBlock(replaceBlock, indent);
|
|
92
|
-
return { ok: true, content: fileContent.slice(0, lineStartOffset(sL)) + reindented + fileContent.slice(lineStartOffset(eL) + fileLines[eL].length) };
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const fuzzy = fuzzyLocate(fileContent, searchBlock);
|
|
97
|
-
if (fuzzy.ok) {
|
|
98
|
-
const at = fileContent.indexOf(fuzzy.span);
|
|
99
|
-
// The model's replacement carries the indentation it wrongly assumed;
|
|
100
|
-
// move it to where the block really sits.
|
|
101
|
-
const text = fuzzy.reindent
|
|
102
|
-
? reindentBlock(replaceBlock, (fuzzy.span.split('\n').find((l) => l.trim()) || '').match(/^[ \t]*/)[0])
|
|
103
|
-
: replaceBlock;
|
|
104
|
-
return { ok: true, content: fileContent.slice(0, at) + text + fileContent.slice(at + fuzzy.span.length), fuzzy: true };
|
|
105
|
-
}
|
|
106
|
-
return { ok: false, error: fuzzy.error === 'multiple' ? 'multiple' : 'notfound' };
|
|
107
|
-
}
|
|
108
|
-
if (matches.length > 1) return { ok: false, error: 'multiple' };
|
|
109
|
-
|
|
110
|
-
const [startLine, endLine] = matches[0];
|
|
111
|
-
const indent = (fileLines[startLine].match(/^[ \t]*/) || [''])[0];
|
|
112
|
-
const reindented = reindentBlock(replaceBlock, indent);
|
|
113
|
-
const startChar = lineStartOffset(startLine);
|
|
114
|
-
const endChar = lineStartOffset(endLine) + fileLines[endLine].length;
|
|
115
|
-
return { ok: true, content: fileContent.slice(0, startChar) + reindented + fileContent.slice(endChar) };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ─── Fallback matchers ──────────────────────────────────────────────────────
|
|
119
|
-
//
|
|
120
|
-
// Adapted from opencode (packages/opencode/src/tool/edit.ts), MIT License,
|
|
121
|
-
// Copyright (c) 2025 opencode - https://github.com/sst/opencode
|
|
122
|
-
//
|
|
123
|
-
// Each matcher yields candidate spans that exist VERBATIM in the file for a
|
|
124
|
-
// search block the model copied imperfectly (escaped quotes, shifted
|
|
125
|
-
// indentation, a middle line reworded). They only run after the matcher above
|
|
126
|
-
// has found nothing, and a candidate is used only when it occurs exactly once
|
|
127
|
-
// and is not far larger than what the model asked for.
|
|
128
|
-
|
|
129
|
-
function levenshtein(a, b) {
|
|
130
|
-
if (a === '' || b === '') return Math.max(a.length, b.length);
|
|
131
|
-
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
132
|
-
for (let i = 1; i <= a.length; i++) {
|
|
133
|
-
const cur = [i];
|
|
134
|
-
for (let j = 1; j <= b.length; j++) {
|
|
135
|
-
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
136
|
-
}
|
|
137
|
-
prev = cur;
|
|
138
|
-
}
|
|
139
|
-
return prev[b.length];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function spanOfLines(lines, start, end) {
|
|
143
|
-
return lines.slice(start, end + 1).join('\n');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const ANCHOR_SIMILARITY = 0.65;
|
|
147
|
-
|
|
148
|
-
// First and last lines match exactly (trimmed); the middle only has to be
|
|
149
|
-
// similar. Recovers a search block with one line misremembered.
|
|
150
|
-
function* blockAnchorMatches(content, find) {
|
|
151
|
-
const lines = content.split('\n');
|
|
152
|
-
const want = find.split('\n');
|
|
153
|
-
if (want[want.length - 1] === '') want.pop();
|
|
154
|
-
if (want.length < 3) return;
|
|
155
|
-
const first = want[0].trim(), last = want[want.length - 1].trim();
|
|
156
|
-
const maxDelta = Math.max(1, Math.floor(want.length * 0.25));
|
|
157
|
-
|
|
158
|
-
const candidates = [];
|
|
159
|
-
for (let i = 0; i < lines.length; i++) {
|
|
160
|
-
if (lines[i].trim() !== first) continue;
|
|
161
|
-
for (let j = i + 2; j < lines.length; j++) {
|
|
162
|
-
if (lines[j].trim() !== last) continue;
|
|
163
|
-
if (Math.abs(j - i + 1 - want.length) <= maxDelta) candidates.push([i, j]);
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let best = null, bestScore = -1;
|
|
169
|
-
for (const [s, e] of candidates) {
|
|
170
|
-
const size = e - s + 1;
|
|
171
|
-
const middle = Math.min(want.length - 2, size - 2);
|
|
172
|
-
let score = 1;
|
|
173
|
-
if (middle > 0) {
|
|
174
|
-
score = 0;
|
|
175
|
-
for (let k = 1; k < want.length - 1 && k < size - 1; k++) {
|
|
176
|
-
const a = lines[s + k].trim(), b = want[k].trim();
|
|
177
|
-
const len = Math.max(a.length, b.length);
|
|
178
|
-
score += len ? 1 - levenshtein(a, b) / len : 1;
|
|
179
|
-
}
|
|
180
|
-
score /= middle;
|
|
181
|
-
}
|
|
182
|
-
if (score > bestScore) { bestScore = score; best = [s, e]; }
|
|
183
|
-
}
|
|
184
|
-
if (best && bestScore >= ANCHOR_SIMILARITY) yield spanOfLines(lines, best[0], best[1]);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Same block with its indentation shifted as a whole.
|
|
188
|
-
function* indentationFlexibleMatches(content, find) {
|
|
189
|
-
const dedent = (text) => {
|
|
190
|
-
const ls = text.split('\n');
|
|
191
|
-
const indents = ls.filter((l) => l.trim()).map((l) => l.match(/^\s*/)[0].length);
|
|
192
|
-
if (!indents.length) return text;
|
|
193
|
-
const min = Math.min(...indents);
|
|
194
|
-
return ls.map((l) => (l.trim() ? l.slice(min) : l)).join('\n');
|
|
195
|
-
};
|
|
196
|
-
const target = dedent(find);
|
|
197
|
-
const lines = content.split('\n');
|
|
198
|
-
const n = find.split('\n').length;
|
|
199
|
-
for (let i = 0; i + n <= lines.length; i++) {
|
|
200
|
-
const block = lines.slice(i, i + n).join('\n');
|
|
201
|
-
if (dedent(block) === target) yield block;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// The model wrote \n, \" or \` escapes where the file has the real characters.
|
|
206
|
-
function* escapeNormalizedMatches(content, find) {
|
|
207
|
-
const unescape = (s) => s.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (m, c) =>
|
|
208
|
-
({ n: '\n', t: '\t', r: '\r', "'": "'", '"': '"', '`': '`', '\\': '\\', '\n': '\n', $: '$' }[c] ?? m));
|
|
209
|
-
const target = unescape(find);
|
|
210
|
-
if (target !== find && content.includes(target)) yield target;
|
|
211
|
-
const lines = content.split('\n');
|
|
212
|
-
const n = target.split('\n').length;
|
|
213
|
-
for (let i = 0; i + n <= lines.length; i++) {
|
|
214
|
-
const block = lines.slice(i, i + n).join('\n');
|
|
215
|
-
if (block !== target && unescape(block) === target) yield block;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Stray blank lines or spaces around the block.
|
|
220
|
-
function* trimmedBoundaryMatches(content, find) {
|
|
221
|
-
const t = find.trim();
|
|
222
|
-
if (t === find) return;
|
|
223
|
-
if (content.includes(t)) yield t;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const FALLBACK_MATCHERS = [indentationFlexibleMatches, escapeNormalizedMatches, trimmedBoundaryMatches, blockAnchorMatches];
|
|
227
|
-
|
|
228
|
-
// A "match" many times bigger than the search block means an anchor latched
|
|
229
|
-
// onto the wrong closing line; replacing it would silently delete code.
|
|
230
|
-
function isDisproportionate(span, find) {
|
|
231
|
-
const spanLines = span.split('\n').length, findLines = find.split('\n').length;
|
|
232
|
-
if (spanLines >= Math.max(findLines + 3, findLines * 2)) return true;
|
|
233
|
-
if (findLines === 1) return false;
|
|
234
|
-
return span.trim().length > Math.max(find.trim().length + 500, find.trim().length * 4);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Find the one verbatim span in `content` that an imperfect search block most
|
|
239
|
-
* likely meant. @returns {{ok:true, span:string} | {ok:false, error:'notfound'|'multiple'|'disproportionate'}}
|
|
240
|
-
*/
|
|
241
|
-
function fuzzyLocate(content, find) {
|
|
242
|
-
let sawMultiple = false;
|
|
243
|
-
for (const matcher of FALLBACK_MATCHERS) {
|
|
244
|
-
for (const span of matcher(content, find)) {
|
|
245
|
-
if (!span.trim()) continue;
|
|
246
|
-
const at = content.indexOf(span);
|
|
247
|
-
if (at === -1) continue;
|
|
248
|
-
if (at !== content.lastIndexOf(span)) { sawMultiple = true; continue; }
|
|
249
|
-
if (isDisproportionate(span, find)) return { ok: false, error: 'disproportionate' };
|
|
250
|
-
return { ok: true, span, reindent: matcher === indentationFlexibleMatches || matcher === blockAnchorMatches };
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return { ok: false, error: sawMultiple ? 'multiple' : 'notfound' };
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Apply an ordered list of verified {search, replace} hunks to file content,
|
|
257
|
-
// all-or-nothing (pure - no disk write).
|
|
258
|
-
function applyEditsToContent(fileContent, edits) {
|
|
259
|
-
let working = (fileContent || '').replace(/\r\n/g, '\n');
|
|
260
|
-
for (let i = 0; i < edits.length; i++) {
|
|
261
|
-
const { search, replace } = edits[i];
|
|
262
|
-
const res = applySearchReplace(working, search, replace);
|
|
263
|
-
if (!res.ok) return { ok: false, error: res.error, index: i };
|
|
264
|
-
working = res.content;
|
|
265
|
-
}
|
|
266
|
-
return { ok: true, content: working };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const SYSTEM_PROMPT = `You are a precise code editor. You receive an INSTRUCTION describing a small change, and the FULL current file. Make ONLY the specific change requested, as surgical SEARCH/REPLACE edits - do NOT reproduce the whole file.
|
|
270
|
-
|
|
271
|
-
STRICT RULES:
|
|
272
|
-
1. Return an "edits" array. Use the FEWEST edits that cleanly express the change.
|
|
273
|
-
2. "search" must be copied VERBATIM from the file below - exact existing text being changed, with just enough surrounding lines to be unique.
|
|
274
|
-
3. "replace" is that same block with ONLY the requested change applied - preserve everything else in it exactly (formatting, unrelated properties, comments, whitespace style).
|
|
275
|
-
4. If the SAME property/selector/value appears more than once in the file (e.g. "top" set in both a "from" and a "to" block), do NOT pick the first or most obvious match - use every clue in the instruction to find the ONE correct occurrence: the CURRENT value it mentions, nearby selectors, rule/keyframe names, or ordering (first/last, start/end). If more than one occurrence still fits equally well after that, return an EMPTY "edits" array with a "reason" naming the ambiguity instead of guessing.
|
|
276
|
-
5. If the instruction genuinely requires large new content that can't be expressed as a small edit (e.g. "build a whole new page/game/module from scratch"), return an EMPTY "edits" array - do not guess badly at a huge diff.
|
|
277
|
-
6. Return ONLY valid JSON, no markdown, no commentary.
|
|
278
|
-
|
|
279
|
-
Response format:
|
|
280
|
-
{
|
|
281
|
-
"edits": [ { "search": "<exact verbatim block from the file>", "replace": "<that block with the change applied>" } ],
|
|
282
|
-
"reason": "<one short sentence>",
|
|
283
|
-
"confidence": <0-100>
|
|
284
|
-
}`;
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Asks the AI for surgical edits, self-corrects up to twice against bad
|
|
288
|
-
* "search" matches, and returns only the edits that verify cleanly against
|
|
289
|
-
* the real file content.
|
|
290
|
-
* @returns {Promise<{success:boolean, edits?:Array, badCount?:number, reason?:string, confidence?:number, tokensUsed?:number, modelUsed?:string, error?:string}>}
|
|
291
|
-
*/
|
|
292
|
-
async function computeInstructionEdits(instruction, filePath) {
|
|
293
|
-
if (!fs.existsSync(filePath)) return { success: false, error: `File not found: ${filePath}` };
|
|
294
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
295
|
-
|
|
296
|
-
const probe = (search) => {
|
|
297
|
-
const res = applySearchReplace(content, search, search);
|
|
298
|
-
return res.ok ? 'ok' : res.error;
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
let totalTokens = 0;
|
|
302
|
-
let lastModelUsed = null;
|
|
303
|
-
|
|
304
|
-
const askModel = async (feedback) => {
|
|
305
|
-
const messages = [
|
|
306
|
-
{ role: 'system', content: SYSTEM_PROMPT },
|
|
307
|
-
// Large/stable content (the file) first, small/always-different content
|
|
308
|
-
// (the instruction) last - the ai-proxy's underlying model caches a
|
|
309
|
-
// repeated PREFIX, so this ordering matters for repeated calls against
|
|
310
|
-
// the same file even when the instruction text differs each time.
|
|
311
|
-
{ role: 'user', content: `FULL FILE (${path.basename(filePath)}):\n${content}\n\nINSTRUCTION:\n${instruction}` },
|
|
312
|
-
];
|
|
313
|
-
if (feedback) messages.push({ role: 'user', content: feedback });
|
|
314
|
-
|
|
315
|
-
const aiResult = await ai.chat(messages, { json: true, meta: { promptText: instruction, filePath } });
|
|
316
|
-
if (!aiResult.success) throw new Error(aiResult.error);
|
|
317
|
-
if (aiResult.modelUsed) lastModelUsed = aiResult.modelUsed;
|
|
318
|
-
const data = aiResult.data;
|
|
319
|
-
totalTokens += (data.usage || {}).total_tokens || 0;
|
|
320
|
-
|
|
321
|
-
let parsed;
|
|
322
|
-
try { parsed = JSON.parse(data.choices[0].message.content); }
|
|
323
|
-
catch { return { edits: [], confidence: 0, reason: '' }; }
|
|
324
|
-
|
|
325
|
-
const edits = Array.isArray(parsed.edits)
|
|
326
|
-
? parsed.edits.filter(e => e && e.search && e.replace != null).map(e => ({ search: e.search, replace: e.replace }))
|
|
327
|
-
: [];
|
|
328
|
-
return { edits, confidence: parsed.confidence == null ? 75 : parsed.confidence, reason: parsed.reason || '' };
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
let edits, confidence, reason;
|
|
332
|
-
try {
|
|
333
|
-
({ edits, confidence, reason } = await askModel());
|
|
334
|
-
} catch (e) {
|
|
335
|
-
return { success: false, error: e.message };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const verify = (list) => list.map((e) => ({ e, status: probe(e.search) }));
|
|
339
|
-
let checked = verify(edits);
|
|
340
|
-
let bad = checked.filter(c => c.status !== 'ok');
|
|
341
|
-
|
|
342
|
-
let pass = 0;
|
|
343
|
-
while (bad.length && pass < 2) {
|
|
344
|
-
pass++;
|
|
345
|
-
const feedback =
|
|
346
|
-
`Some "search" blocks from your last answer are wrong. Return the COMPLETE corrected edits array again, fixing these:\n` +
|
|
347
|
-
bad.map(c => c.status === 'multiple'
|
|
348
|
-
? `- Matched MULTIPLE places - include more surrounding lines so it is unique:\n${c.e.search}`
|
|
349
|
-
: `- NOT found in the file - copy it EXACTLY from the FULL FILE above, character for character:\n${c.e.search}`
|
|
350
|
-
).join('\n') +
|
|
351
|
-
`\nEvery "search" must be copied verbatim from the file shown above.`;
|
|
352
|
-
let retry;
|
|
353
|
-
try { retry = await askModel(feedback); }
|
|
354
|
-
catch (e) { break; }
|
|
355
|
-
if (retry.edits.length) { edits = retry.edits; confidence = retry.confidence; reason = retry.reason || reason; }
|
|
356
|
-
checked = verify(edits);
|
|
357
|
-
bad = checked.filter(c => c.status !== 'ok');
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const good = checked.filter(c => c.status === 'ok').map(c => c.e);
|
|
361
|
-
return { success: true, edits: good, badCount: bad.length, confidence, reason, tokensUsed: totalTokens, modelUsed: lastModelUsed };
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
module.exports = { applySearchReplace, applyEditsToContent, computeInstructionEdits, fuzzyLocate };
|
|
1
|
+
/**
|
|
2
|
+
* Codeply CLI - surgical SEARCH/REPLACE edit engine
|
|
3
|
+
*
|
|
4
|
+
* Adapted from Codeply-App/main.js's computeInstructionEdits/
|
|
5
|
+
* applySearchReplace/applyEditsToContent - same approach (small verified
|
|
6
|
+
* diffs instead of a full-file rewrite), trimmed for a stateless single-shot
|
|
7
|
+
* CLI: no local usage-history file, no .codeply/ response cache, no
|
|
8
|
+
* multi-turn conversation context (each CLI invocation is its own process).
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const ai = require('./ai');
|
|
13
|
+
|
|
14
|
+
// Apply ONE search/replace edit to a string. Exact match first, then
|
|
15
|
+
// whitespace-tolerant (ignore leading/trailing space, then collapse internal
|
|
16
|
+
// runs), skipping blank lines on both sides. Re-indents the replacement to fit.
|
|
17
|
+
// Returns { ok, content } or { ok:false, error:'notfound'|'multiple'|'empty' }.
|
|
18
|
+
function applySearchReplace(fileContent, searchBlock, replaceBlock) {
|
|
19
|
+
// Matching works on LF text; a CRLF file is written back as CRLF so an edit
|
|
20
|
+
// never rewrites every line ending in the file (a whole-file diff, and a
|
|
21
|
+
// mixed-ending file once a later write adds LF lines).
|
|
22
|
+
const res = applySearchReplaceLF(fileContent, searchBlock, replaceBlock);
|
|
23
|
+
if (res.ok && /\r\n/.test(fileContent || '')) res.content = res.content.replace(/\r?\n/g, '\r\n');
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function applySearchReplaceLF(fileContent, searchBlock, replaceBlock) {
|
|
28
|
+
const nl = (s) => (s || '').replace(/\r\n/g, '\n');
|
|
29
|
+
fileContent = nl(fileContent); searchBlock = nl(searchBlock); replaceBlock = nl(replaceBlock);
|
|
30
|
+
if (!searchBlock.trim()) return { ok: false, error: 'empty' };
|
|
31
|
+
|
|
32
|
+
const fileLines = fileContent.split('\n');
|
|
33
|
+
const searchLines = searchBlock.split('\n');
|
|
34
|
+
|
|
35
|
+
const lineStartOffset = (idx) => { let o = 0; for (let k = 0; k < idx; k++) o += fileLines[k].length + 1; return o; };
|
|
36
|
+
|
|
37
|
+
const reindentBlock = (block, indent) => {
|
|
38
|
+
const ls = block.split('\n');
|
|
39
|
+
const firstNonEmpty = ls.find(l => l.trim() !== '') || '';
|
|
40
|
+
const base = (firstNonEmpty.match(/^[ \t]*/) || [''])[0];
|
|
41
|
+
return ls.map(l => {
|
|
42
|
+
if (l.trim() === '') return '';
|
|
43
|
+
const stripped = l.startsWith(base) ? l.slice(base.length) : l.replace(/^[ \t]*/, '');
|
|
44
|
+
return indent + stripped;
|
|
45
|
+
}).join('\n');
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const locate = (normalize) => {
|
|
49
|
+
const s = searchLines.map(normalize).filter(x => x !== '');
|
|
50
|
+
if (!s.length) return [];
|
|
51
|
+
const found = [];
|
|
52
|
+
for (let start = 0; start < fileLines.length; start++) {
|
|
53
|
+
if (normalize(fileLines[start]) !== s[0]) continue;
|
|
54
|
+
let fi = start, si = 0, end = start;
|
|
55
|
+
while (si < s.length && fi < fileLines.length) {
|
|
56
|
+
const nf = normalize(fileLines[fi]);
|
|
57
|
+
if (nf === '') { fi++; continue; } // skip blank lines in the file
|
|
58
|
+
if (nf !== s[si]) break;
|
|
59
|
+
end = fi; fi++; si++;
|
|
60
|
+
}
|
|
61
|
+
if (si === s.length) found.push([start, end]);
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const exact = fileContent.indexOf(searchBlock);
|
|
67
|
+
if (exact !== -1) {
|
|
68
|
+
if (exact !== fileContent.lastIndexOf(searchBlock)) return { ok: false, error: 'multiple' };
|
|
69
|
+
return { ok: true, content: fileContent.slice(0, exact) + replaceBlock + fileContent.slice(exact + searchBlock.length) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let matches = locate(l => l.trim());
|
|
73
|
+
if (matches.length === 0) matches = locate(l => l.trim().replace(/\s+/g, ' '));
|
|
74
|
+
|
|
75
|
+
if (matches.length === 0) {
|
|
76
|
+
// Last resort: anchor on the first AND last non-blank lines of the search
|
|
77
|
+
// block (recovers when the AI dropped/added a line in the middle). Kept
|
|
78
|
+
// conservative - both anchors must be unique and the span must be modest.
|
|
79
|
+
const sNon = searchLines.map(l => l.trim()).filter(x => x !== '');
|
|
80
|
+
if (sNon.length >= 2) {
|
|
81
|
+
const firstHits = [], lastHits = [];
|
|
82
|
+
fileLines.forEach((l, i) => {
|
|
83
|
+
const t = l.trim();
|
|
84
|
+
if (t === sNon[0]) firstHits.push(i);
|
|
85
|
+
if (t === sNon[sNon.length - 1]) lastHits.push(i);
|
|
86
|
+
});
|
|
87
|
+
if (firstHits.length === 1 && lastHits.length === 1 && lastHits[0] >= firstHits[0]) {
|
|
88
|
+
const sL = firstHits[0], eL = lastHits[0];
|
|
89
|
+
if (eL - sL + 1 <= sNon.length * 3 + 5) { // don't grab a huge region
|
|
90
|
+
const indent = (fileLines[sL].match(/^[ \t]*/) || [''])[0];
|
|
91
|
+
const reindented = reindentBlock(replaceBlock, indent);
|
|
92
|
+
return { ok: true, content: fileContent.slice(0, lineStartOffset(sL)) + reindented + fileContent.slice(lineStartOffset(eL) + fileLines[eL].length) };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const fuzzy = fuzzyLocate(fileContent, searchBlock);
|
|
97
|
+
if (fuzzy.ok) {
|
|
98
|
+
const at = fileContent.indexOf(fuzzy.span);
|
|
99
|
+
// The model's replacement carries the indentation it wrongly assumed;
|
|
100
|
+
// move it to where the block really sits.
|
|
101
|
+
const text = fuzzy.reindent
|
|
102
|
+
? reindentBlock(replaceBlock, (fuzzy.span.split('\n').find((l) => l.trim()) || '').match(/^[ \t]*/)[0])
|
|
103
|
+
: replaceBlock;
|
|
104
|
+
return { ok: true, content: fileContent.slice(0, at) + text + fileContent.slice(at + fuzzy.span.length), fuzzy: true };
|
|
105
|
+
}
|
|
106
|
+
return { ok: false, error: fuzzy.error === 'multiple' ? 'multiple' : 'notfound' };
|
|
107
|
+
}
|
|
108
|
+
if (matches.length > 1) return { ok: false, error: 'multiple' };
|
|
109
|
+
|
|
110
|
+
const [startLine, endLine] = matches[0];
|
|
111
|
+
const indent = (fileLines[startLine].match(/^[ \t]*/) || [''])[0];
|
|
112
|
+
const reindented = reindentBlock(replaceBlock, indent);
|
|
113
|
+
const startChar = lineStartOffset(startLine);
|
|
114
|
+
const endChar = lineStartOffset(endLine) + fileLines[endLine].length;
|
|
115
|
+
return { ok: true, content: fileContent.slice(0, startChar) + reindented + fileContent.slice(endChar) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Fallback matchers ──────────────────────────────────────────────────────
|
|
119
|
+
//
|
|
120
|
+
// Adapted from opencode (packages/opencode/src/tool/edit.ts), MIT License,
|
|
121
|
+
// Copyright (c) 2025 opencode - https://github.com/sst/opencode
|
|
122
|
+
//
|
|
123
|
+
// Each matcher yields candidate spans that exist VERBATIM in the file for a
|
|
124
|
+
// search block the model copied imperfectly (escaped quotes, shifted
|
|
125
|
+
// indentation, a middle line reworded). They only run after the matcher above
|
|
126
|
+
// has found nothing, and a candidate is used only when it occurs exactly once
|
|
127
|
+
// and is not far larger than what the model asked for.
|
|
128
|
+
|
|
129
|
+
function levenshtein(a, b) {
|
|
130
|
+
if (a === '' || b === '') return Math.max(a.length, b.length);
|
|
131
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
132
|
+
for (let i = 1; i <= a.length; i++) {
|
|
133
|
+
const cur = [i];
|
|
134
|
+
for (let j = 1; j <= b.length; j++) {
|
|
135
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
136
|
+
}
|
|
137
|
+
prev = cur;
|
|
138
|
+
}
|
|
139
|
+
return prev[b.length];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function spanOfLines(lines, start, end) {
|
|
143
|
+
return lines.slice(start, end + 1).join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const ANCHOR_SIMILARITY = 0.65;
|
|
147
|
+
|
|
148
|
+
// First and last lines match exactly (trimmed); the middle only has to be
|
|
149
|
+
// similar. Recovers a search block with one line misremembered.
|
|
150
|
+
function* blockAnchorMatches(content, find) {
|
|
151
|
+
const lines = content.split('\n');
|
|
152
|
+
const want = find.split('\n');
|
|
153
|
+
if (want[want.length - 1] === '') want.pop();
|
|
154
|
+
if (want.length < 3) return;
|
|
155
|
+
const first = want[0].trim(), last = want[want.length - 1].trim();
|
|
156
|
+
const maxDelta = Math.max(1, Math.floor(want.length * 0.25));
|
|
157
|
+
|
|
158
|
+
const candidates = [];
|
|
159
|
+
for (let i = 0; i < lines.length; i++) {
|
|
160
|
+
if (lines[i].trim() !== first) continue;
|
|
161
|
+
for (let j = i + 2; j < lines.length; j++) {
|
|
162
|
+
if (lines[j].trim() !== last) continue;
|
|
163
|
+
if (Math.abs(j - i + 1 - want.length) <= maxDelta) candidates.push([i, j]);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let best = null, bestScore = -1;
|
|
169
|
+
for (const [s, e] of candidates) {
|
|
170
|
+
const size = e - s + 1;
|
|
171
|
+
const middle = Math.min(want.length - 2, size - 2);
|
|
172
|
+
let score = 1;
|
|
173
|
+
if (middle > 0) {
|
|
174
|
+
score = 0;
|
|
175
|
+
for (let k = 1; k < want.length - 1 && k < size - 1; k++) {
|
|
176
|
+
const a = lines[s + k].trim(), b = want[k].trim();
|
|
177
|
+
const len = Math.max(a.length, b.length);
|
|
178
|
+
score += len ? 1 - levenshtein(a, b) / len : 1;
|
|
179
|
+
}
|
|
180
|
+
score /= middle;
|
|
181
|
+
}
|
|
182
|
+
if (score > bestScore) { bestScore = score; best = [s, e]; }
|
|
183
|
+
}
|
|
184
|
+
if (best && bestScore >= ANCHOR_SIMILARITY) yield spanOfLines(lines, best[0], best[1]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Same block with its indentation shifted as a whole.
|
|
188
|
+
function* indentationFlexibleMatches(content, find) {
|
|
189
|
+
const dedent = (text) => {
|
|
190
|
+
const ls = text.split('\n');
|
|
191
|
+
const indents = ls.filter((l) => l.trim()).map((l) => l.match(/^\s*/)[0].length);
|
|
192
|
+
if (!indents.length) return text;
|
|
193
|
+
const min = Math.min(...indents);
|
|
194
|
+
return ls.map((l) => (l.trim() ? l.slice(min) : l)).join('\n');
|
|
195
|
+
};
|
|
196
|
+
const target = dedent(find);
|
|
197
|
+
const lines = content.split('\n');
|
|
198
|
+
const n = find.split('\n').length;
|
|
199
|
+
for (let i = 0; i + n <= lines.length; i++) {
|
|
200
|
+
const block = lines.slice(i, i + n).join('\n');
|
|
201
|
+
if (dedent(block) === target) yield block;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// The model wrote \n, \" or \` escapes where the file has the real characters.
|
|
206
|
+
function* escapeNormalizedMatches(content, find) {
|
|
207
|
+
const unescape = (s) => s.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (m, c) =>
|
|
208
|
+
({ n: '\n', t: '\t', r: '\r', "'": "'", '"': '"', '`': '`', '\\': '\\', '\n': '\n', $: '$' }[c] ?? m));
|
|
209
|
+
const target = unescape(find);
|
|
210
|
+
if (target !== find && content.includes(target)) yield target;
|
|
211
|
+
const lines = content.split('\n');
|
|
212
|
+
const n = target.split('\n').length;
|
|
213
|
+
for (let i = 0; i + n <= lines.length; i++) {
|
|
214
|
+
const block = lines.slice(i, i + n).join('\n');
|
|
215
|
+
if (block !== target && unescape(block) === target) yield block;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Stray blank lines or spaces around the block.
|
|
220
|
+
function* trimmedBoundaryMatches(content, find) {
|
|
221
|
+
const t = find.trim();
|
|
222
|
+
if (t === find) return;
|
|
223
|
+
if (content.includes(t)) yield t;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const FALLBACK_MATCHERS = [indentationFlexibleMatches, escapeNormalizedMatches, trimmedBoundaryMatches, blockAnchorMatches];
|
|
227
|
+
|
|
228
|
+
// A "match" many times bigger than the search block means an anchor latched
|
|
229
|
+
// onto the wrong closing line; replacing it would silently delete code.
|
|
230
|
+
function isDisproportionate(span, find) {
|
|
231
|
+
const spanLines = span.split('\n').length, findLines = find.split('\n').length;
|
|
232
|
+
if (spanLines >= Math.max(findLines + 3, findLines * 2)) return true;
|
|
233
|
+
if (findLines === 1) return false;
|
|
234
|
+
return span.trim().length > Math.max(find.trim().length + 500, find.trim().length * 4);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Find the one verbatim span in `content` that an imperfect search block most
|
|
239
|
+
* likely meant. @returns {{ok:true, span:string} | {ok:false, error:'notfound'|'multiple'|'disproportionate'}}
|
|
240
|
+
*/
|
|
241
|
+
function fuzzyLocate(content, find) {
|
|
242
|
+
let sawMultiple = false;
|
|
243
|
+
for (const matcher of FALLBACK_MATCHERS) {
|
|
244
|
+
for (const span of matcher(content, find)) {
|
|
245
|
+
if (!span.trim()) continue;
|
|
246
|
+
const at = content.indexOf(span);
|
|
247
|
+
if (at === -1) continue;
|
|
248
|
+
if (at !== content.lastIndexOf(span)) { sawMultiple = true; continue; }
|
|
249
|
+
if (isDisproportionate(span, find)) return { ok: false, error: 'disproportionate' };
|
|
250
|
+
return { ok: true, span, reindent: matcher === indentationFlexibleMatches || matcher === blockAnchorMatches };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { ok: false, error: sawMultiple ? 'multiple' : 'notfound' };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Apply an ordered list of verified {search, replace} hunks to file content,
|
|
257
|
+
// all-or-nothing (pure - no disk write).
|
|
258
|
+
function applyEditsToContent(fileContent, edits) {
|
|
259
|
+
let working = (fileContent || '').replace(/\r\n/g, '\n');
|
|
260
|
+
for (let i = 0; i < edits.length; i++) {
|
|
261
|
+
const { search, replace } = edits[i];
|
|
262
|
+
const res = applySearchReplace(working, search, replace);
|
|
263
|
+
if (!res.ok) return { ok: false, error: res.error, index: i };
|
|
264
|
+
working = res.content;
|
|
265
|
+
}
|
|
266
|
+
return { ok: true, content: working };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const SYSTEM_PROMPT = `You are a precise code editor. You receive an INSTRUCTION describing a small change, and the FULL current file. Make ONLY the specific change requested, as surgical SEARCH/REPLACE edits - do NOT reproduce the whole file.
|
|
270
|
+
|
|
271
|
+
STRICT RULES:
|
|
272
|
+
1. Return an "edits" array. Use the FEWEST edits that cleanly express the change.
|
|
273
|
+
2. "search" must be copied VERBATIM from the file below - exact existing text being changed, with just enough surrounding lines to be unique.
|
|
274
|
+
3. "replace" is that same block with ONLY the requested change applied - preserve everything else in it exactly (formatting, unrelated properties, comments, whitespace style).
|
|
275
|
+
4. If the SAME property/selector/value appears more than once in the file (e.g. "top" set in both a "from" and a "to" block), do NOT pick the first or most obvious match - use every clue in the instruction to find the ONE correct occurrence: the CURRENT value it mentions, nearby selectors, rule/keyframe names, or ordering (first/last, start/end). If more than one occurrence still fits equally well after that, return an EMPTY "edits" array with a "reason" naming the ambiguity instead of guessing.
|
|
276
|
+
5. If the instruction genuinely requires large new content that can't be expressed as a small edit (e.g. "build a whole new page/game/module from scratch"), return an EMPTY "edits" array - do not guess badly at a huge diff.
|
|
277
|
+
6. Return ONLY valid JSON, no markdown, no commentary.
|
|
278
|
+
|
|
279
|
+
Response format:
|
|
280
|
+
{
|
|
281
|
+
"edits": [ { "search": "<exact verbatim block from the file>", "replace": "<that block with the change applied>" } ],
|
|
282
|
+
"reason": "<one short sentence>",
|
|
283
|
+
"confidence": <0-100>
|
|
284
|
+
}`;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Asks the AI for surgical edits, self-corrects up to twice against bad
|
|
288
|
+
* "search" matches, and returns only the edits that verify cleanly against
|
|
289
|
+
* the real file content.
|
|
290
|
+
* @returns {Promise<{success:boolean, edits?:Array, badCount?:number, reason?:string, confidence?:number, tokensUsed?:number, modelUsed?:string, error?:string}>}
|
|
291
|
+
*/
|
|
292
|
+
async function computeInstructionEdits(instruction, filePath) {
|
|
293
|
+
if (!fs.existsSync(filePath)) return { success: false, error: `File not found: ${filePath}` };
|
|
294
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
295
|
+
|
|
296
|
+
const probe = (search) => {
|
|
297
|
+
const res = applySearchReplace(content, search, search);
|
|
298
|
+
return res.ok ? 'ok' : res.error;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
let totalTokens = 0;
|
|
302
|
+
let lastModelUsed = null;
|
|
303
|
+
|
|
304
|
+
const askModel = async (feedback) => {
|
|
305
|
+
const messages = [
|
|
306
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
307
|
+
// Large/stable content (the file) first, small/always-different content
|
|
308
|
+
// (the instruction) last - the ai-proxy's underlying model caches a
|
|
309
|
+
// repeated PREFIX, so this ordering matters for repeated calls against
|
|
310
|
+
// the same file even when the instruction text differs each time.
|
|
311
|
+
{ role: 'user', content: `FULL FILE (${path.basename(filePath)}):\n${content}\n\nINSTRUCTION:\n${instruction}` },
|
|
312
|
+
];
|
|
313
|
+
if (feedback) messages.push({ role: 'user', content: feedback });
|
|
314
|
+
|
|
315
|
+
const aiResult = await ai.chat(messages, { json: true, meta: { promptText: instruction, filePath } });
|
|
316
|
+
if (!aiResult.success) throw new Error(aiResult.error);
|
|
317
|
+
if (aiResult.modelUsed) lastModelUsed = aiResult.modelUsed;
|
|
318
|
+
const data = aiResult.data;
|
|
319
|
+
totalTokens += (data.usage || {}).total_tokens || 0;
|
|
320
|
+
|
|
321
|
+
let parsed;
|
|
322
|
+
try { parsed = JSON.parse(data.choices[0].message.content); }
|
|
323
|
+
catch { return { edits: [], confidence: 0, reason: '' }; }
|
|
324
|
+
|
|
325
|
+
const edits = Array.isArray(parsed.edits)
|
|
326
|
+
? parsed.edits.filter(e => e && e.search && e.replace != null).map(e => ({ search: e.search, replace: e.replace }))
|
|
327
|
+
: [];
|
|
328
|
+
return { edits, confidence: parsed.confidence == null ? 75 : parsed.confidence, reason: parsed.reason || '' };
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
let edits, confidence, reason;
|
|
332
|
+
try {
|
|
333
|
+
({ edits, confidence, reason } = await askModel());
|
|
334
|
+
} catch (e) {
|
|
335
|
+
return { success: false, error: e.message };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const verify = (list) => list.map((e) => ({ e, status: probe(e.search) }));
|
|
339
|
+
let checked = verify(edits);
|
|
340
|
+
let bad = checked.filter(c => c.status !== 'ok');
|
|
341
|
+
|
|
342
|
+
let pass = 0;
|
|
343
|
+
while (bad.length && pass < 2) {
|
|
344
|
+
pass++;
|
|
345
|
+
const feedback =
|
|
346
|
+
`Some "search" blocks from your last answer are wrong. Return the COMPLETE corrected edits array again, fixing these:\n` +
|
|
347
|
+
bad.map(c => c.status === 'multiple'
|
|
348
|
+
? `- Matched MULTIPLE places - include more surrounding lines so it is unique:\n${c.e.search}`
|
|
349
|
+
: `- NOT found in the file - copy it EXACTLY from the FULL FILE above, character for character:\n${c.e.search}`
|
|
350
|
+
).join('\n') +
|
|
351
|
+
`\nEvery "search" must be copied verbatim from the file shown above.`;
|
|
352
|
+
let retry;
|
|
353
|
+
try { retry = await askModel(feedback); }
|
|
354
|
+
catch (e) { break; }
|
|
355
|
+
if (retry.edits.length) { edits = retry.edits; confidence = retry.confidence; reason = retry.reason || reason; }
|
|
356
|
+
checked = verify(edits);
|
|
357
|
+
bad = checked.filter(c => c.status !== 'ok');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const good = checked.filter(c => c.status === 'ok').map(c => c.e);
|
|
361
|
+
return { success: true, edits: good, badCount: bad.length, confidence, reason, tokensUsed: totalTokens, modelUsed: lastModelUsed };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = { applySearchReplace, applyEditsToContent, computeInstructionEdits, fuzzyLocate };
|