@shareai-lab/kode 1.0.69 → 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 +205 -72
  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,371 @@
1
+ import { ImageBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
2
+ import { existsSync, readFileSync, statSync } from 'fs'
3
+ import { Box, Text } from 'ink'
4
+ import * as path from 'path'
5
+ import { extname, relative } from 'path'
6
+ import * as React from 'react'
7
+ import { z } from 'zod'
8
+ import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage'
9
+ import { HighlightedCode } from '../../components/HighlightedCode'
10
+ import type { Tool } from '../../Tool'
11
+ import { getCwd } from '../../utils/state'
12
+ import {
13
+ addLineNumbers,
14
+ findSimilarFile,
15
+ normalizeFilePath,
16
+ readTextContent,
17
+ } from '../../utils/file.js'
18
+ import { logError } from '../../utils/log'
19
+ import { getTheme } from '../../utils/theme'
20
+ import { emitReminderEvent } from '../../services/systemReminder'
21
+ import {
22
+ recordFileRead,
23
+ generateFileModificationReminder,
24
+ } from '../../services/fileFreshness'
25
+ import { DESCRIPTION, PROMPT } from './prompt'
26
+ import { hasReadPermission } from '../../utils/permissions/filesystem'
27
+
28
+ const MAX_LINES_TO_RENDER = 5
29
+ const MAX_OUTPUT_SIZE = 0.25 * 1024 * 1024 // 0.25MB in bytes
30
+
31
+ // Common image extensions
32
+ const IMAGE_EXTENSIONS = new Set([
33
+ '.png',
34
+ '.jpg',
35
+ '.jpeg',
36
+ '.gif',
37
+ '.bmp',
38
+ '.webp',
39
+ ])
40
+
41
+ // Maximum dimensions for images
42
+ const MAX_WIDTH = 2000
43
+ const MAX_HEIGHT = 2000
44
+ const MAX_IMAGE_SIZE = 3.75 * 1024 * 1024 // 5MB in bytes, with base64 encoding
45
+
46
+ const inputSchema = z.strictObject({
47
+ file_path: z.string().describe('The absolute path to the file to read'),
48
+ offset: z
49
+ .number()
50
+ .optional()
51
+ .describe(
52
+ 'The line number to start reading from. Only provide if the file is too large to read at once',
53
+ ),
54
+ limit: z
55
+ .number()
56
+ .optional()
57
+ .describe(
58
+ 'The number of lines to read. Only provide if the file is too large to read at once.',
59
+ ),
60
+ })
61
+
62
+ export const FileReadTool = {
63
+ name: 'View',
64
+ async description() {
65
+ return DESCRIPTION
66
+ },
67
+ async prompt() {
68
+ return PROMPT
69
+ },
70
+ inputSchema,
71
+ isReadOnly() {
72
+ return true
73
+ },
74
+ isConcurrencySafe() {
75
+ return true // FileRead is read-only, safe for concurrent execution
76
+ },
77
+ userFacingName() {
78
+ return 'Read'
79
+ },
80
+ async isEnabled() {
81
+ return true
82
+ },
83
+ needsPermissions({ file_path }) {
84
+ return !hasReadPermission(file_path || getCwd())
85
+ },
86
+ renderToolUseMessage(input, { verbose }) {
87
+ const { file_path, ...rest } = input
88
+ const entries = [
89
+ ['file_path', verbose ? file_path : relative(getCwd(), file_path)],
90
+ ...Object.entries(rest),
91
+ ]
92
+ return entries
93
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
94
+ .join(', ')
95
+ },
96
+ renderToolResultMessage(output, { verbose }) {
97
+ // TODO: Render recursively
98
+ switch (output.type) {
99
+ case 'image':
100
+ return (
101
+ <Box justifyContent="space-between" overflowX="hidden" width="100%">
102
+ <Box flexDirection="row">
103
+ <Text>&nbsp;&nbsp;⎿ &nbsp;</Text>
104
+ <Text>Read image</Text>
105
+ </Box>
106
+ </Box>
107
+ )
108
+ case 'text': {
109
+ const { filePath, content, numLines } = output.file
110
+ const contentWithFallback = content || '(No content)'
111
+ return (
112
+ <Box justifyContent="space-between" overflowX="hidden" width="100%">
113
+ <Box flexDirection="row">
114
+ <Text>&nbsp;&nbsp;⎿ &nbsp;</Text>
115
+ <Box flexDirection="column">
116
+ <HighlightedCode
117
+ code={
118
+ verbose
119
+ ? contentWithFallback
120
+ : contentWithFallback
121
+ .split('\n')
122
+ .slice(0, MAX_LINES_TO_RENDER)
123
+ .filter(_ => _.trim() !== '')
124
+ .join('\n')
125
+ }
126
+ language={extname(filePath).slice(1)}
127
+ />
128
+ {!verbose && numLines > MAX_LINES_TO_RENDER && (
129
+ <Text color={getTheme().secondaryText}>
130
+ ... (+{numLines - MAX_LINES_TO_RENDER} lines)
131
+ </Text>
132
+ )}
133
+ </Box>
134
+ </Box>
135
+ </Box>
136
+ )
137
+ }
138
+ }
139
+ },
140
+ renderToolUseRejectedMessage() {
141
+ return <FallbackToolUseRejectedMessage />
142
+ },
143
+ async validateInput({ file_path, offset, limit }) {
144
+ const fullFilePath = normalizeFilePath(file_path)
145
+
146
+ if (!existsSync(fullFilePath)) {
147
+ // Try to find a similar file with a different extension
148
+ const similarFilename = findSimilarFile(fullFilePath)
149
+ let message = 'File does not exist.'
150
+
151
+ // If we found a similar file, suggest it to the assistant
152
+ if (similarFilename) {
153
+ message += ` Did you mean ${similarFilename}?`
154
+ }
155
+
156
+ return {
157
+ result: false,
158
+ message,
159
+ }
160
+ }
161
+
162
+ // Get file stats to check size
163
+ const stats = statSync(fullFilePath)
164
+ const fileSize = stats.size
165
+ const ext = path.extname(fullFilePath).toLowerCase()
166
+
167
+ // Skip size check for image files - they have their own size limits
168
+ if (!IMAGE_EXTENSIONS.has(ext)) {
169
+ // If file is too large and no offset/limit provided
170
+ if (fileSize > MAX_OUTPUT_SIZE && !offset && !limit) {
171
+ return {
172
+ result: false,
173
+ message: formatFileSizeError(fileSize),
174
+ meta: { fileSize },
175
+ }
176
+ }
177
+ }
178
+
179
+ return { result: true }
180
+ },
181
+ async *call(
182
+ { file_path, offset = 1, limit = undefined },
183
+ { readFileTimestamps },
184
+ ) {
185
+ const ext = path.extname(file_path).toLowerCase()
186
+ const fullFilePath = normalizeFilePath(file_path)
187
+
188
+ // Record file read for freshness tracking
189
+ recordFileRead(fullFilePath)
190
+
191
+ // Emit file read event for system reminders
192
+ emitReminderEvent('file:read', {
193
+ filePath: fullFilePath,
194
+ extension: ext,
195
+ timestamp: Date.now(),
196
+ })
197
+
198
+ // Update read timestamp, to invalidate stale writes
199
+ readFileTimestamps[fullFilePath] = Date.now()
200
+
201
+ // Check for file modifications and generate reminder if needed
202
+ const modificationReminder = generateFileModificationReminder(fullFilePath)
203
+ if (modificationReminder) {
204
+ emitReminderEvent('file:modified', {
205
+ filePath: fullFilePath,
206
+ reminder: modificationReminder,
207
+ timestamp: Date.now(),
208
+ })
209
+ }
210
+
211
+ // If it's an image file, process and return base64 encoded contents
212
+ if (IMAGE_EXTENSIONS.has(ext)) {
213
+ const data = await readImage(fullFilePath, ext)
214
+ yield {
215
+ type: 'result',
216
+ data,
217
+ resultForAssistant: this.renderResultForAssistant(data),
218
+ }
219
+ return
220
+ }
221
+
222
+ // Handle offset properly - if offset is 0, don't subtract 1
223
+ const lineOffset = offset === 0 ? 0 : offset - 1
224
+ const { content, lineCount, totalLines } = readTextContent(
225
+ fullFilePath,
226
+ lineOffset,
227
+ limit,
228
+ )
229
+
230
+ // Add size validation after reading for non-image files
231
+ if (!IMAGE_EXTENSIONS.has(ext) && content.length > MAX_OUTPUT_SIZE) {
232
+ throw new Error(formatFileSizeError(content.length))
233
+ }
234
+
235
+ const data = {
236
+ type: 'text' as const,
237
+ file: {
238
+ filePath: file_path,
239
+ content: content,
240
+ numLines: lineCount,
241
+ startLine: offset,
242
+ totalLines,
243
+ },
244
+ }
245
+
246
+ yield {
247
+ type: 'result',
248
+ data,
249
+ resultForAssistant: this.renderResultForAssistant(data),
250
+ }
251
+ },
252
+ renderResultForAssistant(data) {
253
+ switch (data.type) {
254
+ case 'image':
255
+ return [
256
+ {
257
+ type: 'image',
258
+ source: {
259
+ type: 'base64',
260
+ data: data.file.base64,
261
+ media_type: data.file.type,
262
+ },
263
+ },
264
+ ]
265
+ case 'text':
266
+ return addLineNumbers(data.file)
267
+ }
268
+ },
269
+ } satisfies Tool<
270
+ typeof inputSchema,
271
+ | {
272
+ type: 'text'
273
+ file: {
274
+ filePath: string
275
+ content: string
276
+ numLines: number
277
+ startLine: number
278
+ totalLines: number
279
+ }
280
+ }
281
+ | {
282
+ type: 'image'
283
+ file: { base64: string; type: ImageBlockParam.Source['media_type'] }
284
+ }
285
+ >
286
+
287
+ const formatFileSizeError = (sizeInBytes: number) =>
288
+ `File content (${Math.round(sizeInBytes / 1024)}KB) exceeds maximum allowed size (${Math.round(MAX_OUTPUT_SIZE / 1024)}KB). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.`
289
+
290
+ function createImageResponse(
291
+ buffer: Buffer,
292
+ ext: string,
293
+ ): {
294
+ type: 'image'
295
+ file: { base64: string; type: ImageBlockParam.Source['media_type'] }
296
+ } {
297
+ return {
298
+ type: 'image',
299
+ file: {
300
+ base64: buffer.toString('base64'),
301
+ type: `image/${ext.slice(1)}` as ImageBlockParam.Source['media_type'],
302
+ },
303
+ }
304
+ }
305
+
306
+ async function readImage(
307
+ filePath: string,
308
+ ext: string,
309
+ ): Promise<{
310
+ type: 'image'
311
+ file: { base64: string; type: ImageBlockParam.Source['media_type'] }
312
+ }> {
313
+ try {
314
+ const stats = statSync(filePath)
315
+ const sharp = (
316
+ (await import('sharp')) as unknown as { default: typeof import('sharp') }
317
+ ).default
318
+ const image = sharp(readFileSync(filePath))
319
+ const metadata = await image.metadata()
320
+
321
+ if (!metadata.width || !metadata.height) {
322
+ if (stats.size > MAX_IMAGE_SIZE) {
323
+ const compressedBuffer = await image.jpeg({ quality: 80 }).toBuffer()
324
+ return createImageResponse(compressedBuffer, 'jpeg')
325
+ }
326
+ }
327
+
328
+ // Calculate dimensions while maintaining aspect ratio
329
+ let width = metadata.width || 0
330
+ let height = metadata.height || 0
331
+
332
+ // Check if the original file just works
333
+ if (
334
+ stats.size <= MAX_IMAGE_SIZE &&
335
+ width <= MAX_WIDTH &&
336
+ height <= MAX_HEIGHT
337
+ ) {
338
+ return createImageResponse(readFileSync(filePath), ext)
339
+ }
340
+
341
+ if (width > MAX_WIDTH) {
342
+ height = Math.round((height * MAX_WIDTH) / width)
343
+ width = MAX_WIDTH
344
+ }
345
+
346
+ if (height > MAX_HEIGHT) {
347
+ width = Math.round((width * MAX_HEIGHT) / height)
348
+ height = MAX_HEIGHT
349
+ }
350
+
351
+ // Resize image and convert to buffer
352
+ const resizedImageBuffer = await image
353
+ .resize(width, height, {
354
+ fit: 'inside',
355
+ withoutEnlargement: true,
356
+ })
357
+ .toBuffer()
358
+
359
+ // If still too large after resize, compress quality
360
+ if (resizedImageBuffer.length > MAX_IMAGE_SIZE) {
361
+ const compressedBuffer = await image.jpeg({ quality: 80 }).toBuffer()
362
+ return createImageResponse(compressedBuffer, 'jpeg')
363
+ }
364
+
365
+ return createImageResponse(resizedImageBuffer, ext)
366
+ } catch (e) {
367
+ logError(e)
368
+ // If any error occurs during processing, return original image
369
+ return createImageResponse(readFileSync(filePath), ext)
370
+ }
371
+ }
@@ -0,0 +1,7 @@
1
+ import { NotebookReadTool } from '../NotebookReadTool/NotebookReadTool'
2
+
3
+ const MAX_LINES_TO_READ = 2000
4
+ const MAX_LINE_LENGTH = 2000
5
+
6
+ export const DESCRIPTION = 'Read a file from the local filesystem.'
7
+ export const PROMPT = `Reads a file from the local filesystem. The file_path parameter must be an absolute path, not a relative path. By default, it reads up to ${MAX_LINES_TO_READ} lines starting from the beginning of the file. You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters. Any lines longer than ${MAX_LINE_LENGTH} characters will be truncated. For image files, the tool will display the image for you. For Jupyter notebooks (.ipynb files), use the ${NotebookReadTool.name} instead.`
@@ -0,0 +1,297 @@
1
+ import { Hunk } from 'diff'
2
+ import { existsSync, mkdirSync, readFileSync, statSync } from 'fs'
3
+ import { Box, Text } from 'ink'
4
+ import { EOL } from 'os'
5
+ import { dirname, extname, isAbsolute, relative, resolve, sep } from 'path'
6
+ import * as React from 'react'
7
+ import { z } from 'zod'
8
+ import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage'
9
+ import { HighlightedCode } from '../../components/HighlightedCode'
10
+ import { StructuredDiff } from '../../components/StructuredDiff'
11
+ import { logEvent } from '../../services/statsig'
12
+ import type { Tool } from '../../Tool'
13
+ import { intersperse } from '../../utils/array'
14
+ import {
15
+ addLineNumbers,
16
+ detectFileEncoding,
17
+ detectLineEndings,
18
+ detectRepoLineEndings,
19
+ writeTextContent,
20
+ } from '../../utils/file.js'
21
+ import { logError } from '../../utils/log'
22
+ import { getCwd } from '../../utils/state'
23
+ import { getTheme } from '../../utils/theme'
24
+ import { PROMPT } from './prompt'
25
+ import { hasWritePermission } from '../../utils/permissions/filesystem'
26
+ import { getPatch } from '../../utils/diff'
27
+ import { PROJECT_FILE } from '../../constants/product'
28
+ import { emitReminderEvent } from '../../services/systemReminder'
29
+ import { recordFileEdit } from '../../services/fileFreshness'
30
+
31
+ const MAX_LINES_TO_RENDER = 5
32
+ const MAX_LINES_TO_RENDER_FOR_ASSISTANT = 16000
33
+ const TRUNCATED_MESSAGE =
34
+ '<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with Grep in order to find the line numbers of what you are looking for.</NOTE>'
35
+
36
+ const inputSchema = z.strictObject({
37
+ file_path: z
38
+ .string()
39
+ .describe(
40
+ 'The absolute path to the file to write (must be absolute, not relative)',
41
+ ),
42
+ content: z.string().describe('The content to write to the file'),
43
+ })
44
+
45
+ export const FileWriteTool = {
46
+ name: 'Replace',
47
+ async description() {
48
+ return 'Write a file to the local filesystem.'
49
+ },
50
+ userFacingName: () => 'Write',
51
+ async prompt() {
52
+ return PROMPT
53
+ },
54
+ inputSchema,
55
+ async isEnabled() {
56
+ return true
57
+ },
58
+ isReadOnly() {
59
+ return false
60
+ },
61
+ isConcurrencySafe() {
62
+ return false // FileWriteTool modifies state/files, not safe for concurrent execution
63
+ },
64
+ needsPermissions({ file_path }) {
65
+ return !hasWritePermission(file_path)
66
+ },
67
+ renderToolUseMessage(input, { verbose }) {
68
+ return `file_path: ${verbose ? input.file_path : relative(getCwd(), input.file_path)}`
69
+ },
70
+ renderToolUseRejectedMessage({ file_path, content }, { columns, verbose }) {
71
+ try {
72
+ const fullFilePath = isAbsolute(file_path)
73
+ ? file_path
74
+ : resolve(getCwd(), file_path)
75
+ const oldFileExists = existsSync(fullFilePath)
76
+ const enc = oldFileExists ? detectFileEncoding(fullFilePath) : 'utf-8'
77
+ const oldContent = oldFileExists ? readFileSync(fullFilePath, enc) : null
78
+ const type = oldContent ? 'update' : 'create'
79
+ const patch = getPatch({
80
+ filePath: file_path,
81
+ fileContents: oldContent ?? '',
82
+ oldStr: oldContent ?? '',
83
+ newStr: content,
84
+ })
85
+
86
+ return (
87
+ <Box flexDirection="column">
88
+ <Text>
89
+ {' '}⎿{' '}
90
+ <Text color={getTheme().error}>
91
+ User rejected {type === 'update' ? 'update' : 'write'} to{' '}
92
+ </Text>
93
+ <Text bold>
94
+ {verbose ? file_path : relative(getCwd(), file_path)}
95
+ </Text>
96
+ </Text>
97
+ {intersperse(
98
+ patch.map(_ => (
99
+ <Box flexDirection="column" paddingLeft={5} key={_.newStart}>
100
+ <StructuredDiff patch={_} dim={true} width={columns - 12} />
101
+ </Box>
102
+ )),
103
+ i => (
104
+ <Box paddingLeft={5} key={`ellipsis-${i}`}>
105
+ <Text color={getTheme().secondaryText}>...</Text>
106
+ </Box>
107
+ ),
108
+ )}
109
+ </Box>
110
+ )
111
+ } catch (e) {
112
+ // Handle the case where while we were showing the diff, the user manually made the change.
113
+ // TODO: Find a way to show the diff in this case
114
+ logError(e)
115
+ return (
116
+ <Box flexDirection="column">
117
+ <Text>{' '}⎿ (No changes)</Text>
118
+ </Box>
119
+ )
120
+ }
121
+ },
122
+ renderToolResultMessage(
123
+ { filePath, content, structuredPatch, type },
124
+ { verbose },
125
+ ) {
126
+ switch (type) {
127
+ case 'create': {
128
+ const contentWithFallback = content || '(No content)'
129
+ const numLines = content.split(EOL).length
130
+
131
+ return (
132
+ <Box flexDirection="column">
133
+ <Text>
134
+ {' '}⎿ Wrote {numLines} lines to{' '}
135
+ <Text bold>
136
+ {verbose ? filePath : relative(getCwd(), filePath)}
137
+ </Text>
138
+ </Text>
139
+ <Box flexDirection="column" paddingLeft={5}>
140
+ <HighlightedCode
141
+ code={
142
+ verbose
143
+ ? contentWithFallback
144
+ : contentWithFallback
145
+ .split('\n')
146
+ .slice(0, MAX_LINES_TO_RENDER)
147
+ .filter(_ => _.trim() !== '')
148
+ .join('\n')
149
+ }
150
+ language={extname(filePath).slice(1)}
151
+ />
152
+ {!verbose && numLines > MAX_LINES_TO_RENDER && (
153
+ <Text color={getTheme().secondaryText}>
154
+ ... (+{numLines - MAX_LINES_TO_RENDER} lines)
155
+ </Text>
156
+ )}
157
+ </Box>
158
+ </Box>
159
+ )
160
+ }
161
+ case 'update':
162
+ return (
163
+ <FileEditToolUpdatedMessage
164
+ filePath={filePath}
165
+ structuredPatch={structuredPatch}
166
+ verbose={verbose}
167
+ />
168
+ )
169
+ }
170
+ },
171
+ async validateInput({ file_path }, { readFileTimestamps }) {
172
+ const fullFilePath = isAbsolute(file_path)
173
+ ? file_path
174
+ : resolve(getCwd(), file_path)
175
+ if (!existsSync(fullFilePath)) {
176
+ return { result: true }
177
+ }
178
+
179
+ const readTimestamp = readFileTimestamps[fullFilePath]
180
+ if (!readTimestamp) {
181
+ return {
182
+ result: false,
183
+ message:
184
+ 'File has not been read yet. Read it first before writing to it.',
185
+ }
186
+ }
187
+
188
+ // Check if file exists and get its last modified time
189
+ const stats = statSync(fullFilePath)
190
+ const lastWriteTime = stats.mtimeMs
191
+ if (lastWriteTime > readTimestamp) {
192
+ return {
193
+ result: false,
194
+ message:
195
+ 'File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.',
196
+ }
197
+ }
198
+
199
+ return { result: true }
200
+ },
201
+ async *call({ file_path, content }, { readFileTimestamps }) {
202
+ const fullFilePath = isAbsolute(file_path)
203
+ ? file_path
204
+ : resolve(getCwd(), file_path)
205
+ const dir = dirname(fullFilePath)
206
+ const oldFileExists = existsSync(fullFilePath)
207
+ const enc = oldFileExists ? detectFileEncoding(fullFilePath) : 'utf-8'
208
+ const oldContent = oldFileExists ? readFileSync(fullFilePath, enc) : null
209
+
210
+ const endings = oldFileExists
211
+ ? detectLineEndings(fullFilePath)
212
+ : await detectRepoLineEndings(getCwd())
213
+
214
+ mkdirSync(dir, { recursive: true })
215
+ writeTextContent(fullFilePath, content, enc, endings!)
216
+
217
+ // Record Agent edit operation for file freshness tracking
218
+ recordFileEdit(fullFilePath, content)
219
+
220
+ // Update read timestamp, to invalidate stale writes
221
+ readFileTimestamps[fullFilePath] = statSync(fullFilePath).mtimeMs
222
+
223
+ // Log when writing to CLAUDE.md
224
+ if (fullFilePath.endsWith(`${sep}${PROJECT_FILE}`)) {
225
+ logEvent('tengu_write_claudemd', {})
226
+ }
227
+
228
+ // Emit file edited event for system reminders
229
+ emitReminderEvent('file:edited', {
230
+ filePath: fullFilePath,
231
+ content,
232
+ oldContent: oldContent || '',
233
+ timestamp: Date.now(),
234
+ operation: oldFileExists ? 'update' : 'create',
235
+ })
236
+
237
+ if (oldContent) {
238
+ const patch = getPatch({
239
+ filePath: file_path,
240
+ fileContents: oldContent,
241
+ oldStr: oldContent,
242
+ newStr: content,
243
+ })
244
+
245
+ const data = {
246
+ type: 'update' as const,
247
+ filePath: file_path,
248
+ content,
249
+ structuredPatch: patch,
250
+ }
251
+ yield {
252
+ type: 'result',
253
+ data,
254
+ resultForAssistant: this.renderResultForAssistant(data),
255
+ }
256
+ return
257
+ }
258
+
259
+ const data = {
260
+ type: 'create' as const,
261
+ filePath: file_path,
262
+ content,
263
+ structuredPatch: [],
264
+ }
265
+ yield {
266
+ type: 'result',
267
+ data,
268
+ resultForAssistant: this.renderResultForAssistant(data),
269
+ }
270
+ },
271
+ renderResultForAssistant({ filePath, content, type }) {
272
+ switch (type) {
273
+ case 'create':
274
+ return `File created successfully at: ${filePath}`
275
+ case 'update':
276
+ return `The file ${filePath} has been updated. Here's the result of running \`cat -n\` on a snippet of the edited file:
277
+ ${addLineNumbers({
278
+ content:
279
+ content.split(/\r?\n/).length > MAX_LINES_TO_RENDER_FOR_ASSISTANT
280
+ ? content
281
+ .split(/\r?\n/)
282
+ .slice(0, MAX_LINES_TO_RENDER_FOR_ASSISTANT)
283
+ .join('\n') + TRUNCATED_MESSAGE
284
+ : content,
285
+ startLine: 1,
286
+ })}`
287
+ }
288
+ },
289
+ } satisfies Tool<
290
+ typeof inputSchema,
291
+ {
292
+ type: 'create' | 'update'
293
+ filePath: string
294
+ content: string
295
+ structuredPatch: Hunk[]
296
+ }
297
+ >
@@ -0,0 +1,10 @@
1
+ export const PROMPT = `Write a file to the local filesystem. Overwrites the existing file if there is one.
2
+
3
+ Before using this tool:
4
+
5
+ 1. Use the ReadFile tool to understand the file's contents and context
6
+
7
+ 2. Directory Verification (only applicable when creating new files):
8
+ - Use the LS tool to verify the parent directory exists and is the correct location`
9
+
10
+ export const DESCRIPTION = 'Write a file to the local filesystem.'