ethagent 3.3.4 → 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.
Files changed (322) hide show
  1. package/.claude-plugin/marketplace.json +11 -0
  2. package/.claude-plugin/plugin.json +35 -0
  3. package/LICENSE +1 -1
  4. package/README.md +64 -104
  5. package/commands/ethagent.md +40 -0
  6. package/package.json +16 -16
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -6
  8. package/src/app/keybindings/types.ts +1 -6
  9. package/src/cli/ResetConfirmView.tsx +54 -53
  10. package/src/cli/demo.ts +86 -0
  11. package/src/cli/hookIo.ts +45 -0
  12. package/src/cli/main.tsx +94 -123
  13. package/src/cli/memoryGuard.ts +49 -0
  14. package/src/cli/reset.ts +28 -70
  15. package/src/cli/sessionStart.ts +33 -0
  16. package/src/cli/status.ts +46 -0
  17. package/src/cli/sync.ts +167 -0
  18. package/src/cli/syncAdapters/claude-code.ts +86 -0
  19. package/src/cli/syncAdapters/codex.ts +66 -0
  20. package/src/cli/syncAdapters/index.ts +45 -0
  21. package/src/cli/syncAdapters/managedBlock.ts +175 -0
  22. package/src/cli/syncAdapters/shared.ts +63 -0
  23. package/src/identity/continuity/envelopeParse.ts +20 -1
  24. package/src/identity/continuity/publicSkills.ts +3 -1
  25. package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
  26. package/src/identity/continuity/skills/scaffold.ts +5 -2
  27. package/src/identity/continuity/snapshots.ts +12 -5
  28. package/src/identity/continuity/storage/defaults.ts +20 -19
  29. package/src/identity/continuity/storage/status.ts +1 -1
  30. package/src/identity/ens/ensLookup/constants.ts +1 -1
  31. package/src/identity/manager/IdentityManager.tsx +33 -0
  32. package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
  33. package/src/identity/{hub → manager}/Routes.tsx +48 -34
  34. package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
  35. package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
  36. package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
  37. package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
  38. package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
  39. package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
  40. package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
  41. package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
  42. package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
  43. package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
  44. package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
  45. package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
  46. package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
  47. package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
  48. package/src/identity/{hub → manager}/create/effects.ts +19 -10
  49. package/src/identity/manager/create/importScan.ts +122 -0
  50. package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
  51. package/src/identity/{hub → manager}/custody/actions.ts +1 -15
  52. package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
  53. package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
  54. package/src/identity/{hub → manager}/custody/types.ts +1 -2
  55. package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
  56. package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
  57. package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
  58. package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
  59. package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
  60. package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
  61. package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
  62. package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
  63. package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
  64. package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
  65. package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
  66. package/src/identity/{hub → manager}/profile/effects.ts +1 -3
  67. package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
  68. package/src/identity/{hub → manager}/profile/state.ts +1 -1
  69. package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
  70. package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
  71. package/src/identity/{hub → manager}/restore/apply.ts +1 -1
  72. package/src/identity/{hub → manager}/restore/auth.ts +1 -1
  73. package/src/identity/{hub → manager}/restore/discover.ts +1 -1
  74. package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
  75. package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
  76. package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
  77. package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
  78. package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
  79. package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
  80. package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
  81. package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
  82. package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
  83. package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
  84. package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
  85. package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
  86. package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
  87. package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
  88. package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
  89. package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
  90. package/src/identity/manager/shared/effects/types.ts +30 -0
  91. package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
  92. package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
  93. package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
  94. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
  95. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
  96. package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
  97. package/src/identity/{hub → manager}/shared/utils.ts +6 -10
  98. package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
  99. package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
  100. package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
  101. package/src/identity/{hub → manager}/types.ts +5 -6
  102. package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
  103. package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
  104. package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
  105. package/src/identity/registry/erc8004/discovery.ts +3 -17
  106. package/src/identity/registry/erc8004/utils.ts +1 -1
  107. package/src/identity/storage/ipfs.ts +21 -1
  108. package/src/identity/wallet/browserWallet/html.ts +10 -2
  109. package/src/identity/wallet/browserWallet/http.ts +18 -0
  110. package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
  111. package/src/identity/wallet/browserWallet/requests.ts +10 -28
  112. package/src/identity/wallet/browserWallet/session.ts +26 -33
  113. package/src/identity/wallet/browserWallet/validation.ts +14 -0
  114. package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
  115. package/src/identity/wallet/page/boot.ts +43 -0
  116. package/src/identity/wallet/page/config.ts +59 -0
  117. package/src/identity/wallet/page/constants.ts +12 -0
  118. package/src/identity/wallet/page/copy.ts +47 -68
  119. package/src/identity/wallet/page/css.ts +638 -0
  120. package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
  121. package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
  122. package/src/identity/wallet/page/markup.ts +44 -34
  123. package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
  124. package/src/identity/wallet/page/resize.ts +95 -0
  125. package/src/identity/wallet/page/state.ts +135 -8
  126. package/src/identity/wallet/page/timeline.ts +161 -0
  127. package/src/identity/wallet/page/view.ts +22 -302
  128. package/src/storage/config.ts +30 -80
  129. package/src/storage/reset.ts +31 -0
  130. package/src/storage/secrets.ts +1 -16
  131. package/src/ui/Select.tsx +27 -5
  132. package/src/ui/Spinner.tsx +16 -15
  133. package/src/ui/Surface.tsx +21 -17
  134. package/src/ui/TextArea.tsx +173 -0
  135. package/src/ui/TextInput.tsx +31 -133
  136. package/src/ui/theme.ts +22 -13
  137. package/src/utils/clipboard.ts +0 -140
  138. package/src/app/FirstRun.tsx +0 -577
  139. package/src/app/FirstRunTimeline.tsx +0 -51
  140. package/src/app/firstRunConfig.ts +0 -26
  141. package/src/app/hooks/useCancelRequest.ts +0 -22
  142. package/src/app/hooks/useDoublePress.ts +0 -46
  143. package/src/app/hooks/useExitOnCtrlC.ts +0 -36
  144. package/src/auth/openaiOAuth/credentials.ts +0 -47
  145. package/src/auth/openaiOAuth/crypto.ts +0 -23
  146. package/src/auth/openaiOAuth/index.ts +0 -238
  147. package/src/auth/openaiOAuth/landingPage.ts +0 -116
  148. package/src/auth/openaiOAuth/listener.ts +0 -151
  149. package/src/auth/openaiOAuth/refresh.ts +0 -70
  150. package/src/auth/openaiOAuth/shared.ts +0 -115
  151. package/src/chat/ChatBottomPane.tsx +0 -296
  152. package/src/chat/ChatScreen.tsx +0 -1685
  153. package/src/chat/ConversationStack.tsx +0 -56
  154. package/src/chat/MessageList.tsx +0 -638
  155. package/src/chat/SessionStatus.tsx +0 -53
  156. package/src/chat/chatEnvironment.ts +0 -16
  157. package/src/chat/chatScreenUtils.ts +0 -194
  158. package/src/chat/chatSessionState.ts +0 -146
  159. package/src/chat/chatTurnContext.ts +0 -50
  160. package/src/chat/chatTurnOrchestrator.ts +0 -603
  161. package/src/chat/chatTurnRows.ts +0 -64
  162. package/src/chat/commands.ts +0 -494
  163. package/src/chat/continuityEditReview.ts +0 -42
  164. package/src/chat/display/DiffView.tsx +0 -193
  165. package/src/chat/display/SyntaxText.tsx +0 -192
  166. package/src/chat/display/toolCallDisplay.ts +0 -103
  167. package/src/chat/display/toolResultDisplay.ts +0 -19
  168. package/src/chat/input/ChatInput.tsx +0 -625
  169. package/src/chat/input/chatInputHelpers.ts +0 -62
  170. package/src/chat/input/chatInputState.ts +0 -247
  171. package/src/chat/input/chatPaste.ts +0 -49
  172. package/src/chat/input/imageRefs.ts +0 -30
  173. package/src/chat/input/inputRendering.tsx +0 -93
  174. package/src/chat/input/textCursor.ts +0 -212
  175. package/src/chat/messageMarkdown.ts +0 -220
  176. package/src/chat/messageRows.ts +0 -43
  177. package/src/chat/planImplementation.ts +0 -62
  178. package/src/chat/slashCommandHandlers.ts +0 -122
  179. package/src/chat/slashCommandViews.ts +0 -120
  180. package/src/chat/transcript/TranscriptView.tsx +0 -184
  181. package/src/chat/transcript/transcriptViewport.ts +0 -295
  182. package/src/chat/views/ContextLimitView.tsx +0 -95
  183. package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
  184. package/src/chat/views/CopyPicker.tsx +0 -50
  185. package/src/chat/views/PermissionPrompt.tsx +0 -156
  186. package/src/chat/views/PermissionsView.tsx +0 -165
  187. package/src/chat/views/PlanApprovalView.tsx +0 -91
  188. package/src/chat/views/ResumeView.tsx +0 -273
  189. package/src/chat/views/RewindView.tsx +0 -412
  190. package/src/cli/preview.tsx +0 -14
  191. package/src/cli/updateNotice.ts +0 -54
  192. package/src/identity/continuity/privateEdit/apply.ts +0 -170
  193. package/src/identity/continuity/privateEdit/diff.ts +0 -6
  194. package/src/identity/continuity/privateEdit/files.ts +0 -23
  195. package/src/identity/continuity/privateEdit/types.ts +0 -28
  196. package/src/identity/continuity/privateEdit.ts +0 -46
  197. package/src/identity/hub/IdentityHub.tsx +0 -14
  198. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
  199. package/src/identity/hub/ens/effects.ts +0 -218
  200. package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
  201. package/src/identity/hub/shared/effects/types.ts +0 -53
  202. package/src/identity/hub/shared/reconciliation/index.ts +0 -14
  203. package/src/identity/wallet/page/grainient.ts +0 -278
  204. package/src/identity/wallet/page/html.ts +0 -28
  205. package/src/identity/wallet/page/styles/base.ts +0 -260
  206. package/src/identity/wallet/page/styles/components.ts +0 -262
  207. package/src/identity/wallet/page/styles/index.ts +0 -5
  208. package/src/identity/wallet/page/styles/responsive.ts +0 -247
  209. package/src/identity/wallet/page.tsx +0 -38
  210. package/src/mcp/approvals.ts +0 -113
  211. package/src/mcp/config.ts +0 -235
  212. package/src/mcp/manager.ts +0 -482
  213. package/src/mcp/managerHelpers.ts +0 -70
  214. package/src/mcp/names.ts +0 -19
  215. package/src/mcp/output.ts +0 -96
  216. package/src/models/ModelPicker.tsx +0 -1009
  217. package/src/models/catalog.ts +0 -327
  218. package/src/models/huggingface.ts +0 -712
  219. package/src/models/huggingfaceStorage.ts +0 -136
  220. package/src/models/llamacpp.ts +0 -848
  221. package/src/models/llamacppCommands.ts +0 -44
  222. package/src/models/llamacppConfig.ts +0 -34
  223. package/src/models/llamacppDiscovery.ts +0 -176
  224. package/src/models/llamacppOutput.ts +0 -65
  225. package/src/models/llamacppPreflight.ts +0 -158
  226. package/src/models/modelDisplay.ts +0 -180
  227. package/src/models/modelPickerCatalogFlow.ts +0 -56
  228. package/src/models/modelPickerCredentials.ts +0 -166
  229. package/src/models/modelPickerData.ts +0 -41
  230. package/src/models/modelPickerDisplay.tsx +0 -132
  231. package/src/models/modelPickerHfFlow.ts +0 -192
  232. package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
  233. package/src/models/modelPickerOptions.ts +0 -457
  234. package/src/models/modelPickerTypes.ts +0 -69
  235. package/src/models/modelPickerUninstallFlow.ts +0 -48
  236. package/src/models/modelPickerViewHelpers.ts +0 -174
  237. package/src/models/modelRecommendation.ts +0 -139
  238. package/src/models/providerDisplay.ts +0 -16
  239. package/src/models/runtimeDetection.ts +0 -81
  240. package/src/models/uncensoredCatalog.ts +0 -86
  241. package/src/providers/anthropic.ts +0 -290
  242. package/src/providers/contracts.ts +0 -71
  243. package/src/providers/errors.ts +0 -80
  244. package/src/providers/gemini.ts +0 -391
  245. package/src/providers/openai-chat.ts +0 -474
  246. package/src/providers/openai-responses-format.ts +0 -177
  247. package/src/providers/openai-responses.ts +0 -306
  248. package/src/providers/openaiChatWire.ts +0 -124
  249. package/src/providers/registry.ts +0 -120
  250. package/src/providers/retry.ts +0 -58
  251. package/src/providers/sse.ts +0 -93
  252. package/src/runtime/compaction.ts +0 -395
  253. package/src/runtime/cwd.ts +0 -43
  254. package/src/runtime/providerTurn.ts +0 -38
  255. package/src/runtime/sessionMode.ts +0 -55
  256. package/src/runtime/systemPrompt.ts +0 -213
  257. package/src/runtime/textToolParser.ts +0 -161
  258. package/src/runtime/toolClaimGuards.ts +0 -143
  259. package/src/runtime/toolExecution.ts +0 -304
  260. package/src/runtime/toolIntent.ts +0 -143
  261. package/src/runtime/turn.ts +0 -369
  262. package/src/runtime/turnNudges.ts +0 -223
  263. package/src/runtime/turnTypes.ts +0 -86
  264. package/src/storage/factoryReset.ts +0 -127
  265. package/src/storage/history.ts +0 -58
  266. package/src/storage/permissions.ts +0 -76
  267. package/src/storage/rewind.ts +0 -266
  268. package/src/storage/sessionExport.ts +0 -49
  269. package/src/storage/sessions.ts +0 -495
  270. package/src/tools/bashSafety.ts +0 -186
  271. package/src/tools/bashTool.ts +0 -140
  272. package/src/tools/changeDirectoryTool.ts +0 -213
  273. package/src/tools/contracts.ts +0 -192
  274. package/src/tools/deleteFileTool.ts +0 -116
  275. package/src/tools/editTool.ts +0 -165
  276. package/src/tools/editUtils.ts +0 -170
  277. package/src/tools/fileDiff.ts +0 -261
  278. package/src/tools/listDirectoryTool.ts +0 -55
  279. package/src/tools/listSkillFilesTool.ts +0 -77
  280. package/src/tools/listSkillsTool.ts +0 -68
  281. package/src/tools/mcpResourceTools.ts +0 -95
  282. package/src/tools/permissionRules.ts +0 -85
  283. package/src/tools/privateContinuityEditTool.ts +0 -187
  284. package/src/tools/privateContinuityReadTool.ts +0 -106
  285. package/src/tools/readSkillTool.ts +0 -107
  286. package/src/tools/readTool.ts +0 -85
  287. package/src/tools/registry.ts +0 -103
  288. package/src/tools/writeFileTool.ts +0 -167
  289. package/src/ui/BrandSplash.tsx +0 -133
  290. package/src/ui/terminalTitle.ts +0 -30
  291. package/src/utils/images.ts +0 -140
  292. package/src/utils/markdownSegments.ts +0 -51
  293. package/src/utils/messages.ts +0 -37
  294. package/src/utils/withRetry.ts +0 -324
  295. /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
  296. /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
  297. /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
  298. /package/src/identity/{hub → manager}/custody/state.ts +0 -0
  299. /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
  300. /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
  301. /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
  302. /package/src/identity/{hub → manager}/ens/state.ts +0 -0
  303. /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
  304. /package/src/identity/{hub → manager}/ens/types.ts +0 -0
  305. /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
  306. /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
  307. /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
  308. /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
  309. /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
  310. /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
  311. /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
  312. /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
  313. /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
  314. /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
  315. /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
  316. /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
  317. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
  318. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
  319. /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
  320. /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
  321. /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
  322. /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
@@ -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
- }
@@ -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
- }
@@ -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
- }