@shareai-lab/kode 1.0.70 → 1.0.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +202 -76
  2. package/README.zh-CN.md +246 -0
  3. package/cli.js +62 -0
  4. package/package.json +45 -25
  5. package/scripts/postinstall.js +56 -0
  6. package/src/ProjectOnboarding.tsx +180 -0
  7. package/src/Tool.ts +53 -0
  8. package/src/commands/approvedTools.ts +53 -0
  9. package/src/commands/bug.tsx +20 -0
  10. package/src/commands/clear.ts +43 -0
  11. package/src/commands/compact.ts +120 -0
  12. package/src/commands/config.tsx +19 -0
  13. package/src/commands/cost.ts +18 -0
  14. package/src/commands/ctx_viz.ts +209 -0
  15. package/src/commands/doctor.ts +24 -0
  16. package/src/commands/help.tsx +19 -0
  17. package/src/commands/init.ts +37 -0
  18. package/src/commands/listen.ts +42 -0
  19. package/src/commands/login.tsx +51 -0
  20. package/src/commands/logout.tsx +40 -0
  21. package/src/commands/mcp.ts +41 -0
  22. package/src/commands/model.tsx +40 -0
  23. package/src/commands/modelstatus.tsx +20 -0
  24. package/src/commands/onboarding.tsx +34 -0
  25. package/src/commands/pr_comments.ts +59 -0
  26. package/src/commands/refreshCommands.ts +54 -0
  27. package/src/commands/release-notes.ts +34 -0
  28. package/src/commands/resume.tsx +30 -0
  29. package/src/commands/review.ts +49 -0
  30. package/src/commands/terminalSetup.ts +221 -0
  31. package/src/commands.ts +136 -0
  32. package/src/components/ApproveApiKey.tsx +93 -0
  33. package/src/components/AsciiLogo.tsx +13 -0
  34. package/src/components/AutoUpdater.tsx +148 -0
  35. package/src/components/Bug.tsx +367 -0
  36. package/src/components/Config.tsx +289 -0
  37. package/src/components/ConsoleOAuthFlow.tsx +326 -0
  38. package/src/components/Cost.tsx +23 -0
  39. package/src/components/CostThresholdDialog.tsx +46 -0
  40. package/src/components/CustomSelect/option-map.ts +42 -0
  41. package/src/components/CustomSelect/select-option.tsx +52 -0
  42. package/src/components/CustomSelect/select.tsx +143 -0
  43. package/src/components/CustomSelect/use-select-state.ts +414 -0
  44. package/src/components/CustomSelect/use-select.ts +35 -0
  45. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  46. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  47. package/src/components/Help.tsx +215 -0
  48. package/src/components/HighlightedCode.tsx +33 -0
  49. package/src/components/InvalidConfigDialog.tsx +113 -0
  50. package/src/components/Link.tsx +32 -0
  51. package/src/components/LogSelector.tsx +86 -0
  52. package/src/components/Logo.tsx +145 -0
  53. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  54. package/src/components/MCPServerDialogCopy.tsx +25 -0
  55. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  56. package/src/components/Message.tsx +219 -0
  57. package/src/components/MessageResponse.tsx +15 -0
  58. package/src/components/MessageSelector.tsx +211 -0
  59. package/src/components/ModeIndicator.tsx +88 -0
  60. package/src/components/ModelConfig.tsx +301 -0
  61. package/src/components/ModelListManager.tsx +223 -0
  62. package/src/components/ModelSelector.tsx +3208 -0
  63. package/src/components/ModelStatusDisplay.tsx +228 -0
  64. package/src/components/Onboarding.tsx +274 -0
  65. package/src/components/PressEnterToContinue.tsx +11 -0
  66. package/src/components/PromptInput.tsx +710 -0
  67. package/src/components/SentryErrorBoundary.ts +33 -0
  68. package/src/components/Spinner.tsx +129 -0
  69. package/src/components/StructuredDiff.tsx +184 -0
  70. package/src/components/TextInput.tsx +246 -0
  71. package/src/components/TokenWarning.tsx +31 -0
  72. package/src/components/ToolUseLoader.tsx +40 -0
  73. package/src/components/TrustDialog.tsx +106 -0
  74. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  75. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  76. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  77. package/src/components/binary-feedback/utils.ts +220 -0
  78. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  79. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +45 -0
  80. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  81. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  82. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  83. package/src/components/messages/AssistantToolUseMessage.tsx +123 -0
  84. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  85. package/src/components/messages/UserCommandMessage.tsx +30 -0
  86. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  87. package/src/components/messages/UserPromptMessage.tsx +35 -0
  88. package/src/components/messages/UserTextMessage.tsx +39 -0
  89. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  90. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  91. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  92. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  93. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  94. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  95. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  96. package/src/components/permissions/FallbackPermissionRequest.tsx +155 -0
  97. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  98. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +75 -0
  99. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  100. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +81 -0
  101. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +242 -0
  102. package/src/components/permissions/PermissionRequest.tsx +103 -0
  103. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  104. package/src/components/permissions/hooks.ts +44 -0
  105. package/src/components/permissions/toolUseOptions.ts +59 -0
  106. package/src/components/permissions/utils.ts +23 -0
  107. package/src/constants/betas.ts +5 -0
  108. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  109. package/src/constants/figures.ts +4 -0
  110. package/src/constants/keys.ts +3 -0
  111. package/src/constants/macros.ts +6 -0
  112. package/src/constants/models.ts +935 -0
  113. package/src/constants/oauth.ts +18 -0
  114. package/src/constants/product.ts +17 -0
  115. package/src/constants/prompts.ts +177 -0
  116. package/src/constants/releaseNotes.ts +7 -0
  117. package/src/context/PermissionContext.tsx +149 -0
  118. package/src/context.ts +278 -0
  119. package/src/cost-tracker.ts +84 -0
  120. package/src/entrypoints/cli.tsx +1498 -0
  121. package/src/entrypoints/mcp.ts +176 -0
  122. package/src/history.ts +25 -0
  123. package/src/hooks/useApiKeyVerification.ts +59 -0
  124. package/src/hooks/useArrowKeyHistory.ts +55 -0
  125. package/src/hooks/useCanUseTool.ts +138 -0
  126. package/src/hooks/useCancelRequest.ts +39 -0
  127. package/src/hooks/useDoublePress.ts +42 -0
  128. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  129. package/src/hooks/useInterval.ts +25 -0
  130. package/src/hooks/useLogMessages.ts +16 -0
  131. package/src/hooks/useLogStartupTime.ts +12 -0
  132. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  133. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  134. package/src/hooks/useSlashCommandTypeahead.ts +137 -0
  135. package/src/hooks/useTerminalSize.ts +49 -0
  136. package/src/hooks/useTextInput.ts +315 -0
  137. package/src/messages.ts +37 -0
  138. package/src/permissions.ts +268 -0
  139. package/src/query.ts +704 -0
  140. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  141. package/src/screens/Doctor.tsx +219 -0
  142. package/src/screens/LogList.tsx +68 -0
  143. package/src/screens/REPL.tsx +792 -0
  144. package/src/screens/ResumeConversation.tsx +68 -0
  145. package/src/services/browserMocks.ts +66 -0
  146. package/src/services/claude.ts +1947 -0
  147. package/src/services/customCommands.ts +683 -0
  148. package/src/services/fileFreshness.ts +377 -0
  149. package/src/services/mcpClient.ts +564 -0
  150. package/src/services/mcpServerApproval.tsx +50 -0
  151. package/src/services/notifier.ts +40 -0
  152. package/src/services/oauth.ts +357 -0
  153. package/src/services/openai.ts +796 -0
  154. package/src/services/sentry.ts +3 -0
  155. package/src/services/statsig.ts +171 -0
  156. package/src/services/statsigStorage.ts +86 -0
  157. package/src/services/systemReminder.ts +406 -0
  158. package/src/services/vcr.ts +161 -0
  159. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  160. package/src/tools/ArchitectTool/prompt.ts +15 -0
  161. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +505 -0
  162. package/src/tools/BashTool/BashTool.tsx +270 -0
  163. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  164. package/src/tools/BashTool/OutputLine.tsx +48 -0
  165. package/src/tools/BashTool/prompt.ts +174 -0
  166. package/src/tools/BashTool/utils.ts +56 -0
  167. package/src/tools/FileEditTool/FileEditTool.tsx +316 -0
  168. package/src/tools/FileEditTool/prompt.ts +51 -0
  169. package/src/tools/FileEditTool/utils.ts +58 -0
  170. package/src/tools/FileReadTool/FileReadTool.tsx +371 -0
  171. package/src/tools/FileReadTool/prompt.ts +7 -0
  172. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  173. package/src/tools/FileWriteTool/prompt.ts +10 -0
  174. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  175. package/src/tools/GlobTool/prompt.ts +8 -0
  176. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  177. package/src/tools/GrepTool/prompt.ts +11 -0
  178. package/src/tools/MCPTool/MCPTool.tsx +106 -0
  179. package/src/tools/MCPTool/prompt.ts +3 -0
  180. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  181. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  182. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  183. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  184. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  185. package/src/tools/MultiEditTool/prompt.ts +45 -0
  186. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  187. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  188. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +266 -0
  189. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  190. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  191. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  192. package/src/tools/TaskTool/TaskTool.tsx +382 -0
  193. package/src/tools/TaskTool/constants.ts +1 -0
  194. package/src/tools/TaskTool/prompt.ts +56 -0
  195. package/src/tools/ThinkTool/ThinkTool.tsx +56 -0
  196. package/src/tools/ThinkTool/prompt.ts +12 -0
  197. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +289 -0
  198. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  199. package/src/tools/lsTool/lsTool.tsx +269 -0
  200. package/src/tools/lsTool/prompt.ts +2 -0
  201. package/src/tools.ts +63 -0
  202. package/src/types/PermissionMode.ts +120 -0
  203. package/src/types/RequestContext.ts +72 -0
  204. package/src/utils/Cursor.ts +436 -0
  205. package/src/utils/PersistentShell.ts +373 -0
  206. package/src/utils/agentStorage.ts +97 -0
  207. package/src/utils/array.ts +3 -0
  208. package/src/utils/ask.tsx +98 -0
  209. package/src/utils/auth.ts +13 -0
  210. package/src/utils/autoCompactCore.ts +223 -0
  211. package/src/utils/autoUpdater.ts +318 -0
  212. package/src/utils/betas.ts +20 -0
  213. package/src/utils/browser.ts +14 -0
  214. package/src/utils/cleanup.ts +72 -0
  215. package/src/utils/commands.ts +261 -0
  216. package/src/utils/config.ts +771 -0
  217. package/src/utils/conversationRecovery.ts +54 -0
  218. package/src/utils/debugLogger.ts +1123 -0
  219. package/src/utils/diff.ts +42 -0
  220. package/src/utils/env.ts +57 -0
  221. package/src/utils/errors.ts +21 -0
  222. package/src/utils/exampleCommands.ts +108 -0
  223. package/src/utils/execFileNoThrow.ts +51 -0
  224. package/src/utils/expertChatStorage.ts +136 -0
  225. package/src/utils/file.ts +402 -0
  226. package/src/utils/fileRecoveryCore.ts +71 -0
  227. package/src/utils/format.tsx +44 -0
  228. package/src/utils/generators.ts +62 -0
  229. package/src/utils/git.ts +92 -0
  230. package/src/utils/globalLogger.ts +77 -0
  231. package/src/utils/http.ts +10 -0
  232. package/src/utils/imagePaste.ts +38 -0
  233. package/src/utils/json.ts +13 -0
  234. package/src/utils/log.ts +382 -0
  235. package/src/utils/markdown.ts +213 -0
  236. package/src/utils/messageContextManager.ts +289 -0
  237. package/src/utils/messages.tsx +938 -0
  238. package/src/utils/model.ts +836 -0
  239. package/src/utils/permissions/filesystem.ts +118 -0
  240. package/src/utils/ripgrep.ts +167 -0
  241. package/src/utils/sessionState.ts +49 -0
  242. package/src/utils/state.ts +25 -0
  243. package/src/utils/style.ts +29 -0
  244. package/src/utils/terminal.ts +49 -0
  245. package/src/utils/theme.ts +122 -0
  246. package/src/utils/thinking.ts +144 -0
  247. package/src/utils/todoStorage.ts +431 -0
  248. package/src/utils/tokens.ts +43 -0
  249. package/src/utils/toolExecutionController.ts +163 -0
  250. package/src/utils/unaryLogging.ts +26 -0
  251. package/src/utils/user.ts +37 -0
  252. package/src/utils/validate.ts +165 -0
  253. package/cli.mjs +0 -1803
@@ -0,0 +1,771 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
2
+ import { resolve, join } from 'path'
3
+ import { cloneDeep, memoize, pick } from 'lodash-es'
4
+ import { homedir } from 'os'
5
+ import { GLOBAL_CLAUDE_FILE } from './env'
6
+ import { getCwd } from './state'
7
+ import { randomBytes } from 'crypto'
8
+ import { safeParseJSON } from './json'
9
+ import { checkGate, logEvent } from '../services/statsig'
10
+ import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
11
+ import { ConfigParseError } from './errors'
12
+ import type { ThemeNames } from './theme'
13
+ import { debug as debugLogger } from './debugLogger'
14
+ import { getSessionState, setSessionState } from './sessionState'
15
+
16
+ export type McpStdioServerConfig = {
17
+ type?: 'stdio' // Optional for backwards compatibility
18
+ command: string
19
+ args: string[]
20
+ env?: Record<string, string>
21
+ }
22
+
23
+ export type McpSSEServerConfig = {
24
+ type: 'sse'
25
+ url: string
26
+ }
27
+
28
+ export type McpServerConfig = McpStdioServerConfig | McpSSEServerConfig
29
+
30
+ export type ProjectConfig = {
31
+ allowedTools: string[]
32
+ context: Record<string, string>
33
+ contextFiles?: string[]
34
+ history: string[]
35
+ dontCrawlDirectory?: boolean
36
+ enableArchitectTool?: boolean
37
+ mcpContextUris: string[]
38
+ mcpServers?: Record<string, McpServerConfig>
39
+ approvedMcprcServers?: string[]
40
+ rejectedMcprcServers?: string[]
41
+ lastAPIDuration?: number
42
+ lastCost?: number
43
+ lastDuration?: number
44
+ lastSessionId?: string
45
+ exampleFiles?: string[]
46
+ exampleFilesGeneratedAt?: number
47
+ hasTrustDialogAccepted?: boolean
48
+ hasCompletedProjectOnboarding?: boolean
49
+ }
50
+
51
+ const DEFAULT_PROJECT_CONFIG: ProjectConfig = {
52
+ allowedTools: [],
53
+ context: {},
54
+ history: [],
55
+ dontCrawlDirectory: false,
56
+ enableArchitectTool: false,
57
+ mcpContextUris: [],
58
+ mcpServers: {},
59
+ approvedMcprcServers: [],
60
+ rejectedMcprcServers: [],
61
+ hasTrustDialogAccepted: false,
62
+ }
63
+
64
+ function defaultConfigForProject(projectPath: string): ProjectConfig {
65
+ const config = { ...DEFAULT_PROJECT_CONFIG }
66
+ if (projectPath === homedir()) {
67
+ config.dontCrawlDirectory = true
68
+ }
69
+ return config
70
+ }
71
+
72
+ export type AutoUpdaterStatus =
73
+ | 'disabled'
74
+ | 'enabled'
75
+ | 'no_permissions'
76
+ | 'not_configured'
77
+
78
+ export function isAutoUpdaterStatus(value: string): value is AutoUpdaterStatus {
79
+ return ['disabled', 'enabled', 'no_permissions', 'not_configured'].includes(
80
+ value as AutoUpdaterStatus,
81
+ )
82
+ }
83
+
84
+ export type NotificationChannel =
85
+ | 'iterm2'
86
+ | 'terminal_bell'
87
+ | 'iterm2_with_bell'
88
+ | 'notifications_disabled'
89
+
90
+ export type ProviderType =
91
+ | 'anthropic'
92
+ | 'openai'
93
+ | 'mistral'
94
+ | 'deepseek'
95
+ | 'kimi'
96
+ | 'qwen'
97
+ | 'glm'
98
+ | 'minimax'
99
+ | 'baidu-qianfan'
100
+ | 'siliconflow'
101
+ | 'bigdream'
102
+ | 'opendev'
103
+ | 'xai'
104
+ | 'groq'
105
+ | 'gemini'
106
+ | 'ollama'
107
+ | 'azure'
108
+ | 'custom'
109
+ | 'custom-openai'
110
+
111
+ // New model system types
112
+ export type ModelProfile = {
113
+ name: string // User-friendly name
114
+ provider: ProviderType // Provider type
115
+ modelName: string // Primary key - actual model identifier
116
+ baseURL?: string // Custom endpoint
117
+ apiKey: string
118
+ maxTokens: number // Output token limit
119
+ contextLength: number // Context window size
120
+ reasoningEffort?: 'low' | 'medium' | 'high'
121
+ isActive: boolean // Whether profile is enabled
122
+ createdAt: number // Creation timestamp
123
+ lastUsed?: number // Last usage timestamp
124
+ }
125
+
126
+ export type ModelPointerType = 'main' | 'task' | 'reasoning' | 'quick'
127
+
128
+ export type ModelPointers = {
129
+ main: string // Main dialog model ID
130
+ task: string // Task tool model ID
131
+ reasoning: string // Reasoning model ID
132
+ quick: string // Quick model ID
133
+ }
134
+
135
+ export type AccountInfo = {
136
+ accountUuid: string
137
+ emailAddress: string
138
+ organizationUuid?: string
139
+ }
140
+
141
+ export type GlobalConfig = {
142
+ projects?: Record<string, ProjectConfig>
143
+ numStartups: number
144
+ autoUpdaterStatus?: AutoUpdaterStatus
145
+ userID?: string
146
+ theme: ThemeNames
147
+ hasCompletedOnboarding?: boolean
148
+ // Tracks the last version that reset onboarding, used with MIN_VERSION_REQUIRING_ONBOARDING_RESET
149
+ lastOnboardingVersion?: string
150
+ // Tracks the last version for which release notes were seen, used for managing release notes
151
+ lastReleaseNotesSeen?: string
152
+ mcpServers?: Record<string, McpServerConfig>
153
+ preferredNotifChannel: NotificationChannel
154
+ verbose: boolean
155
+ customApiKeyResponses?: {
156
+ approved?: string[]
157
+ rejected?: string[]
158
+ }
159
+ primaryProvider?: ProviderType
160
+ maxTokens?: number
161
+ hasAcknowledgedCostThreshold?: boolean
162
+ oauthAccount?: AccountInfo
163
+ iterm2KeyBindingInstalled?: boolean // Legacy - keeping for backward compatibility
164
+ shiftEnterKeyBindingInstalled?: boolean
165
+ proxy?: string
166
+ stream?: boolean
167
+
168
+ // New model system
169
+ modelProfiles?: ModelProfile[] // Model configuration list
170
+ modelPointers?: ModelPointers // Model pointer system
171
+ defaultModelName?: string // Default model
172
+ }
173
+
174
+ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
175
+ numStartups: 0,
176
+ autoUpdaterStatus: 'not_configured',
177
+ theme: 'dark' as ThemeNames,
178
+ preferredNotifChannel: 'iterm2',
179
+ verbose: false,
180
+ primaryProvider: 'anthropic' as ProviderType,
181
+ customApiKeyResponses: {
182
+ approved: [],
183
+ rejected: [],
184
+ },
185
+ stream: true,
186
+
187
+ // New model system defaults
188
+ modelProfiles: [],
189
+ modelPointers: {
190
+ main: '',
191
+ task: '',
192
+ reasoning: '',
193
+ quick: '',
194
+ },
195
+ }
196
+
197
+ export const GLOBAL_CONFIG_KEYS = [
198
+ 'autoUpdaterStatus',
199
+ 'theme',
200
+ 'hasCompletedOnboarding',
201
+ 'lastOnboardingVersion',
202
+ 'lastReleaseNotesSeen',
203
+ 'verbose',
204
+ 'customApiKeyResponses',
205
+ 'primaryProvider',
206
+ 'preferredNotifChannel',
207
+ 'shiftEnterKeyBindingInstalled',
208
+ 'maxTokens',
209
+ ] as const
210
+
211
+ export type GlobalConfigKey = (typeof GLOBAL_CONFIG_KEYS)[number]
212
+
213
+ export function isGlobalConfigKey(key: string): key is GlobalConfigKey {
214
+ return GLOBAL_CONFIG_KEYS.includes(key as GlobalConfigKey)
215
+ }
216
+
217
+ export const PROJECT_CONFIG_KEYS = [
218
+ 'dontCrawlDirectory',
219
+ 'enableArchitectTool',
220
+ 'hasTrustDialogAccepted',
221
+ 'hasCompletedProjectOnboarding',
222
+ ] as const
223
+
224
+ export type ProjectConfigKey = (typeof PROJECT_CONFIG_KEYS)[number]
225
+
226
+ export function checkHasTrustDialogAccepted(): boolean {
227
+ let currentPath = getCwd()
228
+ const config = getConfig(GLOBAL_CLAUDE_FILE, DEFAULT_GLOBAL_CONFIG)
229
+
230
+ while (true) {
231
+ const projectConfig = config.projects?.[currentPath]
232
+ if (projectConfig?.hasTrustDialogAccepted) {
233
+ return true
234
+ }
235
+ const parentPath = resolve(currentPath, '..')
236
+ // Stop if we've reached the root (when parent is same as current)
237
+ if (parentPath === currentPath) {
238
+ break
239
+ }
240
+ currentPath = parentPath
241
+ }
242
+
243
+ return false
244
+ }
245
+
246
+ // We have to put this test code here because Jest doesn't support mocking ES modules :O
247
+ const TEST_GLOBAL_CONFIG_FOR_TESTING: GlobalConfig = {
248
+ ...DEFAULT_GLOBAL_CONFIG,
249
+ autoUpdaterStatus: 'disabled',
250
+ }
251
+ const TEST_PROJECT_CONFIG_FOR_TESTING: ProjectConfig = {
252
+ ...DEFAULT_PROJECT_CONFIG,
253
+ }
254
+
255
+ export function isProjectConfigKey(key: string): key is ProjectConfigKey {
256
+ return PROJECT_CONFIG_KEYS.includes(key as ProjectConfigKey)
257
+ }
258
+
259
+ export function saveGlobalConfig(config: GlobalConfig): void {
260
+ if (process.env.NODE_ENV === 'test') {
261
+ for (const key in config) {
262
+ TEST_GLOBAL_CONFIG_FOR_TESTING[key] = config[key]
263
+ }
264
+ return
265
+ }
266
+
267
+ // 直接保存配置(无需清除缓存,因为已移除缓存)
268
+ saveConfig(
269
+ GLOBAL_CLAUDE_FILE,
270
+ {
271
+ ...config,
272
+ projects: getConfig(GLOBAL_CLAUDE_FILE, DEFAULT_GLOBAL_CONFIG).projects,
273
+ },
274
+ DEFAULT_GLOBAL_CONFIG,
275
+ )
276
+ }
277
+
278
+ // 临时移除缓存,确保总是获取最新配置
279
+ export function getGlobalConfig(): GlobalConfig {
280
+ if (process.env.NODE_ENV === 'test') {
281
+ return TEST_GLOBAL_CONFIG_FOR_TESTING
282
+ }
283
+ const config = getConfig(GLOBAL_CLAUDE_FILE, DEFAULT_GLOBAL_CONFIG)
284
+ return migrateModelProfilesRemoveId(config)
285
+ }
286
+
287
+ export function getAnthropicApiKey(): null | string {
288
+ return process.env.ANTHROPIC_API_KEY || null
289
+ }
290
+
291
+ export function normalizeApiKeyForConfig(apiKey: string): string {
292
+ return apiKey?.slice(-20) ?? ''
293
+ }
294
+
295
+ export function getCustomApiKeyStatus(
296
+ truncatedApiKey: string,
297
+ ): 'approved' | 'rejected' | 'new' {
298
+ const config = getGlobalConfig()
299
+ if (config.customApiKeyResponses?.approved?.includes(truncatedApiKey)) {
300
+ return 'approved'
301
+ }
302
+ if (config.customApiKeyResponses?.rejected?.includes(truncatedApiKey)) {
303
+ return 'rejected'
304
+ }
305
+ return 'new'
306
+ }
307
+
308
+ function saveConfig<A extends object>(
309
+ file: string,
310
+ config: A,
311
+ defaultConfig: A,
312
+ ): void {
313
+ // Filter out any values that match the defaults
314
+ const filteredConfig = Object.fromEntries(
315
+ Object.entries(config).filter(
316
+ ([key, value]) =>
317
+ JSON.stringify(value) !== JSON.stringify(defaultConfig[key as keyof A]),
318
+ ),
319
+ )
320
+ writeFileSync(file, JSON.stringify(filteredConfig, null, 2), 'utf-8')
321
+ }
322
+
323
+ // Flag to track if config reading is allowed
324
+ let configReadingAllowed = false
325
+
326
+ export function enableConfigs(): void {
327
+ // Any reads to configuration before this flag is set show an console warning
328
+ // to prevent us from adding config reading during module initialization
329
+ configReadingAllowed = true
330
+ // We only check the global config because currently all the configs share a file
331
+ getConfig(
332
+ GLOBAL_CLAUDE_FILE,
333
+ DEFAULT_GLOBAL_CONFIG,
334
+ true /* throw on invalid */,
335
+ )
336
+ }
337
+
338
+ function getConfig<A>(
339
+ file: string,
340
+ defaultConfig: A,
341
+ throwOnInvalid?: boolean,
342
+ ): A {
343
+ // 简化配置访问逻辑,移除复杂的时序检查
344
+
345
+ debugLogger.state('CONFIG_LOAD_START', {
346
+ file,
347
+ fileExists: String(existsSync(file)),
348
+ throwOnInvalid: String(!!throwOnInvalid),
349
+ })
350
+
351
+ if (!existsSync(file)) {
352
+ debugLogger.state('CONFIG_LOAD_DEFAULT', {
353
+ file,
354
+ reason: 'file_not_exists',
355
+ defaultConfigKeys: Object.keys(defaultConfig as object).join(', '),
356
+ })
357
+ return cloneDeep(defaultConfig)
358
+ }
359
+
360
+ try {
361
+ const fileContent = readFileSync(file, 'utf-8')
362
+ debugLogger.state('CONFIG_FILE_READ', {
363
+ file,
364
+ contentLength: String(fileContent.length),
365
+ contentPreview:
366
+ fileContent.substring(0, 100) + (fileContent.length > 100 ? '...' : ''),
367
+ })
368
+
369
+ try {
370
+ const parsedConfig = JSON.parse(fileContent)
371
+ debugLogger.state('CONFIG_JSON_PARSED', {
372
+ file,
373
+ parsedKeys: Object.keys(parsedConfig).join(', '),
374
+ })
375
+
376
+ // Handle backward compatibility - remove logic for deleted fields
377
+ const finalConfig = {
378
+ ...cloneDeep(defaultConfig),
379
+ ...parsedConfig,
380
+ }
381
+
382
+ debugLogger.state('CONFIG_LOAD_SUCCESS', {
383
+ file,
384
+ finalConfigKeys: Object.keys(finalConfig as object).join(', '),
385
+ })
386
+
387
+ return finalConfig
388
+ } catch (error) {
389
+ // Throw a ConfigParseError with the file path and default config
390
+ const errorMessage =
391
+ error instanceof Error ? error.message : String(error)
392
+
393
+ debugLogger.error('CONFIG_JSON_PARSE_ERROR', {
394
+ file,
395
+ errorMessage,
396
+ errorType:
397
+ error instanceof Error ? error.constructor.name : typeof error,
398
+ contentLength: String(fileContent.length),
399
+ })
400
+
401
+ throw new ConfigParseError(errorMessage, file, defaultConfig)
402
+ }
403
+ } catch (error: unknown) {
404
+ // Re-throw ConfigParseError if throwOnInvalid is true
405
+ if (error instanceof ConfigParseError && throwOnInvalid) {
406
+ debugLogger.error('CONFIG_PARSE_ERROR_RETHROWN', {
407
+ file,
408
+ throwOnInvalid: String(throwOnInvalid),
409
+ errorMessage: error.message,
410
+ })
411
+ throw error
412
+ }
413
+
414
+ debugLogger.warn('CONFIG_FALLBACK_TO_DEFAULT', {
415
+ file,
416
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
417
+ errorMessage: error instanceof Error ? error.message : String(error),
418
+ action: 'using_default_config',
419
+ })
420
+
421
+ return cloneDeep(defaultConfig)
422
+ }
423
+ }
424
+
425
+ export function getCurrentProjectConfig(): ProjectConfig {
426
+ if (process.env.NODE_ENV === 'test') {
427
+ return TEST_PROJECT_CONFIG_FOR_TESTING
428
+ }
429
+
430
+ const absolutePath = resolve(getCwd())
431
+ const config = getConfig(GLOBAL_CLAUDE_FILE, DEFAULT_GLOBAL_CONFIG)
432
+
433
+ if (!config.projects) {
434
+ return defaultConfigForProject(absolutePath)
435
+ }
436
+
437
+ const projectConfig =
438
+ config.projects[absolutePath] ?? defaultConfigForProject(absolutePath)
439
+ // Not sure how this became a string
440
+ // TODO: Fix upstream
441
+ if (typeof projectConfig.allowedTools === 'string') {
442
+ projectConfig.allowedTools =
443
+ (safeParseJSON(projectConfig.allowedTools) as string[]) ?? []
444
+ }
445
+ return projectConfig
446
+ }
447
+
448
+ export function saveCurrentProjectConfig(projectConfig: ProjectConfig): void {
449
+ if (process.env.NODE_ENV === 'test') {
450
+ for (const key in projectConfig) {
451
+ TEST_PROJECT_CONFIG_FOR_TESTING[key] = projectConfig[key]
452
+ }
453
+ return
454
+ }
455
+ const config = getConfig(GLOBAL_CLAUDE_FILE, DEFAULT_GLOBAL_CONFIG)
456
+ saveConfig(
457
+ GLOBAL_CLAUDE_FILE,
458
+ {
459
+ ...config,
460
+ projects: {
461
+ ...config.projects,
462
+ [resolve(getCwd())]: projectConfig,
463
+ },
464
+ },
465
+ DEFAULT_GLOBAL_CONFIG,
466
+ )
467
+ }
468
+
469
+ export async function isAutoUpdaterDisabled(): Promise<boolean> {
470
+ const useExternalUpdater = await checkGate(GATE_USE_EXTERNAL_UPDATER)
471
+ return (
472
+ useExternalUpdater || getGlobalConfig().autoUpdaterStatus === 'disabled'
473
+ )
474
+ }
475
+
476
+ export const TEST_MCPRC_CONFIG_FOR_TESTING: Record<string, McpServerConfig> = {}
477
+
478
+ export function clearMcprcConfigForTesting(): void {
479
+ if (process.env.NODE_ENV === 'test') {
480
+ Object.keys(TEST_MCPRC_CONFIG_FOR_TESTING).forEach(key => {
481
+ delete TEST_MCPRC_CONFIG_FOR_TESTING[key]
482
+ })
483
+ }
484
+ }
485
+
486
+ export function addMcprcServerForTesting(
487
+ name: string,
488
+ server: McpServerConfig,
489
+ ): void {
490
+ if (process.env.NODE_ENV === 'test') {
491
+ TEST_MCPRC_CONFIG_FOR_TESTING[name] = server
492
+ }
493
+ }
494
+
495
+ export function removeMcprcServerForTesting(name: string): void {
496
+ if (process.env.NODE_ENV === 'test') {
497
+ if (!TEST_MCPRC_CONFIG_FOR_TESTING[name]) {
498
+ throw new Error(`No MCP server found with name: ${name} in .mcprc`)
499
+ }
500
+ delete TEST_MCPRC_CONFIG_FOR_TESTING[name]
501
+ }
502
+ }
503
+
504
+ export const getMcprcConfig = memoize(
505
+ (): Record<string, McpServerConfig> => {
506
+ if (process.env.NODE_ENV === 'test') {
507
+ return TEST_MCPRC_CONFIG_FOR_TESTING
508
+ }
509
+
510
+ const mcprcPath = join(getCwd(), '.mcprc')
511
+ if (!existsSync(mcprcPath)) {
512
+ return {}
513
+ }
514
+
515
+ try {
516
+ const mcprcContent = readFileSync(mcprcPath, 'utf-8')
517
+ const config = safeParseJSON(mcprcContent)
518
+ if (config && typeof config === 'object') {
519
+ logEvent('tengu_mcprc_found', {
520
+ numServers: Object.keys(config).length.toString(),
521
+ })
522
+ return config as Record<string, McpServerConfig>
523
+ }
524
+ } catch {
525
+ // Ignore errors reading/parsing .mcprc (they're logged in safeParseJSON)
526
+ }
527
+ return {}
528
+ },
529
+ // This function returns the same value as long as the cwd and mcprc file content remain the same
530
+ () => {
531
+ const cwd = getCwd()
532
+ const mcprcPath = join(cwd, '.mcprc')
533
+ if (existsSync(mcprcPath)) {
534
+ try {
535
+ const stat = readFileSync(mcprcPath, 'utf-8')
536
+ return `${cwd}:${stat}`
537
+ } catch {
538
+ return cwd
539
+ }
540
+ }
541
+ return cwd
542
+ },
543
+ )
544
+
545
+ export function getOrCreateUserID(): string {
546
+ const config = getGlobalConfig()
547
+ if (config.userID) {
548
+ return config.userID
549
+ }
550
+
551
+ const userID = randomBytes(32).toString('hex')
552
+ saveGlobalConfig({ ...config, userID })
553
+ return userID
554
+ }
555
+
556
+ export function getConfigForCLI(key: string, global: boolean): unknown {
557
+ logEvent('tengu_config_get', {
558
+ key,
559
+ global: global?.toString() ?? 'false',
560
+ })
561
+ if (global) {
562
+ if (!isGlobalConfigKey(key)) {
563
+ console.error(
564
+ `Error: '${key}' is not a valid config key. Valid keys are: ${GLOBAL_CONFIG_KEYS.join(', ')}`,
565
+ )
566
+ process.exit(1)
567
+ }
568
+ return getGlobalConfig()[key]
569
+ } else {
570
+ if (!isProjectConfigKey(key)) {
571
+ console.error(
572
+ `Error: '${key}' is not a valid config key. Valid keys are: ${PROJECT_CONFIG_KEYS.join(', ')}`,
573
+ )
574
+ process.exit(1)
575
+ }
576
+ return getCurrentProjectConfig()[key]
577
+ }
578
+ }
579
+
580
+ export function setConfigForCLI(
581
+ key: string,
582
+ value: unknown,
583
+ global: boolean,
584
+ ): void {
585
+ logEvent('tengu_config_set', {
586
+ key,
587
+ global: global?.toString() ?? 'false',
588
+ })
589
+ if (global) {
590
+ if (!isGlobalConfigKey(key)) {
591
+ console.error(
592
+ `Error: Cannot set '${key}'. Only these keys can be modified: ${GLOBAL_CONFIG_KEYS.join(', ')}`,
593
+ )
594
+ process.exit(1)
595
+ }
596
+
597
+ if (key === 'autoUpdaterStatus' && !isAutoUpdaterStatus(value as string)) {
598
+ console.error(
599
+ `Error: Invalid value for autoUpdaterStatus. Must be one of: disabled, enabled, no_permissions, not_configured`,
600
+ )
601
+ process.exit(1)
602
+ }
603
+
604
+ const currentConfig = getGlobalConfig()
605
+ saveGlobalConfig({
606
+ ...currentConfig,
607
+ [key]: value,
608
+ })
609
+ } else {
610
+ if (!isProjectConfigKey(key)) {
611
+ console.error(
612
+ `Error: Cannot set '${key}'. Only these keys can be modified: ${PROJECT_CONFIG_KEYS.join(', ')}. Did you mean --global?`,
613
+ )
614
+ process.exit(1)
615
+ }
616
+ const currentConfig = getCurrentProjectConfig()
617
+ saveCurrentProjectConfig({
618
+ ...currentConfig,
619
+ [key]: value,
620
+ })
621
+ }
622
+ // Wait for the output to be flushed, to avoid clearing the screen.
623
+ setTimeout(() => {
624
+ // Without this we hang indefinitely.
625
+ process.exit(0)
626
+ }, 100)
627
+ }
628
+
629
+ export function deleteConfigForCLI(key: string, global: boolean): void {
630
+ logEvent('tengu_config_delete', {
631
+ key,
632
+ global: global?.toString() ?? 'false',
633
+ })
634
+ if (global) {
635
+ if (!isGlobalConfigKey(key)) {
636
+ console.error(
637
+ `Error: Cannot delete '${key}'. Only these keys can be modified: ${GLOBAL_CONFIG_KEYS.join(', ')}`,
638
+ )
639
+ process.exit(1)
640
+ }
641
+ const currentConfig = getGlobalConfig()
642
+ delete currentConfig[key]
643
+ saveGlobalConfig(currentConfig)
644
+ } else {
645
+ if (!isProjectConfigKey(key)) {
646
+ console.error(
647
+ `Error: Cannot delete '${key}'. Only these keys can be modified: ${PROJECT_CONFIG_KEYS.join(', ')}. Did you mean --global?`,
648
+ )
649
+ process.exit(1)
650
+ }
651
+ const currentConfig = getCurrentProjectConfig()
652
+ delete currentConfig[key]
653
+ saveCurrentProjectConfig(currentConfig)
654
+ }
655
+ }
656
+
657
+ export function listConfigForCLI(global: true): GlobalConfig
658
+ export function listConfigForCLI(global: false): ProjectConfig
659
+ export function listConfigForCLI(global: boolean): object {
660
+ logEvent('tengu_config_list', {
661
+ global: global?.toString() ?? 'false',
662
+ })
663
+ if (global) {
664
+ const currentConfig = pick(getGlobalConfig(), GLOBAL_CONFIG_KEYS)
665
+ return currentConfig
666
+ } else {
667
+ return pick(getCurrentProjectConfig(), PROJECT_CONFIG_KEYS)
668
+ }
669
+ }
670
+
671
+ export function getOpenAIApiKey(): string | undefined {
672
+ return process.env.OPENAI_API_KEY
673
+ }
674
+
675
+ // Configuration migration utility functions
676
+ function migrateModelProfilesRemoveId(config: GlobalConfig): GlobalConfig {
677
+ if (!config.modelProfiles) return config
678
+
679
+ // 1. Remove id field from ModelProfile objects and build ID to modelName mapping
680
+ const idToModelNameMap = new Map<string, string>()
681
+ const migratedProfiles = config.modelProfiles.map(profile => {
682
+ // Build mapping before removing id field
683
+ if ((profile as any).id && profile.modelName) {
684
+ idToModelNameMap.set((profile as any).id, profile.modelName)
685
+ }
686
+
687
+ // Remove id field, keep everything else
688
+ const { id, ...profileWithoutId } = profile as any
689
+ return profileWithoutId as ModelProfile
690
+ })
691
+
692
+ // 2. Migrate ModelPointers from IDs to modelNames
693
+ const migratedPointers: ModelPointers = {
694
+ main: '',
695
+ task: '',
696
+ reasoning: '',
697
+ quick: '',
698
+ }
699
+
700
+ if (config.modelPointers) {
701
+ Object.entries(config.modelPointers).forEach(([pointer, value]) => {
702
+ if (value) {
703
+ // If value looks like an old ID (model_xxx), map it to modelName
704
+ const modelName = idToModelNameMap.get(value) || value
705
+ migratedPointers[pointer as ModelPointerType] = modelName
706
+ }
707
+ })
708
+ }
709
+
710
+ // 3. Migrate legacy config fields
711
+ let defaultModelName: string | undefined
712
+ if ((config as any).defaultModelId) {
713
+ defaultModelName =
714
+ idToModelNameMap.get((config as any).defaultModelId) ||
715
+ (config as any).defaultModelId
716
+ } else if ((config as any).defaultModelName) {
717
+ defaultModelName = (config as any).defaultModelName
718
+ }
719
+
720
+ // 4. Remove legacy fields and return migrated config
721
+ const migratedConfig = { ...config }
722
+ delete (migratedConfig as any).defaultModelId
723
+ delete (migratedConfig as any).currentSelectedModelId
724
+ delete (migratedConfig as any).mainAgentModelId
725
+ delete (migratedConfig as any).taskToolModelId
726
+
727
+ return {
728
+ ...migratedConfig,
729
+ modelProfiles: migratedProfiles,
730
+ modelPointers: migratedPointers,
731
+ defaultModelName,
732
+ }
733
+ }
734
+
735
+ // New model system utility functions
736
+
737
+ export function setAllPointersToModel(modelName: string): void {
738
+ const config = getGlobalConfig()
739
+ const updatedConfig = {
740
+ ...config,
741
+ modelPointers: {
742
+ main: modelName,
743
+ task: modelName,
744
+ reasoning: modelName,
745
+ quick: modelName,
746
+ },
747
+ defaultModelName: modelName,
748
+ }
749
+ saveGlobalConfig(updatedConfig)
750
+ }
751
+
752
+ export function setModelPointer(
753
+ pointer: ModelPointerType,
754
+ modelName: string,
755
+ ): void {
756
+ const config = getGlobalConfig()
757
+ const updatedConfig = {
758
+ ...config,
759
+ modelPointers: {
760
+ ...config.modelPointers,
761
+ [pointer]: modelName,
762
+ },
763
+ }
764
+ saveGlobalConfig(updatedConfig)
765
+
766
+ // 🔧 Fix: Force ModelManager reload after config change
767
+ // Import here to avoid circular dependency
768
+ import('./model').then(({ reloadModelManager }) => {
769
+ reloadModelManager()
770
+ })
771
+ }