minimal-agent 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/package.json +52 -5
- package/plugins/computer_use/.claude-plugin/plugin.json +6 -0
- package/plugins/computer_use/README.md +94 -0
- package/plugins/computer_use/app/_template.md +142 -0
- package/plugins/computer_use/app/chrome.md +165 -0
- package/plugins/computer_use/app/finder.md +76 -0
- package/plugins/computer_use/app/macos-system-settings.md +81 -0
- package/plugins/computer_use/app/microsoft-excel.md +80 -0
- package/plugins/computer_use/app/microsoft-powerpoint.md +80 -0
- package/plugins/computer_use/app/microsoft-word.md +79 -0
- package/plugins/computer_use/app/wechat.md +413 -0
- package/plugins/computer_use/app/weread.md +319 -0
- package/plugins/computer_use/commands/computeruse.md +25 -0
- package/plugins/computer_use/commands/cu.md +9 -0
- package/plugins/computer_use/plugin.js +26 -0
- package/plugins/computer_use/plugin.ts +30 -0
- package/plugins/computer_use/src/capture.js +90 -0
- package/plugins/computer_use/src/capture.ts +104 -0
- package/plugins/computer_use/src/commands.js +91 -0
- package/plugins/computer_use/src/commands.ts +103 -0
- package/plugins/computer_use/src/daemon.js +134 -0
- package/plugins/computer_use/src/daemon.ts +161 -0
- package/plugins/computer_use/src/manuals.js +135 -0
- package/plugins/computer_use/src/manuals.ts +159 -0
- package/plugins/computer_use/src/verify.js +168 -0
- package/plugins/computer_use/src/verify.ts +219 -0
- package/plugins/computer_use/test/commands.test.ts +73 -0
- package/plugins/cron-runner/plugin.ts +38 -0
- package/plugins/cron-runner/src/commands.ts +373 -0
- package/plugins/cron-runner/src/cronTime.ts +155 -0
- package/plugins/cron-runner/src/lock.ts +79 -0
- package/plugins/cron-runner/src/runs.ts +99 -0
- package/plugins/cron-runner/src/scheduleEngine.ts +76 -0
- package/plugins/cron-runner/src/scheduler.ts +376 -0
- package/plugins/cron-runner/src/schema.ts +139 -0
- package/plugins/cron-runner/src/service.ts +193 -0
- package/plugins/cron-runner/src/store.ts +333 -0
- package/plugins/cron-runner/src/types.ts +141 -0
- package/plugins/cron-runner/test/cronTime.test.ts +41 -0
- package/plugins/cron-runner/test/plugin-integration.test.ts +47 -0
- package/plugins/cron-runner/test/scheduleEngine.test.ts +41 -0
- package/plugins/cron-runner/test/service.test.ts +76 -0
- package/plugins/cron-runner/test/store-and-scheduler.test.ts +501 -0
- package/plugins/workflow-runner/plugin.js +56 -2
- package/plugins/workflow-runner/plugin.ts +113 -0
- package/plugins/workflow-runner/src/agentCommon.js +43 -4
- package/plugins/workflow-runner/src/agentCommon.ts +435 -0
- package/plugins/workflow-runner/src/controlFlow/branch.js +40 -0
- package/plugins/workflow-runner/src/controlFlow/branch.ts +46 -0
- package/plugins/workflow-runner/src/controlFlow/loop.js +211 -0
- package/plugins/workflow-runner/src/controlFlow/loop.ts +248 -0
- package/plugins/workflow-runner/src/controlFlow/pause.js +129 -0
- package/plugins/workflow-runner/src/controlFlow/pause.ts +141 -0
- package/plugins/workflow-runner/src/controlFlow/signal.js +21 -0
- package/plugins/workflow-runner/src/controlFlow/signal.ts +29 -0
- package/plugins/workflow-runner/src/controlFlow/types.js +18 -0
- package/plugins/workflow-runner/src/controlFlow/types.ts +63 -0
- package/plugins/workflow-runner/src/dslManifest.ts +127 -0
- package/plugins/workflow-runner/src/expressions.ts +622 -0
- package/plugins/workflow-runner/src/index.js +13 -4
- package/plugins/workflow-runner/src/index.ts +338 -0
- package/plugins/workflow-runner/src/jsonPayload.ts +84 -0
- package/plugins/workflow-runner/src/loader.js +23 -0
- package/plugins/workflow-runner/src/loader.ts +721 -0
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +64 -0
- package/plugins/workflow-runner/src/recovery.ts +149 -0
- package/plugins/workflow-runner/src/runner.js +75 -659
- package/plugins/workflow-runner/src/runner.ts +604 -0
- package/plugins/workflow-runner/src/stepExecutors/agent.ts +234 -0
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +50 -0
- package/plugins/workflow-runner/src/stepExecutors/echo.js +31 -0
- package/plugins/workflow-runner/src/stepExecutors/echo.ts +38 -0
- package/plugins/workflow-runner/src/stepExecutors/llm.ts +133 -0
- package/plugins/workflow-runner/src/stepExecutors/set.js +49 -0
- package/plugins/workflow-runner/src/stepExecutors/set.ts +56 -0
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +140 -0
- package/plugins/workflow-runner/src/stepExecutors/tool.js +80 -8
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +256 -0
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +68 -0
- package/plugins/workflow-runner/src/stepPipeline.js +277 -0
- package/plugins/workflow-runner/src/stepPipeline.ts +366 -0
- package/plugins/workflow-runner/src/taskIdentity.ts +405 -0
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.js +37 -1
- package/plugins/workflow-runner/src/types.ts +602 -0
- package/plugins/workflow-runner/src/workflowState.ts +733 -0
- package/plugins/workflow-runner/test/agent.test.ts +587 -0
- package/plugins/workflow-runner/test/agentLlmError.test.ts +373 -0
- package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +65 -0
- package/plugins/workflow-runner/test/captureMisbind.test.ts +99 -0
- package/plugins/workflow-runner/test/cli.e2e.test.ts +147 -0
- package/plugins/workflow-runner/test/context-files.test.ts +320 -0
- package/plugins/workflow-runner/test/e2e.test.ts +370 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +226 -0
- package/plugins/workflow-runner/test/expressions.test.ts +397 -0
- package/plugins/workflow-runner/test/foundation.test.ts +126 -0
- package/plugins/workflow-runner/test/graceful.test.ts +139 -0
- package/plugins/workflow-runner/test/greedyInputs.load.test.ts +47 -0
- package/plugins/workflow-runner/test/hostCapability.test.ts +108 -0
- package/plugins/workflow-runner/test/illustrate.load.test.ts +183 -0
- package/plugins/workflow-runner/test/jsonPayload.test.ts +83 -0
- package/plugins/workflow-runner/test/loader.test.ts +617 -0
- package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +33 -0
- package/plugins/workflow-runner/test/multimodal.test.ts +342 -0
- package/plugins/workflow-runner/test/output-schema.test.ts +213 -0
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +87 -0
- package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +269 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +1220 -0
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +220 -0
- package/plugins/workflow-runner/test/recovery.test.ts +265 -0
- package/plugins/workflow-runner/test/runner.test.ts +911 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +379 -0
- package/plugins/workflow-runner/test/skillTurnDone.test.ts +71 -0
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +150 -0
- package/plugins/workflow-runner/test/stepPipeline.test.ts +114 -0
- package/plugins/workflow-runner/test/taskArbitration.test.ts +338 -0
- package/plugins/workflow-runner/test/taskIdentity.test.ts +237 -0
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +270 -0
- package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +221 -0
- package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +29 -0
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/plugins/workflow-runner/test/webppt.load.test.ts +250 -0
- package/plugins/workflow-runner/test/word.load.test.ts +208 -0
- package/plugins/workflow-runner/test/workflows.smoke.test.ts +76 -0
- package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +207 -0
- package/skills/browser-automation/SKILL.md +9 -19
- package/skills/web-crawler/SKILL.md +9 -11
- package/src/askBridge.ts +249 -0
- package/src/bootstrap/cwdArg.js +10 -3
- package/src/bootstrap/cwdArg.ts +31 -0
- package/src/bootstrap/workingDir.ts +48 -0
- package/src/cli/args.js +70 -7
- package/src/cli/args.ts +146 -0
- package/src/cli/computerUse.js +185 -0
- package/src/cli/computerUse.ts +221 -0
- package/src/cli/configMenu.js +274 -202
- package/src/cli/configMenu.tsx +521 -0
- package/src/cli/memoryAccess.js +45 -0
- package/src/cli/memoryAccess.ts +53 -0
- package/src/cli/print.js +119 -5
- package/src/cli/print.ts +597 -0
- package/src/cli/quickModelSetup.js +60 -0
- package/src/cli/quickModelSetup.tsx +96 -0
- package/src/cli/streamJson.js +80 -4
- package/src/cli/streamJson.ts +433 -0
- package/src/cli/wizards/advisorWizard.js +1 -0
- package/src/cli/wizards/advisorWizard.tsx +195 -0
- package/src/cli/wizards/agentRuntimeWizard.js +84 -0
- package/src/cli/wizards/agentRuntimeWizard.tsx +134 -0
- package/src/cli/wizards/browserWizard.js +55 -0
- package/src/cli/wizards/browserWizard.tsx +77 -0
- package/src/cli/wizards/contextAdvisorWizard.js +84 -0
- package/src/cli/wizards/contextAdvisorWizard.tsx +124 -0
- package/src/cli/wizards/fieldForm.js +7 -4
- package/src/cli/wizards/fieldForm.tsx +250 -0
- package/src/cli/wizards/imageWizard.js +4 -5
- package/src/cli/wizards/imageWizard.tsx +96 -0
- package/src/cli/wizards/openRouterWizard.tsx +48 -0
- package/src/cli/wizards/providerPresets.js +1 -8
- package/src/cli/wizards/providerPresets.ts +106 -0
- package/src/cli/wizards/providerWizard.js +152 -205
- package/src/cli/wizards/providerWizard.tsx +266 -0
- package/src/cli/wizards/searchWizard.tsx +47 -0
- package/src/cli/wizards/ttsWizard.js +4 -3
- package/src/cli/wizards/ttsWizard.tsx +61 -0
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configDiagnostics.js +117 -0
- package/src/config/configDiagnostics.ts +166 -0
- package/src/config/configFile.js +52 -32
- package/src/config/configFile.ts +633 -0
- package/src/config/configManagement.js +126 -0
- package/src/config/configManagement.ts +159 -0
- package/src/config/configService.js +66 -11
- package/src/config/configService.ts +554 -0
- package/src/config/numericLimits.js +61 -0
- package/src/config/numericLimits.ts +80 -0
- package/src/config.ts +109 -0
- package/src/context/agentHome.ts +11 -0
- package/src/context/archive.ts +144 -0
- package/src/context/budgetPlan.js +54 -0
- package/src/context/budgetPlan.ts +88 -0
- package/src/context/compact.js +179 -106
- package/src/context/compact.ts +584 -0
- package/src/context/contextBundle.js +119 -0
- package/src/context/contextBundle.ts +176 -0
- package/src/context/contextLock.ts +84 -0
- package/src/context/microCompactLite.ts +218 -0
- package/src/context/persistContext.ts +177 -0
- package/src/context/reactiveCompact.js +19 -4
- package/src/context/reactiveCompact.ts +240 -0
- package/src/context/recentDirs.ts +42 -0
- package/src/context/sessionPath.ts +206 -0
- package/src/context/sessionRegistry.js +8 -2
- package/src/context/sessionRegistry.ts +414 -0
- package/src/context/snipCompact.ts +163 -0
- package/src/context/tokenCounter.ts +106 -0
- package/src/context/workspaceIndex.js +2 -2
- package/src/context/workspaceIndex.ts +751 -0
- package/src/entry.compiled.tsx +20 -0
- package/src/harness/claimValidation.js +9 -0
- package/src/harness/claimValidation.ts +22 -0
- package/src/harness/evidenceLedger.js +92 -0
- package/src/harness/evidenceLedger.ts +96 -0
- package/src/harness/stallGuard.js +60 -0
- package/src/harness/stallGuard.ts +81 -0
- package/src/humanIntervention.js +129 -0
- package/src/humanIntervention.ts +216 -0
- package/src/infra/verification.ts +295 -0
- package/src/irreversible/rules.js +216 -0
- package/src/irreversible/rules.ts +261 -0
- package/src/llm/client.js +40 -3
- package/src/llm/client.ts +621 -0
- package/src/llm/promptCache.js +27 -0
- package/src/llm/promptCache.ts +41 -0
- package/src/llm/providers/anthropicFormat.js +41 -5
- package/src/llm/providers/anthropicFormat.ts +290 -0
- package/src/llm/providers/openaiFormat.js +186 -8
- package/src/llm/providers/openaiFormat.ts +428 -0
- package/src/loop.js +614 -63
- package/src/loop.ts +1454 -0
- package/src/main.js +67 -10
- package/src/main.tsx +344 -0
- package/src/memory/codec.js +204 -0
- package/src/memory/codec.ts +249 -0
- package/src/memory/constants.js +34 -0
- package/src/memory/constants.ts +50 -0
- package/src/memory/episodicMemory.js +684 -0
- package/src/memory/episodicMemory.ts +815 -0
- package/src/memory/fileStore.js +148 -0
- package/src/memory/fileStore.ts +184 -0
- package/src/memory/memoryService.js +570 -0
- package/src/memory/memoryService.ts +859 -0
- package/src/memory/migration.js +128 -0
- package/src/memory/migration.ts +160 -0
- package/src/memory/repository.js +789 -0
- package/src/memory/repository.ts +893 -0
- package/src/memory/retrieval.js +108 -0
- package/src/memory/retrieval.ts +145 -0
- package/src/memory/searchIndex.js +107 -0
- package/src/memory/searchIndex.ts +153 -0
- package/src/memory/topicView.js +82 -0
- package/src/memory/topicView.ts +95 -0
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.js +2 -0
- package/src/memory/types.ts +186 -0
- package/src/messageQueue.ts +123 -0
- package/src/multimodal.js +128 -46
- package/src/multimodal.ts +349 -0
- package/src/plugin-sdk.js +21 -1
- package/src/plugin-sdk.ts +134 -0
- package/src/plugins/builtinPluginIds.js +47 -0
- package/src/plugins/builtinPluginIds.ts +51 -0
- package/src/plugins/builtinRegistry.ts +43 -0
- package/src/plugins/commandRouter.js +40 -26
- package/src/plugins/commandRouter.ts +261 -0
- package/src/plugins/hookEngine.ts +341 -0
- package/src/plugins/pluginApi.ts +133 -0
- package/src/plugins/pluginLifecycle.ts +34 -0
- package/src/plugins/pluginLoader.ts +147 -0
- package/src/plugins/pluginManifest.js +127 -0
- package/src/plugins/pluginManifest.ts +152 -0
- package/src/plugins/pluginRunner.js +47 -2
- package/src/plugins/pluginRunner.ts +207 -0
- package/src/plugins/transcript.ts +208 -0
- package/src/prompts/lint.js +169 -0
- package/src/prompts/lint.ts +220 -0
- package/src/prompts/pluginList.js +32 -16
- package/src/prompts/pluginList.ts +86 -0
- package/src/prompts/policyRegistry.js +318 -0
- package/src/prompts/policyRegistry.ts +358 -0
- package/src/prompts/projectInstructions.js +24 -0
- package/src/prompts/projectInstructions.ts +81 -0
- package/src/prompts/promptAssembly.js +311 -202
- package/src/prompts/promptAssembly.ts +510 -0
- package/src/prompts/skillList.js +106 -45
- package/src/prompts/skillList.ts +198 -0
- package/src/prompts/system.js +31 -6
- package/src/prompts/system.ts +81 -0
- package/src/runtime/agentRuntimeContext.js +53 -0
- package/src/runtime/agentRuntimeContext.ts +112 -0
- package/src/session/runAgentTurn.js +71 -12
- package/src/session/runAgentTurn.ts +181 -0
- package/src/session/runTurn.js +29 -0
- package/src/session/runTurn.ts +160 -0
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +79 -3
- package/src/session/sessionState.ts +134 -0
- package/src/toolGate.ts +50 -0
- package/src/tools/advisor/advisor.js +76 -15
- package/src/tools/advisor/advisor.ts +512 -0
- package/src/tools/agent/agent.js +245 -29
- package/src/tools/agent/agent.ts +837 -0
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +20 -8
- package/src/tools/agent/agentTypes.ts +94 -0
- package/src/tools/agent/coordinator.ts +111 -0
- package/src/tools/agent/worktree.ts +296 -0
- package/src/tools/ask/ask.js +17 -0
- package/src/tools/ask/ask.ts +122 -0
- package/src/tools/bash/attachImage.js +223 -0
- package/src/tools/bash/attachImage.ts +281 -0
- package/src/tools/bash/bash.js +132 -14
- package/src/tools/bash/bash.ts +642 -0
- package/src/tools/bash/semantics.ts +103 -0
- package/src/tools/bash/warnings.js +17 -11
- package/src/tools/bash/warnings.ts +117 -0
- package/src/tools/capabilities.js +42 -0
- package/src/tools/capabilities.ts +69 -0
- package/src/tools/computeruse/accessibilityPermission.js +261 -0
- package/src/tools/computeruse/accessibilityPermission.ts +294 -0
- package/src/tools/computeruse/axSearch.js +96 -0
- package/src/tools/computeruse/axSearch.ts +120 -0
- package/src/tools/computeruse/axSnap.js +272 -0
- package/src/tools/computeruse/axSnap.ts +349 -0
- package/src/tools/computeruse/capabilityLabels.js +95 -0
- package/src/tools/computeruse/capabilityLabels.ts +115 -0
- package/src/tools/computeruse/computerUseService.js +2235 -0
- package/src/tools/computeruse/computerUseService.ts +2686 -0
- package/src/tools/computeruse/computeruse.js +2742 -0
- package/src/tools/computeruse/computeruse.ts +3265 -0
- package/src/tools/computeruse/diagnosticTrace.js +154 -0
- package/src/tools/computeruse/diagnosticTrace.ts +195 -0
- package/src/tools/computeruse/diagnostics.js +369 -0
- package/src/tools/computeruse/diagnostics.ts +453 -0
- package/src/tools/computeruse/grants.js +53 -0
- package/src/tools/computeruse/grants.ts +70 -0
- package/src/tools/computeruse/humanBoundary.js +159 -0
- package/src/tools/computeruse/humanBoundary.ts +199 -0
- package/src/tools/computeruse/intervention.js +77 -0
- package/src/tools/computeruse/intervention.ts +110 -0
- package/src/tools/computeruse/manualSchema.js +709 -0
- package/src/tools/computeruse/manualSchema.ts +916 -0
- package/src/tools/computeruse/observationImages.js +705 -0
- package/src/tools/computeruse/observationImages.ts +889 -0
- package/src/tools/computeruse/protocol.js +60 -0
- package/src/tools/computeruse/protocol.ts +515 -0
- package/src/tools/computeruse/telemetryLog.js +57 -0
- package/src/tools/computeruse/telemetryLog.ts +67 -0
- package/src/tools/computeruse/vision/appGuide.js +325 -0
- package/src/tools/computeruse/vision/appGuide.ts +482 -0
- package/src/tools/computeruse/vision/focus.js +291 -0
- package/src/tools/computeruse/vision/focus.ts +382 -0
- package/src/tools/computeruse/vision/frames.js +248 -0
- package/src/tools/computeruse/vision/frames.ts +329 -0
- package/src/tools/computeruse/vision/geometry.js +131 -0
- package/src/tools/computeruse/vision/geometry.ts +163 -0
- package/src/tools/computeruse/vision/imaging.js +201 -0
- package/src/tools/computeruse/vision/imaging.ts +281 -0
- package/src/tools/computeruse/vision/png.js +274 -0
- package/src/tools/computeruse/vision/png.ts +317 -0
- package/src/tools/computeruse/vision/raster.js +320 -0
- package/src/tools/computeruse/vision/raster.ts +408 -0
- package/src/tools/computeruse/vision/settle.js +131 -0
- package/src/tools/computeruse/vision/settle.ts +220 -0
- package/src/tools/computeruse/vision/telemetry.js +196 -0
- package/src/tools/computeruse/vision/telemetry.ts +241 -0
- package/src/tools/computeruse/visionOcr.js +115 -0
- package/src/tools/computeruse/visionOcr.ts +128 -0
- package/src/tools/computeruse/waitFor.js +237 -0
- package/src/tools/computeruse/waitFor.ts +291 -0
- package/src/tools/download/download.js +10 -6
- package/src/tools/download/download.ts +187 -0
- package/src/tools/edit/edit.js +30 -18
- package/src/tools/edit/edit.ts +349 -0
- package/src/tools/edit/multi-edit.js +25 -14
- package/src/tools/edit/multi-edit.ts +223 -0
- package/src/tools/effectiveToolSet.js +73 -0
- package/src/tools/effectiveToolSet.ts +91 -0
- package/src/tools/genimage/genimage.ts +257 -0
- package/src/tools/genspeech/genspeech.ts +83 -0
- package/src/tools/glob/glob.js +176 -29
- package/src/tools/glob/glob.ts +283 -0
- package/src/tools/grep/grep.js +61 -19
- package/src/tools/grep/grep.ts +239 -0
- package/src/tools/grep/rgPath.ts +206 -0
- package/src/tools/index.js +99 -5
- package/src/tools/index.ts +293 -0
- package/src/tools/lsp/config.js +113 -0
- package/src/tools/lsp/config.ts +113 -0
- package/src/tools/lsp/lsp.js +256 -0
- package/src/tools/lsp/lsp.ts +274 -0
- package/src/tools/lsp/service.js +303 -0
- package/src/tools/lsp/service.ts +296 -0
- package/src/tools/memory/memory.js +520 -0
- package/src/tools/memory/memory.ts +625 -0
- package/src/tools/memory/policy.js +53 -0
- package/src/tools/memory/policy.ts +72 -0
- package/src/tools/notebook/notebookEdit.js +114 -0
- package/src/tools/notebook/notebookEdit.ts +126 -0
- package/src/tools/powershell/powershell.js +210 -0
- package/src/tools/powershell/powershell.ts +239 -0
- package/src/tools/presentresult/presentresult.js +145 -0
- package/src/tools/presentresult/presentresult.ts +156 -0
- package/src/tools/read/read.js +371 -163
- package/src/tools/read/read.ts +558 -0
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +51 -0
- package/src/tools/shared/fileActivity.ts +66 -0
- package/src/tools/shared/fileMutationBus.js +13 -0
- package/src/tools/shared/fileMutationBus.ts +14 -0
- package/src/tools/shared/fileState.js +73 -65
- package/src/tools/shared/fileState.ts +145 -0
- package/src/tools/shared/fileUtils.js +81 -4
- package/src/tools/shared/fileUtils.ts +504 -0
- package/src/tools/shared/killTree.ts +50 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/schemas.ts +20 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +275 -0
- package/src/tools/tasks/backgroundTaskService.ts +369 -0
- package/src/tools/tasks/processEvidence.js +24 -0
- package/src/tools/tasks/processEvidence.ts +34 -0
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +113 -0
- package/src/tools/tasks/taskOutput.ts +145 -0
- package/src/tools/tasks/taskStop.js +74 -0
- package/src/tools/tasks/taskStop.ts +99 -0
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/types.js +1 -1
- package/src/tools/types.ts +24 -0
- package/src/tools/webbrowser/agentBrowserPath.ts +286 -0
- package/src/tools/webbrowser/browser.js +214 -11
- package/src/tools/webbrowser/browser.ts +293 -0
- package/src/tools/webbrowser/browserControl.js +153 -48
- package/src/tools/webbrowser/browserControl.ts +262 -0
- package/src/tools/webbrowser/browserDaemon.js +147 -39
- package/src/tools/webbrowser/browserDaemon.ts +270 -0
- package/src/tools/webbrowser/browserOutcome.js +273 -0
- package/src/tools/webbrowser/browserOutcome.ts +306 -0
- package/src/tools/webbrowser/browserRunRegistry.js +150 -0
- package/src/tools/webbrowser/browserRunRegistry.ts +179 -0
- package/src/tools/webbrowser/browserService.js +20 -6
- package/src/tools/webbrowser/browserService.ts +56 -0
- package/src/tools/webbrowser/browserStream.js +30 -1
- package/src/tools/webbrowser/browserStream.ts +216 -0
- package/src/tools/webbrowser/browserTabs.js +143 -0
- package/src/tools/webbrowser/browserTabs.ts +210 -0
- package/src/tools/webbrowser/webbrowser.js +831 -150
- package/src/tools/webbrowser/webbrowser.ts +1537 -0
- package/src/tools/webfetch/preapproved.ts +274 -0
- package/src/tools/webfetch/webfetch.js +24 -23
- package/src/tools/webfetch/webfetch.ts +383 -0
- package/src/tools/websearch/websearch.js +12 -11
- package/src/tools/websearch/websearch.ts +292 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +27 -44
- package/src/tools/write/write.ts +136 -0
- package/src/types.ts +960 -0
- package/src/ui/App.js +216 -12
- package/src/ui/App.tsx +879 -0
- package/src/ui/AskQuestion.tsx +300 -0
- package/src/ui/InputBox.js +77 -1
- package/src/ui/InputBox.tsx +567 -0
- package/src/ui/LiveArea.tsx +344 -0
- package/src/ui/MessageList.js +27 -6
- package/src/ui/MessageList.tsx +559 -0
- package/src/ui/ModelPicker.tsx +107 -0
- package/src/ui/QueuedMessagesBanner.tsx +51 -0
- package/src/ui/Root.js +6 -8
- package/src/ui/Root.tsx +100 -0
- package/src/ui/SessionPicker.tsx +169 -0
- package/src/ui/StatusLine.js +22 -1
- package/src/ui/StatusLine.tsx +145 -0
- package/src/ui/WorkspacePicker.tsx +84 -0
- package/src/ui/advisorCommand.js +54 -0
- package/src/ui/advisorCommand.ts +63 -0
- package/src/ui/computerCommand.js +19 -0
- package/src/ui/computerCommand.ts +35 -0
- package/src/ui/figures.ts +45 -0
- package/src/ui/hooks/askSlot.ts +112 -0
- package/src/ui/hooks/useChat.js +271 -7
- package/src/ui/hooks/useChat.ts +1578 -0
- package/src/ui/hooks/useInputHistory.ts +235 -0
- package/src/ui/hooks/usePasteHandler.ts +220 -0
- package/src/ui/hooks/useTerminalSize.ts +41 -0
- package/src/ui/hooks/useTextBuffer.ts +108 -0
- package/src/ui/hooks/useTokenUsage.ts +44 -0
- package/src/ui/liveViewport.ts +189 -0
- package/src/ui/markdown.ts +230 -0
- package/src/ui/textBuffer.ts +239 -0
- package/src/ui/theme.ts +35 -0
- package/src/utils/clipboardImage.js +358 -0
- package/src/utils/clipboardImage.ts +420 -0
- package/src/utils/debugLog.ts +50 -0
- package/src/utils/greenRoot.ts +35 -0
- package/src/utils/packageRoot.ts +40 -0
- package/src/utils/pathContains.js +28 -0
- package/src/utils/pathContains.ts +31 -0
- package/src/utils/resourcePaths.ts +71 -0
- package/src/utils/stripThink.ts +49 -0
- package/src/utils/zodToJson.ts +73 -0
- package/src/web/browserBridge.js +365 -66
- package/src/web/browserBridge.ts +672 -0
- package/src/web/protocol.ts +1804 -0
- package/vendor/computer-use/arm64-darwin/cu-helperd +0 -0
- package/workflows/ppt.yaml +13 -2
- package/workflows/wechat-reading-shots.yaml +315 -0
- package/workflows/word.yaml +11 -1
- package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
- /package/skills/ai-image/scripts/{generate_test.mjs → generate.selftest.mjs} +0 -0
- /package/skills/ai-image/scripts/{generate_from_plan_test.mjs → generate_from_plan.selftest.mjs} +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* src/tools/edit.ts —— Edit 工具(精确字符串替换)
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 对应 kakadeai 主仓库的 FileEditTool。
|
|
6
|
+
* prompt 文本和 Zod schema 与 kakadeai 一致。
|
|
7
|
+
*
|
|
8
|
+
* v0.2 增强:
|
|
9
|
+
* - 路径安全校验(null byte / UNC 路径)
|
|
10
|
+
* - 文件大小上限(1GB)
|
|
11
|
+
* - old_string 找不到时提供模糊匹配提示(最常见模型错误)
|
|
12
|
+
* ============================================================
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
|
|
18
|
+
import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
|
|
19
|
+
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
20
|
+
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
21
|
+
import {
|
|
22
|
+
resolveToolPath,
|
|
23
|
+
findActualString,
|
|
24
|
+
preserveQuoteStyle,
|
|
25
|
+
detectLineEndingsForString,
|
|
26
|
+
applyLineEnding,
|
|
27
|
+
countOccurrences,
|
|
28
|
+
splitReplaceAll,
|
|
29
|
+
tryAutoSnapshot,
|
|
30
|
+
atomicWriteFile,
|
|
31
|
+
encodeTextFile,
|
|
32
|
+
readTextFile,
|
|
33
|
+
} from '../shared/fileUtils.js';
|
|
34
|
+
import { filePathField } from '../shared/schemas.js';
|
|
35
|
+
import { assertFresh, hashFileBytes, recordRead, wasTruncated, type FileReadState } from '../shared/fileState.js';
|
|
36
|
+
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
37
|
+
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
38
|
+
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
39
|
+
|
|
40
|
+
/** 编辑文件大小上限:1GB。防止模型试图 Edit 超大文件撑爆内存。 */
|
|
41
|
+
const MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
42
|
+
|
|
43
|
+
// ---------------- 1. Zod 输入 schema ----------------
|
|
44
|
+
const inputSchema = z.object({
|
|
45
|
+
file_path: filePathField('编辑'),
|
|
46
|
+
old_string: z
|
|
47
|
+
.string()
|
|
48
|
+
.describe(
|
|
49
|
+
'要替换的原文本(必须在文件中唯一,除非 replace_all=true);为空字符串且文件不存在时创建新文件',
|
|
50
|
+
),
|
|
51
|
+
new_string: z.string().describe('替换为的新文本(与 old_string 必须不同)'),
|
|
52
|
+
replace_all: z
|
|
53
|
+
.boolean()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('是否替换所有出现位置(默认 false)'),
|
|
56
|
+
});
|
|
57
|
+
type EditInput = z.infer<typeof inputSchema>;
|
|
58
|
+
|
|
59
|
+
// ---------------- 2. JSON Schema(由 Zod 自动派生) ----------------
|
|
60
|
+
const parameters = toToolParameters(inputSchema);
|
|
61
|
+
|
|
62
|
+
// ---------------- 3. Description(搬自 kakadeai/tools/FileEditTool/prompt.ts) ----------------
|
|
63
|
+
const description = `Performs exact string replacements in files.
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
- You MUST use your \`Read\` tool to read the current content of the file BEFORE calling Edit.
|
|
67
|
+
Memory is unreliable — if you think you "remember" how the code looks, you are probably wrong.
|
|
68
|
+
Re-read the relevant sections (function, module, or area you plan to change) every time.
|
|
69
|
+
- To create a new file, pass an empty \`old_string\` and the desired contents as \`new_string\`. When creating a new file (path does not yet exist), you may call Edit directly — no prior Read is needed.
|
|
70
|
+
- If the file already exists and you pass an empty \`old_string\`, the edit is rejected with a clear error. To replace the entire content of an existing file, use the Write tool instead.
|
|
71
|
+
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
72
|
+
- The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
|
|
73
|
+
- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
|
|
74
|
+
- Preserve exact indentation (tabs/spaces).`;
|
|
75
|
+
|
|
76
|
+
// ---------------- 4. call() ----------------
|
|
77
|
+
async function call(input: EditInput, state?: FileReadState, cwd?: string): Promise<ToolResult> {
|
|
78
|
+
const { old_string, new_string } = input;
|
|
79
|
+
const replaceAll = input.replace_all ?? false;
|
|
80
|
+
|
|
81
|
+
// 5a. 路径规范化 + 安全校验(含 null byte / UNC / 设备文件拦截 / ~ 展开)
|
|
82
|
+
const pathResult = resolveToolPath(input.file_path, cwd);
|
|
83
|
+
if (!pathResult.ok) return pathResult;
|
|
84
|
+
const filePath = pathResult.resolvedPath;
|
|
85
|
+
|
|
86
|
+
// 5a-bis. Pre-read guard:文件已存在则必须先 Read(创建新文件场景下 assertFresh 返回 ok:true)
|
|
87
|
+
// 拦截路径自动注入 snippet:让模型在同一轮 tool result 里看到当前内容,无需多绕一次 Read。
|
|
88
|
+
// 安全语义保留:模型必须显式重发 Edit 才会落盘;snippet 只是给模型看的,不静默放行。
|
|
89
|
+
const freshness = await assertFresh(filePath, {}, state);
|
|
90
|
+
if (!freshness.ok) {
|
|
91
|
+
const snap = await tryAutoSnapshot(filePath);
|
|
92
|
+
if (snap.ok) {
|
|
93
|
+
// 关键:truncated=true 时 wasTruncated 会守住 Write 路径,防止基于残缺 snippet 重写整文件
|
|
94
|
+
recordRead(filePath, { truncated: snap.truncated, partial: snap.truncated }, state);
|
|
95
|
+
const tail = snap.truncated
|
|
96
|
+
? `\n\n⚠️ 仅注入了前 200 行(文件共 ${snap.totalLines} 行 / 上限 32KB)。若要改的位置在范围外,请显式 Read offset=X limit=Y 后再 Edit。`
|
|
97
|
+
: '';
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error:
|
|
101
|
+
freshness.error +
|
|
102
|
+
`\n\n以下是当前文件${snap.truncated ? '前 200 行' : '全部'}内容(带行号),请基于此重新构造 old_string 后立即重试 Edit,不要再单独调 Read:\n\n${snap.snippet}` +
|
|
103
|
+
tail,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const why =
|
|
107
|
+
snap.reason === 'too-large'
|
|
108
|
+
? `文件 ${((snap.sizeBytes ?? 0) / 1024).toFixed(1)} KB 超过 32KB 自动注入上限`
|
|
109
|
+
: snap.reason === 'binary'
|
|
110
|
+
? '文件命中二进制扩展名黑名单'
|
|
111
|
+
: '文件读取失败';
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: `${freshness.error}\n\n(${why},未注入内容快照,请用 Read 工具显式读取后再 Edit)`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (old_string === new_string) {
|
|
119
|
+
return { ok: false, error: 'old_string 与 new_string 完全相同,没有可改的内容。' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 5b. 创建新文件路径:old_string === '' 且文件不存在
|
|
123
|
+
if (old_string === '' && !existsSync(filePath)) {
|
|
124
|
+
try {
|
|
125
|
+
await atomicWriteFile(filePath, new_string);
|
|
126
|
+
recordRead(filePath, { contentHash: hashFileBytes(encodeTextFile(new_string, 'utf8')) }, state);
|
|
127
|
+
emitFileMutation(filePath);
|
|
128
|
+
const patch = buildPatchSummary('', new_string, input.file_path);
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
content: `已创建新文件:${filePath}(${new_string.length} 字符)\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
|
|
132
|
+
metadata: await fileActivityMetadata(filePath, 'created', 'not_run', 'support', cwd, patch),
|
|
133
|
+
};
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return { ok: false, error: `创建文件失败:${(e as Error).message}` };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 5c. 既有文件:读 → 改 → 写
|
|
140
|
+
if (!existsSync(filePath)) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: `文件不存在:${filePath}\n(要创建新文件,请把 old_string 设为空字符串)`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let original: string;
|
|
148
|
+
let sourceEncoding: Awaited<ReturnType<typeof readTextFile>>['encoding'];
|
|
149
|
+
try {
|
|
150
|
+
const snapshot = await readTextFile(filePath);
|
|
151
|
+
original = snapshot.content;
|
|
152
|
+
sourceEncoding = snapshot.encoding;
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return { ok: false, error: `读取失败:${(e as Error).message}` };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 5d-1. 记录原文件行尾符风格(写入时保持一致)
|
|
158
|
+
const originalLineEnding = detectLineEndingsForString(original);
|
|
159
|
+
|
|
160
|
+
// 5d. 文件大小上限检查
|
|
161
|
+
const fileSize = Buffer.byteLength(original, 'utf8');
|
|
162
|
+
if (fileSize > MAX_EDIT_FILE_SIZE_BYTES) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
error: `文件过大(${fileSize} 字节 > ${MAX_EDIT_FILE_SIZE_BYTES} 字节 ≈ 1GB)。Edit 不适合操作超大文件,请用 Write 工具。`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 唯性检查
|
|
170
|
+
if (old_string === '') {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
error: 'old_string 为空但文件已存在 —— 这通常是错误的。要替换全文请用 Write 工具。',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 5e. 引号风格归一化:处理模型输出的直引号 vs 文件中的弯引号差异
|
|
178
|
+
let searchTarget = old_string;
|
|
179
|
+
let processedNewString = new_string;
|
|
180
|
+
const actualOld = findActualString(original, old_string);
|
|
181
|
+
if (actualOld !== null && actualOld !== old_string) {
|
|
182
|
+
searchTarget = actualOld;
|
|
183
|
+
processedNewString = preserveQuoteStyle(old_string, actualOld, new_string);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// splitCount = 出现次数
|
|
187
|
+
const occurrences = countOccurrences(original, searchTarget);
|
|
188
|
+
const truncHint = wasTruncated(filePath, state)
|
|
189
|
+
? `\n\n⚠️ 上次 Read 该文件时被截断了(只看到部分内容)。当前 old_string 可能在截断范围外。请用更小的 limit + offset 精确读取目标行号附近后再 Edit。`
|
|
190
|
+
: '';
|
|
191
|
+
if (occurrences === 0) {
|
|
192
|
+
// 模糊匹配提示:找找有没有"接近"的内容
|
|
193
|
+
const hint = findFuzzyMatchHint(original, searchTarget);
|
|
194
|
+
const extraMsg = hint ? `\n\n💡 提示:${hint}` : '';
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
error: `在 ${filePath} 中找不到 old_string。请先用 Read 工具核对内容(注意空格/缩进/换行)。${extraMsg}${truncHint}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (occurrences > 1 && !replaceAll) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: `old_string 在文件中出现了 ${occurrences} 次,不唯一。\n请扩大 old_string 包含更多上下文,或显式传 replace_all=true。${truncHint}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const replaced = replaceAll
|
|
208
|
+
? splitReplaceAll(original, searchTarget, processedNewString)
|
|
209
|
+
: original.replace(searchTarget, processedNewString); // 只替换第一处(已确认 occurrences === 1)
|
|
210
|
+
|
|
211
|
+
// 5f. 保持原文件行尾符风格
|
|
212
|
+
const normalizedReplaced = applyLineEnding(replaced, originalLineEnding);
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await atomicWriteFile(filePath, normalizedReplaced, sourceEncoding);
|
|
216
|
+
recordRead(filePath, {
|
|
217
|
+
contentHash: hashFileBytes(encodeTextFile(normalizedReplaced, sourceEncoding)),
|
|
218
|
+
}, state);
|
|
219
|
+
emitFileMutation(filePath);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
return { ok: false, error: `写入失败:${(e as Error).message}` };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 输出:报告改了几处 + 行变化
|
|
225
|
+
const linesBefore = original.split('\n').length;
|
|
226
|
+
const linesAfter = replaced.split('\n').length;
|
|
227
|
+
const patch = buildPatchSummary(original, normalizedReplaced, input.file_path);
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
content:
|
|
231
|
+
`已编辑 ${filePath}\n` +
|
|
232
|
+
`替换次数:${replaceAll ? occurrences : 1}\n` +
|
|
233
|
+
`行数:${linesBefore} → ${linesAfter}\n` + (patch.unifiedDiff ?? '[diff omitted: too large]'),
|
|
234
|
+
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------- 5. 模糊匹配提示 ----------------
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 当 old_string 精确匹配失败时,尝试在文件内容中找到相似行。
|
|
242
|
+
* 常见场景:模型传入的 old_string 缩进差一个 tab、末尾多一个空格等。
|
|
243
|
+
*
|
|
244
|
+
* 策略:
|
|
245
|
+
* 1. 按 \n 分割成行
|
|
246
|
+
* 2. 对每行计算与 old_string 的字符重叠率(最长公共子序列长度 / max(len))
|
|
247
|
+
* 3. 返回重叠率最高的前 N 行作为提示
|
|
248
|
+
*/
|
|
249
|
+
function findFuzzyMatchHint(fileContent: string, target: string): string | null {
|
|
250
|
+
const MIN_OVERLAP_RATIO = 0.5; // 至少 50% 字符重叠才认为"接近"
|
|
251
|
+
const MAX_HINTS = 3; // 最多提示 3 个候选
|
|
252
|
+
|
|
253
|
+
const targetTrimmed = target.trim();
|
|
254
|
+
if (targetTrimmed.length < 5) return null; // 太短的 target 不做模糊匹配
|
|
255
|
+
|
|
256
|
+
const lines = fileContent.split('\n');
|
|
257
|
+
|
|
258
|
+
type Candidate = { lineNum: number; text: string; ratio: number };
|
|
259
|
+
const candidates: Candidate[] = [];
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < lines.length; i++) {
|
|
262
|
+
const line = lines[i];
|
|
263
|
+
const trimmedLine = line.trim();
|
|
264
|
+
|
|
265
|
+
// 跳过空行和太短的行
|
|
266
|
+
if (trimmedLine.length < 5) continue;
|
|
267
|
+
|
|
268
|
+
const ratio = lcsRatio(targetTrimmed, trimmedLine);
|
|
269
|
+
if (ratio >= MIN_OVERLAP_RATIO) {
|
|
270
|
+
candidates.push({ lineNum: i + 1, text: trimmedLine, ratio });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 也检查跨行情况:如果 target 包含换行,尝试匹配连续多行
|
|
275
|
+
if (target.includes('\n') && candidates.length < MAX_HINTS) {
|
|
276
|
+
const targetLines = target.split('\n').filter((l) => l.trim().length > 0);
|
|
277
|
+
if (targetLines.length >= 2) {
|
|
278
|
+
for (let start = 0; start <= lines.length - targetLines.length; start++) {
|
|
279
|
+
const window = lines.slice(start, start + targetLines.length).map((l) => l.trim()).join('\n');
|
|
280
|
+
const ratio = lcsRatio(target.trim(), window);
|
|
281
|
+
if (ratio >= MIN_OVERLAP_RATIO) {
|
|
282
|
+
candidates.push({ lineNum: start + 1, text: window.slice(0, 80) + (window.length > 80 ? '...' : ''), ratio });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (candidates.length === 0) return null;
|
|
289
|
+
|
|
290
|
+
// 按重叠率降序排列,取前 N 个
|
|
291
|
+
candidates.sort((a, b) => b.ratio - a.ratio);
|
|
292
|
+
const top = candidates.slice(0, MAX_HINTS);
|
|
293
|
+
|
|
294
|
+
const hints = top.map(
|
|
295
|
+
(c) =>
|
|
296
|
+
`第 ${c.lineNum} 行附近有相似内容(相似度 ${(c.ratio * 100).toFixed(0)}%):\n "${c.text.slice(0, 100)}${c.text.length > 100 ? '…' : ''}"`,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
return `可能你想要修改以下位置之一?\n ${hints.join('\n ')}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 计算两个字符串的最长公共子序列(LCS)长度比。
|
|
304
|
+
* 使用动态规划,时间复杂度 O(m*n),但对短字符串足够快。
|
|
305
|
+
* 返回值范围 [0, 1],1 表示完全相同。
|
|
306
|
+
*/
|
|
307
|
+
function lcsRatio(a: string, b: string): number {
|
|
308
|
+
const m = a.length;
|
|
309
|
+
const n = b.length;
|
|
310
|
+
|
|
311
|
+
// 对太长的字符串截断处理(防止性能问题)
|
|
312
|
+
const MAX_LEN = 500;
|
|
313
|
+
const aa = m > MAX_LEN ? a.slice(0, MAX_LEN) : a;
|
|
314
|
+
const bb = n > MAX_LEN ? b.slice(0, MAX_LEN) : b;
|
|
315
|
+
const mm = aa.length;
|
|
316
|
+
const nn = bb.length;
|
|
317
|
+
|
|
318
|
+
// 用一维 DP 优化空间
|
|
319
|
+
const dp: number[] = new Array(nn + 1).fill(0);
|
|
320
|
+
|
|
321
|
+
for (let i = 1; i <= mm; i++) {
|
|
322
|
+
let prev = 0;
|
|
323
|
+
for (let j = 1; j <= nn; j++) {
|
|
324
|
+
const temp = dp[j];
|
|
325
|
+
if (aa[i - 1] === bb[j - 1]) {
|
|
326
|
+
dp[j] = prev + 1;
|
|
327
|
+
} else {
|
|
328
|
+
dp[j] = Math.max(dp[j], dp[j - 1]);
|
|
329
|
+
}
|
|
330
|
+
prev = temp;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return dp[nn] / Math.max(mm, nn);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---------------- 6. 导出 ----------------
|
|
338
|
+
export const editTool: Tool<EditInput> = {
|
|
339
|
+
name: 'Edit',
|
|
340
|
+
description,
|
|
341
|
+
inputSchema,
|
|
342
|
+
parameters,
|
|
343
|
+
isReadOnly: false,
|
|
344
|
+
isConcurrencySafe: false,
|
|
345
|
+
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
346
|
+
interruptBehavior: () => 'block',
|
|
347
|
+
call: (input) => call(input),
|
|
348
|
+
callWithContext: (input, _signal, context: ToolCallContext) => call(input, context.sessionState?.fileReads, context.cwd),
|
|
349
|
+
};
|
|
@@ -13,14 +13,16 @@
|
|
|
13
13
|
* 4. 不支持 "old_string='' 创建文件"(那是 Edit 的单文件场景,多 edit 无意义)
|
|
14
14
|
* ============================================================
|
|
15
15
|
*/
|
|
16
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
17
16
|
import { existsSync } from 'node:fs';
|
|
18
17
|
import { z } from 'zod';
|
|
19
18
|
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
20
19
|
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
21
|
-
import { resolveToolPath,
|
|
20
|
+
import { resolveToolPath, detectLineEndingsForString, applyLineEnding, findActualString, preserveQuoteStyle, countOccurrences, splitReplaceAll, tryAutoSnapshot, atomicWriteFile, encodeTextFile, readTextFile, } from '../shared/fileUtils.js';
|
|
22
21
|
import { filePathField } from '../shared/schemas.js';
|
|
23
|
-
import { assertFresh, recordRead, wasTruncated } from '../shared/fileState.js';
|
|
22
|
+
import { assertFresh, hashFileBytes, recordRead, wasTruncated } from '../shared/fileState.js';
|
|
23
|
+
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
24
|
+
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
25
|
+
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
24
26
|
// ---------------- 1. Zod 输入 schema ----------------
|
|
25
27
|
const editItemSchema = z.object({
|
|
26
28
|
old_string: z.string().min(1).describe('要替换的原文本(不允许为空 —— 创建新文件请用 Edit 工具)'),
|
|
@@ -52,18 +54,18 @@ Usage:
|
|
|
52
54
|
- Empty \`old_string\` is NOT allowed in MultiEdit. To create a new file, use the \`Edit\` tool with a single empty-old_string call.
|
|
53
55
|
- Preserve exact indentation (tabs/spaces).`;
|
|
54
56
|
// ---------------- 4. call() ----------------
|
|
55
|
-
async function call(input) {
|
|
57
|
+
async function call(input, state, cwd) {
|
|
56
58
|
// 4a. 路径规范化 + 安全校验
|
|
57
|
-
const pathResult = resolveToolPath(input.file_path);
|
|
59
|
+
const pathResult = resolveToolPath(input.file_path, cwd);
|
|
58
60
|
if (!pathResult.ok)
|
|
59
61
|
return pathResult;
|
|
60
62
|
const filePath = pathResult.resolvedPath;
|
|
61
63
|
// 4b. Pre-read guard —— 入口处一次性检查 + 注入 snippet(循环内多个 edit 不重复注入)
|
|
62
|
-
const freshness = assertFresh(filePath);
|
|
64
|
+
const freshness = await assertFresh(filePath, {}, state);
|
|
63
65
|
if (!freshness.ok) {
|
|
64
66
|
const snap = await tryAutoSnapshot(filePath);
|
|
65
67
|
if (snap.ok) {
|
|
66
|
-
recordRead(filePath, { truncated: snap.truncated });
|
|
68
|
+
recordRead(filePath, { truncated: snap.truncated, partial: snap.truncated }, state);
|
|
67
69
|
const tail = snap.truncated
|
|
68
70
|
? `\n\n⚠️ 仅注入了前 200 行(文件共 ${snap.totalLines} 行 / 上限 32KB)。若要改的位置在范围外,请显式 Read offset=X limit=Y 后再 MultiEdit。`
|
|
69
71
|
: '';
|
|
@@ -106,15 +108,18 @@ async function call(input) {
|
|
|
106
108
|
}
|
|
107
109
|
// 4e. 读原文
|
|
108
110
|
let originalContent;
|
|
111
|
+
let sourceEncoding;
|
|
109
112
|
try {
|
|
110
|
-
|
|
113
|
+
const snapshot = await readTextFile(filePath);
|
|
114
|
+
originalContent = snapshot.content;
|
|
115
|
+
sourceEncoding = snapshot.encoding;
|
|
111
116
|
}
|
|
112
117
|
catch (e) {
|
|
113
118
|
return { ok: false, error: `读取失败:${e.message}` };
|
|
114
119
|
}
|
|
115
120
|
// 4f. 在内存中顺序应用所有 edit(任一失败立即返回,磁盘不动)
|
|
116
121
|
let currentContent = originalContent;
|
|
117
|
-
const truncHint = wasTruncated(filePath)
|
|
122
|
+
const truncHint = wasTruncated(filePath, state)
|
|
118
123
|
? `\n\n⚠️ 上次 Read 该文件时被截断了(只看到部分内容)。当前 old_string 可能在截断范围外。请用更小的 limit + offset 精确读取目标行号附近后再 MultiEdit。`
|
|
119
124
|
: '';
|
|
120
125
|
for (let i = 0; i < edits.length; i++) {
|
|
@@ -152,18 +157,23 @@ async function call(input) {
|
|
|
152
157
|
: currentContent.replace(searchTarget, processedNewString);
|
|
153
158
|
}
|
|
154
159
|
// 4g. 全部成功 → 保持原文件行尾符风格,写盘
|
|
155
|
-
const lineEnding =
|
|
160
|
+
const lineEnding = detectLineEndingsForString(originalContent);
|
|
156
161
|
const finalContent = applyLineEnding(currentContent, lineEnding);
|
|
157
162
|
try {
|
|
158
|
-
await
|
|
159
|
-
recordRead(filePath
|
|
163
|
+
await atomicWriteFile(filePath, finalContent, sourceEncoding);
|
|
164
|
+
recordRead(filePath, {
|
|
165
|
+
contentHash: hashFileBytes(encodeTextFile(finalContent, sourceEncoding)),
|
|
166
|
+
}, state);
|
|
167
|
+
emitFileMutation(filePath);
|
|
160
168
|
}
|
|
161
169
|
catch (e) {
|
|
162
170
|
return { ok: false, error: `写入失败:${e.message}` };
|
|
163
171
|
}
|
|
172
|
+
const patch = buildPatchSummary(originalContent, finalContent, input.file_path);
|
|
164
173
|
return {
|
|
165
174
|
ok: true,
|
|
166
|
-
content: `已对 ${filePath} 应用 ${edits.length}
|
|
175
|
+
content: `已对 ${filePath} 应用 ${edits.length} 处修改\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
|
|
176
|
+
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
167
177
|
};
|
|
168
178
|
}
|
|
169
179
|
// ---------------- 5. 导出 ----------------
|
|
@@ -176,5 +186,6 @@ export const multiEditTool = {
|
|
|
176
186
|
isConcurrencySafe: false,
|
|
177
187
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
178
188
|
interruptBehavior: () => 'block',
|
|
179
|
-
call,
|
|
189
|
+
call: (input) => call(input),
|
|
190
|
+
callWithContext: (input, _signal, context) => call(input, context.sessionState?.fileReads, context.cwd),
|
|
180
191
|
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* src/tools/edit/multi-edit.ts —— MultiEdit 工具(原子化多处替换)
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 对应 kakadeai 主仓库的 FileEditTool.getPatchForEdits 设计思路,
|
|
6
|
+
* 但比上游更严格:**all-or-nothing**。
|
|
7
|
+
*
|
|
8
|
+
* 与 Edit 的关键差异:
|
|
9
|
+
* 1. 一次处理多条 edit,按顺序在内存中应用,全部成功才写盘
|
|
10
|
+
* 2. 任何一条匹配失败 / 多义匹配 → 全部回退(磁盘不动一字)
|
|
11
|
+
* 3. 依赖检测:edits[i].old_string 若是 edits[j<i].new_string 的子串,
|
|
12
|
+
* 会命中前序产物,直接拒绝(要求调用方重排序或合并)
|
|
13
|
+
* 4. 不支持 "old_string='' 创建文件"(那是 Edit 的单文件场景,多 edit 无意义)
|
|
14
|
+
* ============================================================
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
|
|
21
|
+
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
22
|
+
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
23
|
+
import {
|
|
24
|
+
resolveToolPath,
|
|
25
|
+
detectLineEndingsForString,
|
|
26
|
+
applyLineEnding,
|
|
27
|
+
findActualString,
|
|
28
|
+
preserveQuoteStyle,
|
|
29
|
+
countOccurrences,
|
|
30
|
+
splitReplaceAll,
|
|
31
|
+
tryAutoSnapshot,
|
|
32
|
+
atomicWriteFile,
|
|
33
|
+
encodeTextFile,
|
|
34
|
+
readTextFile,
|
|
35
|
+
} from '../shared/fileUtils.js';
|
|
36
|
+
import { filePathField } from '../shared/schemas.js';
|
|
37
|
+
import { assertFresh, hashFileBytes, recordRead, wasTruncated, type FileReadState } from '../shared/fileState.js';
|
|
38
|
+
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
39
|
+
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
40
|
+
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
41
|
+
|
|
42
|
+
// ---------------- 1. Zod 输入 schema ----------------
|
|
43
|
+
const editItemSchema = z.object({
|
|
44
|
+
old_string: z.string().min(1).describe('要替换的原文本(不允许为空 —— 创建新文件请用 Edit 工具)'),
|
|
45
|
+
new_string: z.string().describe('替换为的新文本'),
|
|
46
|
+
replace_all: z
|
|
47
|
+
.boolean()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('是否替换所有出现位置(默认 false,要求 old_string 在当前内容中唯一)'),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const inputSchema = z.object({
|
|
53
|
+
file_path: filePathField('编辑'),
|
|
54
|
+
edits: z
|
|
55
|
+
.array(editItemSchema)
|
|
56
|
+
.min(1)
|
|
57
|
+
.max(50)
|
|
58
|
+
.describe('按顺序应用的 edit 列表(1-50 条),原子化执行:全部成功才落盘'),
|
|
59
|
+
});
|
|
60
|
+
type MultiEditInput = z.infer<typeof inputSchema>;
|
|
61
|
+
|
|
62
|
+
// ---------------- 2. JSON Schema ----------------
|
|
63
|
+
const parameters = toToolParameters(inputSchema);
|
|
64
|
+
|
|
65
|
+
// ---------------- 3. Description ----------------
|
|
66
|
+
const description = `Performs multiple exact string replacements in a single file, applied atomically (all-or-nothing).
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
- You MUST use your \`Read\` tool to read the current content of the file BEFORE calling MultiEdit.
|
|
70
|
+
- Provide a list of \`edits\`, each with \`old_string\`, \`new_string\`, and optional \`replace_all\`.
|
|
71
|
+
- All edits are applied sequentially in memory; if ANY edit fails (string not found, ambiguous match, or dependency conflict), the file on disk is UNTOUCHED.
|
|
72
|
+
- Order matters: later edits operate on the result of earlier edits. If a later edit's \`old_string\` is a substring of an earlier edit's \`new_string\`, MultiEdit refuses (reorder or merge instead).
|
|
73
|
+
- Each \`old_string\` must be unique in the current content (after prior edits) unless \`replace_all=true\`.
|
|
74
|
+
- Empty \`old_string\` is NOT allowed in MultiEdit. To create a new file, use the \`Edit\` tool with a single empty-old_string call.
|
|
75
|
+
- Preserve exact indentation (tabs/spaces).`;
|
|
76
|
+
|
|
77
|
+
// ---------------- 4. call() ----------------
|
|
78
|
+
async function call(input: MultiEditInput, state?: FileReadState, cwd?: string): Promise<ToolResult> {
|
|
79
|
+
// 4a. 路径规范化 + 安全校验
|
|
80
|
+
const pathResult = resolveToolPath(input.file_path, cwd);
|
|
81
|
+
if (!pathResult.ok) return pathResult;
|
|
82
|
+
const filePath = pathResult.resolvedPath;
|
|
83
|
+
|
|
84
|
+
// 4b. Pre-read guard —— 入口处一次性检查 + 注入 snippet(循环内多个 edit 不重复注入)
|
|
85
|
+
const freshness = await assertFresh(filePath, {}, state);
|
|
86
|
+
if (!freshness.ok) {
|
|
87
|
+
const snap = await tryAutoSnapshot(filePath);
|
|
88
|
+
if (snap.ok) {
|
|
89
|
+
recordRead(filePath, { truncated: snap.truncated, partial: snap.truncated }, state);
|
|
90
|
+
const tail = snap.truncated
|
|
91
|
+
? `\n\n⚠️ 仅注入了前 200 行(文件共 ${snap.totalLines} 行 / 上限 32KB)。若要改的位置在范围外,请显式 Read offset=X limit=Y 后再 MultiEdit。`
|
|
92
|
+
: '';
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error:
|
|
96
|
+
freshness.error +
|
|
97
|
+
`\n\n以下是当前文件${snap.truncated ? '前 200 行' : '全部'}内容(带行号),请基于此重新构造各 edit 的 old_string 后立即重试 MultiEdit,不要再单独调 Read:\n\n${snap.snippet}` +
|
|
98
|
+
tail,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const why =
|
|
102
|
+
snap.reason === 'too-large'
|
|
103
|
+
? `文件 ${((snap.sizeBytes ?? 0) / 1024).toFixed(1)} KB 超过 32KB 自动注入上限`
|
|
104
|
+
: snap.reason === 'binary'
|
|
105
|
+
? '文件命中二进制扩展名黑名单'
|
|
106
|
+
: '文件读取失败';
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: `${freshness.error}\n\n(${why},未注入内容快照,请用 Read 工具显式读取后再 MultiEdit)`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 4c. 文件必须存在(MultiEdit 不做"创建新文件"语义)
|
|
114
|
+
if (!existsSync(filePath)) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: `文件不存在:${filePath}\n(MultiEdit 不支持创建新文件,请改用 Edit 工具)`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 4d. 依赖检测:edits[i].old_string 若是 edits[j<i].new_string 的子串,
|
|
122
|
+
// 后续 edit 会命中前序产物,直接拒绝。
|
|
123
|
+
const edits = input.edits;
|
|
124
|
+
for (let i = 0; i < edits.length; i++) {
|
|
125
|
+
for (let j = 0; j < i; j++) {
|
|
126
|
+
if (edits[j]!.new_string.includes(edits[i]!.old_string)) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
error: `edits[${i}].old_string 是 edits[${j}].new_string 的子串,会导致后续 edit 命中前序产物,请重新排序或合并`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 4e. 读原文
|
|
136
|
+
let originalContent: string;
|
|
137
|
+
let sourceEncoding: Awaited<ReturnType<typeof readTextFile>>['encoding'];
|
|
138
|
+
try {
|
|
139
|
+
const snapshot = await readTextFile(filePath);
|
|
140
|
+
originalContent = snapshot.content;
|
|
141
|
+
sourceEncoding = snapshot.encoding;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
return { ok: false, error: `读取失败:${(e as Error).message}` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 4f. 在内存中顺序应用所有 edit(任一失败立即返回,磁盘不动)
|
|
147
|
+
let currentContent = originalContent;
|
|
148
|
+
const truncHint = wasTruncated(filePath, state)
|
|
149
|
+
? `\n\n⚠️ 上次 Read 该文件时被截断了(只看到部分内容)。当前 old_string 可能在截断范围外。请用更小的 limit + offset 精确读取目标行号附近后再 MultiEdit。`
|
|
150
|
+
: '';
|
|
151
|
+
for (let i = 0; i < edits.length; i++) {
|
|
152
|
+
const edit = edits[i]!;
|
|
153
|
+
const replaceAll = edit.replace_all ?? false;
|
|
154
|
+
|
|
155
|
+
// 引号风格归一化
|
|
156
|
+
let searchTarget = edit.old_string;
|
|
157
|
+
let processedNewString = edit.new_string;
|
|
158
|
+
const actualOld = findActualString(currentContent, edit.old_string);
|
|
159
|
+
if (actualOld === null) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
error: `edits[${i}] 未匹配到 old_string(在已应用前序 ${i} 处修改后的内容中找不到)。请先用 Read 工具核对当前内容(注意空格/缩进/换行),并检查 edit 顺序。${truncHint}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (actualOld !== edit.old_string) {
|
|
166
|
+
searchTarget = actualOld;
|
|
167
|
+
processedNewString = preserveQuoteStyle(edit.old_string, actualOld, edit.new_string);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const occurrences = countOccurrences(currentContent, searchTarget);
|
|
171
|
+
if (occurrences === 0) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
error: `edits[${i}] 未匹配到 old_string(已应用前序 ${i} 处修改后内容中出现 0 次)。${truncHint}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (occurrences > 1 && !replaceAll) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
error: `edits[${i}].old_string 在当前内容中出现 ${occurrences} 次,不唯一。请扩大 old_string 包含更多上下文,或显式传 replace_all=true。${truncHint}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
currentContent = replaceAll
|
|
185
|
+
? splitReplaceAll(currentContent, searchTarget, processedNewString)
|
|
186
|
+
: currentContent.replace(searchTarget, processedNewString);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 4g. 全部成功 → 保持原文件行尾符风格,写盘
|
|
190
|
+
const lineEnding = detectLineEndingsForString(originalContent);
|
|
191
|
+
const finalContent = applyLineEnding(currentContent, lineEnding);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await atomicWriteFile(filePath, finalContent, sourceEncoding);
|
|
195
|
+
recordRead(filePath, {
|
|
196
|
+
contentHash: hashFileBytes(encodeTextFile(finalContent, sourceEncoding)),
|
|
197
|
+
}, state);
|
|
198
|
+
emitFileMutation(filePath);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
return { ok: false, error: `写入失败:${(e as Error).message}` };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const patch = buildPatchSummary(originalContent, finalContent, input.file_path);
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
content: `已对 ${filePath} 应用 ${edits.length} 处修改\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
|
|
207
|
+
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------- 5. 导出 ----------------
|
|
212
|
+
export const multiEditTool: Tool<MultiEditInput> = {
|
|
213
|
+
name: 'MultiEdit',
|
|
214
|
+
description,
|
|
215
|
+
inputSchema,
|
|
216
|
+
parameters,
|
|
217
|
+
isReadOnly: false,
|
|
218
|
+
isConcurrencySafe: false,
|
|
219
|
+
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
220
|
+
interruptBehavior: () => 'block',
|
|
221
|
+
call: (input) => call(input),
|
|
222
|
+
callWithContext: (input, _signal, context: ToolCallContext) => call(input, context.sessionState?.fileReads, context.cwd),
|
|
223
|
+
};
|