@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,402 @@
1
+ import {
2
+ readFileSync,
3
+ writeFileSync,
4
+ openSync,
5
+ readSync,
6
+ closeSync,
7
+ existsSync,
8
+ readdirSync,
9
+ } from 'fs'
10
+ import { logError } from './log'
11
+ import {
12
+ isAbsolute,
13
+ normalize,
14
+ resolve,
15
+ resolve as resolvePath,
16
+ relative,
17
+ sep,
18
+ basename,
19
+ dirname,
20
+ extname,
21
+ join,
22
+ } from 'path'
23
+ import { glob as globLib } from 'glob'
24
+ import { cwd } from 'process'
25
+ import { listAllContentFiles } from './ripgrep'
26
+ import { LRUCache } from 'lru-cache'
27
+ import { getCwd } from './state'
28
+
29
+ export type File = {
30
+ filename: string
31
+ content: string
32
+ }
33
+
34
+ export type LineEndingType = 'CRLF' | 'LF'
35
+
36
+ export async function glob(
37
+ filePattern: string,
38
+ cwd: string,
39
+ { limit, offset }: { limit: number; offset: number },
40
+ abortSignal: AbortSignal,
41
+ ): Promise<{ files: string[]; truncated: boolean }> {
42
+ // TODO: Use worker threads
43
+ const paths = await globLib([filePattern], {
44
+ cwd,
45
+ nocase: true,
46
+ nodir: true,
47
+ signal: abortSignal,
48
+ stat: true,
49
+ withFileTypes: true,
50
+ })
51
+ const sortedPaths = paths.sort((a, b) => (a.mtimeMs ?? 0) - (b.mtimeMs ?? 0))
52
+ const truncated = sortedPaths.length > offset + limit
53
+ return {
54
+ files: sortedPaths
55
+ .slice(offset, offset + limit)
56
+ .map(path => path.fullpath()),
57
+ truncated,
58
+ }
59
+ }
60
+
61
+ export function readFileSafe(filepath: string): string | null {
62
+ try {
63
+ return readFileSync(filepath, 'utf-8')
64
+ } catch (error) {
65
+ logError(error)
66
+ return null
67
+ }
68
+ }
69
+
70
+ export function isInDirectory(
71
+ relativePath: string,
72
+ relativeCwd: string,
73
+ ): boolean {
74
+ if (relativePath === '.') {
75
+ return true
76
+ }
77
+
78
+ // Reject paths starting with ~ (home directory)
79
+ if (relativePath.startsWith('~')) {
80
+ return false
81
+ }
82
+
83
+ // Reject paths containing null bytes or other sneaky characters
84
+ if (relativePath.includes('\0') || relativeCwd.includes('\0')) {
85
+ return false
86
+ }
87
+
88
+ // Normalize paths to resolve any '..' or '.' segments
89
+ // and add trailing slashes
90
+ let normalizedPath = normalize(relativePath)
91
+ let normalizedCwd = normalize(relativeCwd)
92
+
93
+ normalizedPath = normalizedPath.endsWith(sep)
94
+ ? normalizedPath
95
+ : normalizedPath + sep
96
+ normalizedCwd = normalizedCwd.endsWith(sep)
97
+ ? normalizedCwd
98
+ : normalizedCwd + sep
99
+
100
+ // Join with a base directory to make them absolute-like for comparison
101
+ // Using 'dummy' as base to avoid any actual file system dependencies
102
+ const fullPath = resolvePath(cwd(), normalizedCwd, normalizedPath)
103
+ const fullCwd = resolvePath(cwd(), normalizedCwd)
104
+
105
+ // Check if the path starts with the cwd
106
+ return fullPath.startsWith(fullCwd)
107
+ }
108
+
109
+ export function readTextContent(
110
+ filePath: string,
111
+ offset = 0,
112
+ maxLines?: number,
113
+ ): { content: string; lineCount: number; totalLines: number } {
114
+ const enc = detectFileEncoding(filePath)
115
+ const content = readFileSync(filePath, enc)
116
+ const lines = content.split(/\r?\n/)
117
+
118
+ // Truncate number of lines if needed
119
+ const toReturn =
120
+ maxLines !== undefined && lines.length - offset > maxLines
121
+ ? lines.slice(offset, offset + maxLines)
122
+ : lines.slice(offset)
123
+
124
+ return {
125
+ content: toReturn.join('\n'), // TODO: This probably won't work for Windows
126
+ lineCount: toReturn.length,
127
+ totalLines: lines.length,
128
+ }
129
+ }
130
+
131
+ export function writeTextContent(
132
+ filePath: string,
133
+ content: string,
134
+ encoding: BufferEncoding,
135
+ endings: LineEndingType,
136
+ ): void {
137
+ let toWrite = content
138
+ if (endings === 'CRLF') {
139
+ toWrite = content.split('\n').join('\r\n')
140
+ }
141
+
142
+ writeFileSync(filePath, toWrite, { encoding, flush: true })
143
+ }
144
+
145
+ const repoEndingCache = new LRUCache<string, LineEndingType>({
146
+ fetchMethod: path => detectRepoLineEndingsDirect(path),
147
+ ttl: 5 * 60 * 1000,
148
+ ttlAutopurge: false,
149
+ max: 1000,
150
+ })
151
+
152
+ export async function detectRepoLineEndings(
153
+ filePath: string,
154
+ ): Promise<LineEndingType | undefined> {
155
+ return repoEndingCache.fetch(resolve(filePath))
156
+ }
157
+
158
+ export async function detectRepoLineEndingsDirect(
159
+ cwd: string,
160
+ ): Promise<LineEndingType> {
161
+ const abortController = new AbortController()
162
+ setTimeout(() => {
163
+ abortController.abort()
164
+ }, 1_000)
165
+ const allFiles = await listAllContentFiles(cwd, abortController.signal, 15)
166
+
167
+ let crlfCount = 0
168
+ for (const file of allFiles) {
169
+ const lineEnding = detectLineEndings(file)
170
+ if (lineEnding === 'CRLF') {
171
+ crlfCount++
172
+ }
173
+ }
174
+
175
+ return crlfCount > 3 ? 'CRLF' : 'LF'
176
+ }
177
+
178
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
179
+ function fetch<K extends {}, V extends {}>(
180
+ cache: LRUCache<K, V>,
181
+ key: K,
182
+ value: () => V,
183
+ ): V {
184
+ if (cache.has(key)) {
185
+ return cache.get(key)!
186
+ }
187
+
188
+ const v = value()
189
+ cache.set(key, v)
190
+ return v
191
+ }
192
+
193
+ const fileEncodingCache = new LRUCache<string, BufferEncoding>({
194
+ fetchMethod: path => detectFileEncodingDirect(path),
195
+ ttl: 5 * 60 * 1000,
196
+ ttlAutopurge: false,
197
+ max: 1000,
198
+ })
199
+
200
+ export function detectFileEncoding(filePath: string): BufferEncoding {
201
+ const k = resolve(filePath)
202
+ return fetch(fileEncodingCache, k, () => detectFileEncodingDirect(k))
203
+ }
204
+
205
+ export function detectFileEncodingDirect(filePath: string): BufferEncoding {
206
+ const BUFFER_SIZE = 4096
207
+ const buffer = Buffer.alloc(BUFFER_SIZE)
208
+
209
+ let fd: number | undefined = undefined
210
+ try {
211
+ fd = openSync(filePath, 'r')
212
+ const bytesRead = readSync(fd, buffer, 0, BUFFER_SIZE, 0)
213
+
214
+ if (bytesRead >= 2) {
215
+ if (buffer[0] === 0xff && buffer[1] === 0xfe) return 'utf16le'
216
+ }
217
+
218
+ if (
219
+ bytesRead >= 3 &&
220
+ buffer[0] === 0xef &&
221
+ buffer[1] === 0xbb &&
222
+ buffer[2] === 0xbf
223
+ ) {
224
+ return 'utf8'
225
+ }
226
+
227
+ const isUtf8 = buffer.slice(0, bytesRead).toString('utf8').length > 0
228
+ return isUtf8 ? 'utf8' : 'ascii'
229
+ } catch (error) {
230
+ logError(`Error detecting encoding for file ${filePath}: ${error}`)
231
+ return 'utf8'
232
+ } finally {
233
+ if (fd) closeSync(fd)
234
+ }
235
+ }
236
+
237
+ const lineEndingCache = new LRUCache<string, LineEndingType>({
238
+ fetchMethod: path => detectLineEndingsDirect(path),
239
+ ttl: 5 * 60 * 1000,
240
+ ttlAutopurge: false,
241
+ max: 1000,
242
+ })
243
+
244
+ export function detectLineEndings(filePath: string): LineEndingType {
245
+ const k = resolve(filePath)
246
+ return fetch(lineEndingCache, k, () => detectLineEndingsDirect(k))
247
+ }
248
+
249
+ export function detectLineEndingsDirect(
250
+ filePath: string,
251
+ encoding: BufferEncoding = 'utf8',
252
+ ): LineEndingType {
253
+ try {
254
+ const buffer = Buffer.alloc(4096)
255
+ const fd = openSync(filePath, 'r')
256
+ const bytesRead = readSync(fd, buffer, 0, 4096, 0)
257
+ closeSync(fd)
258
+
259
+ const content = buffer.toString(encoding, 0, bytesRead)
260
+ let crlfCount = 0
261
+ let lfCount = 0
262
+
263
+ for (let i = 0; i < content.length; i++) {
264
+ if (content[i] === '\n') {
265
+ if (i > 0 && content[i - 1] === '\r') {
266
+ crlfCount++
267
+ } else {
268
+ lfCount++
269
+ }
270
+ }
271
+ }
272
+
273
+ return crlfCount > lfCount ? 'CRLF' : 'LF'
274
+ } catch (error) {
275
+ logError(`Error detecting line endings for file ${filePath}: ${error}`)
276
+ return 'LF'
277
+ }
278
+ }
279
+
280
+ export function normalizeFilePath(filePath: string): string {
281
+ const absoluteFilePath = isAbsolute(filePath)
282
+ ? filePath
283
+ : resolve(getCwd(), filePath)
284
+
285
+ // One weird trick for half-width space characters in MacOS screenshot filenames
286
+ if (absoluteFilePath.endsWith(' AM.png')) {
287
+ return absoluteFilePath.replace(
288
+ ' AM.png',
289
+ `${String.fromCharCode(8239)}AM.png`,
290
+ )
291
+ }
292
+
293
+ // One weird trick for half-width space characters in MacOS screenshot filenames
294
+ if (absoluteFilePath.endsWith(' PM.png')) {
295
+ return absoluteFilePath.replace(
296
+ ' PM.png',
297
+ `${String.fromCharCode(8239)}PM.png`,
298
+ )
299
+ }
300
+
301
+ return absoluteFilePath
302
+ }
303
+
304
+ export function getAbsolutePath(path: string | undefined): string | undefined {
305
+ return path ? (isAbsolute(path) ? path : resolve(getCwd(), path)) : undefined
306
+ }
307
+
308
+ export function getAbsoluteAndRelativePaths(path: string | undefined): {
309
+ absolutePath: string | undefined
310
+ relativePath: string | undefined
311
+ } {
312
+ const absolutePath = getAbsolutePath(path)
313
+ const relativePath = absolutePath
314
+ ? relative(getCwd(), absolutePath)
315
+ : undefined
316
+ return { absolutePath, relativePath }
317
+ }
318
+
319
+ /**
320
+ * Find files with the same name but different extensions in the same directory
321
+ * @param filePath The path to the file that doesn't exist
322
+ * @returns The found file with a different extension, or undefined if none found
323
+ */
324
+
325
+ export function findSimilarFile(filePath: string): string | undefined {
326
+ try {
327
+ const dir = dirname(filePath)
328
+ const fileBaseName = basename(filePath, extname(filePath))
329
+
330
+ // Check if directory exists
331
+ if (!existsSync(dir)) {
332
+ return undefined
333
+ }
334
+
335
+ // Get all files in the directory
336
+ const files = readdirSync(dir)
337
+
338
+ // Find files with the same base name but different extension
339
+ const similarFiles = files.filter(
340
+ file =>
341
+ basename(file, extname(file)) === fileBaseName &&
342
+ join(dir, file) !== filePath,
343
+ )
344
+
345
+ // Return just the filename of the first match if found
346
+ const firstMatch = similarFiles[0]
347
+ if (firstMatch) {
348
+ return firstMatch
349
+ }
350
+ return undefined
351
+ } catch (error) {
352
+ // In case of any errors, return undefined
353
+ logError(`Error finding similar file for ${filePath}: ${error}`)
354
+ return undefined
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Adds cat -n style line numbers to the content
360
+ */
361
+ export function addLineNumbers({
362
+ content,
363
+ // 1-indexed
364
+ startLine,
365
+ }: {
366
+ content: string
367
+ startLine: number
368
+ }): string {
369
+ if (!content) {
370
+ return ''
371
+ }
372
+
373
+ return content
374
+ .split(/\r?\n/)
375
+ .map((line, index) => {
376
+ const lineNum = index + startLine
377
+ const numStr = String(lineNum)
378
+ // Handle large numbers differently
379
+ if (numStr.length >= 6) {
380
+ return `${numStr}\t${line}`
381
+ }
382
+ // Regular numbers get padding to 6 characters
383
+ const n = numStr.padStart(6, ' ')
384
+ return `${n}\t${line}`
385
+ })
386
+ .join('\n') // TODO: This probably won't work for Windows
387
+ }
388
+
389
+ /**
390
+ * Checks if a directory is empty by efficiently reading just the first entry
391
+ * @param dirPath The path to the directory to check
392
+ * @returns true if the directory is empty, false otherwise
393
+ */
394
+ export function isDirEmpty(dirPath: string): boolean {
395
+ try {
396
+ const entries = readdirSync(dirPath)
397
+ return entries.length === 0
398
+ } catch (error) {
399
+ logError(`Error checking directory: ${error}`)
400
+ return false
401
+ }
402
+ }
@@ -0,0 +1,71 @@
1
+ import { readTextContent } from './file'
2
+ import { fileFreshnessService } from '../services/fileFreshness'
3
+
4
+ /**
5
+ * File recovery configuration for auto-compact feature
6
+ * These limits ensure recovered files don't overwhelm the compressed context
7
+ */
8
+ const MAX_FILES_TO_RECOVER = 5
9
+ const MAX_TOKENS_PER_FILE = 10_000
10
+ const MAX_TOTAL_FILE_TOKENS = 50_000
11
+
12
+ /**
13
+ * Selects and reads recently accessed files for context recovery
14
+ *
15
+ * During auto-compact, this function preserves development context by:
16
+ * - Selecting files based on recent access patterns
17
+ * - Enforcing token budgets to prevent context bloat
18
+ * - Truncating large files while preserving essential content
19
+ *
20
+ * @returns Array of file data with content, token counts, and truncation flags
21
+ */
22
+ export async function selectAndReadFiles(): Promise<
23
+ Array<{
24
+ path: string
25
+ content: string
26
+ tokens: number
27
+ truncated: boolean
28
+ }>
29
+ > {
30
+ const importantFiles =
31
+ fileFreshnessService.getImportantFiles(MAX_FILES_TO_RECOVER)
32
+ const results = []
33
+ let totalTokens = 0
34
+
35
+ for (const fileInfo of importantFiles) {
36
+ try {
37
+ const { content } = readTextContent(fileInfo.path)
38
+ const estimatedTokens = Math.ceil(content.length * 0.25)
39
+
40
+ // Apply per-file token limit to prevent any single file from dominating context
41
+ let finalContent = content
42
+ let truncated = false
43
+
44
+ if (estimatedTokens > MAX_TOKENS_PER_FILE) {
45
+ const maxChars = Math.floor(MAX_TOKENS_PER_FILE / 0.25)
46
+ finalContent = content.substring(0, maxChars)
47
+ truncated = true
48
+ }
49
+
50
+ const finalTokens = Math.min(estimatedTokens, MAX_TOKENS_PER_FILE)
51
+
52
+ // Enforce total token budget to maintain auto-compact effectiveness
53
+ if (totalTokens + finalTokens > MAX_TOTAL_FILE_TOKENS) {
54
+ break
55
+ }
56
+
57
+ totalTokens += finalTokens
58
+ results.push({
59
+ path: fileInfo.path,
60
+ content: finalContent,
61
+ tokens: finalTokens,
62
+ truncated,
63
+ })
64
+ } catch (error) {
65
+ // Skip files that cannot be read, don't let one failure stop the process
66
+ console.error(`Failed to read file for recovery: ${fileInfo.path}`, error)
67
+ }
68
+ }
69
+
70
+ return results
71
+ }
@@ -0,0 +1,44 @@
1
+ export function wrapText(text: string, width: number): string[] {
2
+ const lines: string[] = []
3
+ let currentLine = ''
4
+
5
+ for (const char of text) {
6
+ // Important: we need the spread to properly count multi-plane UTF-8 characters (eg. 𑚖)
7
+ if ([...currentLine].length < width) {
8
+ currentLine += char
9
+ } else {
10
+ lines.push(currentLine)
11
+ currentLine = char
12
+ }
13
+ }
14
+
15
+ if (currentLine) lines.push(currentLine)
16
+ return lines
17
+ }
18
+
19
+ export function formatDuration(ms: number): string {
20
+ if (ms < 60000) {
21
+ return `${(ms / 1000).toFixed(1)}s`
22
+ }
23
+
24
+ const hours = Math.floor(ms / 3600000)
25
+ const minutes = Math.floor((ms % 3600000) / 60000)
26
+ const seconds = ((ms % 60000) / 1000).toFixed(1)
27
+
28
+ if (hours > 0) {
29
+ return `${hours}h ${minutes}m ${seconds}s`
30
+ }
31
+ if (minutes > 0) {
32
+ return `${minutes}m ${seconds}s`
33
+ }
34
+ return `${seconds}s`
35
+ }
36
+
37
+ export function formatNumber(number: number): string {
38
+ return new Intl.NumberFormat('en', {
39
+ notation: 'compact',
40
+ maximumFractionDigits: 1,
41
+ })
42
+ .format(number) // eg. "1321" => "1.3K"
43
+ .toLowerCase() // eg. "1.3K" => "1.3k"
44
+ }