@shareai-lab/kode 1.0.70 → 1.0.73

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 (278) hide show
  1. package/README.md +342 -75
  2. package/README.zh-CN.md +292 -0
  3. package/cli.js +62 -0
  4. package/package.json +49 -25
  5. package/scripts/postinstall.js +56 -0
  6. package/src/ProjectOnboarding.tsx +198 -0
  7. package/src/Tool.ts +82 -0
  8. package/src/commands/agents.tsx +3401 -0
  9. package/src/commands/approvedTools.ts +53 -0
  10. package/src/commands/bug.tsx +20 -0
  11. package/src/commands/clear.ts +43 -0
  12. package/src/commands/compact.ts +120 -0
  13. package/src/commands/config.tsx +19 -0
  14. package/src/commands/cost.ts +18 -0
  15. package/src/commands/ctx_viz.ts +209 -0
  16. package/src/commands/doctor.ts +24 -0
  17. package/src/commands/help.tsx +19 -0
  18. package/src/commands/init.ts +37 -0
  19. package/src/commands/listen.ts +42 -0
  20. package/src/commands/login.tsx +51 -0
  21. package/src/commands/logout.tsx +40 -0
  22. package/src/commands/mcp.ts +41 -0
  23. package/src/commands/model.tsx +40 -0
  24. package/src/commands/modelstatus.tsx +20 -0
  25. package/src/commands/onboarding.tsx +34 -0
  26. package/src/commands/pr_comments.ts +59 -0
  27. package/src/commands/refreshCommands.ts +54 -0
  28. package/src/commands/release-notes.ts +34 -0
  29. package/src/commands/resume.tsx +31 -0
  30. package/src/commands/review.ts +49 -0
  31. package/src/commands/terminalSetup.ts +221 -0
  32. package/src/commands.ts +139 -0
  33. package/src/components/ApproveApiKey.tsx +93 -0
  34. package/src/components/AsciiLogo.tsx +13 -0
  35. package/src/components/AutoUpdater.tsx +148 -0
  36. package/src/components/Bug.tsx +367 -0
  37. package/src/components/Config.tsx +293 -0
  38. package/src/components/ConsoleOAuthFlow.tsx +327 -0
  39. package/src/components/Cost.tsx +23 -0
  40. package/src/components/CostThresholdDialog.tsx +46 -0
  41. package/src/components/CustomSelect/option-map.ts +42 -0
  42. package/src/components/CustomSelect/select-option.tsx +78 -0
  43. package/src/components/CustomSelect/select.tsx +152 -0
  44. package/src/components/CustomSelect/theme.ts +45 -0
  45. package/src/components/CustomSelect/use-select-state.ts +414 -0
  46. package/src/components/CustomSelect/use-select.ts +35 -0
  47. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  48. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  49. package/src/components/Help.tsx +215 -0
  50. package/src/components/HighlightedCode.tsx +33 -0
  51. package/src/components/InvalidConfigDialog.tsx +113 -0
  52. package/src/components/Link.tsx +32 -0
  53. package/src/components/LogSelector.tsx +86 -0
  54. package/src/components/Logo.tsx +145 -0
  55. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  56. package/src/components/MCPServerDialogCopy.tsx +25 -0
  57. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  58. package/src/components/Message.tsx +221 -0
  59. package/src/components/MessageResponse.tsx +15 -0
  60. package/src/components/MessageSelector.tsx +211 -0
  61. package/src/components/ModeIndicator.tsx +88 -0
  62. package/src/components/ModelConfig.tsx +301 -0
  63. package/src/components/ModelListManager.tsx +227 -0
  64. package/src/components/ModelSelector.tsx +3386 -0
  65. package/src/components/ModelStatusDisplay.tsx +230 -0
  66. package/src/components/Onboarding.tsx +274 -0
  67. package/src/components/PressEnterToContinue.tsx +11 -0
  68. package/src/components/PromptInput.tsx +740 -0
  69. package/src/components/SentryErrorBoundary.ts +33 -0
  70. package/src/components/Spinner.tsx +129 -0
  71. package/src/components/StickerRequestForm.tsx +16 -0
  72. package/src/components/StructuredDiff.tsx +191 -0
  73. package/src/components/TextInput.tsx +259 -0
  74. package/src/components/TodoItem.tsx +11 -0
  75. package/src/components/TokenWarning.tsx +31 -0
  76. package/src/components/ToolUseLoader.tsx +40 -0
  77. package/src/components/TrustDialog.tsx +106 -0
  78. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  79. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  80. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  81. package/src/components/binary-feedback/utils.ts +220 -0
  82. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  83. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +49 -0
  84. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  85. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  86. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  87. package/src/components/messages/AssistantToolUseMessage.tsx +133 -0
  88. package/src/components/messages/TaskProgressMessage.tsx +32 -0
  89. package/src/components/messages/TaskToolMessage.tsx +58 -0
  90. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  91. package/src/components/messages/UserCommandMessage.tsx +30 -0
  92. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  93. package/src/components/messages/UserPromptMessage.tsx +35 -0
  94. package/src/components/messages/UserTextMessage.tsx +39 -0
  95. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  96. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  97. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  98. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  99. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  100. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  101. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  102. package/src/components/permissions/FallbackPermissionRequest.tsx +153 -0
  103. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  104. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +77 -0
  105. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  106. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +83 -0
  107. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +240 -0
  108. package/src/components/permissions/PermissionRequest.tsx +101 -0
  109. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  110. package/src/components/permissions/hooks.ts +44 -0
  111. package/src/components/permissions/toolUseOptions.ts +59 -0
  112. package/src/components/permissions/utils.ts +23 -0
  113. package/src/constants/betas.ts +5 -0
  114. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  115. package/src/constants/figures.ts +4 -0
  116. package/src/constants/keys.ts +3 -0
  117. package/src/constants/macros.ts +8 -0
  118. package/src/constants/modelCapabilities.ts +179 -0
  119. package/src/constants/models.ts +1025 -0
  120. package/src/constants/oauth.ts +18 -0
  121. package/src/constants/product.ts +17 -0
  122. package/src/constants/prompts.ts +177 -0
  123. package/src/constants/releaseNotes.ts +7 -0
  124. package/src/context/PermissionContext.tsx +149 -0
  125. package/src/context.ts +278 -0
  126. package/src/cost-tracker.ts +84 -0
  127. package/src/entrypoints/cli.tsx +1518 -0
  128. package/src/entrypoints/mcp.ts +176 -0
  129. package/src/history.ts +25 -0
  130. package/src/hooks/useApiKeyVerification.ts +59 -0
  131. package/src/hooks/useArrowKeyHistory.ts +55 -0
  132. package/src/hooks/useCanUseTool.ts +138 -0
  133. package/src/hooks/useCancelRequest.ts +39 -0
  134. package/src/hooks/useDoublePress.ts +42 -0
  135. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  136. package/src/hooks/useInterval.ts +25 -0
  137. package/src/hooks/useLogMessages.ts +16 -0
  138. package/src/hooks/useLogStartupTime.ts +12 -0
  139. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  140. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  141. package/src/hooks/useTerminalSize.ts +49 -0
  142. package/src/hooks/useTextInput.ts +318 -0
  143. package/src/hooks/useUnifiedCompletion.ts +1404 -0
  144. package/src/messages.ts +38 -0
  145. package/src/permissions.ts +268 -0
  146. package/src/query.ts +707 -0
  147. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  148. package/src/screens/Doctor.tsx +219 -0
  149. package/src/screens/LogList.tsx +68 -0
  150. package/src/screens/REPL.tsx +798 -0
  151. package/src/screens/ResumeConversation.tsx +68 -0
  152. package/src/services/adapters/base.ts +38 -0
  153. package/src/services/adapters/chatCompletions.ts +90 -0
  154. package/src/services/adapters/responsesAPI.ts +170 -0
  155. package/src/services/browserMocks.ts +66 -0
  156. package/src/services/claude.ts +2083 -0
  157. package/src/services/customCommands.ts +704 -0
  158. package/src/services/fileFreshness.ts +377 -0
  159. package/src/services/gpt5ConnectionTest.ts +340 -0
  160. package/src/services/mcpClient.ts +564 -0
  161. package/src/services/mcpServerApproval.tsx +50 -0
  162. package/src/services/mentionProcessor.ts +273 -0
  163. package/src/services/modelAdapterFactory.ts +69 -0
  164. package/src/services/notifier.ts +40 -0
  165. package/src/services/oauth.ts +357 -0
  166. package/src/services/openai.ts +1305 -0
  167. package/src/services/responseStateManager.ts +90 -0
  168. package/src/services/sentry.ts +3 -0
  169. package/src/services/statsig.ts +171 -0
  170. package/src/services/statsigStorage.ts +86 -0
  171. package/src/services/systemReminder.ts +507 -0
  172. package/src/services/vcr.ts +161 -0
  173. package/src/test/testAdapters.ts +96 -0
  174. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  175. package/src/tools/ArchitectTool/prompt.ts +15 -0
  176. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +569 -0
  177. package/src/tools/BashTool/BashTool.tsx +243 -0
  178. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  179. package/src/tools/BashTool/OutputLine.tsx +49 -0
  180. package/src/tools/BashTool/prompt.ts +174 -0
  181. package/src/tools/BashTool/utils.ts +56 -0
  182. package/src/tools/FileEditTool/FileEditTool.tsx +315 -0
  183. package/src/tools/FileEditTool/prompt.ts +51 -0
  184. package/src/tools/FileEditTool/utils.ts +58 -0
  185. package/src/tools/FileReadTool/FileReadTool.tsx +404 -0
  186. package/src/tools/FileReadTool/prompt.ts +7 -0
  187. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  188. package/src/tools/FileWriteTool/prompt.ts +10 -0
  189. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  190. package/src/tools/GlobTool/prompt.ts +8 -0
  191. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  192. package/src/tools/GrepTool/prompt.ts +11 -0
  193. package/src/tools/MCPTool/MCPTool.tsx +107 -0
  194. package/src/tools/MCPTool/prompt.ts +3 -0
  195. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  196. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  197. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  198. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  199. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  200. package/src/tools/MultiEditTool/prompt.ts +45 -0
  201. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  202. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  203. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +258 -0
  204. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  205. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  206. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  207. package/src/tools/TaskTool/TaskTool.tsx +466 -0
  208. package/src/tools/TaskTool/constants.ts +1 -0
  209. package/src/tools/TaskTool/prompt.ts +92 -0
  210. package/src/tools/ThinkTool/ThinkTool.tsx +54 -0
  211. package/src/tools/ThinkTool/prompt.ts +12 -0
  212. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +290 -0
  213. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  214. package/src/tools/lsTool/lsTool.tsx +272 -0
  215. package/src/tools/lsTool/prompt.ts +2 -0
  216. package/src/tools.ts +63 -0
  217. package/src/types/PermissionMode.ts +120 -0
  218. package/src/types/RequestContext.ts +72 -0
  219. package/src/types/conversation.ts +51 -0
  220. package/src/types/logs.ts +58 -0
  221. package/src/types/modelCapabilities.ts +64 -0
  222. package/src/types/notebook.ts +87 -0
  223. package/src/utils/Cursor.ts +436 -0
  224. package/src/utils/PersistentShell.ts +373 -0
  225. package/src/utils/advancedFuzzyMatcher.ts +290 -0
  226. package/src/utils/agentLoader.ts +284 -0
  227. package/src/utils/agentStorage.ts +97 -0
  228. package/src/utils/array.ts +3 -0
  229. package/src/utils/ask.tsx +99 -0
  230. package/src/utils/auth.ts +13 -0
  231. package/src/utils/autoCompactCore.ts +223 -0
  232. package/src/utils/autoUpdater.ts +318 -0
  233. package/src/utils/betas.ts +20 -0
  234. package/src/utils/browser.ts +14 -0
  235. package/src/utils/cleanup.ts +72 -0
  236. package/src/utils/commands.ts +261 -0
  237. package/src/utils/commonUnixCommands.ts +161 -0
  238. package/src/utils/config.ts +942 -0
  239. package/src/utils/conversationRecovery.ts +55 -0
  240. package/src/utils/debugLogger.ts +1123 -0
  241. package/src/utils/diff.ts +42 -0
  242. package/src/utils/env.ts +57 -0
  243. package/src/utils/errors.ts +21 -0
  244. package/src/utils/exampleCommands.ts +109 -0
  245. package/src/utils/execFileNoThrow.ts +51 -0
  246. package/src/utils/expertChatStorage.ts +136 -0
  247. package/src/utils/file.ts +402 -0
  248. package/src/utils/fileRecoveryCore.ts +71 -0
  249. package/src/utils/format.tsx +44 -0
  250. package/src/utils/fuzzyMatcher.ts +328 -0
  251. package/src/utils/generators.ts +62 -0
  252. package/src/utils/git.ts +92 -0
  253. package/src/utils/globalLogger.ts +77 -0
  254. package/src/utils/http.ts +10 -0
  255. package/src/utils/imagePaste.ts +38 -0
  256. package/src/utils/json.ts +13 -0
  257. package/src/utils/log.ts +382 -0
  258. package/src/utils/markdown.ts +213 -0
  259. package/src/utils/messageContextManager.ts +289 -0
  260. package/src/utils/messages.tsx +939 -0
  261. package/src/utils/model.ts +836 -0
  262. package/src/utils/permissions/filesystem.ts +118 -0
  263. package/src/utils/responseState.ts +23 -0
  264. package/src/utils/ripgrep.ts +167 -0
  265. package/src/utils/secureFile.ts +559 -0
  266. package/src/utils/sessionState.ts +49 -0
  267. package/src/utils/state.ts +25 -0
  268. package/src/utils/style.ts +29 -0
  269. package/src/utils/terminal.ts +50 -0
  270. package/src/utils/theme.ts +133 -0
  271. package/src/utils/thinking.ts +144 -0
  272. package/src/utils/todoStorage.ts +431 -0
  273. package/src/utils/tokens.ts +43 -0
  274. package/src/utils/toolExecutionController.ts +163 -0
  275. package/src/utils/unaryLogging.ts +26 -0
  276. package/src/utils/user.ts +37 -0
  277. package/src/utils/validate.ts +165 -0
  278. package/cli.mjs +0 -1803
@@ -0,0 +1,404 @@
1
+ import { ImageBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
2
+ import { statSync } from 'node:fs'
3
+ import { Box, Text } from 'ink'
4
+ import * as path from 'node:path'
5
+ import { extname, relative } from 'node: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
+ import { secureFileService } from '../../utils/secureFile'
28
+
29
+ const MAX_LINES_TO_RENDER = 5
30
+ const MAX_OUTPUT_SIZE = 0.25 * 1024 * 1024 // 0.25MB in bytes
31
+
32
+ // Common image extensions
33
+ const IMAGE_EXTENSIONS = new Set([
34
+ '.png',
35
+ '.jpg',
36
+ '.jpeg',
37
+ '.gif',
38
+ '.bmp',
39
+ '.webp',
40
+ ])
41
+
42
+ // Maximum dimensions for images
43
+ const MAX_WIDTH = 2000
44
+ const MAX_HEIGHT = 2000
45
+ const MAX_IMAGE_SIZE = 3.75 * 1024 * 1024 // 5MB in bytes, with base64 encoding
46
+
47
+ const inputSchema = z.strictObject({
48
+ file_path: z.string().describe('The absolute path to the file to read'),
49
+ offset: z
50
+ .number()
51
+ .optional()
52
+ .describe(
53
+ 'The line number to start reading from. Only provide if the file is too large to read at once',
54
+ ),
55
+ limit: z
56
+ .number()
57
+ .optional()
58
+ .describe(
59
+ 'The number of lines to read. Only provide if the file is too large to read at once.',
60
+ ),
61
+ })
62
+
63
+ export const FileReadTool = {
64
+ name: 'View',
65
+ async description() {
66
+ return DESCRIPTION
67
+ },
68
+ async prompt() {
69
+ return PROMPT
70
+ },
71
+ inputSchema,
72
+ isReadOnly() {
73
+ return true
74
+ },
75
+ isConcurrencySafe() {
76
+ return true // FileRead is read-only, safe for concurrent execution
77
+ },
78
+ userFacingName() {
79
+ return 'Read'
80
+ },
81
+ async isEnabled() {
82
+ return true
83
+ },
84
+ needsPermissions({ file_path }) {
85
+ return !hasReadPermission(file_path || getCwd())
86
+ },
87
+ renderToolUseMessage(input, { verbose }) {
88
+ const { file_path, ...rest } = input
89
+ const entries = [
90
+ ['file_path', verbose ? file_path : relative(getCwd(), file_path)],
91
+ ...Object.entries(rest),
92
+ ]
93
+ return entries
94
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
95
+ .join(', ')
96
+ },
97
+ renderToolResultMessage(output) {
98
+ const verbose = false // Set default value for verbose
99
+ // TODO: Render recursively
100
+ switch (output.type) {
101
+ case 'image':
102
+ return (
103
+ <Box justifyContent="space-between" overflowX="hidden" width="100%">
104
+ <Box flexDirection="row">
105
+ <Text>&nbsp;&nbsp;⎿ &nbsp;</Text>
106
+ <Text>Read image</Text>
107
+ </Box>
108
+ </Box>
109
+ )
110
+ case 'text': {
111
+ const { filePath, content, numLines } = output.file
112
+ const contentWithFallback = content || '(No content)'
113
+ return (
114
+ <Box justifyContent="space-between" overflowX="hidden" width="100%">
115
+ <Box flexDirection="row">
116
+ <Text>&nbsp;&nbsp;⎿ &nbsp;</Text>
117
+ <Box flexDirection="column">
118
+ <HighlightedCode
119
+ code={
120
+ verbose
121
+ ? contentWithFallback
122
+ : contentWithFallback
123
+ .split('\n')
124
+ .slice(0, MAX_LINES_TO_RENDER)
125
+ .filter(_ => _.trim() !== '')
126
+ .join('\n')
127
+ }
128
+ language={extname(filePath).slice(1)}
129
+ />
130
+ {!verbose && numLines > MAX_LINES_TO_RENDER && (
131
+ <Text color={getTheme().secondaryText}>
132
+ ... (+{numLines - MAX_LINES_TO_RENDER} lines)
133
+ </Text>
134
+ )}
135
+ </Box>
136
+ </Box>
137
+ </Box>
138
+ )
139
+ }
140
+ }
141
+ },
142
+ renderToolUseRejectedMessage() {
143
+ return <FallbackToolUseRejectedMessage />
144
+ },
145
+ async validateInput({ file_path, offset, limit }) {
146
+ const fullFilePath = normalizeFilePath(file_path)
147
+
148
+ // Use secure file service to check if file exists and get file info
149
+ const fileCheck = secureFileService.safeGetFileInfo(fullFilePath)
150
+ if (!fileCheck.success) {
151
+ // Try to find a similar file with a different extension
152
+ const similarFilename = findSimilarFile(fullFilePath)
153
+ let message = 'File does not exist.'
154
+
155
+ // If we found a similar file, suggest it to the assistant
156
+ if (similarFilename) {
157
+ message += ` Did you mean ${similarFilename}?`
158
+ }
159
+
160
+ return {
161
+ result: false,
162
+ message,
163
+ }
164
+ }
165
+
166
+ const stats = fileCheck.stats!
167
+ const fileSize = stats.size
168
+ const ext = path.extname(fullFilePath).toLowerCase()
169
+
170
+ // Skip size check for image files - they have their own size limits
171
+ if (!IMAGE_EXTENSIONS.has(ext)) {
172
+ // If file is too large and no offset/limit provided
173
+ if (fileSize > MAX_OUTPUT_SIZE && !offset && !limit) {
174
+ return {
175
+ result: false,
176
+ message: formatFileSizeError(fileSize),
177
+ meta: { fileSize },
178
+ }
179
+ }
180
+ }
181
+
182
+ return { result: true }
183
+ },
184
+ async *call(
185
+ { file_path, offset = 1, limit = undefined },
186
+ { readFileTimestamps },
187
+ ) {
188
+ const ext = path.extname(file_path).toLowerCase()
189
+ const fullFilePath = normalizeFilePath(file_path)
190
+
191
+ // Record file read for freshness tracking
192
+ recordFileRead(fullFilePath)
193
+
194
+ // Emit file read event for system reminders
195
+ emitReminderEvent('file:read', {
196
+ filePath: fullFilePath,
197
+ extension: ext,
198
+ timestamp: Date.now(),
199
+ })
200
+
201
+ // Update read timestamp, to invalidate stale writes
202
+ readFileTimestamps[fullFilePath] = Date.now()
203
+
204
+ // Check for file modifications and generate reminder if needed
205
+ const modificationReminder = generateFileModificationReminder(fullFilePath)
206
+ if (modificationReminder) {
207
+ emitReminderEvent('file:modified', {
208
+ filePath: fullFilePath,
209
+ reminder: modificationReminder,
210
+ timestamp: Date.now(),
211
+ })
212
+ }
213
+
214
+ // If it's an image file, process and return base64 encoded contents
215
+ if (IMAGE_EXTENSIONS.has(ext)) {
216
+ const data = await readImage(fullFilePath, ext)
217
+ yield {
218
+ type: 'result',
219
+ data,
220
+ resultForAssistant: this.renderResultForAssistant(data),
221
+ }
222
+ return
223
+ }
224
+
225
+ // Handle offset properly - if offset is 0, don't subtract 1
226
+ const lineOffset = offset === 0 ? 0 : offset - 1
227
+ const { content, lineCount, totalLines } = readTextContent(
228
+ fullFilePath,
229
+ lineOffset,
230
+ limit,
231
+ )
232
+
233
+ // Add size validation after reading for non-image files
234
+ if (!IMAGE_EXTENSIONS.has(ext) && content.length > MAX_OUTPUT_SIZE) {
235
+ throw new Error(formatFileSizeError(content.length))
236
+ }
237
+
238
+ const data = {
239
+ type: 'text' as const,
240
+ file: {
241
+ filePath: file_path,
242
+ content: content,
243
+ numLines: lineCount,
244
+ startLine: offset,
245
+ totalLines,
246
+ },
247
+ }
248
+
249
+ yield {
250
+ type: 'result',
251
+ data,
252
+ resultForAssistant: this.renderResultForAssistant(data),
253
+ }
254
+ },
255
+ renderResultForAssistant(data) {
256
+ switch (data.type) {
257
+ case 'image':
258
+ return [
259
+ {
260
+ type: 'image',
261
+ source: {
262
+ type: 'base64',
263
+ data: data.file.base64,
264
+ media_type: data.file.type,
265
+ },
266
+ },
267
+ ]
268
+ case 'text':
269
+ return addLineNumbers(data.file)
270
+ }
271
+ },
272
+ } satisfies Tool<
273
+ typeof inputSchema,
274
+ | {
275
+ type: 'text'
276
+ file: {
277
+ filePath: string
278
+ content: string
279
+ numLines: number
280
+ startLine: number
281
+ totalLines: number
282
+ }
283
+ }
284
+ | {
285
+ type: 'image'
286
+ file: { base64: string; type: ImageBlockParam.Source['media_type'] }
287
+ }
288
+ >
289
+
290
+ const formatFileSizeError = (sizeInBytes: number) =>
291
+ `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.`
292
+
293
+ function createImageResponse(
294
+ buffer: Buffer,
295
+ ext: string,
296
+ ): {
297
+ type: 'image'
298
+ file: { base64: string; type: ImageBlockParam.Source['media_type'] }
299
+ } {
300
+ return {
301
+ type: 'image',
302
+ file: {
303
+ base64: buffer.toString('base64'),
304
+ type: `image/${ext.slice(1)}` as ImageBlockParam.Source['media_type'],
305
+ },
306
+ }
307
+ }
308
+
309
+ async function readImage(
310
+ filePath: string,
311
+ ext: string,
312
+ ): Promise<{
313
+ type: 'image'
314
+ file: { base64: string; type: ImageBlockParam.Source['media_type'] }
315
+ }> {
316
+ try {
317
+ const stats = statSync(filePath)
318
+ const sharp = (
319
+ (await import('sharp')) as unknown as { default: typeof import('sharp') }
320
+ ).default
321
+
322
+ // Use secure file service to read the file
323
+ const fileReadResult = secureFileService.safeReadFile(filePath, {
324
+ encoding: 'buffer' as BufferEncoding,
325
+ maxFileSize: MAX_IMAGE_SIZE
326
+ })
327
+
328
+ if (!fileReadResult.success) {
329
+ throw new Error(`Failed to read image file: ${fileReadResult.error}`)
330
+ }
331
+
332
+ const image = sharp(fileReadResult.content as Buffer)
333
+ const metadata = await image.metadata()
334
+
335
+ if (!metadata.width || !metadata.height) {
336
+ if (stats.size > MAX_IMAGE_SIZE) {
337
+ const compressedBuffer = await image.jpeg({ quality: 80 }).toBuffer()
338
+ return createImageResponse(compressedBuffer, 'jpeg')
339
+ }
340
+ }
341
+
342
+ // Calculate dimensions while maintaining aspect ratio
343
+ let width = metadata.width || 0
344
+ let height = metadata.height || 0
345
+
346
+ // Check if the original file just works
347
+ if (
348
+ stats.size <= MAX_IMAGE_SIZE &&
349
+ width <= MAX_WIDTH &&
350
+ height <= MAX_HEIGHT
351
+ ) {
352
+ // Use secure file service to read the file
353
+ const fileReadResult = secureFileService.safeReadFile(filePath, {
354
+ encoding: 'buffer' as BufferEncoding,
355
+ maxFileSize: MAX_IMAGE_SIZE
356
+ })
357
+
358
+ if (!fileReadResult.success) {
359
+ throw new Error(`Failed to read image file: ${fileReadResult.error}`)
360
+ }
361
+
362
+ return createImageResponse(fileReadResult.content as Buffer, ext)
363
+ }
364
+
365
+ if (width > MAX_WIDTH) {
366
+ height = Math.round((height * MAX_WIDTH) / width)
367
+ width = MAX_WIDTH
368
+ }
369
+
370
+ if (height > MAX_HEIGHT) {
371
+ width = Math.round((width * MAX_HEIGHT) / height)
372
+ height = MAX_HEIGHT
373
+ }
374
+
375
+ // Resize image and convert to buffer
376
+ const resizedImageBuffer = await image
377
+ .resize(width, height, {
378
+ fit: 'inside',
379
+ withoutEnlargement: true,
380
+ })
381
+ .toBuffer()
382
+
383
+ // If still too large after resize, compress quality
384
+ if (resizedImageBuffer.length > MAX_IMAGE_SIZE) {
385
+ const compressedBuffer = await image.jpeg({ quality: 80 }).toBuffer()
386
+ return createImageResponse(compressedBuffer, 'jpeg')
387
+ }
388
+
389
+ return createImageResponse(resizedImageBuffer, ext)
390
+ } catch (e) {
391
+ logError(e)
392
+ // If any error occurs during processing, return original image
393
+ const fileReadResult = secureFileService.safeReadFile(filePath, {
394
+ encoding: 'buffer' as BufferEncoding,
395
+ maxFileSize: MAX_IMAGE_SIZE
396
+ })
397
+
398
+ if (!fileReadResult.success) {
399
+ throw new Error(`Failed to read image file: ${fileReadResult.error}`)
400
+ }
401
+
402
+ return createImageResponse(fileReadResult.content as Buffer, ext)
403
+ }
404
+ }
@@ -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
+ >