ethagent 3.3.3 → 4.0.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/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +35 -0
- package/LICENSE +1 -1
- package/README.md +64 -104
- package/commands/ethagent.md +40 -0
- package/package.json +16 -16
- package/src/app/keybindings/KeybindingProvider.tsx +1 -6
- package/src/app/keybindings/types.ts +1 -6
- package/src/cli/ResetConfirmView.tsx +54 -53
- package/src/cli/demo.ts +86 -0
- package/src/cli/hookIo.ts +45 -0
- package/src/cli/main.tsx +94 -123
- package/src/cli/memoryGuard.ts +49 -0
- package/src/cli/reset.ts +28 -70
- package/src/cli/sessionStart.ts +33 -0
- package/src/cli/status.ts +46 -0
- package/src/cli/sync.ts +167 -0
- package/src/cli/syncAdapters/claude-code.ts +86 -0
- package/src/cli/syncAdapters/codex.ts +66 -0
- package/src/cli/syncAdapters/index.ts +45 -0
- package/src/cli/syncAdapters/managedBlock.ts +175 -0
- package/src/cli/syncAdapters/shared.ts +63 -0
- package/src/identity/continuity/envelopeParse.ts +20 -1
- package/src/identity/continuity/publicSkills.ts +3 -1
- package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
- package/src/identity/continuity/skills/scaffold.ts +5 -2
- package/src/identity/continuity/snapshots.ts +12 -5
- package/src/identity/continuity/storage/defaults.ts +20 -19
- package/src/identity/continuity/storage/status.ts +1 -1
- package/src/identity/ens/ensLookup/constants.ts +1 -1
- package/src/identity/manager/IdentityManager.tsx +33 -0
- package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
- package/src/identity/{hub → manager}/Routes.tsx +48 -34
- package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
- package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
- package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
- package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
- package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
- package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
- package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
- package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
- package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
- package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
- package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
- package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
- package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
- package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
- package/src/identity/{hub → manager}/create/effects.ts +19 -10
- package/src/identity/manager/create/importScan.ts +122 -0
- package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
- package/src/identity/{hub → manager}/custody/actions.ts +1 -15
- package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
- package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
- package/src/identity/{hub → manager}/custody/types.ts +1 -2
- package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
- package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
- package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
- package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
- package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
- package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
- package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
- package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
- package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
- package/src/identity/{hub → manager}/profile/effects.ts +1 -3
- package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
- package/src/identity/{hub → manager}/profile/state.ts +1 -1
- package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
- package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
- package/src/identity/{hub → manager}/restore/apply.ts +1 -1
- package/src/identity/{hub → manager}/restore/auth.ts +1 -1
- package/src/identity/{hub → manager}/restore/discover.ts +1 -1
- package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
- package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
- package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
- package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
- package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
- package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
- package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
- package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
- package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
- package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
- package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
- package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
- package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
- package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
- package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
- package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
- package/src/identity/manager/shared/effects/types.ts +30 -0
- package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
- package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
- package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
- package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
- package/src/identity/{hub → manager}/shared/utils.ts +6 -10
- package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
- package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
- package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
- package/src/identity/{hub → manager}/types.ts +5 -6
- package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
- package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
- package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
- package/src/identity/registry/erc8004/discovery.ts +3 -17
- package/src/identity/registry/erc8004/utils.ts +1 -1
- package/src/identity/storage/ipfs.ts +21 -1
- package/src/identity/wallet/browserWallet/html.ts +10 -2
- package/src/identity/wallet/browserWallet/http.ts +18 -0
- package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
- package/src/identity/wallet/browserWallet/requests.ts +10 -28
- package/src/identity/wallet/browserWallet/session.ts +26 -33
- package/src/identity/wallet/browserWallet/validation.ts +14 -0
- package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
- package/src/identity/wallet/page/boot.ts +43 -0
- package/src/identity/wallet/page/config.ts +59 -0
- package/src/identity/wallet/page/constants.ts +12 -0
- package/src/identity/wallet/page/copy.ts +47 -68
- package/src/identity/wallet/page/css.ts +638 -0
- package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
- package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
- package/src/identity/wallet/page/markup.ts +44 -34
- package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
- package/src/identity/wallet/page/resize.ts +95 -0
- package/src/identity/wallet/page/state.ts +135 -8
- package/src/identity/wallet/page/timeline.ts +161 -0
- package/src/identity/wallet/page/view.ts +22 -302
- package/src/storage/config.ts +30 -80
- package/src/storage/reset.ts +31 -0
- package/src/storage/secrets.ts +1 -16
- package/src/ui/Select.tsx +27 -5
- package/src/ui/Spinner.tsx +16 -15
- package/src/ui/Surface.tsx +21 -17
- package/src/ui/TextArea.tsx +173 -0
- package/src/ui/TextInput.tsx +31 -133
- package/src/ui/theme.ts +22 -13
- package/src/utils/clipboard.ts +0 -140
- package/src/app/FirstRun.tsx +0 -577
- package/src/app/FirstRunTimeline.tsx +0 -51
- package/src/app/firstRunConfig.ts +0 -26
- package/src/app/hooks/useCancelRequest.ts +0 -22
- package/src/app/hooks/useDoublePress.ts +0 -46
- package/src/app/hooks/useExitOnCtrlC.ts +0 -36
- package/src/auth/openaiOAuth/credentials.ts +0 -47
- package/src/auth/openaiOAuth/crypto.ts +0 -23
- package/src/auth/openaiOAuth/index.ts +0 -238
- package/src/auth/openaiOAuth/landingPage.ts +0 -116
- package/src/auth/openaiOAuth/listener.ts +0 -151
- package/src/auth/openaiOAuth/refresh.ts +0 -70
- package/src/auth/openaiOAuth/shared.ts +0 -115
- package/src/chat/ChatBottomPane.tsx +0 -296
- package/src/chat/ChatScreen.tsx +0 -1685
- package/src/chat/ConversationStack.tsx +0 -56
- package/src/chat/MessageList.tsx +0 -638
- package/src/chat/SessionStatus.tsx +0 -53
- package/src/chat/chatEnvironment.ts +0 -16
- package/src/chat/chatScreenUtils.ts +0 -194
- package/src/chat/chatSessionState.ts +0 -146
- package/src/chat/chatTurnContext.ts +0 -50
- package/src/chat/chatTurnOrchestrator.ts +0 -603
- package/src/chat/chatTurnRows.ts +0 -64
- package/src/chat/commands.ts +0 -494
- package/src/chat/continuityEditReview.ts +0 -42
- package/src/chat/display/DiffView.tsx +0 -193
- package/src/chat/display/SyntaxText.tsx +0 -192
- package/src/chat/display/toolCallDisplay.ts +0 -103
- package/src/chat/display/toolResultDisplay.ts +0 -19
- package/src/chat/input/ChatInput.tsx +0 -625
- package/src/chat/input/chatInputHelpers.ts +0 -62
- package/src/chat/input/chatInputState.ts +0 -247
- package/src/chat/input/chatPaste.ts +0 -49
- package/src/chat/input/imageRefs.ts +0 -30
- package/src/chat/input/inputRendering.tsx +0 -93
- package/src/chat/input/textCursor.ts +0 -212
- package/src/chat/messageMarkdown.ts +0 -220
- package/src/chat/messageRows.ts +0 -43
- package/src/chat/planImplementation.ts +0 -62
- package/src/chat/slashCommandHandlers.ts +0 -122
- package/src/chat/slashCommandViews.ts +0 -120
- package/src/chat/transcript/TranscriptView.tsx +0 -184
- package/src/chat/transcript/transcriptViewport.ts +0 -295
- package/src/chat/views/ContextLimitView.tsx +0 -95
- package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
- package/src/chat/views/CopyPicker.tsx +0 -50
- package/src/chat/views/PermissionPrompt.tsx +0 -156
- package/src/chat/views/PermissionsView.tsx +0 -165
- package/src/chat/views/PlanApprovalView.tsx +0 -91
- package/src/chat/views/ResumeView.tsx +0 -273
- package/src/chat/views/RewindView.tsx +0 -412
- package/src/cli/preview.tsx +0 -14
- package/src/cli/updateNotice.ts +0 -54
- package/src/identity/continuity/privateEdit/apply.ts +0 -170
- package/src/identity/continuity/privateEdit/diff.ts +0 -6
- package/src/identity/continuity/privateEdit/files.ts +0 -23
- package/src/identity/continuity/privateEdit/types.ts +0 -28
- package/src/identity/continuity/privateEdit.ts +0 -46
- package/src/identity/hub/IdentityHub.tsx +0 -14
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
- package/src/identity/hub/ens/effects.ts +0 -218
- package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
- package/src/identity/hub/shared/effects/types.ts +0 -53
- package/src/identity/hub/shared/reconciliation/index.ts +0 -14
- package/src/identity/wallet/page/grainient.ts +0 -278
- package/src/identity/wallet/page/html.ts +0 -28
- package/src/identity/wallet/page/styles/base.ts +0 -259
- package/src/identity/wallet/page/styles/components.ts +0 -262
- package/src/identity/wallet/page/styles/index.ts +0 -5
- package/src/identity/wallet/page/styles/responsive.ts +0 -247
- package/src/identity/wallet/page.tsx +0 -38
- package/src/mcp/approvals.ts +0 -113
- package/src/mcp/config.ts +0 -235
- package/src/mcp/manager.ts +0 -482
- package/src/mcp/managerHelpers.ts +0 -70
- package/src/mcp/names.ts +0 -19
- package/src/mcp/output.ts +0 -96
- package/src/models/ModelPicker.tsx +0 -1009
- package/src/models/catalog.ts +0 -327
- package/src/models/huggingface.ts +0 -712
- package/src/models/huggingfaceStorage.ts +0 -136
- package/src/models/llamacpp.ts +0 -848
- package/src/models/llamacppCommands.ts +0 -44
- package/src/models/llamacppConfig.ts +0 -34
- package/src/models/llamacppDiscovery.ts +0 -176
- package/src/models/llamacppOutput.ts +0 -65
- package/src/models/llamacppPreflight.ts +0 -158
- package/src/models/modelDisplay.ts +0 -180
- package/src/models/modelPickerCatalogFlow.ts +0 -56
- package/src/models/modelPickerCredentials.ts +0 -166
- package/src/models/modelPickerData.ts +0 -41
- package/src/models/modelPickerDisplay.tsx +0 -132
- package/src/models/modelPickerHfFlow.ts +0 -192
- package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
- package/src/models/modelPickerOptions.ts +0 -457
- package/src/models/modelPickerTypes.ts +0 -69
- package/src/models/modelPickerUninstallFlow.ts +0 -48
- package/src/models/modelPickerViewHelpers.ts +0 -174
- package/src/models/modelRecommendation.ts +0 -139
- package/src/models/providerDisplay.ts +0 -16
- package/src/models/runtimeDetection.ts +0 -81
- package/src/models/uncensoredCatalog.ts +0 -86
- package/src/providers/anthropic.ts +0 -290
- package/src/providers/contracts.ts +0 -71
- package/src/providers/errors.ts +0 -80
- package/src/providers/gemini.ts +0 -391
- package/src/providers/openai-chat.ts +0 -474
- package/src/providers/openai-responses-format.ts +0 -177
- package/src/providers/openai-responses.ts +0 -306
- package/src/providers/openaiChatWire.ts +0 -124
- package/src/providers/registry.ts +0 -120
- package/src/providers/retry.ts +0 -58
- package/src/providers/sse.ts +0 -93
- package/src/runtime/compaction.ts +0 -395
- package/src/runtime/cwd.ts +0 -43
- package/src/runtime/providerTurn.ts +0 -38
- package/src/runtime/sessionMode.ts +0 -55
- package/src/runtime/systemPrompt.ts +0 -213
- package/src/runtime/textToolParser.ts +0 -161
- package/src/runtime/toolClaimGuards.ts +0 -143
- package/src/runtime/toolExecution.ts +0 -304
- package/src/runtime/toolIntent.ts +0 -143
- package/src/runtime/turn.ts +0 -369
- package/src/runtime/turnNudges.ts +0 -223
- package/src/runtime/turnTypes.ts +0 -86
- package/src/storage/factoryReset.ts +0 -127
- package/src/storage/history.ts +0 -58
- package/src/storage/permissions.ts +0 -76
- package/src/storage/rewind.ts +0 -266
- package/src/storage/sessionExport.ts +0 -49
- package/src/storage/sessions.ts +0 -495
- package/src/tools/bashSafety.ts +0 -186
- package/src/tools/bashTool.ts +0 -140
- package/src/tools/changeDirectoryTool.ts +0 -213
- package/src/tools/contracts.ts +0 -192
- package/src/tools/deleteFileTool.ts +0 -116
- package/src/tools/editTool.ts +0 -165
- package/src/tools/editUtils.ts +0 -170
- package/src/tools/fileDiff.ts +0 -261
- package/src/tools/listDirectoryTool.ts +0 -55
- package/src/tools/listSkillFilesTool.ts +0 -77
- package/src/tools/listSkillsTool.ts +0 -68
- package/src/tools/mcpResourceTools.ts +0 -95
- package/src/tools/permissionRules.ts +0 -85
- package/src/tools/privateContinuityEditTool.ts +0 -187
- package/src/tools/privateContinuityReadTool.ts +0 -106
- package/src/tools/readSkillTool.ts +0 -107
- package/src/tools/readTool.ts +0 -85
- package/src/tools/registry.ts +0 -103
- package/src/tools/writeFileTool.ts +0 -167
- package/src/ui/BrandSplash.tsx +0 -133
- package/src/ui/terminalTitle.ts +0 -30
- package/src/utils/images.ts +0 -140
- package/src/utils/markdownSegments.ts +0 -51
- package/src/utils/messages.ts +0 -37
- package/src/utils/withRetry.ts +0 -324
- /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
- /package/src/identity/{hub → manager}/custody/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
- /package/src/identity/{hub → manager}/ens/state.ts +0 -0
- /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
- /package/src/identity/{hub → manager}/ens/types.ts +0 -0
- /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
- /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
- /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
- /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
- /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
- /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
- /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
- /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
- /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
package/src/tools/editTool.ts
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { z } from 'zod'
|
|
4
|
-
import { recordRewindSnapshot } from '../storage/rewind.js'
|
|
5
|
-
import type { EthagentConfig } from '../storage/config.js'
|
|
6
|
-
import type { Tool } from './contracts.js'
|
|
7
|
-
import { applyRequestedEdit } from './editUtils.js'
|
|
8
|
-
import { formatFileChangeResult, renderUnifiedFileDiff } from './fileDiff.js'
|
|
9
|
-
import { resolveWorkspacePath } from './readTool.js'
|
|
10
|
-
|
|
11
|
-
const schema = z.object({
|
|
12
|
-
path: z.string().min(1),
|
|
13
|
-
oldText: z.string().optional(),
|
|
14
|
-
newText: z.string(),
|
|
15
|
-
replaceAll: z.boolean().optional(),
|
|
16
|
-
replaceWholeFile: z.boolean().optional(),
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
export const editTool: Tool<typeof schema> = {
|
|
20
|
-
name: 'edit_file',
|
|
21
|
-
kind: 'edit',
|
|
22
|
-
description: 'Edit a workspace text file. Provide oldText and newText for targeted replacement, or just newText only for ordinary whole-file workspace edits. Do not use for private SOUL.md or MEMORY.md when an identity is linked; use propose_private_continuity_edit instead.',
|
|
23
|
-
inputSchema: schema,
|
|
24
|
-
inputSchemaJson: {
|
|
25
|
-
type: 'object',
|
|
26
|
-
properties: {
|
|
27
|
-
path: { type: 'string', description: 'Path to the file to edit.' },
|
|
28
|
-
oldText: { type: 'string', description: 'Exact text to find and replace. Prefer this for existing files. Omit only for ordinary whole-file workspace edits.' },
|
|
29
|
-
newText: { type: 'string', description: 'Replacement text, or entire file contents if oldText is omitted.' },
|
|
30
|
-
replaceAll: { type: 'boolean', description: 'Replace every exact oldText match. Prefer false unless you are certain.' },
|
|
31
|
-
},
|
|
32
|
-
required: ['path', 'newText'],
|
|
33
|
-
},
|
|
34
|
-
parse(input) {
|
|
35
|
-
return schema.parse(input)
|
|
36
|
-
},
|
|
37
|
-
async buildPermissionRequest(input, context) {
|
|
38
|
-
const { fullPath, relativePath, applied } = await prepareEdit(input, context)
|
|
39
|
-
return {
|
|
40
|
-
kind: 'edit',
|
|
41
|
-
path: fullPath,
|
|
42
|
-
relativePath,
|
|
43
|
-
directoryPath: path.dirname(fullPath),
|
|
44
|
-
title: 'Allow file edit?',
|
|
45
|
-
subtitle: fullPath,
|
|
46
|
-
before: applied.previewBefore,
|
|
47
|
-
after: applied.previewAfter,
|
|
48
|
-
diff: renderUnifiedFileDiff({ filePath: relativePath, before: applied.before, after: applied.after }),
|
|
49
|
-
changeSummary: applied.summary,
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
async execute(input, context) {
|
|
53
|
-
const { fullPath, relativePath, applied, existedBefore, before } = await prepareEdit(input, context)
|
|
54
|
-
const rewindWarning = await tryRecordRewindSnapshot({
|
|
55
|
-
workspaceRoot: context.workspaceRoot,
|
|
56
|
-
filePath: fullPath,
|
|
57
|
-
relativePath,
|
|
58
|
-
existedBefore,
|
|
59
|
-
previousContent: before,
|
|
60
|
-
changeSummary: applied.summary,
|
|
61
|
-
createdAt: new Date().toISOString(),
|
|
62
|
-
sessionId: context.checkpoint?.sessionId,
|
|
63
|
-
turnId: context.checkpoint?.turnId,
|
|
64
|
-
messageRole: context.checkpoint?.messageRole,
|
|
65
|
-
promptSnippet: context.checkpoint?.promptSnippet,
|
|
66
|
-
checkpointLabel: context.checkpoint?.checkpointLabel,
|
|
67
|
-
})
|
|
68
|
-
await fs.mkdir(path.dirname(fullPath), { recursive: true })
|
|
69
|
-
await fs.writeFile(fullPath, applied.after, 'utf8')
|
|
70
|
-
return {
|
|
71
|
-
ok: true,
|
|
72
|
-
summary: applied.summary,
|
|
73
|
-
content: formatFileChangeResult(
|
|
74
|
-
rewindWarning
|
|
75
|
-
? `updated ${fullPath}\nwarning: ${rewindWarning}`
|
|
76
|
-
: `updated ${fullPath}`,
|
|
77
|
-
renderUnifiedFileDiff({ filePath: relativePath, before: applied.before, after: applied.after }),
|
|
78
|
-
),
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function tryRecordRewindSnapshot(
|
|
84
|
-
snapshot: Parameters<typeof recordRewindSnapshot>[0],
|
|
85
|
-
): Promise<string | undefined> {
|
|
86
|
-
try {
|
|
87
|
-
await recordRewindSnapshot(snapshot)
|
|
88
|
-
return undefined
|
|
89
|
-
} catch (error: unknown) {
|
|
90
|
-
const message = (error as Error).message || 'rewind checkpoint could not be recorded'
|
|
91
|
-
return `rewind checkpoint was not recorded (${message})`
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function prepareEdit(input: z.infer<typeof schema>, context: { workspaceRoot: string; config?: EthagentConfig }) {
|
|
96
|
-
assertSafeEditPath(input.path)
|
|
97
|
-
assertNotPrivateContinuityWorkspacePath(input.path, context.config, 'edit_file')
|
|
98
|
-
const fullPath = resolveWorkspacePath(context.workspaceRoot, input.path)
|
|
99
|
-
await assertEditableFileTarget(fullPath)
|
|
100
|
-
const { content: before, existed } = await readOptionalTextFile(fullPath)
|
|
101
|
-
const applied = applyRequestedEdit(
|
|
102
|
-
input.path,
|
|
103
|
-
before,
|
|
104
|
-
input.oldText,
|
|
105
|
-
input.newText,
|
|
106
|
-
input.replaceAll ?? false,
|
|
107
|
-
input.replaceWholeFile ?? false,
|
|
108
|
-
)
|
|
109
|
-
return {
|
|
110
|
-
fullPath,
|
|
111
|
-
relativePath: path.relative(context.workspaceRoot, fullPath) || path.basename(fullPath),
|
|
112
|
-
existedBefore: existed,
|
|
113
|
-
before,
|
|
114
|
-
applied,
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function assertEditableFileTarget(fullPath: string): Promise<void> {
|
|
119
|
-
try {
|
|
120
|
-
const stats = await fs.stat(fullPath)
|
|
121
|
-
if (stats.isDirectory()) {
|
|
122
|
-
throw new Error('Tool edit_file path points to a directory; provide a file path')
|
|
123
|
-
}
|
|
124
|
-
} catch (error: unknown) {
|
|
125
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
|
126
|
-
throw error
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function assertSafeEditPath(requestedPath: string): void {
|
|
131
|
-
const trimmed = requestedPath.trim()
|
|
132
|
-
if (trimmed !== requestedPath || trimmed.length === 0) {
|
|
133
|
-
throw new Error('Tool edit_file path must be a clean workspace-relative file path')
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (/[|;&<>`]/.test(trimmed)) {
|
|
137
|
-
throw new Error('Tool edit_file path must not contain shell operators')
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (/^(?:rm|del|erase|rmdir|remove-item|mkdir|type|cat|echo|copy|move|mv|cp)\b/i.test(trimmed)) {
|
|
141
|
-
throw new Error('Tool edit_file path looks like a shell command; pass only the file path')
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function assertNotPrivateContinuityWorkspacePath(
|
|
146
|
-
requestedPath: string,
|
|
147
|
-
config: EthagentConfig | undefined,
|
|
148
|
-
toolName: string,
|
|
149
|
-
): void {
|
|
150
|
-
if (!config?.identity) return
|
|
151
|
-
const basename = path.basename(requestedPath.replaceAll('\\', '/')).toUpperCase()
|
|
152
|
-
if (basename !== 'SOUL.MD' && basename !== 'MEMORY.MD') return
|
|
153
|
-
throw new Error(
|
|
154
|
-
`${toolName} must not create or overwrite ${basename}; use propose_private_continuity_edit to patch the existing identity-vault scaffold`,
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function readOptionalTextFile(fullPath: string): Promise<{ content: string; existed: boolean }> {
|
|
159
|
-
try {
|
|
160
|
-
return { content: await fs.readFile(fullPath, 'utf8'), existed: true }
|
|
161
|
-
} catch (error: unknown) {
|
|
162
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { content: '', existed: false }
|
|
163
|
-
throw error
|
|
164
|
-
}
|
|
165
|
-
}
|
package/src/tools/editUtils.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
const LEFT_SINGLE_CURLY_QUOTE = '\u2018'
|
|
2
|
-
const RIGHT_SINGLE_CURLY_QUOTE = '\u2019'
|
|
3
|
-
const LEFT_DOUBLE_CURLY_QUOTE = '\u201c'
|
|
4
|
-
const RIGHT_DOUBLE_CURLY_QUOTE = '\u201d'
|
|
5
|
-
|
|
6
|
-
export type AppliedEdit = {
|
|
7
|
-
before: string
|
|
8
|
-
after: string
|
|
9
|
-
summary: string
|
|
10
|
-
previewBefore: string
|
|
11
|
-
previewAfter: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function applyRequestedEdit(
|
|
15
|
-
filePath: string,
|
|
16
|
-
before: string,
|
|
17
|
-
oldText: string | undefined,
|
|
18
|
-
newText: string,
|
|
19
|
-
replaceAll = false,
|
|
20
|
-
_replaceWholeFile = false,
|
|
21
|
-
): AppliedEdit {
|
|
22
|
-
if (!oldText) {
|
|
23
|
-
if (newText.length === 0) {
|
|
24
|
-
throw new Error('Field newText is empty; empty whole-file writes are not valid unless replacing a specific oldText range')
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
before,
|
|
28
|
-
after: newText,
|
|
29
|
-
summary: before.length === 0 ? `create ${filePath}` : `replace entire ${filePath}`,
|
|
30
|
-
previewBefore: previewText(before),
|
|
31
|
-
previewAfter: previewText(newText),
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (replaceAll) {
|
|
36
|
-
const matchCount = countOccurrences(before, oldText)
|
|
37
|
-
if (matchCount === 0) throw new Error('Field oldText was not found in the file')
|
|
38
|
-
return {
|
|
39
|
-
before,
|
|
40
|
-
after: before.replaceAll(oldText, () => newText),
|
|
41
|
-
summary: `replace ${matchCount} match${matchCount === 1 ? '' : 'es'} in ${filePath}`,
|
|
42
|
-
previewBefore: previewText(oldText),
|
|
43
|
-
previewAfter: previewText(newText),
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const actualOldText = findUniqueEditableMatch(before, oldText)
|
|
48
|
-
if (!actualOldText) throw new Error('Field oldText was not found in the file')
|
|
49
|
-
if (countOccurrences(before, actualOldText) > 1) {
|
|
50
|
-
throw new Error('Field oldText matched multiple locations; provide more context or use replaceAll')
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const adjustedNewText = preserveQuoteStyle(oldText, actualOldText, newText)
|
|
54
|
-
return {
|
|
55
|
-
before,
|
|
56
|
-
after: replaceSingleOccurrence(before, actualOldText, adjustedNewText),
|
|
57
|
-
summary: `edit ${filePath}`,
|
|
58
|
-
previewBefore: previewText(actualOldText),
|
|
59
|
-
previewAfter: previewText(adjustedNewText),
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function replaceSingleOccurrence(content: string, search: string, replace: string): string {
|
|
64
|
-
const index = content.indexOf(search)
|
|
65
|
-
if (index === -1) throw new Error('Field oldText was not found in the file')
|
|
66
|
-
return `${content.slice(0, index)}${replace}${content.slice(index + search.length)}`
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function findUniqueEditableMatch(fileContent: string, searchText: string): string | null {
|
|
70
|
-
const exactCount = countOccurrences(fileContent, searchText)
|
|
71
|
-
if (exactCount === 1) return searchText
|
|
72
|
-
if (exactCount > 1) return searchText
|
|
73
|
-
|
|
74
|
-
const normalizedSearch = normalizeQuotes(searchText)
|
|
75
|
-
const normalizedFile = normalizeQuotes(fileContent)
|
|
76
|
-
const firstIndex = normalizedFile.indexOf(normalizedSearch)
|
|
77
|
-
if (firstIndex === -1) return null
|
|
78
|
-
|
|
79
|
-
const secondIndex = normalizedFile.indexOf(normalizedSearch, firstIndex + normalizedSearch.length)
|
|
80
|
-
if (secondIndex !== -1) return null
|
|
81
|
-
|
|
82
|
-
return fileContent.slice(firstIndex, firstIndex + searchText.length)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function countOccurrences(content: string, search: string): number {
|
|
86
|
-
if (!search) return 0
|
|
87
|
-
let count = 0
|
|
88
|
-
let offset = 0
|
|
89
|
-
while (true) {
|
|
90
|
-
const index = content.indexOf(search, offset)
|
|
91
|
-
if (index === -1) return count
|
|
92
|
-
count += 1
|
|
93
|
-
offset = index + search.length
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function normalizeQuotes(text: string): string {
|
|
98
|
-
return text
|
|
99
|
-
.replaceAll(LEFT_SINGLE_CURLY_QUOTE, "'")
|
|
100
|
-
.replaceAll(RIGHT_SINGLE_CURLY_QUOTE, "'")
|
|
101
|
-
.replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '"')
|
|
102
|
-
.replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '"')
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function preserveQuoteStyle(oldText: string, actualOldText: string, newText: string): string {
|
|
106
|
-
if (oldText === actualOldText) return newText
|
|
107
|
-
|
|
108
|
-
let result = newText
|
|
109
|
-
if (containsCurlyDoubleQuotes(actualOldText)) result = applyCurlyDoubleQuotes(result)
|
|
110
|
-
if (containsCurlySingleQuotes(actualOldText)) result = applyCurlySingleQuotes(result)
|
|
111
|
-
return result
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function containsCurlyDoubleQuotes(text: string): boolean {
|
|
115
|
-
return text.includes(LEFT_DOUBLE_CURLY_QUOTE) || text.includes(RIGHT_DOUBLE_CURLY_QUOTE)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function containsCurlySingleQuotes(text: string): boolean {
|
|
119
|
-
return text.includes(LEFT_SINGLE_CURLY_QUOTE) || text.includes(RIGHT_SINGLE_CURLY_QUOTE)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function applyCurlyDoubleQuotes(text: string): string {
|
|
123
|
-
const chars = [...text]
|
|
124
|
-
const out: string[] = []
|
|
125
|
-
|
|
126
|
-
for (let index = 0; index < chars.length; index += 1) {
|
|
127
|
-
const char = chars[index]
|
|
128
|
-
if (char !== '"') {
|
|
129
|
-
out.push(char!)
|
|
130
|
-
continue
|
|
131
|
-
}
|
|
132
|
-
out.push(isOpeningContext(chars, index) ? LEFT_DOUBLE_CURLY_QUOTE : RIGHT_DOUBLE_CURLY_QUOTE)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return out.join('')
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function applyCurlySingleQuotes(text: string): string {
|
|
139
|
-
const chars = [...text]
|
|
140
|
-
const out: string[] = []
|
|
141
|
-
|
|
142
|
-
for (let index = 0; index < chars.length; index += 1) {
|
|
143
|
-
const char = chars[index]
|
|
144
|
-
if (char !== "'") {
|
|
145
|
-
out.push(char!)
|
|
146
|
-
continue
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const prev = index > 0 ? chars[index - 1] : undefined
|
|
150
|
-
const next = index < chars.length - 1 ? chars[index + 1] : undefined
|
|
151
|
-
if (prev && next && /\p{L}/u.test(prev) && /\p{L}/u.test(next)) {
|
|
152
|
-
out.push(RIGHT_SINGLE_CURLY_QUOTE)
|
|
153
|
-
continue
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
out.push(isOpeningContext(chars, index) ? LEFT_SINGLE_CURLY_QUOTE : RIGHT_SINGLE_CURLY_QUOTE)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return out.join('')
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function isOpeningContext(chars: string[], index: number): boolean {
|
|
163
|
-
if (index === 0) return true
|
|
164
|
-
return [' ', '\t', '\n', '\r', '(', '[', '{'].includes(chars[index - 1] ?? '')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function previewText(text: string, max = 700): string {
|
|
168
|
-
if (text.length <= max) return text
|
|
169
|
-
return `${text.slice(0, max - 3)}...`
|
|
170
|
-
}
|
package/src/tools/fileDiff.ts
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_DIFF_CONTEXT_LINES = 3
|
|
2
|
-
export const DEFAULT_DIFF_MAX_CHARS = 2400
|
|
3
|
-
export const FILE_DIFF_RESULT_MARKER = '\n\n<!-- ethagent:file-diff:v1 -->\n'
|
|
4
|
-
|
|
5
|
-
type DiffOp =
|
|
6
|
-
| { type: 'equal'; line: string }
|
|
7
|
-
| { type: 'delete'; line: string }
|
|
8
|
-
| { type: 'insert'; line: string }
|
|
9
|
-
|
|
10
|
-
type HunkRange = {
|
|
11
|
-
start: number
|
|
12
|
-
end: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type RenderUnifiedFileDiffInput = {
|
|
16
|
-
filePath: string
|
|
17
|
-
before: string
|
|
18
|
-
after: string
|
|
19
|
-
contextLines?: number
|
|
20
|
-
maxChars?: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const LARGE_DIFF_MATRIX_LIMIT = 500_000
|
|
24
|
-
|
|
25
|
-
export function renderUnifiedFileDiff(input: RenderUnifiedFileDiffInput): string {
|
|
26
|
-
const contextLines = input.contextLines ?? DEFAULT_DIFF_CONTEXT_LINES
|
|
27
|
-
const maxChars = input.maxChars ?? DEFAULT_DIFF_MAX_CHARS
|
|
28
|
-
const header = [`--- ${input.filePath}`, `+++ ${input.filePath}`]
|
|
29
|
-
|
|
30
|
-
if (input.before === input.after) {
|
|
31
|
-
return header.concat('(no changes)').join('\n')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const beforeLines = normalizedLines(input.before)
|
|
35
|
-
const afterLines = normalizedLines(input.after)
|
|
36
|
-
const ops = buildLineDiff(beforeLines, afterLines)
|
|
37
|
-
const hunks = buildHunkRanges(ops, contextLines)
|
|
38
|
-
|
|
39
|
-
if (hunks.length === 0) {
|
|
40
|
-
return header.concat('(only line ending changes)').join('\n')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const lines = [...header]
|
|
44
|
-
for (const hunk of hunks) {
|
|
45
|
-
const oldStart = countOldLines(ops, 0, hunk.start) + 1
|
|
46
|
-
const newStart = countNewLines(ops, 0, hunk.start) + 1
|
|
47
|
-
const oldCount = countOldLines(ops, hunk.start, hunk.end)
|
|
48
|
-
const newCount = countNewLines(ops, hunk.start, hunk.end)
|
|
49
|
-
lines.push(`@@ -${formatRange(oldStart, oldCount)} +${formatRange(newStart, newCount)} @@`)
|
|
50
|
-
for (const op of ops.slice(hunk.start, hunk.end)) {
|
|
51
|
-
if (op.type === 'equal') lines.push(` ${op.line}`)
|
|
52
|
-
else if (op.type === 'delete') lines.push(`-${op.line}`)
|
|
53
|
-
else lines.push(`+${op.line}`)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return truncateDiff(lines, maxChars)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function formatFileChangeResult(content: string, diff: string): string {
|
|
61
|
-
return `${content}${FILE_DIFF_RESULT_MARKER}${diff}`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function splitFileChangeResult(content: string): { content: string; diff?: string } {
|
|
65
|
-
const markerIndex = content.indexOf(FILE_DIFF_RESULT_MARKER)
|
|
66
|
-
if (markerIndex === -1) return { content }
|
|
67
|
-
const diff = content.slice(markerIndex + FILE_DIFF_RESULT_MARKER.length).trimEnd()
|
|
68
|
-
return {
|
|
69
|
-
content: content.slice(0, markerIndex).trimEnd(),
|
|
70
|
-
diff: diff.length > 0 ? diff : undefined,
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function stripFileChangeResultDiff(content: string): string {
|
|
75
|
-
return splitFileChangeResult(content).content
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function computeLineDiffStats(before: string, after: string): { inserts: number; deletes: number } {
|
|
79
|
-
if (before === after) return { inserts: 0, deletes: 0 }
|
|
80
|
-
const beforeLines = normalizedLines(before)
|
|
81
|
-
const afterLines = normalizedLines(after)
|
|
82
|
-
const ops = buildLineDiff(beforeLines, afterLines)
|
|
83
|
-
let inserts = 0
|
|
84
|
-
let deletes = 0
|
|
85
|
-
for (const op of ops) {
|
|
86
|
-
if (op.type === 'insert') inserts += 1
|
|
87
|
-
else if (op.type === 'delete') deletes += 1
|
|
88
|
-
}
|
|
89
|
-
return { inserts, deletes }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function buildLineDiff(beforeLines: string[], afterLines: string[]): DiffOp[] {
|
|
93
|
-
if (beforeLines.length * afterLines.length > LARGE_DIFF_MATRIX_LIMIT) {
|
|
94
|
-
return buildPrefixSuffixDiff(beforeLines, afterLines)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const lengths = lcsLengths(beforeLines, afterLines)
|
|
98
|
-
const ops: DiffOp[] = []
|
|
99
|
-
let beforeIndex = 0
|
|
100
|
-
let afterIndex = 0
|
|
101
|
-
|
|
102
|
-
while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) {
|
|
103
|
-
const beforeLine = beforeLines[beforeIndex]!
|
|
104
|
-
const afterLine = afterLines[afterIndex]!
|
|
105
|
-
if (beforeLine === afterLine) {
|
|
106
|
-
ops.push({ type: 'equal', line: beforeLine })
|
|
107
|
-
beforeIndex += 1
|
|
108
|
-
afterIndex += 1
|
|
109
|
-
continue
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const deleteScore = lengths[beforeIndex + 1]![afterIndex]!
|
|
113
|
-
const insertScore = lengths[beforeIndex]![afterIndex + 1]!
|
|
114
|
-
const deleteRevealsMatch = beforeLines[beforeIndex + 1] === afterLine
|
|
115
|
-
const insertRevealsMatch = beforeLine === afterLines[afterIndex + 1]
|
|
116
|
-
|
|
117
|
-
if (insertRevealsMatch && insertScore >= deleteScore) {
|
|
118
|
-
ops.push({ type: 'insert', line: afterLine })
|
|
119
|
-
afterIndex += 1
|
|
120
|
-
} else if (deleteRevealsMatch && deleteScore >= insertScore) {
|
|
121
|
-
ops.push({ type: 'delete', line: beforeLine })
|
|
122
|
-
beforeIndex += 1
|
|
123
|
-
} else if (deleteScore >= insertScore) {
|
|
124
|
-
ops.push({ type: 'delete', line: beforeLine })
|
|
125
|
-
beforeIndex += 1
|
|
126
|
-
} else {
|
|
127
|
-
ops.push({ type: 'insert', line: afterLine })
|
|
128
|
-
afterIndex += 1
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
while (beforeIndex < beforeLines.length) {
|
|
133
|
-
ops.push({ type: 'delete', line: beforeLines[beforeIndex]! })
|
|
134
|
-
beforeIndex += 1
|
|
135
|
-
}
|
|
136
|
-
while (afterIndex < afterLines.length) {
|
|
137
|
-
ops.push({ type: 'insert', line: afterLines[afterIndex]! })
|
|
138
|
-
afterIndex += 1
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return ops
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function buildPrefixSuffixDiff(beforeLines: string[], afterLines: string[]): DiffOp[] {
|
|
145
|
-
let prefixLength = 0
|
|
146
|
-
while (
|
|
147
|
-
prefixLength < beforeLines.length &&
|
|
148
|
-
prefixLength < afterLines.length &&
|
|
149
|
-
beforeLines[prefixLength] === afterLines[prefixLength]
|
|
150
|
-
) {
|
|
151
|
-
prefixLength += 1
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
let beforeEnd = beforeLines.length
|
|
155
|
-
let afterEnd = afterLines.length
|
|
156
|
-
while (
|
|
157
|
-
beforeEnd > prefixLength &&
|
|
158
|
-
afterEnd > prefixLength &&
|
|
159
|
-
beforeLines[beforeEnd - 1] === afterLines[afterEnd - 1]
|
|
160
|
-
) {
|
|
161
|
-
beforeEnd -= 1
|
|
162
|
-
afterEnd -= 1
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const ops: DiffOp[] = []
|
|
166
|
-
for (let index = 0; index < prefixLength; index += 1) {
|
|
167
|
-
ops.push({ type: 'equal', line: beforeLines[index]! })
|
|
168
|
-
}
|
|
169
|
-
for (let index = prefixLength; index < beforeEnd; index += 1) {
|
|
170
|
-
ops.push({ type: 'delete', line: beforeLines[index]! })
|
|
171
|
-
}
|
|
172
|
-
for (let index = prefixLength; index < afterEnd; index += 1) {
|
|
173
|
-
ops.push({ type: 'insert', line: afterLines[index]! })
|
|
174
|
-
}
|
|
175
|
-
for (let index = beforeEnd; index < beforeLines.length; index += 1) {
|
|
176
|
-
ops.push({ type: 'equal', line: beforeLines[index]! })
|
|
177
|
-
}
|
|
178
|
-
return ops
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function lcsLengths(beforeLines: string[], afterLines: string[]): number[][] {
|
|
182
|
-
const lengths = Array.from(
|
|
183
|
-
{ length: beforeLines.length + 1 },
|
|
184
|
-
() => Array<number>(afterLines.length + 1).fill(0),
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
for (let beforeIndex = beforeLines.length - 1; beforeIndex >= 0; beforeIndex -= 1) {
|
|
188
|
-
for (let afterIndex = afterLines.length - 1; afterIndex >= 0; afterIndex -= 1) {
|
|
189
|
-
lengths[beforeIndex]![afterIndex] = beforeLines[beforeIndex] === afterLines[afterIndex]
|
|
190
|
-
? lengths[beforeIndex + 1]![afterIndex + 1]! + 1
|
|
191
|
-
: Math.max(lengths[beforeIndex + 1]![afterIndex]!, lengths[beforeIndex]![afterIndex + 1]!)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return lengths
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function buildHunkRanges(ops: DiffOp[], contextLines: number): HunkRange[] {
|
|
199
|
-
const ranges: HunkRange[] = []
|
|
200
|
-
const context = Math.max(0, contextLines)
|
|
201
|
-
|
|
202
|
-
for (let index = 0; index < ops.length; index += 1) {
|
|
203
|
-
if (ops[index]?.type === 'equal') continue
|
|
204
|
-
const start = Math.max(0, index - context)
|
|
205
|
-
const end = Math.min(ops.length, index + context + 1)
|
|
206
|
-
const previous = ranges[ranges.length - 1]
|
|
207
|
-
if (previous && start <= previous.end) {
|
|
208
|
-
previous.end = Math.max(previous.end, end)
|
|
209
|
-
} else {
|
|
210
|
-
ranges.push({ start, end })
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return ranges
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function countOldLines(ops: DiffOp[], start: number, end: number): number {
|
|
218
|
-
let count = 0
|
|
219
|
-
for (let index = start; index < end; index += 1) {
|
|
220
|
-
const type = ops[index]?.type
|
|
221
|
-
if (type === 'equal' || type === 'delete') count += 1
|
|
222
|
-
}
|
|
223
|
-
return count
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function countNewLines(ops: DiffOp[], start: number, end: number): number {
|
|
227
|
-
let count = 0
|
|
228
|
-
for (let index = start; index < end; index += 1) {
|
|
229
|
-
const type = ops[index]?.type
|
|
230
|
-
if (type === 'equal' || type === 'insert') count += 1
|
|
231
|
-
}
|
|
232
|
-
return count
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function formatRange(start: number, count: number): string {
|
|
236
|
-
if (count === 0) return `${Math.max(0, start - 1)},0`
|
|
237
|
-
if (count === 1) return String(start)
|
|
238
|
-
return `${start},${count}`
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function normalizedLines(value: string): string[] {
|
|
242
|
-
if (value.length === 0) return []
|
|
243
|
-
const lines = value.replace(/\r\n?/g, '\n').split('\n')
|
|
244
|
-
if (lines[lines.length - 1] === '') lines.pop()
|
|
245
|
-
return lines
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function truncateDiff(lines: string[], maxChars: number): string {
|
|
249
|
-
const diff = lines.join('\n')
|
|
250
|
-
if (diff.length <= maxChars) return diff
|
|
251
|
-
|
|
252
|
-
const out: string[] = []
|
|
253
|
-
let length = 0
|
|
254
|
-
for (const line of lines) {
|
|
255
|
-
const nextLength = length + (out.length > 0 ? 1 : 0) + line.length
|
|
256
|
-
if (nextLength > maxChars) break
|
|
257
|
-
out.push(line)
|
|
258
|
-
length = nextLength
|
|
259
|
-
}
|
|
260
|
-
return out.join('\n')
|
|
261
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { z } from 'zod'
|
|
4
|
-
import type { Tool } from './contracts.js'
|
|
5
|
-
import { resolveWorkspacePath } from './readTool.js'
|
|
6
|
-
|
|
7
|
-
const schema = z.object({
|
|
8
|
-
path: z.string().optional(),
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
export const listDirectoryTool: Tool<typeof schema> = {
|
|
12
|
-
name: 'list_directory',
|
|
13
|
-
kind: 'read',
|
|
14
|
-
description: 'List files and folders in the current workspace. Use this first when you need to discover existing files before reading or editing them.',
|
|
15
|
-
inputSchema: schema,
|
|
16
|
-
inputSchemaJson: {
|
|
17
|
-
type: 'object',
|
|
18
|
-
properties: {
|
|
19
|
-
path: { type: 'string', description: 'Optional directory path relative to the current workspace.' },
|
|
20
|
-
},
|
|
21
|
-
required: [],
|
|
22
|
-
},
|
|
23
|
-
parse(input) {
|
|
24
|
-
return schema.parse(input)
|
|
25
|
-
},
|
|
26
|
-
async buildPermissionRequest(input, context) {
|
|
27
|
-
const fullPath = resolveWorkspacePath(context.workspaceRoot, input.path ?? '.')
|
|
28
|
-
const relativePath = path.relative(context.workspaceRoot, fullPath) || '.'
|
|
29
|
-
return {
|
|
30
|
-
kind: 'read',
|
|
31
|
-
path: fullPath,
|
|
32
|
-
relativePath,
|
|
33
|
-
directoryPath: fullPath,
|
|
34
|
-
title: 'Allow directory listing?',
|
|
35
|
-
subtitle: fullPath,
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
async execute(input, context) {
|
|
39
|
-
const fullPath = resolveWorkspacePath(context.workspaceRoot, input.path ?? '.')
|
|
40
|
-
const entries = await fs.readdir(fullPath, { withFileTypes: true })
|
|
41
|
-
const lines = entries
|
|
42
|
-
.sort((left, right) => {
|
|
43
|
-
if (left.isDirectory() && !right.isDirectory()) return -1
|
|
44
|
-
if (!left.isDirectory() && right.isDirectory()) return 1
|
|
45
|
-
return left.name.localeCompare(right.name)
|
|
46
|
-
})
|
|
47
|
-
.map(entry => `${entry.isDirectory() ? '[dir]' : ' '} ${entry.name}`)
|
|
48
|
-
const relativePath = path.relative(context.workspaceRoot, fullPath) || '.'
|
|
49
|
-
return {
|
|
50
|
-
ok: true,
|
|
51
|
-
summary: `listed ${relativePath}`,
|
|
52
|
-
content: lines.length > 0 ? lines.join('\n') : '(empty directory)',
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
}
|