@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,223 @@
1
+ import { Message } from '../query'
2
+ import { countTokens } from './tokens'
3
+ import { getMessagesGetter, getMessagesSetter } from '../messages'
4
+ import { getContext } from '../context'
5
+ import { getCodeStyle } from '../utils/style'
6
+ import { clearTerminal } from '../utils/terminal'
7
+ import { resetFileFreshnessSession } from '../services/fileFreshness'
8
+ import { createUserMessage, normalizeMessagesForAPI } from '../utils/messages'
9
+ import { queryLLM } from '../services/claude'
10
+ import { selectAndReadFiles } from './fileRecoveryCore'
11
+ import { addLineNumbers } from './file'
12
+ import { getModelManager } from './model'
13
+
14
+ /**
15
+ * Threshold ratio for triggering automatic context compression
16
+ * When context usage exceeds 92% of the model's limit, auto-compact activates
17
+ */
18
+ const AUTO_COMPACT_THRESHOLD_RATIO = 0.92
19
+
20
+ /**
21
+ * Retrieves the context length for the main model that should execute compression
22
+ * Uses ModelManager to get the current model's context length
23
+ */
24
+ async function getCompressionModelContextLimit(): Promise<number> {
25
+ try {
26
+ // 🔧 Fix: Use ModelManager instead of legacy config
27
+ const modelManager = getModelManager()
28
+ const modelProfile = modelManager.getModel('main')
29
+
30
+ if (modelProfile?.contextLength) {
31
+ return modelProfile.contextLength
32
+ }
33
+
34
+ // Fallback to a reasonable default
35
+ return 200_000
36
+ } catch (error) {
37
+ return 200_000
38
+ }
39
+ }
40
+
41
+ const COMPRESSION_PROMPT = `Please provide a comprehensive summary of our conversation structured as follows:
42
+
43
+ ## Technical Context
44
+ Development environment, tools, frameworks, and configurations in use. Programming languages, libraries, and technical constraints. File structure, directory organization, and project architecture.
45
+
46
+ ## Project Overview
47
+ Main project goals, features, and scope. Key components, modules, and their relationships. Data models, APIs, and integration patterns.
48
+
49
+ ## Code Changes
50
+ Files created, modified, or analyzed during our conversation. Specific code implementations, functions, and algorithms added. Configuration changes and structural modifications.
51
+
52
+ ## Debugging & Issues
53
+ Problems encountered and their root causes. Solutions implemented and their effectiveness. Error messages, logs, and diagnostic information.
54
+
55
+ ## Current Status
56
+ What we just completed successfully. Current state of the codebase and any ongoing work. Test results, validation steps, and verification performed.
57
+
58
+ ## Pending Tasks
59
+ Immediate next steps and priorities. Planned features, improvements, and refactoring. Known issues, technical debt, and areas needing attention.
60
+
61
+ ## User Preferences
62
+ Coding style, formatting, and organizational preferences. Communication patterns and feedback style. Tool choices and workflow preferences.
63
+
64
+ ## Key Decisions
65
+ Important technical decisions made and their rationale. Alternative approaches considered and why they were rejected. Trade-offs accepted and their implications.
66
+
67
+ Focus on information essential for continuing the conversation effectively, including specific details about code, files, errors, and plans.`
68
+
69
+ /**
70
+ * Calculates context usage thresholds based on the main model's capabilities
71
+ * Uses the main model context length since compression tasks require a capable model
72
+ */
73
+ async function calculateThresholds(tokenCount: number) {
74
+ const contextLimit = await getCompressionModelContextLimit()
75
+ const autoCompactThreshold = contextLimit * AUTO_COMPACT_THRESHOLD_RATIO
76
+
77
+ return {
78
+ isAboveAutoCompactThreshold: tokenCount >= autoCompactThreshold,
79
+ percentUsed: Math.round((tokenCount / contextLimit) * 100),
80
+ tokensRemaining: Math.max(0, autoCompactThreshold - tokenCount),
81
+ contextLimit,
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Determines if auto-compact should trigger based on token usage
87
+ * Uses the main model context limit since compression requires a capable model
88
+ */
89
+ async function shouldAutoCompact(messages: Message[]): Promise<boolean> {
90
+ if (messages.length < 3) return false
91
+
92
+ const tokenCount = countTokens(messages)
93
+ const { isAboveAutoCompactThreshold } = await calculateThresholds(tokenCount)
94
+
95
+ return isAboveAutoCompactThreshold
96
+ }
97
+
98
+ /**
99
+ * Main entry point for automatic context compression
100
+ *
101
+ * This function is called before each query to check if the conversation
102
+ * has grown too large and needs compression. When triggered, it:
103
+ * - Generates a structured summary of the conversation using the main model
104
+ * - Recovers recently accessed files to maintain development context
105
+ * - Resets conversation state while preserving essential information
106
+ *
107
+ * Uses the main model for compression tasks to ensure high-quality summaries
108
+ *
109
+ * @param messages Current conversation messages
110
+ * @param toolUseContext Execution context with model and tool configuration
111
+ * @returns Updated messages (compressed if needed) and compression status
112
+ */
113
+ export async function checkAutoCompact(
114
+ messages: Message[],
115
+ toolUseContext: any,
116
+ ): Promise<{ messages: Message[]; wasCompacted: boolean }> {
117
+ if (!(await shouldAutoCompact(messages))) {
118
+ return { messages, wasCompacted: false }
119
+ }
120
+
121
+ try {
122
+ const compactedMessages = await executeAutoCompact(messages, toolUseContext)
123
+
124
+ return {
125
+ messages: compactedMessages,
126
+ wasCompacted: true,
127
+ }
128
+ } catch (error) {
129
+ // Graceful degradation: if auto-compact fails, continue with original messages
130
+ // This ensures system remains functional even if compression encounters issues
131
+ console.error(
132
+ 'Auto-compact failed, continuing with original messages:',
133
+ error,
134
+ )
135
+ return { messages, wasCompacted: false }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Executes the conversation compression process using the main model
141
+ *
142
+ * This function generates a comprehensive summary using the main model
143
+ * which is better suited for complex summarization tasks. It also
144
+ * automatically recovers important files to maintain development context.
145
+ */
146
+ async function executeAutoCompact(
147
+ messages: Message[],
148
+ toolUseContext: any,
149
+ ): Promise<Message[]> {
150
+ const summaryRequest = createUserMessage(COMPRESSION_PROMPT)
151
+
152
+ const summaryResponse = await queryLLM(
153
+ normalizeMessagesForAPI([...messages, summaryRequest]),
154
+ [
155
+ 'You are a helpful AI assistant tasked with creating comprehensive conversation summaries that preserve all essential context for continuing development work.',
156
+ ],
157
+ 0,
158
+ toolUseContext.options.tools,
159
+ toolUseContext.abortController.signal,
160
+ {
161
+ safeMode: false,
162
+ model: 'main', // 使用模型指针,让queryLLM统一解析
163
+ prependCLISysprompt: true,
164
+ },
165
+ )
166
+
167
+ const content = summaryResponse.message.content
168
+ const summary =
169
+ typeof content === 'string'
170
+ ? content
171
+ : content.length > 0 && content[0]?.type === 'text'
172
+ ? content[0].text
173
+ : null
174
+
175
+ if (!summary) {
176
+ throw new Error(
177
+ 'Failed to generate conversation summary - response did not contain valid text content',
178
+ )
179
+ }
180
+
181
+ summaryResponse.message.usage = {
182
+ input_tokens: 0,
183
+ output_tokens: summaryResponse.message.usage.output_tokens,
184
+ cache_creation_input_tokens: 0,
185
+ cache_read_input_tokens: 0,
186
+ }
187
+
188
+ // Automatic file recovery: preserve recently accessed development files
189
+ // This maintains coding context even after conversation compression
190
+ const recoveredFiles = await selectAndReadFiles()
191
+
192
+ const compactedMessages = [
193
+ createUserMessage(
194
+ 'Context automatically compressed due to token limit. Essential information preserved.',
195
+ ),
196
+ summaryResponse,
197
+ ]
198
+
199
+ // Append recovered files to maintain development workflow continuity
200
+ // Files are prioritized by recency and importance, with strict token limits
201
+ if (recoveredFiles.length > 0) {
202
+ for (const file of recoveredFiles) {
203
+ const contentWithLines = addLineNumbers({
204
+ content: file.content,
205
+ startLine: 1,
206
+ })
207
+ const recoveryMessage = createUserMessage(
208
+ `**Recovered File: ${file.path}**\n\n\`\`\`\n${contentWithLines}\n\`\`\`\n\n` +
209
+ `*Automatically recovered (${file.tokens} tokens)${file.truncated ? ' [truncated]' : ''}*`,
210
+ )
211
+ compactedMessages.push(recoveryMessage)
212
+ }
213
+ }
214
+
215
+ // State cleanup to ensure fresh context after compression
216
+ // Mirrors the cleanup sequence from manual /compact command
217
+ getMessagesSetter()([])
218
+ getContext.cache.clear?.()
219
+ getCodeStyle.cache.clear?.()
220
+ resetFileFreshnessSession()
221
+
222
+ return compactedMessages
223
+ }
@@ -0,0 +1,318 @@
1
+ import { homedir } from 'os'
2
+ import { join } from 'path'
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ appendFileSync,
7
+ readFileSync,
8
+ constants,
9
+ writeFileSync,
10
+ unlinkSync,
11
+ statSync,
12
+ } from 'fs'
13
+ import { platform } from 'process'
14
+ import { execFileNoThrow } from './execFileNoThrow'
15
+ import { logError } from './log'
16
+ import { accessSync } from 'fs'
17
+ import { CLAUDE_BASE_DIR } from './env'
18
+ import { logEvent, getDynamicConfig } from '../services/statsig'
19
+ import { lt } from 'semver'
20
+ import { MACRO } from '../constants/macros'
21
+ import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
22
+ export type InstallStatus =
23
+ | 'success'
24
+ | 'no_permissions'
25
+ | 'install_failed'
26
+ | 'in_progress'
27
+
28
+ export type AutoUpdaterResult = {
29
+ version: string | null
30
+ status: InstallStatus
31
+ }
32
+
33
+ export type VersionConfig = {
34
+ minVersion: string
35
+ }
36
+
37
+ /**
38
+ * Checks if the current version meets the minimum required version from Statsig config
39
+ * Terminates the process with an error message if the version is too old
40
+ */
41
+ export async function assertMinVersion(): Promise<void> {
42
+ try {
43
+ const versionConfig = await getDynamicConfig<VersionConfig>(
44
+ 'tengu_version_config',
45
+ { minVersion: '0.0.0' },
46
+ )
47
+
48
+ if (
49
+ versionConfig.minVersion &&
50
+ lt(MACRO.VERSION, versionConfig.minVersion)
51
+ ) {
52
+ console.error(`
53
+ It looks like your version of ${PRODUCT_NAME} (${MACRO.VERSION}) needs an update.
54
+ A newer version (${versionConfig.minVersion} or higher) is required to continue.
55
+
56
+ To update, please run:
57
+ ${PRODUCT_COMMAND} update
58
+
59
+ This will ensure you have access to the latest features and improvements.
60
+ `)
61
+ process.exit(1)
62
+ }
63
+ } catch (error) {
64
+ logError(`Error checking minimum version: ${error}`)
65
+ }
66
+ }
67
+
68
+ // Lock file for auto-updater to prevent concurrent updates
69
+ export const LOCK_FILE_PATH = join(CLAUDE_BASE_DIR, '.update.lock')
70
+ const LOCK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minute timeout for locks
71
+
72
+ /**
73
+ * Attempts to acquire a lock for auto-updater
74
+ * @returns {boolean} true if lock was acquired, false if another process holds the lock
75
+ */
76
+ function acquireLock(): boolean {
77
+ try {
78
+ // Ensure the base directory exists
79
+ if (!existsSync(CLAUDE_BASE_DIR)) {
80
+ mkdirSync(CLAUDE_BASE_DIR, { recursive: true })
81
+ }
82
+
83
+ // Check if lock file exists and is not stale
84
+ if (existsSync(LOCK_FILE_PATH)) {
85
+ const stats = statSync(LOCK_FILE_PATH)
86
+ const age = Date.now() - stats.mtimeMs
87
+
88
+ // If lock file is older than timeout, consider it stale
89
+ if (age < LOCK_TIMEOUT_MS) {
90
+ return false
91
+ }
92
+
93
+ // Lock is stale, we can take over
94
+ try {
95
+ unlinkSync(LOCK_FILE_PATH)
96
+ } catch (err) {
97
+ logError(`Failed to remove stale lock file: ${err}`)
98
+ return false
99
+ }
100
+ }
101
+
102
+ // Create lock file with current pid
103
+ writeFileSync(LOCK_FILE_PATH, `${process.pid}`, 'utf8')
104
+ return true
105
+ } catch (err) {
106
+ logError(`Failed to acquire lock: ${err}`)
107
+ return false
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Releases the update lock if it's held by this process
113
+ */
114
+ function releaseLock(): void {
115
+ try {
116
+ if (existsSync(LOCK_FILE_PATH)) {
117
+ const lockData = readFileSync(LOCK_FILE_PATH, 'utf8')
118
+ if (lockData === `${process.pid}`) {
119
+ unlinkSync(LOCK_FILE_PATH)
120
+ }
121
+ }
122
+ } catch (err) {
123
+ logError(`Failed to release lock: ${err}`)
124
+ }
125
+ }
126
+
127
+ export async function checkNpmPermissions(): Promise<{
128
+ hasPermissions: boolean
129
+ npmPrefix: string | null
130
+ }> {
131
+ try {
132
+ const prefixResult = await execFileNoThrow('npm', [
133
+ '-g',
134
+ 'config',
135
+ 'get',
136
+ 'prefix',
137
+ ])
138
+ if (prefixResult.code !== 0) {
139
+ logError('Failed to check npm permissions')
140
+ return { hasPermissions: false, npmPrefix: null }
141
+ }
142
+
143
+ const prefix = prefixResult.stdout.trim()
144
+
145
+ let testWriteResult = false
146
+ try {
147
+ accessSync(prefix, constants.W_OK)
148
+ testWriteResult = true
149
+ } catch {
150
+ testWriteResult = false
151
+ }
152
+
153
+ if (testWriteResult) {
154
+ return { hasPermissions: true, npmPrefix: prefix }
155
+ }
156
+
157
+ logError('Insufficient permissions for global npm install.')
158
+ return { hasPermissions: false, npmPrefix: prefix }
159
+ } catch (error) {
160
+ logError(`Failed to verify npm global install permissions: ${error}`)
161
+ return { hasPermissions: false, npmPrefix: null }
162
+ }
163
+ }
164
+
165
+ export async function setupNewPrefix(prefix: string): Promise<void> {
166
+ if (!acquireLock()) {
167
+ // Log the lock contention to statsig
168
+ logEvent('tengu_auto_updater_prefix_lock_contention', {
169
+ pid: String(process.pid),
170
+ currentVersion: MACRO.VERSION,
171
+ prefix,
172
+ })
173
+ throw new Error('Another process is currently setting up npm prefix')
174
+ }
175
+
176
+ try {
177
+ // Create directory if it doesn't exist
178
+ if (!existsSync(prefix)) {
179
+ mkdirSync(prefix, { recursive: true })
180
+ }
181
+
182
+ // Set npm prefix
183
+ const setPrefix = await execFileNoThrow('npm', [
184
+ '-g',
185
+ 'config',
186
+ 'set',
187
+ 'prefix',
188
+ prefix,
189
+ ])
190
+
191
+ if (setPrefix.code !== 0) {
192
+ throw new Error(`Failed to set npm prefix: ${setPrefix.stderr}`)
193
+ }
194
+
195
+ // Update shell config files
196
+ const pathUpdate = `\n# npm global path\nexport PATH="${prefix}/bin:$PATH"\n`
197
+
198
+ if (platform === 'win32') {
199
+ // On Windows, update user PATH environment variable
200
+ const setxResult = await execFileNoThrow('setx', [
201
+ 'PATH',
202
+ `${process.env.PATH};${prefix}`,
203
+ ])
204
+ if (setxResult.code !== 0) {
205
+ throw new Error(
206
+ `Failed to update PATH on Windows: ${setxResult.stderr}`,
207
+ )
208
+ }
209
+ } else {
210
+ // Unix-like systems
211
+ const shellConfigs = [
212
+ // Bash
213
+ join(homedir(), '.bashrc'),
214
+ join(homedir(), '.bash_profile'),
215
+ // Zsh
216
+ join(homedir(), '.zshrc'),
217
+ // Fish
218
+ join(homedir(), '.config', 'fish', 'config.fish'),
219
+ ]
220
+
221
+ for (const config of shellConfigs) {
222
+ if (existsSync(config)) {
223
+ try {
224
+ const content = readFileSync(config, 'utf8')
225
+ if (!content.includes(prefix)) {
226
+ if (config.includes('fish')) {
227
+ // Fish shell has different syntax
228
+ const fishPath = `\n# npm global path\nset -gx PATH ${prefix}/bin $PATH\n`
229
+ appendFileSync(config, fishPath)
230
+ } else {
231
+ appendFileSync(config, pathUpdate)
232
+ }
233
+
234
+ logEvent('npm_prefix_path_updated', {
235
+ configPath: config,
236
+ })
237
+ }
238
+ } catch (err) {
239
+ // Log but don't throw - continue with other configs
240
+ logEvent('npm_prefix_path_update_failed', {
241
+ configPath: config,
242
+ error:
243
+ err instanceof Error
244
+ ? err.message.slice(0, 200)
245
+ : String(err).slice(0, 200),
246
+ })
247
+ logError(`Failed to update shell config ${config}: ${err}`)
248
+ }
249
+ }
250
+ }
251
+ }
252
+ } finally {
253
+ releaseLock()
254
+ }
255
+ }
256
+
257
+ export function getDefaultNpmPrefix(): string {
258
+ return join(homedir(), '.npm-global')
259
+ }
260
+
261
+ export function getPermissionsCommand(npmPrefix: string): string {
262
+ const windowsCommand = `icacls "${npmPrefix}" /grant "%USERNAME%:(OI)(CI)F"`
263
+ const prefixPath = npmPrefix || '$(npm -g config get prefix)'
264
+ const unixCommand = `sudo chown -R $USER:$(id -gn) ${prefixPath} && sudo chmod -R u+w ${prefixPath}`
265
+
266
+ return platform === 'win32' ? windowsCommand : unixCommand
267
+ }
268
+
269
+ export async function getLatestVersion(): Promise<string | null> {
270
+ const abortController = new AbortController()
271
+ setTimeout(() => abortController.abort(), 5000)
272
+
273
+ const result = await execFileNoThrow(
274
+ 'npm',
275
+ ['view', MACRO.PACKAGE_URL, 'version'],
276
+ abortController.signal,
277
+ )
278
+ if (result.code !== 0) {
279
+ return null
280
+ }
281
+ return result.stdout.trim()
282
+ }
283
+
284
+ export async function installGlobalPackage(): Promise<InstallStatus> {
285
+ if (!acquireLock()) {
286
+ logError('Another process is currently installing an update')
287
+ // Log the lock contention to statsig
288
+ logEvent('tengu_auto_updater_lock_contention', {
289
+ pid: String(process.pid),
290
+ currentVersion: MACRO.VERSION,
291
+ })
292
+ return 'in_progress'
293
+ }
294
+
295
+ try {
296
+ const { hasPermissions } = await checkNpmPermissions()
297
+ if (!hasPermissions) {
298
+ return 'no_permissions'
299
+ }
300
+
301
+ const installResult = await execFileNoThrow('npm', [
302
+ 'install',
303
+ '-g',
304
+ MACRO.PACKAGE_URL,
305
+ ])
306
+ if (installResult.code !== 0) {
307
+ logError(
308
+ `Failed to install new version of claude: ${installResult.stdout} ${installResult.stderr}`,
309
+ )
310
+ return 'install_failed'
311
+ }
312
+
313
+ return 'success'
314
+ } finally {
315
+ // Ensure we always release the lock
316
+ releaseLock()
317
+ }
318
+ }
@@ -0,0 +1,20 @@
1
+ import { memoize } from 'lodash-es'
2
+ import { checkGate } from '../services/statsig'
3
+ import {
4
+ GATE_TOKEN_EFFICIENT_TOOLS,
5
+ BETA_HEADER_TOKEN_EFFICIENT_TOOLS,
6
+ CLAUDE_CODE_20250219_BETA_HEADER,
7
+ } from '../constants/betas.js'
8
+
9
+ export const getBetas = memoize(async (): Promise<string[]> => {
10
+ const betaHeaders = [CLAUDE_CODE_20250219_BETA_HEADER]
11
+
12
+ if (process.env.USER_TYPE === 'ant' || process.env.SWE_BENCH) {
13
+ const useTokenEfficientTools = await checkGate(GATE_TOKEN_EFFICIENT_TOOLS)
14
+ if (useTokenEfficientTools) {
15
+ betaHeaders.push(BETA_HEADER_TOKEN_EFFICIENT_TOOLS)
16
+ }
17
+ }
18
+
19
+ return betaHeaders
20
+ })
@@ -0,0 +1,14 @@
1
+ import { execFileNoThrow } from './execFileNoThrow'
2
+
3
+ export async function openBrowser(url: string): Promise<boolean> {
4
+ const platform = process.platform
5
+ const command =
6
+ platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open'
7
+
8
+ try {
9
+ const { code } = await execFileNoThrow(command, [url])
10
+ return code === 0
11
+ } catch (_) {
12
+ return false
13
+ }
14
+ }
@@ -0,0 +1,72 @@
1
+ import { promises as fs } from 'fs'
2
+ import { join } from 'path'
3
+ import { logError } from './log'
4
+ import { CACHE_PATHS } from './log'
5
+
6
+ const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000
7
+
8
+ export type CleanupResult = {
9
+ messages: number
10
+ errors: number
11
+ }
12
+
13
+ export function convertFileNameToDate(filename: string): Date {
14
+ const isoStr = filename
15
+ .split('.')[0]!
16
+ .replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/, 'T$1:$2:$3.$4Z')
17
+ return new Date(isoStr)
18
+ }
19
+
20
+ export async function cleanupOldMessageFiles(): Promise<CleanupResult> {
21
+ const messagePath = CACHE_PATHS.messages()
22
+ const errorPath = CACHE_PATHS.errors()
23
+ const thirtyDaysAgo = new Date(Date.now() - THIRTY_DAYS_MS)
24
+ const deletedCounts: CleanupResult = { messages: 0, errors: 0 }
25
+
26
+ for (const path of [messagePath, errorPath]) {
27
+ try {
28
+ const files = await fs.readdir(path)
29
+
30
+ for (const file of files) {
31
+ try {
32
+ // Convert filename format where all ':.' were replaced with '-'
33
+ const timestamp = convertFileNameToDate(file)
34
+ if (timestamp < thirtyDaysAgo) {
35
+ await fs.unlink(join(path, file))
36
+ // Increment the appropriate counter
37
+ if (path === messagePath) {
38
+ deletedCounts.messages++
39
+ } else {
40
+ deletedCounts.errors++
41
+ }
42
+ }
43
+ } catch (error: unknown) {
44
+ // Log but continue processing other files
45
+ logError(
46
+ `Failed to process file ${file}: ${error instanceof Error ? error.message : String(error)}`,
47
+ )
48
+ }
49
+ }
50
+ } catch (error: unknown) {
51
+ // Ignore if directory doesn't exist
52
+ if (
53
+ error instanceof Error &&
54
+ 'code' in error &&
55
+ error.code !== 'ENOENT'
56
+ ) {
57
+ logError(
58
+ `Failed to cleanup directory ${path}: ${error instanceof Error ? error.message : String(error)}`,
59
+ )
60
+ }
61
+ }
62
+ }
63
+
64
+ return deletedCounts
65
+ }
66
+
67
+ export function cleanupOldMessageFilesInBackground(): void {
68
+ const immediate = setImmediate(cleanupOldMessageFiles)
69
+
70
+ // Prevent the setImmediate from keeping the process alive
71
+ immediate.unref()
72
+ }