@shareai-lab/kode 1.0.70 → 1.0.71

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 (253) hide show
  1. package/README.md +202 -76
  2. package/README.zh-CN.md +246 -0
  3. package/cli.js +62 -0
  4. package/package.json +45 -25
  5. package/scripts/postinstall.js +56 -0
  6. package/src/ProjectOnboarding.tsx +180 -0
  7. package/src/Tool.ts +53 -0
  8. package/src/commands/approvedTools.ts +53 -0
  9. package/src/commands/bug.tsx +20 -0
  10. package/src/commands/clear.ts +43 -0
  11. package/src/commands/compact.ts +120 -0
  12. package/src/commands/config.tsx +19 -0
  13. package/src/commands/cost.ts +18 -0
  14. package/src/commands/ctx_viz.ts +209 -0
  15. package/src/commands/doctor.ts +24 -0
  16. package/src/commands/help.tsx +19 -0
  17. package/src/commands/init.ts +37 -0
  18. package/src/commands/listen.ts +42 -0
  19. package/src/commands/login.tsx +51 -0
  20. package/src/commands/logout.tsx +40 -0
  21. package/src/commands/mcp.ts +41 -0
  22. package/src/commands/model.tsx +40 -0
  23. package/src/commands/modelstatus.tsx +20 -0
  24. package/src/commands/onboarding.tsx +34 -0
  25. package/src/commands/pr_comments.ts +59 -0
  26. package/src/commands/refreshCommands.ts +54 -0
  27. package/src/commands/release-notes.ts +34 -0
  28. package/src/commands/resume.tsx +30 -0
  29. package/src/commands/review.ts +49 -0
  30. package/src/commands/terminalSetup.ts +221 -0
  31. package/src/commands.ts +136 -0
  32. package/src/components/ApproveApiKey.tsx +93 -0
  33. package/src/components/AsciiLogo.tsx +13 -0
  34. package/src/components/AutoUpdater.tsx +148 -0
  35. package/src/components/Bug.tsx +367 -0
  36. package/src/components/Config.tsx +289 -0
  37. package/src/components/ConsoleOAuthFlow.tsx +326 -0
  38. package/src/components/Cost.tsx +23 -0
  39. package/src/components/CostThresholdDialog.tsx +46 -0
  40. package/src/components/CustomSelect/option-map.ts +42 -0
  41. package/src/components/CustomSelect/select-option.tsx +52 -0
  42. package/src/components/CustomSelect/select.tsx +143 -0
  43. package/src/components/CustomSelect/use-select-state.ts +414 -0
  44. package/src/components/CustomSelect/use-select.ts +35 -0
  45. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  46. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  47. package/src/components/Help.tsx +215 -0
  48. package/src/components/HighlightedCode.tsx +33 -0
  49. package/src/components/InvalidConfigDialog.tsx +113 -0
  50. package/src/components/Link.tsx +32 -0
  51. package/src/components/LogSelector.tsx +86 -0
  52. package/src/components/Logo.tsx +145 -0
  53. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  54. package/src/components/MCPServerDialogCopy.tsx +25 -0
  55. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  56. package/src/components/Message.tsx +219 -0
  57. package/src/components/MessageResponse.tsx +15 -0
  58. package/src/components/MessageSelector.tsx +211 -0
  59. package/src/components/ModeIndicator.tsx +88 -0
  60. package/src/components/ModelConfig.tsx +301 -0
  61. package/src/components/ModelListManager.tsx +223 -0
  62. package/src/components/ModelSelector.tsx +3208 -0
  63. package/src/components/ModelStatusDisplay.tsx +228 -0
  64. package/src/components/Onboarding.tsx +274 -0
  65. package/src/components/PressEnterToContinue.tsx +11 -0
  66. package/src/components/PromptInput.tsx +710 -0
  67. package/src/components/SentryErrorBoundary.ts +33 -0
  68. package/src/components/Spinner.tsx +129 -0
  69. package/src/components/StructuredDiff.tsx +184 -0
  70. package/src/components/TextInput.tsx +246 -0
  71. package/src/components/TokenWarning.tsx +31 -0
  72. package/src/components/ToolUseLoader.tsx +40 -0
  73. package/src/components/TrustDialog.tsx +106 -0
  74. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  75. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  76. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  77. package/src/components/binary-feedback/utils.ts +220 -0
  78. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  79. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +45 -0
  80. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  81. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  82. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  83. package/src/components/messages/AssistantToolUseMessage.tsx +123 -0
  84. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  85. package/src/components/messages/UserCommandMessage.tsx +30 -0
  86. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  87. package/src/components/messages/UserPromptMessage.tsx +35 -0
  88. package/src/components/messages/UserTextMessage.tsx +39 -0
  89. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  90. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  91. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  92. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  93. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  94. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  95. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  96. package/src/components/permissions/FallbackPermissionRequest.tsx +155 -0
  97. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  98. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +75 -0
  99. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  100. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +81 -0
  101. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +242 -0
  102. package/src/components/permissions/PermissionRequest.tsx +103 -0
  103. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  104. package/src/components/permissions/hooks.ts +44 -0
  105. package/src/components/permissions/toolUseOptions.ts +59 -0
  106. package/src/components/permissions/utils.ts +23 -0
  107. package/src/constants/betas.ts +5 -0
  108. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  109. package/src/constants/figures.ts +4 -0
  110. package/src/constants/keys.ts +3 -0
  111. package/src/constants/macros.ts +6 -0
  112. package/src/constants/models.ts +935 -0
  113. package/src/constants/oauth.ts +18 -0
  114. package/src/constants/product.ts +17 -0
  115. package/src/constants/prompts.ts +177 -0
  116. package/src/constants/releaseNotes.ts +7 -0
  117. package/src/context/PermissionContext.tsx +149 -0
  118. package/src/context.ts +278 -0
  119. package/src/cost-tracker.ts +84 -0
  120. package/src/entrypoints/cli.tsx +1498 -0
  121. package/src/entrypoints/mcp.ts +176 -0
  122. package/src/history.ts +25 -0
  123. package/src/hooks/useApiKeyVerification.ts +59 -0
  124. package/src/hooks/useArrowKeyHistory.ts +55 -0
  125. package/src/hooks/useCanUseTool.ts +138 -0
  126. package/src/hooks/useCancelRequest.ts +39 -0
  127. package/src/hooks/useDoublePress.ts +42 -0
  128. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  129. package/src/hooks/useInterval.ts +25 -0
  130. package/src/hooks/useLogMessages.ts +16 -0
  131. package/src/hooks/useLogStartupTime.ts +12 -0
  132. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  133. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  134. package/src/hooks/useSlashCommandTypeahead.ts +137 -0
  135. package/src/hooks/useTerminalSize.ts +49 -0
  136. package/src/hooks/useTextInput.ts +315 -0
  137. package/src/messages.ts +37 -0
  138. package/src/permissions.ts +268 -0
  139. package/src/query.ts +704 -0
  140. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  141. package/src/screens/Doctor.tsx +219 -0
  142. package/src/screens/LogList.tsx +68 -0
  143. package/src/screens/REPL.tsx +792 -0
  144. package/src/screens/ResumeConversation.tsx +68 -0
  145. package/src/services/browserMocks.ts +66 -0
  146. package/src/services/claude.ts +1947 -0
  147. package/src/services/customCommands.ts +683 -0
  148. package/src/services/fileFreshness.ts +377 -0
  149. package/src/services/mcpClient.ts +564 -0
  150. package/src/services/mcpServerApproval.tsx +50 -0
  151. package/src/services/notifier.ts +40 -0
  152. package/src/services/oauth.ts +357 -0
  153. package/src/services/openai.ts +796 -0
  154. package/src/services/sentry.ts +3 -0
  155. package/src/services/statsig.ts +171 -0
  156. package/src/services/statsigStorage.ts +86 -0
  157. package/src/services/systemReminder.ts +406 -0
  158. package/src/services/vcr.ts +161 -0
  159. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  160. package/src/tools/ArchitectTool/prompt.ts +15 -0
  161. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +505 -0
  162. package/src/tools/BashTool/BashTool.tsx +270 -0
  163. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  164. package/src/tools/BashTool/OutputLine.tsx +48 -0
  165. package/src/tools/BashTool/prompt.ts +174 -0
  166. package/src/tools/BashTool/utils.ts +56 -0
  167. package/src/tools/FileEditTool/FileEditTool.tsx +316 -0
  168. package/src/tools/FileEditTool/prompt.ts +51 -0
  169. package/src/tools/FileEditTool/utils.ts +58 -0
  170. package/src/tools/FileReadTool/FileReadTool.tsx +371 -0
  171. package/src/tools/FileReadTool/prompt.ts +7 -0
  172. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  173. package/src/tools/FileWriteTool/prompt.ts +10 -0
  174. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  175. package/src/tools/GlobTool/prompt.ts +8 -0
  176. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  177. package/src/tools/GrepTool/prompt.ts +11 -0
  178. package/src/tools/MCPTool/MCPTool.tsx +106 -0
  179. package/src/tools/MCPTool/prompt.ts +3 -0
  180. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  181. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  182. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  183. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  184. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  185. package/src/tools/MultiEditTool/prompt.ts +45 -0
  186. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  187. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  188. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +266 -0
  189. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  190. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  191. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  192. package/src/tools/TaskTool/TaskTool.tsx +382 -0
  193. package/src/tools/TaskTool/constants.ts +1 -0
  194. package/src/tools/TaskTool/prompt.ts +56 -0
  195. package/src/tools/ThinkTool/ThinkTool.tsx +56 -0
  196. package/src/tools/ThinkTool/prompt.ts +12 -0
  197. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +289 -0
  198. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  199. package/src/tools/lsTool/lsTool.tsx +269 -0
  200. package/src/tools/lsTool/prompt.ts +2 -0
  201. package/src/tools.ts +63 -0
  202. package/src/types/PermissionMode.ts +120 -0
  203. package/src/types/RequestContext.ts +72 -0
  204. package/src/utils/Cursor.ts +436 -0
  205. package/src/utils/PersistentShell.ts +373 -0
  206. package/src/utils/agentStorage.ts +97 -0
  207. package/src/utils/array.ts +3 -0
  208. package/src/utils/ask.tsx +98 -0
  209. package/src/utils/auth.ts +13 -0
  210. package/src/utils/autoCompactCore.ts +223 -0
  211. package/src/utils/autoUpdater.ts +318 -0
  212. package/src/utils/betas.ts +20 -0
  213. package/src/utils/browser.ts +14 -0
  214. package/src/utils/cleanup.ts +72 -0
  215. package/src/utils/commands.ts +261 -0
  216. package/src/utils/config.ts +771 -0
  217. package/src/utils/conversationRecovery.ts +54 -0
  218. package/src/utils/debugLogger.ts +1123 -0
  219. package/src/utils/diff.ts +42 -0
  220. package/src/utils/env.ts +57 -0
  221. package/src/utils/errors.ts +21 -0
  222. package/src/utils/exampleCommands.ts +108 -0
  223. package/src/utils/execFileNoThrow.ts +51 -0
  224. package/src/utils/expertChatStorage.ts +136 -0
  225. package/src/utils/file.ts +402 -0
  226. package/src/utils/fileRecoveryCore.ts +71 -0
  227. package/src/utils/format.tsx +44 -0
  228. package/src/utils/generators.ts +62 -0
  229. package/src/utils/git.ts +92 -0
  230. package/src/utils/globalLogger.ts +77 -0
  231. package/src/utils/http.ts +10 -0
  232. package/src/utils/imagePaste.ts +38 -0
  233. package/src/utils/json.ts +13 -0
  234. package/src/utils/log.ts +382 -0
  235. package/src/utils/markdown.ts +213 -0
  236. package/src/utils/messageContextManager.ts +289 -0
  237. package/src/utils/messages.tsx +938 -0
  238. package/src/utils/model.ts +836 -0
  239. package/src/utils/permissions/filesystem.ts +118 -0
  240. package/src/utils/ripgrep.ts +167 -0
  241. package/src/utils/sessionState.ts +49 -0
  242. package/src/utils/state.ts +25 -0
  243. package/src/utils/style.ts +29 -0
  244. package/src/utils/terminal.ts +49 -0
  245. package/src/utils/theme.ts +122 -0
  246. package/src/utils/thinking.ts +144 -0
  247. package/src/utils/todoStorage.ts +431 -0
  248. package/src/utils/tokens.ts +43 -0
  249. package/src/utils/toolExecutionController.ts +163 -0
  250. package/src/utils/unaryLogging.ts +26 -0
  251. package/src/utils/user.ts +37 -0
  252. package/src/utils/validate.ts +165 -0
  253. package/cli.mjs +0 -1803
@@ -0,0 +1,366 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync } from 'fs'
2
+ import { Box, Text } from 'ink'
3
+ import { dirname, isAbsolute, relative, resolve, sep } from 'path'
4
+ import * as React from 'react'
5
+ import { z } from 'zod'
6
+ import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage'
7
+ import { StructuredDiff } from '../../components/StructuredDiff'
8
+ import { logEvent } from '../../services/statsig'
9
+ import { Tool, ValidationResult } from '../../Tool'
10
+ import { intersperse } from '../../utils/array'
11
+ import {
12
+ addLineNumbers,
13
+ detectFileEncoding,
14
+ detectLineEndings,
15
+ findSimilarFile,
16
+ writeTextContent,
17
+ } from '../../utils/file.js'
18
+ import { logError } from '../../utils/log'
19
+ import { getCwd } from '../../utils/state'
20
+ import { getTheme } from '../../utils/theme'
21
+ import { NotebookEditTool } from '../NotebookEditTool/NotebookEditTool'
22
+ import { applyEdit } from '../FileEditTool/utils'
23
+ import { hasWritePermission } from '../../utils/permissions/filesystem'
24
+ import { PROJECT_FILE } from '../../constants/product'
25
+ import { DESCRIPTION, PROMPT } from './prompt'
26
+ import { emitReminderEvent } from '../../services/systemReminder'
27
+ import { recordFileEdit } from '../../services/fileFreshness'
28
+
29
+ const EditSchema = z.object({
30
+ old_string: z.string().describe('The text to replace'),
31
+ new_string: z.string().describe('The text to replace it with'),
32
+ replace_all: z
33
+ .boolean()
34
+ .optional()
35
+ .default(false)
36
+ .describe('Replace all occurences of old_string (default false)'),
37
+ })
38
+
39
+ const inputSchema = z.strictObject({
40
+ file_path: z.string().describe('The absolute path to the file to modify'),
41
+ edits: z
42
+ .array(EditSchema)
43
+ .min(1)
44
+ .describe('Array of edit operations to perform sequentially on the file'),
45
+ })
46
+
47
+ export type In = typeof inputSchema
48
+
49
+ // Number of lines of context to include before/after the change in our result message
50
+ const N_LINES_SNIPPET = 4
51
+
52
+ export const MultiEditTool = {
53
+ name: 'MultiEdit',
54
+ async description() {
55
+ return 'A tool for making multiple edits to a single file atomically'
56
+ },
57
+ async prompt() {
58
+ return PROMPT
59
+ },
60
+ inputSchema,
61
+ userFacingName({ edits }) {
62
+ return `Multi-Edit (${edits.length} changes)`
63
+ },
64
+ async isEnabled() {
65
+ return true
66
+ },
67
+ isReadOnly() {
68
+ return false
69
+ },
70
+ isConcurrencySafe() {
71
+ return false // MultiEdit modifies files, not safe for concurrent execution
72
+ },
73
+ needsPermissions(input?: z.infer<typeof inputSchema>) {
74
+ if (!input) return true
75
+ return !hasWritePermission(input.file_path)
76
+ },
77
+ renderResultForAssistant(content) {
78
+ return content
79
+ },
80
+ renderToolUseMessage(input, { verbose }) {
81
+ const { file_path, edits } = input
82
+ const workingDir = getCwd()
83
+ const relativePath = isAbsolute(file_path)
84
+ ? relative(workingDir, file_path)
85
+ : file_path
86
+
87
+ if (verbose) {
88
+ const editSummary = edits
89
+ .map(
90
+ (edit, index) =>
91
+ `${index + 1}. Replace "${edit.old_string.substring(0, 50)}${edit.old_string.length > 50 ? '...' : ''}" with "${edit.new_string.substring(0, 50)}${edit.new_string.length > 50 ? '...' : ''}"`,
92
+ )
93
+ .join('\n')
94
+ return `Multiple edits to ${relativePath}:\n${editSummary}`
95
+ }
96
+
97
+ return `Making ${edits.length} edits to ${relativePath}`
98
+ },
99
+ renderToolUseRejectedMessage() {
100
+ return (
101
+ <Box>
102
+ <Text color={getTheme().error}>⚠ Edit request rejected</Text>
103
+ </Box>
104
+ )
105
+ },
106
+ renderToolResultMessage(output) {
107
+ if (typeof output === 'string') {
108
+ const isError = output.includes('Error:')
109
+ return (
110
+ <Box flexDirection="column">
111
+ <Text color={isError ? getTheme().error : getTheme().success}>
112
+ {output}
113
+ </Text>
114
+ </Box>
115
+ )
116
+ }
117
+
118
+ return <FileEditToolUpdatedMessage {...output} />
119
+ },
120
+ async validateInput(
121
+ { file_path, edits }: z.infer<typeof inputSchema>,
122
+ context?: { readFileTimestamps?: Record<string, number> },
123
+ ): Promise<ValidationResult> {
124
+ const workingDir = getCwd()
125
+ const normalizedPath = isAbsolute(file_path)
126
+ ? resolve(file_path)
127
+ : resolve(workingDir, file_path)
128
+
129
+ // Check if it's a notebook file
130
+ if (normalizedPath.endsWith('.ipynb')) {
131
+ return {
132
+ result: false,
133
+ errorCode: 1,
134
+ message: `For Jupyter notebooks (.ipynb files), use the ${NotebookEditTool.name} tool instead.`,
135
+ }
136
+ }
137
+
138
+ // For new files, check parent directory exists
139
+ if (!existsSync(normalizedPath)) {
140
+ const parentDir = dirname(normalizedPath)
141
+ if (!existsSync(parentDir)) {
142
+ return {
143
+ result: false,
144
+ errorCode: 2,
145
+ message: `Parent directory does not exist: ${parentDir}`,
146
+ }
147
+ }
148
+
149
+ // For new files, ensure first edit creates the file (empty old_string)
150
+ if (edits.length === 0 || edits[0].old_string !== '') {
151
+ return {
152
+ result: false,
153
+ errorCode: 6,
154
+ message:
155
+ 'For new files, the first edit must have an empty old_string to create the file content.',
156
+ }
157
+ }
158
+ } else {
159
+ // For existing files, apply file protection mechanisms
160
+ const readFileTimestamps = context?.readFileTimestamps || {}
161
+ const readTimestamp = readFileTimestamps[normalizedPath]
162
+
163
+ if (!readTimestamp) {
164
+ return {
165
+ result: false,
166
+ errorCode: 7,
167
+ message:
168
+ 'File has not been read yet. Read it first before editing it.',
169
+ meta: {
170
+ filePath: normalizedPath,
171
+ isFilePathAbsolute: String(isAbsolute(file_path)),
172
+ },
173
+ }
174
+ }
175
+
176
+ // Check if file has been modified since last read
177
+ const stats = statSync(normalizedPath)
178
+ const lastWriteTime = stats.mtimeMs
179
+ if (lastWriteTime > readTimestamp) {
180
+ return {
181
+ result: false,
182
+ errorCode: 8,
183
+ message:
184
+ 'File has been modified since read, either by the user or by a linter. Read it again before attempting to edit it.',
185
+ meta: {
186
+ filePath: normalizedPath,
187
+ lastWriteTime,
188
+ readTimestamp,
189
+ },
190
+ }
191
+ }
192
+
193
+ // Pre-validate that all old_strings exist in the file
194
+ const encoding = detectFileEncoding(normalizedPath)
195
+ if (encoding === 'binary') {
196
+ return {
197
+ result: false,
198
+ errorCode: 9,
199
+ message: 'Cannot edit binary files.',
200
+ }
201
+ }
202
+
203
+ const currentContent = readFileSync(normalizedPath, 'utf-8')
204
+ for (let i = 0; i < edits.length; i++) {
205
+ const edit = edits[i]
206
+ if (
207
+ edit.old_string !== '' &&
208
+ !currentContent.includes(edit.old_string)
209
+ ) {
210
+ return {
211
+ result: false,
212
+ errorCode: 10,
213
+ message: `Edit ${i + 1}: String to replace not found in file: "${edit.old_string.substring(0, 100)}${edit.old_string.length > 100 ? '...' : ''}"`,
214
+ meta: {
215
+ editIndex: i + 1,
216
+ oldString: edit.old_string.substring(0, 200),
217
+ },
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ // Validate each edit
224
+ for (let i = 0; i < edits.length; i++) {
225
+ const edit = edits[i]
226
+ if (edit.old_string === edit.new_string) {
227
+ return {
228
+ result: false,
229
+ errorCode: 3,
230
+ message: `Edit ${i + 1}: old_string and new_string cannot be the same`,
231
+ }
232
+ }
233
+ }
234
+
235
+ return { result: true }
236
+ },
237
+ async *call({ file_path, edits }, { readFileTimestamps }) {
238
+ const startTime = Date.now()
239
+ const workingDir = getCwd()
240
+ const filePath = isAbsolute(file_path)
241
+ ? resolve(file_path)
242
+ : resolve(workingDir, file_path)
243
+
244
+ try {
245
+ // Read current file content (or empty for new files)
246
+ let currentContent = ''
247
+ let fileExists = existsSync(filePath)
248
+
249
+ if (fileExists) {
250
+ const encoding = detectFileEncoding(filePath)
251
+ if (encoding === 'binary') {
252
+ yield {
253
+ type: 'result',
254
+ data: 'Error: Cannot edit binary files',
255
+ resultForAssistant: 'Error: Cannot edit binary files',
256
+ }
257
+ return
258
+ }
259
+ currentContent = readFileSync(filePath, 'utf-8')
260
+ } else {
261
+ // For new files, ensure parent directory exists
262
+ const parentDir = dirname(filePath)
263
+ if (!existsSync(parentDir)) {
264
+ mkdirSync(parentDir, { recursive: true })
265
+ }
266
+ }
267
+
268
+ // Apply all edits sequentially
269
+ let modifiedContent = currentContent
270
+ const appliedEdits = []
271
+
272
+ for (let i = 0; i < edits.length; i++) {
273
+ const edit = edits[i]
274
+ const { old_string, new_string, replace_all } = edit
275
+
276
+ try {
277
+ const result = applyEdit(
278
+ modifiedContent,
279
+ old_string,
280
+ new_string,
281
+ replace_all,
282
+ )
283
+ modifiedContent = result.newContent
284
+ appliedEdits.push({
285
+ editIndex: i + 1,
286
+ success: true,
287
+ old_string: old_string.substring(0, 100),
288
+ new_string: new_string.substring(0, 100),
289
+ occurrences: result.occurrences,
290
+ })
291
+ } catch (error) {
292
+ // If any edit fails, abort the entire operation
293
+ const errorMessage =
294
+ error instanceof Error ? error.message : 'Unknown error'
295
+ yield {
296
+ type: 'result',
297
+ data: `Error in edit ${i + 1}: ${errorMessage}`,
298
+ resultForAssistant: `Error in edit ${i + 1}: ${errorMessage}`,
299
+ }
300
+ return
301
+ }
302
+ }
303
+
304
+ // Write the modified content
305
+ const lineEndings = fileExists ? detectLineEndings(currentContent) : '\n'
306
+ writeTextContent(filePath, modifiedContent, lineEndings)
307
+
308
+ // Record Agent edit operation for file freshness tracking
309
+ recordFileEdit(filePath, modifiedContent)
310
+
311
+ // Update readFileTimestamps to prevent stale file warnings
312
+ readFileTimestamps[filePath] = Date.now()
313
+
314
+ // Emit file edited event for system reminders
315
+ emitReminderEvent('file:edited', {
316
+ filePath,
317
+ edits: edits.map(e => ({
318
+ oldString: e.old_string,
319
+ newString: e.new_string,
320
+ })),
321
+ originalContent: currentContent,
322
+ newContent: modifiedContent,
323
+ timestamp: Date.now(),
324
+ operation: fileExists ? 'update' : 'create',
325
+ })
326
+
327
+ // Generate result data
328
+ const relativePath = relative(workingDir, filePath)
329
+ const summary = `Successfully applied ${edits.length} edits to ${relativePath}`
330
+
331
+ const resultData = {
332
+ filePath: relativePath,
333
+ wasNewFile: !fileExists,
334
+ editsApplied: appliedEdits,
335
+ totalEdits: edits.length,
336
+ summary,
337
+ }
338
+
339
+ // Log the operation
340
+ logEvent('multi_edit_tool_used', {
341
+ file_path: relativePath,
342
+ edits_count: String(edits.length),
343
+ was_new_file: String(!fileExists),
344
+ duration_ms: String(Date.now() - startTime),
345
+ })
346
+
347
+ yield {
348
+ type: 'result',
349
+ data: resultData,
350
+ resultForAssistant: summary,
351
+ }
352
+ } catch (error) {
353
+ const errorMessage =
354
+ error instanceof Error ? error.message : 'Unknown error occurred'
355
+ const errorResult = `Error applying multi-edit: ${errorMessage}`
356
+
357
+ logError(error)
358
+
359
+ yield {
360
+ type: 'result',
361
+ data: errorResult,
362
+ resultForAssistant: errorResult,
363
+ }
364
+ }
365
+ },
366
+ } satisfies Tool<typeof inputSchema, any>
@@ -0,0 +1,45 @@
1
+ import { NotebookEditTool } from '../NotebookEditTool/NotebookEditTool'
2
+
3
+ export const DESCRIPTION = `This is a tool for making multiple edits to a single file in one operation. It is built on top of the Edit tool and allows you to perform multiple find-and-replace operations efficiently. Prefer this tool over the Edit tool when you need to make multiple edits to the same file.
4
+
5
+ Before using this tool:
6
+
7
+ 1. Use the Read tool to understand the file's contents and context
8
+ 2. Verify the directory path is correct
9
+
10
+ To make multiple file edits, provide the following:
11
+ 1. file_path: The absolute path to the file to modify (must be absolute, not relative)
12
+ 2. edits: An array of edit operations to perform, where each edit contains:
13
+ - old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)
14
+ - new_string: The edited text to replace the old_string
15
+ - replace_all: Replace all occurences of old_string. This parameter is optional and defaults to false.
16
+
17
+ IMPORTANT:
18
+ - All edits are applied in sequence, in the order they are provided
19
+ - Each edit operates on the result of the previous edit
20
+ - All edits must be valid for the operation to succeed - if any edit fails, none will be applied
21
+ - This tool is ideal when you need to make several changes to different parts of the same file
22
+ - For Jupyter notebooks (.ipynb files), use the ${NotebookEditTool.name} instead
23
+
24
+ CRITICAL REQUIREMENTS:
25
+ 1. All edits follow the same requirements as the single Edit tool
26
+ 2. The edits are atomic - either all succeed or none are applied
27
+ 3. Plan your edits carefully to avoid conflicts between sequential operations
28
+
29
+ WARNING:
30
+ - The tool will fail if edits.old_string doesn't match the file contents exactly (including whitespace)
31
+ - The tool will fail if edits.old_string and edits.new_string are the same
32
+ - Since edits are applied in sequence, ensure that earlier edits don't affect the text that later edits are trying to find
33
+
34
+ When making edits:
35
+ - Ensure all edits result in idiomatic, correct code
36
+ - Do not leave the code in a broken state
37
+ - Always use absolute file paths (starting with /)
38
+ - Use replace_all for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
39
+
40
+ If you want to create a new file, use:
41
+ - A new file path, including dir name if needed
42
+ - First edit: empty old_string and the new file's contents as new_string
43
+ - Subsequent edits: normal edit operations on the created content`
44
+
45
+ export const PROMPT = DESCRIPTION
@@ -0,0 +1,298 @@
1
+ import { existsSync, readFileSync } from 'fs'
2
+ import { Box, Text } from 'ink'
3
+ import { extname, isAbsolute, relative, resolve } from 'path'
4
+ import * as React from 'react'
5
+ import { z } from 'zod'
6
+ import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage'
7
+ import { HighlightedCode } from '../../components/HighlightedCode'
8
+ import type { Tool } from '../../Tool'
9
+ import { NotebookCellType, NotebookContent } from '../../types/notebook'
10
+ import {
11
+ detectFileEncoding,
12
+ detectLineEndings,
13
+ writeTextContent,
14
+ } from '../../utils/file.js'
15
+ import { safeParseJSON } from '../../utils/json'
16
+ import { getCwd } from '../../utils/state'
17
+ import { DESCRIPTION, PROMPT } from './prompt'
18
+ import { hasWritePermission } from '../../utils/permissions/filesystem'
19
+ import { emitReminderEvent } from '../../services/systemReminder'
20
+ import { recordFileEdit } from '../../services/fileFreshness'
21
+
22
+ const inputSchema = z.strictObject({
23
+ notebook_path: z
24
+ .string()
25
+ .describe(
26
+ 'The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)',
27
+ ),
28
+ cell_number: z.number().describe('The index of the cell to edit (0-based)'),
29
+ new_source: z.string().describe('The new source for the cell'),
30
+ cell_type: z
31
+ .enum(['code', 'markdown'])
32
+ .optional()
33
+ .describe(
34
+ 'The type of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required.',
35
+ ),
36
+ edit_mode: z
37
+ .string()
38
+ .optional()
39
+ .describe(
40
+ 'The type of edit to make (replace, insert, delete). Defaults to replace.',
41
+ ),
42
+ })
43
+
44
+ export const NotebookEditTool = {
45
+ name: 'NotebookEditCell',
46
+ async description() {
47
+ return DESCRIPTION
48
+ },
49
+ async prompt() {
50
+ return PROMPT
51
+ },
52
+ inputSchema,
53
+ userFacingName() {
54
+ return 'Edit Notebook'
55
+ },
56
+ async isEnabled() {
57
+ return true
58
+ },
59
+ isReadOnly() {
60
+ return false
61
+ },
62
+ isConcurrencySafe() {
63
+ return false // NotebookEditTool modifies state/files, not safe for concurrent execution
64
+ },
65
+ needsPermissions({ notebook_path }) {
66
+ return !hasWritePermission(notebook_path)
67
+ },
68
+ renderResultForAssistant({ cell_number, edit_mode, new_source, error }) {
69
+ if (error) {
70
+ return error
71
+ }
72
+ switch (edit_mode) {
73
+ case 'replace':
74
+ return `Updated cell ${cell_number} with ${new_source}`
75
+ case 'insert':
76
+ return `Inserted cell ${cell_number} with ${new_source}`
77
+ case 'delete':
78
+ return `Deleted cell ${cell_number}`
79
+ }
80
+ },
81
+ renderToolUseMessage(input, { verbose }) {
82
+ return `notebook_path: ${verbose ? input.notebook_path : relative(getCwd(), input.notebook_path)}, cell: ${input.cell_number}, content: ${input.new_source.slice(0, 30)}…, cell_type: ${input.cell_type}, edit_mode: ${input.edit_mode ?? 'replace'}`
83
+ },
84
+ renderToolUseRejectedMessage() {
85
+ return <FallbackToolUseRejectedMessage />
86
+ },
87
+ renderToolResultMessage({ cell_number, new_source, language, error }) {
88
+ if (error) {
89
+ return (
90
+ <Box flexDirection="column">
91
+ <Text color="red">{error}</Text>
92
+ </Box>
93
+ )
94
+ }
95
+
96
+ return (
97
+ <Box flexDirection="column">
98
+ <Text>Updated cell {cell_number}:</Text>
99
+ <Box marginLeft={2}>
100
+ <HighlightedCode code={new_source} language={language} />
101
+ </Box>
102
+ </Box>
103
+ )
104
+ },
105
+ async validateInput({
106
+ notebook_path,
107
+ cell_number,
108
+ cell_type,
109
+ edit_mode = 'replace',
110
+ }) {
111
+ const fullPath = isAbsolute(notebook_path)
112
+ ? notebook_path
113
+ : resolve(getCwd(), notebook_path)
114
+
115
+ if (!existsSync(fullPath)) {
116
+ return {
117
+ result: false,
118
+ message: 'Notebook file does not exist.',
119
+ }
120
+ }
121
+
122
+ if (extname(fullPath) !== '.ipynb') {
123
+ return {
124
+ result: false,
125
+ message:
126
+ 'File must be a Jupyter notebook (.ipynb file). For editing other file types, use the FileEdit tool.',
127
+ }
128
+ }
129
+
130
+ if (cell_number < 0) {
131
+ return {
132
+ result: false,
133
+ message: 'Cell number must be non-negative.',
134
+ }
135
+ }
136
+
137
+ if (
138
+ edit_mode !== 'replace' &&
139
+ edit_mode !== 'insert' &&
140
+ edit_mode !== 'delete'
141
+ ) {
142
+ return {
143
+ result: false,
144
+ message: 'Edit mode must be replace, insert, or delete.',
145
+ }
146
+ }
147
+
148
+ if (edit_mode === 'insert' && !cell_type) {
149
+ return {
150
+ result: false,
151
+ message: 'Cell type is required when using edit_mode=insert.',
152
+ }
153
+ }
154
+
155
+ const enc = detectFileEncoding(fullPath)
156
+ const content = readFileSync(fullPath, enc)
157
+ const notebook = safeParseJSON(content) as NotebookContent | null
158
+ if (!notebook) {
159
+ return {
160
+ result: false,
161
+ message: 'Notebook is not valid JSON.',
162
+ }
163
+ }
164
+
165
+ if (edit_mode === 'insert' && cell_number > notebook.cells.length) {
166
+ return {
167
+ result: false,
168
+ message: `Cell number is out of bounds. For insert mode, the maximum value is ${notebook.cells.length} (to append at the end).`,
169
+ }
170
+ } else if (
171
+ (edit_mode === 'replace' || edit_mode === 'delete') &&
172
+ (cell_number >= notebook.cells.length || !notebook.cells[cell_number])
173
+ ) {
174
+ return {
175
+ result: false,
176
+ message: `Cell number is out of bounds. Notebook has ${notebook.cells.length} cells.`,
177
+ }
178
+ }
179
+
180
+ return { result: true }
181
+ },
182
+ async *call({
183
+ notebook_path,
184
+ cell_number,
185
+ new_source,
186
+ cell_type,
187
+ edit_mode,
188
+ }) {
189
+ const fullPath = isAbsolute(notebook_path)
190
+ ? notebook_path
191
+ : resolve(getCwd(), notebook_path)
192
+
193
+ try {
194
+ const enc = detectFileEncoding(fullPath)
195
+ const content = readFileSync(fullPath, enc)
196
+ const notebook = JSON.parse(content) as NotebookContent
197
+ const language = notebook.metadata.language_info?.name ?? 'python'
198
+
199
+ if (edit_mode === 'delete') {
200
+ // Delete the specified cell
201
+ notebook.cells.splice(cell_number, 1)
202
+ } else if (edit_mode === 'insert') {
203
+ // Insert the new cell
204
+ const new_cell = {
205
+ cell_type: cell_type!, // validateInput ensures cell_type is not undefined
206
+ source: new_source,
207
+ metadata: {},
208
+ }
209
+ notebook.cells.splice(
210
+ cell_number,
211
+ 0,
212
+ cell_type == 'markdown' ? new_cell : { ...new_cell, outputs: [] },
213
+ )
214
+ } else {
215
+ // Find the specified cell
216
+ const targetCell = notebook.cells[cell_number]! // validateInput ensures cell_number is in bounds
217
+ targetCell.source = new_source
218
+ // Reset execution count and clear outputs since cell was modified
219
+ targetCell.execution_count = undefined
220
+ targetCell.outputs = []
221
+ if (cell_type && cell_type !== targetCell.cell_type) {
222
+ targetCell.cell_type = cell_type
223
+ }
224
+ }
225
+ // Write back to file
226
+ const endings = detectLineEndings(fullPath)
227
+ const updatedNotebook = JSON.stringify(notebook, null, 1)
228
+ writeTextContent(fullPath, updatedNotebook, enc, endings!)
229
+
230
+ // Record Agent edit operation for file freshness tracking
231
+ recordFileEdit(fullPath, updatedNotebook)
232
+
233
+ // Emit file edited event for system reminders
234
+ emitReminderEvent('file:edited', {
235
+ filePath: fullPath,
236
+ cellNumber: cell_number,
237
+ newSource: new_source,
238
+ cellType: cell_type,
239
+ editMode: edit_mode || 'replace',
240
+ timestamp: Date.now(),
241
+ operation: 'notebook_edit',
242
+ })
243
+ const data = {
244
+ cell_number,
245
+ new_source,
246
+ cell_type: cell_type ?? 'code',
247
+ language,
248
+ edit_mode: edit_mode ?? 'replace',
249
+ error: '',
250
+ }
251
+ yield {
252
+ type: 'result',
253
+ data,
254
+ resultForAssistant: this.renderResultForAssistant(data),
255
+ }
256
+ } catch (error) {
257
+ if (error instanceof Error) {
258
+ const data = {
259
+ cell_number,
260
+ new_source,
261
+ cell_type: cell_type ?? 'code',
262
+ language: 'python',
263
+ edit_mode: 'replace',
264
+ error: error.message,
265
+ }
266
+ yield {
267
+ type: 'result',
268
+ data,
269
+ resultForAssistant: this.renderResultForAssistant(data),
270
+ }
271
+ return
272
+ }
273
+ const data = {
274
+ cell_number,
275
+ new_source,
276
+ cell_type: cell_type ?? 'code',
277
+ language: 'python',
278
+ edit_mode: 'replace',
279
+ error: 'Unknown error occurred while editing notebook',
280
+ }
281
+ yield {
282
+ type: 'result',
283
+ data,
284
+ resultForAssistant: this.renderResultForAssistant(data),
285
+ }
286
+ }
287
+ },
288
+ } satisfies Tool<
289
+ typeof inputSchema,
290
+ {
291
+ cell_number: number
292
+ new_source: string
293
+ cell_type: NotebookCellType
294
+ language: string
295
+ edit_mode: string
296
+ error?: string
297
+ }
298
+ >