@shareai-lab/kode 1.0.9

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 (286) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +426 -0
  3. package/README.zh-CN.md +326 -0
  4. package/cli.js +79 -0
  5. package/package.json +119 -0
  6. package/scripts/postinstall.js +18 -0
  7. package/src/ProjectOnboarding.tsx +198 -0
  8. package/src/Tool.ts +82 -0
  9. package/src/commands/agents.tsx +3410 -0
  10. package/src/commands/approvedTools.ts +53 -0
  11. package/src/commands/bug.tsx +20 -0
  12. package/src/commands/clear.ts +43 -0
  13. package/src/commands/compact.ts +120 -0
  14. package/src/commands/config.tsx +19 -0
  15. package/src/commands/cost.ts +18 -0
  16. package/src/commands/ctx_viz.ts +209 -0
  17. package/src/commands/doctor.ts +24 -0
  18. package/src/commands/help.tsx +19 -0
  19. package/src/commands/init.ts +37 -0
  20. package/src/commands/listen.ts +42 -0
  21. package/src/commands/login.tsx +51 -0
  22. package/src/commands/logout.tsx +40 -0
  23. package/src/commands/mcp.ts +41 -0
  24. package/src/commands/model.tsx +40 -0
  25. package/src/commands/modelstatus.tsx +20 -0
  26. package/src/commands/onboarding.tsx +34 -0
  27. package/src/commands/pr_comments.ts +59 -0
  28. package/src/commands/refreshCommands.ts +54 -0
  29. package/src/commands/release-notes.ts +34 -0
  30. package/src/commands/resume.tsx +31 -0
  31. package/src/commands/review.ts +49 -0
  32. package/src/commands/terminalSetup.ts +221 -0
  33. package/src/commands.ts +139 -0
  34. package/src/components/ApproveApiKey.tsx +93 -0
  35. package/src/components/AsciiLogo.tsx +13 -0
  36. package/src/components/AutoUpdater.tsx +148 -0
  37. package/src/components/Bug.tsx +367 -0
  38. package/src/components/Config.tsx +293 -0
  39. package/src/components/ConsoleOAuthFlow.tsx +327 -0
  40. package/src/components/Cost.tsx +23 -0
  41. package/src/components/CostThresholdDialog.tsx +46 -0
  42. package/src/components/CustomSelect/option-map.ts +42 -0
  43. package/src/components/CustomSelect/select-option.tsx +78 -0
  44. package/src/components/CustomSelect/select.tsx +152 -0
  45. package/src/components/CustomSelect/theme.ts +45 -0
  46. package/src/components/CustomSelect/use-select-state.ts +414 -0
  47. package/src/components/CustomSelect/use-select.ts +35 -0
  48. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  49. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  50. package/src/components/Help.tsx +215 -0
  51. package/src/components/HighlightedCode.tsx +33 -0
  52. package/src/components/InvalidConfigDialog.tsx +113 -0
  53. package/src/components/Link.tsx +32 -0
  54. package/src/components/LogSelector.tsx +86 -0
  55. package/src/components/Logo.tsx +170 -0
  56. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  57. package/src/components/MCPServerDialogCopy.tsx +25 -0
  58. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  59. package/src/components/Message.tsx +221 -0
  60. package/src/components/MessageResponse.tsx +15 -0
  61. package/src/components/MessageSelector.tsx +211 -0
  62. package/src/components/ModeIndicator.tsx +88 -0
  63. package/src/components/ModelConfig.tsx +301 -0
  64. package/src/components/ModelListManager.tsx +227 -0
  65. package/src/components/ModelSelector.tsx +3387 -0
  66. package/src/components/ModelStatusDisplay.tsx +230 -0
  67. package/src/components/Onboarding.tsx +274 -0
  68. package/src/components/PressEnterToContinue.tsx +11 -0
  69. package/src/components/PromptInput.tsx +760 -0
  70. package/src/components/SentryErrorBoundary.ts +39 -0
  71. package/src/components/Spinner.tsx +129 -0
  72. package/src/components/StickerRequestForm.tsx +16 -0
  73. package/src/components/StructuredDiff.tsx +191 -0
  74. package/src/components/TextInput.tsx +259 -0
  75. package/src/components/TodoItem.tsx +47 -0
  76. package/src/components/TokenWarning.tsx +31 -0
  77. package/src/components/ToolUseLoader.tsx +40 -0
  78. package/src/components/TrustDialog.tsx +106 -0
  79. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  80. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  81. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  82. package/src/components/binary-feedback/utils.ts +220 -0
  83. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  84. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +49 -0
  85. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  86. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  87. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  88. package/src/components/messages/AssistantToolUseMessage.tsx +133 -0
  89. package/src/components/messages/TaskProgressMessage.tsx +32 -0
  90. package/src/components/messages/TaskToolMessage.tsx +58 -0
  91. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  92. package/src/components/messages/UserCommandMessage.tsx +30 -0
  93. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  94. package/src/components/messages/UserPromptMessage.tsx +35 -0
  95. package/src/components/messages/UserTextMessage.tsx +39 -0
  96. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  97. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  98. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  99. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  100. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  101. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  102. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  103. package/src/components/permissions/FallbackPermissionRequest.tsx +153 -0
  104. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  105. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +77 -0
  106. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  107. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +83 -0
  108. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +240 -0
  109. package/src/components/permissions/PermissionRequest.tsx +101 -0
  110. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  111. package/src/components/permissions/hooks.ts +44 -0
  112. package/src/components/permissions/toolUseOptions.ts +59 -0
  113. package/src/components/permissions/utils.ts +23 -0
  114. package/src/constants/betas.ts +5 -0
  115. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  116. package/src/constants/figures.ts +4 -0
  117. package/src/constants/keys.ts +3 -0
  118. package/src/constants/macros.ts +8 -0
  119. package/src/constants/modelCapabilities.ts +179 -0
  120. package/src/constants/models.ts +1025 -0
  121. package/src/constants/oauth.ts +18 -0
  122. package/src/constants/product.ts +17 -0
  123. package/src/constants/prompts.ts +168 -0
  124. package/src/constants/releaseNotes.ts +7 -0
  125. package/src/context/PermissionContext.tsx +149 -0
  126. package/src/context.ts +278 -0
  127. package/src/cost-tracker.ts +84 -0
  128. package/src/entrypoints/cli.tsx +1542 -0
  129. package/src/entrypoints/mcp.ts +176 -0
  130. package/src/history.ts +25 -0
  131. package/src/hooks/useApiKeyVerification.ts +59 -0
  132. package/src/hooks/useArrowKeyHistory.ts +55 -0
  133. package/src/hooks/useCanUseTool.ts +138 -0
  134. package/src/hooks/useCancelRequest.ts +39 -0
  135. package/src/hooks/useDoublePress.ts +42 -0
  136. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  137. package/src/hooks/useInterval.ts +25 -0
  138. package/src/hooks/useLogMessages.ts +16 -0
  139. package/src/hooks/useLogStartupTime.ts +12 -0
  140. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  141. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  142. package/src/hooks/useTerminalSize.ts +49 -0
  143. package/src/hooks/useTextInput.ts +318 -0
  144. package/src/hooks/useUnifiedCompletion.ts +1405 -0
  145. package/src/messages.ts +38 -0
  146. package/src/permissions.ts +268 -0
  147. package/src/query.ts +715 -0
  148. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  149. package/src/screens/Doctor.tsx +219 -0
  150. package/src/screens/LogList.tsx +68 -0
  151. package/src/screens/REPL.tsx +809 -0
  152. package/src/screens/ResumeConversation.tsx +68 -0
  153. package/src/services/adapters/base.ts +38 -0
  154. package/src/services/adapters/chatCompletions.ts +90 -0
  155. package/src/services/adapters/responsesAPI.ts +170 -0
  156. package/src/services/browserMocks.ts +66 -0
  157. package/src/services/claude.ts +2197 -0
  158. package/src/services/customCommands.ts +704 -0
  159. package/src/services/fileFreshness.ts +377 -0
  160. package/src/services/gpt5ConnectionTest.ts +340 -0
  161. package/src/services/mcpClient.ts +564 -0
  162. package/src/services/mcpServerApproval.tsx +50 -0
  163. package/src/services/mentionProcessor.ts +273 -0
  164. package/src/services/modelAdapterFactory.ts +69 -0
  165. package/src/services/notifier.ts +40 -0
  166. package/src/services/oauth.ts +357 -0
  167. package/src/services/openai.ts +1338 -0
  168. package/src/services/responseStateManager.ts +90 -0
  169. package/src/services/sentry.ts +3 -0
  170. package/src/services/statsig.ts +172 -0
  171. package/src/services/statsigStorage.ts +86 -0
  172. package/src/services/systemReminder.ts +507 -0
  173. package/src/services/vcr.ts +161 -0
  174. package/src/test/testAdapters.ts +96 -0
  175. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  176. package/src/tools/ArchitectTool/prompt.ts +15 -0
  177. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +569 -0
  178. package/src/tools/BashTool/BashTool.tsx +243 -0
  179. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  180. package/src/tools/BashTool/OutputLine.tsx +49 -0
  181. package/src/tools/BashTool/prompt.ts +174 -0
  182. package/src/tools/BashTool/utils.ts +56 -0
  183. package/src/tools/FileEditTool/FileEditTool.tsx +315 -0
  184. package/src/tools/FileEditTool/prompt.ts +51 -0
  185. package/src/tools/FileEditTool/utils.ts +58 -0
  186. package/src/tools/FileReadTool/FileReadTool.tsx +404 -0
  187. package/src/tools/FileReadTool/prompt.ts +7 -0
  188. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  189. package/src/tools/FileWriteTool/prompt.ts +10 -0
  190. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  191. package/src/tools/GlobTool/prompt.ts +8 -0
  192. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  193. package/src/tools/GrepTool/prompt.ts +11 -0
  194. package/src/tools/MCPTool/MCPTool.tsx +107 -0
  195. package/src/tools/MCPTool/prompt.ts +3 -0
  196. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  197. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  198. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  199. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  200. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  201. package/src/tools/MultiEditTool/prompt.ts +45 -0
  202. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  203. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  204. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +258 -0
  205. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  206. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  207. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  208. package/src/tools/TaskTool/TaskTool.tsx +466 -0
  209. package/src/tools/TaskTool/constants.ts +1 -0
  210. package/src/tools/TaskTool/prompt.ts +92 -0
  211. package/src/tools/ThinkTool/ThinkTool.tsx +54 -0
  212. package/src/tools/ThinkTool/prompt.ts +12 -0
  213. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +313 -0
  214. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  215. package/src/tools/URLFetcherTool/URLFetcherTool.tsx +178 -0
  216. package/src/tools/URLFetcherTool/cache.ts +55 -0
  217. package/src/tools/URLFetcherTool/htmlToMarkdown.ts +55 -0
  218. package/src/tools/URLFetcherTool/prompt.ts +17 -0
  219. package/src/tools/WebSearchTool/WebSearchTool.tsx +103 -0
  220. package/src/tools/WebSearchTool/prompt.ts +13 -0
  221. package/src/tools/WebSearchTool/searchProviders.ts +66 -0
  222. package/src/tools/lsTool/lsTool.tsx +272 -0
  223. package/src/tools/lsTool/prompt.ts +2 -0
  224. package/src/tools.ts +67 -0
  225. package/src/types/PermissionMode.ts +120 -0
  226. package/src/types/RequestContext.ts +72 -0
  227. package/src/types/conversation.ts +51 -0
  228. package/src/types/logs.ts +58 -0
  229. package/src/types/modelCapabilities.ts +64 -0
  230. package/src/types/notebook.ts +87 -0
  231. package/src/utils/Cursor.ts +436 -0
  232. package/src/utils/PersistentShell.ts +552 -0
  233. package/src/utils/advancedFuzzyMatcher.ts +290 -0
  234. package/src/utils/agentLoader.ts +278 -0
  235. package/src/utils/agentStorage.ts +97 -0
  236. package/src/utils/array.ts +3 -0
  237. package/src/utils/ask.tsx +99 -0
  238. package/src/utils/auth.ts +13 -0
  239. package/src/utils/autoCompactCore.ts +223 -0
  240. package/src/utils/autoUpdater.ts +458 -0
  241. package/src/utils/betas.ts +20 -0
  242. package/src/utils/browser.ts +14 -0
  243. package/src/utils/cleanup.ts +72 -0
  244. package/src/utils/commands.ts +261 -0
  245. package/src/utils/commonUnixCommands.ts +161 -0
  246. package/src/utils/config.ts +945 -0
  247. package/src/utils/conversationRecovery.ts +55 -0
  248. package/src/utils/debugLogger.ts +1235 -0
  249. package/src/utils/diff.ts +42 -0
  250. package/src/utils/env.ts +57 -0
  251. package/src/utils/errors.ts +21 -0
  252. package/src/utils/exampleCommands.ts +109 -0
  253. package/src/utils/execFileNoThrow.ts +51 -0
  254. package/src/utils/expertChatStorage.ts +136 -0
  255. package/src/utils/file.ts +405 -0
  256. package/src/utils/fileRecoveryCore.ts +71 -0
  257. package/src/utils/format.tsx +44 -0
  258. package/src/utils/fuzzyMatcher.ts +328 -0
  259. package/src/utils/generators.ts +62 -0
  260. package/src/utils/git.ts +92 -0
  261. package/src/utils/globalLogger.ts +77 -0
  262. package/src/utils/http.ts +10 -0
  263. package/src/utils/imagePaste.ts +38 -0
  264. package/src/utils/json.ts +13 -0
  265. package/src/utils/log.ts +382 -0
  266. package/src/utils/markdown.ts +213 -0
  267. package/src/utils/messageContextManager.ts +289 -0
  268. package/src/utils/messages.tsx +939 -0
  269. package/src/utils/model.ts +914 -0
  270. package/src/utils/permissions/filesystem.ts +127 -0
  271. package/src/utils/responseState.ts +23 -0
  272. package/src/utils/ripgrep.ts +167 -0
  273. package/src/utils/secureFile.ts +564 -0
  274. package/src/utils/sessionState.ts +49 -0
  275. package/src/utils/state.ts +25 -0
  276. package/src/utils/style.ts +29 -0
  277. package/src/utils/terminal.ts +50 -0
  278. package/src/utils/theme.ts +127 -0
  279. package/src/utils/thinking.ts +144 -0
  280. package/src/utils/todoStorage.ts +431 -0
  281. package/src/utils/tokens.ts +43 -0
  282. package/src/utils/toolExecutionController.ts +163 -0
  283. package/src/utils/unaryLogging.ts +26 -0
  284. package/src/utils/user.ts +37 -0
  285. package/src/utils/validate.ts +165 -0
  286. package/yoga.wasm +0 -0
@@ -0,0 +1,1542 @@
1
+ #!/usr/bin/env -S node --no-warnings=ExperimentalWarning --enable-source-maps
2
+ import { initSentry } from '../services/sentry'
3
+ import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
4
+ initSentry() // Initialize Sentry as early as possible
5
+
6
+ // Ensure YOGA_WASM_PATH is set for Ink across run modes (wrapper/dev)
7
+ // Resolve yoga.wasm relative to this file when missing using ESM-friendly APIs
8
+ try {
9
+ if (!process.env.YOGA_WASM_PATH) {
10
+ const { fileURLToPath } = await import('node:url')
11
+ const { dirname, join } = await import('node:path')
12
+ const { existsSync } = await import('node:fs')
13
+ const __filename = fileURLToPath(import.meta.url)
14
+ const __dirname = dirname(__filename)
15
+ const devCandidate = join(__dirname, '../../yoga.wasm')
16
+ // Prefer dev path; wrapper already sets env for normal runs
17
+ process.env.YOGA_WASM_PATH = existsSync(devCandidate)
18
+ ? devCandidate
19
+ : process.env.YOGA_WASM_PATH
20
+ }
21
+ } catch {}
22
+
23
+ // XXX: Without this line (and the Object.keys, even though it seems like it does nothing!),
24
+ // there is a bug in Bun only on Win32 that causes this import to be removed, even though
25
+ // its use is solely because of its side-effects.
26
+ import * as dontcare from '@anthropic-ai/sdk/shims/node'
27
+ Object.keys(dontcare)
28
+
29
+ import React from 'react'
30
+ import { ReadStream } from 'tty'
31
+ import { openSync, existsSync } from 'fs'
32
+ import { render, RenderOptions } from 'ink'
33
+ import { REPL } from '../screens/REPL'
34
+ import { addToHistory } from '../history'
35
+ import { getContext, setContext, removeContext } from '../context'
36
+ import { Command } from '@commander-js/extra-typings'
37
+ import { ask } from '../utils/ask'
38
+ import { hasPermissionsToUseTool } from '../permissions'
39
+ import { getTools } from '../tools'
40
+ import {
41
+ getGlobalConfig,
42
+ getCurrentProjectConfig,
43
+ saveGlobalConfig,
44
+ saveCurrentProjectConfig,
45
+ getCustomApiKeyStatus,
46
+ normalizeApiKeyForConfig,
47
+ setConfigForCLI,
48
+ deleteConfigForCLI,
49
+ getConfigForCLI,
50
+ listConfigForCLI,
51
+ enableConfigs,
52
+ validateAndRepairAllGPT5Profiles,
53
+ } from '../utils/config'
54
+ import { cwd } from 'process'
55
+ import { dateToFilename, logError, parseLogFilename } from '../utils/log'
56
+ import { initDebugLogger } from '../utils/debugLogger'
57
+ import { Onboarding } from '../components/Onboarding'
58
+ import { Doctor } from '../screens/Doctor'
59
+ import { ApproveApiKey } from '../components/ApproveApiKey'
60
+ import { TrustDialog } from '../components/TrustDialog'
61
+ import { checkHasTrustDialogAccepted, McpServerConfig } from '../utils/config'
62
+ import { isDefaultSlowAndCapableModel } from '../utils/model'
63
+ import { LogList } from '../screens/LogList'
64
+ import { ResumeConversation } from '../screens/ResumeConversation'
65
+ import { startMCPServer } from './mcp'
66
+ import { env } from '../utils/env'
67
+ import { getCwd, setCwd, setOriginalCwd } from '../utils/state'
68
+ import { omit } from 'lodash-es'
69
+ import { getCommands } from '../commands'
70
+ import { getNextAvailableLogForkNumber, loadLogList } from '../utils/log'
71
+ import { loadMessagesFromLog } from '../utils/conversationRecovery'
72
+ import { cleanupOldMessageFilesInBackground } from '../utils/cleanup'
73
+ import {
74
+ handleListApprovedTools,
75
+ handleRemoveApprovedTool,
76
+ } from '../commands/approvedTools'
77
+ import {
78
+ addMcpServer,
79
+ getMcpServer,
80
+ listMCPServers,
81
+ parseEnvVars,
82
+ removeMcpServer,
83
+ getClients,
84
+ ensureConfigScope,
85
+ } from '../services/mcpClient'
86
+ import { handleMcprcServerApprovals } from '../services/mcpServerApproval'
87
+ import { checkGate, initializeStatsig, logEvent } from '../services/statsig'
88
+ import { getExampleCommands } from '../utils/exampleCommands'
89
+ import { cursorShow } from 'ansi-escapes'
90
+ import {
91
+ getLatestVersion,
92
+ installGlobalPackage,
93
+ assertMinVersion,
94
+ getUpdateCommandSuggestions,
95
+ } from '../utils/autoUpdater'
96
+ import { gt } from 'semver'
97
+ import { CACHE_PATHS } from '../utils/log'
98
+ // import { checkAndNotifyUpdate } from '../utils/autoUpdater'
99
+ import { PersistentShell } from '../utils/PersistentShell'
100
+ import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
101
+ import { clearTerminal } from '../utils/terminal'
102
+ import { showInvalidConfigDialog } from '../components/InvalidConfigDialog'
103
+ import { ConfigParseError } from '../utils/errors'
104
+ import { grantReadPermissionForOriginalDir } from '../utils/permissions/filesystem'
105
+ import { MACRO } from '../constants/macros'
106
+ export function completeOnboarding(): void {
107
+ const config = getGlobalConfig()
108
+ saveGlobalConfig({
109
+ ...config,
110
+ hasCompletedOnboarding: true,
111
+ lastOnboardingVersion: MACRO.VERSION,
112
+ })
113
+ }
114
+
115
+ async function showSetupScreens(
116
+ safeMode?: boolean,
117
+ print?: boolean,
118
+ ): Promise<void> {
119
+ if (process.env.NODE_ENV === 'test') {
120
+ return
121
+ }
122
+
123
+ const config = getGlobalConfig()
124
+ if (
125
+ !config.theme ||
126
+ !config.hasCompletedOnboarding // always show onboarding at least once
127
+ ) {
128
+ await clearTerminal()
129
+ await new Promise<void>(resolve => {
130
+ render(
131
+ <Onboarding
132
+ onDone={async () => {
133
+ completeOnboarding()
134
+ await clearTerminal()
135
+ resolve()
136
+ }}
137
+ />,
138
+ {
139
+ exitOnCtrlC: false,
140
+ },
141
+ )
142
+ })
143
+ }
144
+
145
+ // // Check for custom API key (only allowed for ants)
146
+ // if (process.env.ANTHROPIC_API_KEY && process.env.USER_TYPE === 'ant') {
147
+ // const customApiKeyTruncated = normalizeApiKeyForConfig(
148
+ // process.env.ANTHROPIC_API_KEY!,
149
+ // )
150
+ // const keyStatus = getCustomApiKeyStatus(customApiKeyTruncated)
151
+ // if (keyStatus === 'new') {
152
+ // await new Promise<void>(resolve => {
153
+ // render(
154
+ // <ApproveApiKey
155
+ // customApiKeyTruncated={customApiKeyTruncated}
156
+ // onDone={async () => {
157
+ // await clearTerminal()
158
+ // resolve()
159
+ // }}
160
+ // />,
161
+ // {
162
+ // exitOnCtrlC: false,
163
+ // },
164
+ // )
165
+ // })
166
+ // }
167
+ // }
168
+
169
+ // In non-interactive mode, only show trust dialog in safe mode
170
+ if (!print && safeMode) {
171
+ if (!checkHasTrustDialogAccepted()) {
172
+ await new Promise<void>(resolve => {
173
+ const onDone = () => {
174
+ // Grant read permission to the current working directory
175
+ grantReadPermissionForOriginalDir()
176
+ resolve()
177
+ }
178
+ render(<TrustDialog onDone={onDone} />, {
179
+ exitOnCtrlC: false,
180
+ })
181
+ })
182
+ }
183
+
184
+ // After trust dialog, check for any mcprc servers that need approval
185
+ if (process.env.USER_TYPE === 'ant') {
186
+ await handleMcprcServerApprovals()
187
+ }
188
+ }
189
+ }
190
+
191
+ function logStartup(): void {
192
+ const config = getGlobalConfig()
193
+ saveGlobalConfig({
194
+ ...config,
195
+ numStartups: (config.numStartups ?? 0) + 1,
196
+ })
197
+ }
198
+
199
+ async function setup(cwd: string, safeMode?: boolean): Promise<void> {
200
+ // Set both current and original working directory if --cwd was provided
201
+ if (cwd !== process.cwd()) {
202
+ setOriginalCwd(cwd)
203
+ }
204
+ await setCwd(cwd)
205
+
206
+ // Always grant read permissions for original working dir
207
+ grantReadPermissionForOriginalDir()
208
+
209
+ // Start watching agent configuration files for changes
210
+ const { startAgentWatcher, clearAgentCache } = await import('../utils/agentLoader')
211
+ await startAgentWatcher(() => {
212
+ // Cache is already cleared in the watcher, just log
213
+ console.log('✅ Agent configurations hot-reloaded')
214
+ })
215
+
216
+ // If --safe mode is enabled, prevent root/sudo usage for security
217
+ if (safeMode) {
218
+ // Check if running as root/sudo on Unix-like systems
219
+ if (
220
+ process.platform !== 'win32' &&
221
+ typeof process.getuid === 'function' &&
222
+ process.getuid() === 0
223
+ ) {
224
+ console.error(
225
+ `--safe mode cannot be used with root/sudo privileges for security reasons`,
226
+ )
227
+ process.exit(1)
228
+ }
229
+ }
230
+
231
+ if (process.env.NODE_ENV === 'test') {
232
+ return
233
+ }
234
+
235
+ cleanupOldMessageFilesInBackground()
236
+ // getExampleCommands() // Pre-fetch example commands
237
+ getContext() // Pre-fetch all context data at once
238
+ // initializeStatsig() // Kick off statsig initialization
239
+
240
+ // Migrate old iterm2KeyBindingInstalled config to new shiftEnterKeyBindingInstalled
241
+ const globalConfig = getGlobalConfig()
242
+ if (
243
+ globalConfig.iterm2KeyBindingInstalled === true &&
244
+ globalConfig.shiftEnterKeyBindingInstalled !== true
245
+ ) {
246
+ const updatedConfig = {
247
+ ...globalConfig,
248
+ shiftEnterKeyBindingInstalled: true,
249
+ }
250
+ // Remove the old config property
251
+ delete updatedConfig.iterm2KeyBindingInstalled
252
+ saveGlobalConfig(updatedConfig)
253
+ }
254
+
255
+ // Check for last session's cost and duration
256
+ const projectConfig = getCurrentProjectConfig()
257
+ if (
258
+ projectConfig.lastCost !== undefined &&
259
+ projectConfig.lastDuration !== undefined
260
+ ) {
261
+ logEvent('tengu_exit', {
262
+ last_session_cost: String(projectConfig.lastCost),
263
+ last_session_api_duration: String(projectConfig.lastAPIDuration),
264
+ last_session_duration: String(projectConfig.lastDuration),
265
+ last_session_id: projectConfig.lastSessionId,
266
+ })
267
+ // Clear the values after logging
268
+ // saveCurrentProjectConfig({
269
+ // ...projectConfig,
270
+ // lastCost: undefined,
271
+ // lastAPIDuration: undefined,
272
+ // lastDuration: undefined,
273
+ // lastSessionId: undefined,
274
+ // })
275
+ }
276
+
277
+ // Check auto-updater permissions
278
+ const autoUpdaterStatus = globalConfig.autoUpdaterStatus ?? 'not_configured'
279
+ if (autoUpdaterStatus === 'not_configured') {
280
+ logEvent('tengu_setup_auto_updater_not_configured', {})
281
+ await new Promise<void>(resolve => {
282
+ render(<Doctor onDone={() => resolve()} />)
283
+ })
284
+ }
285
+ }
286
+
287
+ async function main() {
288
+ // 初始化调试日志系统
289
+ initDebugLogger()
290
+
291
+ // Validate configs are valid and enable configuration system
292
+ try {
293
+ enableConfigs()
294
+
295
+ // 🔧 Validate and auto-repair GPT-5 model profiles
296
+ try {
297
+ const repairResult = validateAndRepairAllGPT5Profiles()
298
+ if (repairResult.repaired > 0) {
299
+ console.log(`🔧 Auto-repaired ${repairResult.repaired} GPT-5 model configurations`)
300
+ }
301
+ } catch (repairError) {
302
+ // Don't block startup if GPT-5 validation fails
303
+ console.warn('⚠️ GPT-5 configuration validation failed:', repairError)
304
+ }
305
+ } catch (error: unknown) {
306
+ if (error instanceof ConfigParseError) {
307
+ // Show the invalid config dialog with the error object
308
+ await showInvalidConfigDialog({ error })
309
+ return // Exit after handling the config error
310
+ }
311
+ }
312
+
313
+ // Disabled background notifier to avoid mid-screen logs during REPL
314
+
315
+ let inputPrompt = ''
316
+ let renderContext: RenderOptions | undefined = {
317
+ exitOnCtrlC: false,
318
+ // @ts-expect-error - onFlicker not in RenderOptions interface
319
+ onFlicker() {
320
+ logEvent('tengu_flicker', {})
321
+ },
322
+ } as any
323
+
324
+ if (
325
+ !process.stdin.isTTY &&
326
+ !process.env.CI &&
327
+ // Input hijacking breaks MCP.
328
+ !process.argv.includes('mcp')
329
+ ) {
330
+ inputPrompt = await stdin()
331
+ if (process.platform !== 'win32') {
332
+ try {
333
+ const ttyFd = openSync('/dev/tty', 'r')
334
+ renderContext = { ...renderContext, stdin: new ReadStream(ttyFd) }
335
+ } catch (err) {
336
+ logError(`Could not open /dev/tty: ${err}`)
337
+ }
338
+ }
339
+ }
340
+ await parseArgs(inputPrompt, renderContext)
341
+ }
342
+
343
+ async function parseArgs(
344
+ stdinContent: string,
345
+ renderContext: RenderOptions | undefined,
346
+ ): Promise<Command> {
347
+ const program = new Command()
348
+
349
+ const renderContextWithExitOnCtrlC = {
350
+ ...renderContext,
351
+ exitOnCtrlC: true,
352
+ }
353
+
354
+ // Get the initial list of commands filtering based on user type
355
+ const commands = await getCommands()
356
+
357
+ // Format command list for help text (using same filter as in help.ts)
358
+ const commandList = commands
359
+ .filter(cmd => !cmd.isHidden)
360
+ .map(cmd => `/${cmd.name} - ${cmd.description}`)
361
+ .join('\n')
362
+
363
+ program
364
+ .name(PRODUCT_COMMAND)
365
+ .description(
366
+ `${PRODUCT_NAME} - starts an interactive session by default, use -p/--print for non-interactive output
367
+
368
+ Slash commands available during an interactive session:
369
+ ${commandList}`,
370
+ )
371
+ .argument('[prompt]', 'Your prompt', String)
372
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
373
+ .option('-d, --debug', 'Enable debug mode', () => true)
374
+ .option(
375
+ '--debug-verbose',
376
+ 'Enable verbose debug terminal output',
377
+ () => true,
378
+ )
379
+ .option(
380
+ '--verbose',
381
+ 'Override verbose mode setting from config',
382
+ () => true,
383
+ )
384
+ .option('-e, --enable-architect', 'Enable the Architect tool', () => true)
385
+ .option(
386
+ '-p, --print',
387
+ 'Print response and exit (useful for pipes)',
388
+ () => true,
389
+ )
390
+ .option(
391
+ '--safe',
392
+ 'Enable strict permission checking mode (default is permissive)',
393
+ () => true,
394
+ )
395
+ .action(
396
+ async (prompt, { cwd, debug, verbose, enableArchitect, print, safe }) => {
397
+ await showSetupScreens(safe, print)
398
+ logEvent('tengu_init', {
399
+ entrypoint: PRODUCT_COMMAND,
400
+ hasInitialPrompt: Boolean(prompt).toString(),
401
+ hasStdin: Boolean(stdinContent).toString(),
402
+ enableArchitect: enableArchitect?.toString() ?? 'false',
403
+ verbose: verbose?.toString() ?? 'false',
404
+ debug: debug?.toString() ?? 'false',
405
+ print: print?.toString() ?? 'false',
406
+ })
407
+ await setup(cwd, safe)
408
+
409
+ assertMinVersion()
410
+
411
+ const [tools, mcpClients] = await Promise.all([
412
+ getTools(
413
+ enableArchitect ?? getCurrentProjectConfig().enableArchitectTool,
414
+ ),
415
+ getClients(),
416
+ ])
417
+ // logStartup()
418
+ const inputPrompt = [prompt, stdinContent].filter(Boolean).join('\n')
419
+ if (print) {
420
+ if (!inputPrompt) {
421
+ console.error(
422
+ 'Error: Input must be provided either through stdin or as a prompt argument when using --print',
423
+ )
424
+ process.exit(1)
425
+ }
426
+
427
+ addToHistory(inputPrompt)
428
+ const { resultText: response } = await ask({
429
+ commands,
430
+ hasPermissionsToUseTool,
431
+ messageLogName: dateToFilename(new Date()),
432
+ prompt: inputPrompt,
433
+ cwd,
434
+ tools,
435
+ safeMode: safe,
436
+ })
437
+ console.log(response)
438
+ process.exit(0)
439
+ } else {
440
+ const isDefaultModel = await isDefaultSlowAndCapableModel()
441
+
442
+ // Prefetch update info before first render to place banner at top
443
+ const updateInfo = await (async () => {
444
+ try {
445
+ const latest = await getLatestVersion()
446
+ if (latest && gt(latest, MACRO.VERSION)) {
447
+ const cmds = await getUpdateCommandSuggestions()
448
+ return { version: latest as string, commands: cmds as string[] }
449
+ }
450
+ } catch {}
451
+ return { version: null as string | null, commands: null as string[] | null }
452
+ })()
453
+
454
+ render(
455
+ <REPL
456
+ commands={commands}
457
+ debug={debug}
458
+ initialPrompt={inputPrompt}
459
+ messageLogName={dateToFilename(new Date())}
460
+ shouldShowPromptInput={true}
461
+ verbose={verbose}
462
+ tools={tools}
463
+ safeMode={safe}
464
+ mcpClients={mcpClients}
465
+ isDefaultModel={isDefaultModel}
466
+ initialUpdateVersion={updateInfo.version}
467
+ initialUpdateCommands={updateInfo.commands}
468
+ />,
469
+ renderContext,
470
+ )
471
+ }
472
+ },
473
+ )
474
+ .version(MACRO.VERSION, '-v, --version')
475
+
476
+ // Enable melon mode for ants if --melon is passed
477
+ // For bun tree shaking to work, this has to be a top level --define, not inside MACRO
478
+ // if (process.env.USER_TYPE === 'ant') {
479
+ // program
480
+ // .option('--melon', 'Enable melon mode')
481
+ // .hook('preAction', async () => {
482
+ // if ((program.opts() as { melon?: boolean }).melon) {
483
+ // const { runMelonWrapper } = await import('../utils/melonWrapper')
484
+ // const melonArgs = process.argv.slice(
485
+ // process.argv.indexOf('--melon') + 1,
486
+ // )
487
+ // const exitCode = runMelonWrapper(melonArgs)
488
+ // process.exit(exitCode)
489
+ // }
490
+ // })
491
+ // }
492
+
493
+ // claude config
494
+ const config = program
495
+ .command('config')
496
+ .description(
497
+ `Manage configuration (eg. ${PRODUCT_COMMAND} config set -g theme dark)`,
498
+ )
499
+
500
+ config
501
+ .command('get <key>')
502
+ .description('Get a config value')
503
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
504
+ .option('-g, --global', 'Use global config')
505
+ .action(async (key, { cwd, global }) => {
506
+ await setup(cwd, false)
507
+ console.log(getConfigForCLI(key, global ?? false))
508
+ process.exit(0)
509
+ })
510
+
511
+ config
512
+ .command('set <key> <value>')
513
+ .description('Set a config value')
514
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
515
+ .option('-g, --global', 'Use global config')
516
+ .action(async (key, value, { cwd, global }) => {
517
+ await setup(cwd, false)
518
+ setConfigForCLI(key, value, global ?? false)
519
+ console.log(`Set ${key} to ${value}`)
520
+ process.exit(0)
521
+ })
522
+
523
+ config
524
+ .command('remove <key>')
525
+ .description('Remove a config value')
526
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
527
+ .option('-g, --global', 'Use global config')
528
+ .action(async (key, { cwd, global }) => {
529
+ await setup(cwd, false)
530
+ deleteConfigForCLI(key, global ?? false)
531
+ console.log(`Removed ${key}`)
532
+ process.exit(0)
533
+ })
534
+
535
+ config
536
+ .command('list')
537
+ .description('List all config values')
538
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
539
+ .option('-g, --global', 'Use global config', false)
540
+ .action(async ({ cwd, global }) => {
541
+ await setup(cwd, false)
542
+ console.log(
543
+ JSON.stringify(listConfigForCLI(global ? (true as const) : (false as const)), null, 2),
544
+ )
545
+ process.exit(0)
546
+ })
547
+
548
+ // claude approved-tools
549
+
550
+ const allowedTools = program
551
+ .command('approved-tools')
552
+ .description('Manage approved tools')
553
+
554
+ allowedTools
555
+ .command('list')
556
+ .description('List all approved tools')
557
+ .action(async () => {
558
+ const result = handleListApprovedTools(getCwd())
559
+ console.log(result)
560
+ process.exit(0)
561
+ })
562
+
563
+ allowedTools
564
+ .command('remove <tool>')
565
+ .description('Remove a tool from the list of approved tools')
566
+ .action(async (tool: string) => {
567
+ const result = handleRemoveApprovedTool(tool)
568
+ logEvent('tengu_approved_tool_remove', {
569
+ tool,
570
+ success: String(result.success),
571
+ })
572
+ console.log(result.message)
573
+ process.exit(result.success ? 0 : 1)
574
+ })
575
+
576
+ // claude mcp
577
+
578
+ const mcp = program
579
+ .command('mcp')
580
+ .description('Configure and manage MCP servers')
581
+
582
+ mcp
583
+ .command('serve')
584
+ .description(`Start the ${PRODUCT_NAME} MCP server`)
585
+ .action(async () => {
586
+ const providedCwd = (program.opts() as { cwd?: string }).cwd ?? cwd()
587
+ logEvent('tengu_mcp_start', { providedCwd })
588
+
589
+ // Verify the directory exists
590
+ if (!existsSync(providedCwd)) {
591
+ console.error(`Error: Directory ${providedCwd} does not exist`)
592
+ process.exit(1)
593
+ }
594
+
595
+ try {
596
+ await setup(providedCwd, false)
597
+ await startMCPServer(providedCwd)
598
+ } catch (error) {
599
+ console.error('Error: Failed to start MCP server:', error)
600
+ process.exit(1)
601
+ }
602
+ })
603
+
604
+ mcp
605
+ .command('add-sse <name> <url>')
606
+ .description('Add an SSE server')
607
+ .option(
608
+ '-s, --scope <scope>',
609
+ 'Configuration scope (project or global)',
610
+ 'project',
611
+ )
612
+ .action(async (name, url, options) => {
613
+ try {
614
+ const scope = ensureConfigScope(options.scope)
615
+ logEvent('tengu_mcp_add', { name, type: 'sse', scope })
616
+
617
+ addMcpServer(name, { type: 'sse', url }, scope)
618
+ console.log(
619
+ `Added SSE MCP server ${name} with URL ${url} to ${scope} config`,
620
+ )
621
+ process.exit(0)
622
+ } catch (error) {
623
+ console.error((error as Error).message)
624
+ process.exit(1)
625
+ }
626
+ })
627
+
628
+ mcp
629
+ .command('add [name] [commandOrUrl] [args...]')
630
+ .description('Add a server (run without arguments for interactive wizard)')
631
+ .option(
632
+ '-s, --scope <scope>',
633
+ 'Configuration scope (project or global)',
634
+ 'project',
635
+ )
636
+ .option(
637
+ '-e, --env <env...>',
638
+ 'Set environment variables (e.g. -e KEY=value)',
639
+ )
640
+ .action(async (name, commandOrUrl, args, options) => {
641
+ try {
642
+ // If name is not provided, start interactive wizard
643
+ if (!name) {
644
+ console.log('Interactive wizard mode: Enter the server details')
645
+ const { createInterface } = await import('readline')
646
+ const rl = createInterface({
647
+ input: process.stdin,
648
+ output: process.stdout,
649
+ })
650
+
651
+ const question = (query: string) =>
652
+ new Promise<string>(resolve => rl.question(query, resolve))
653
+
654
+ // Get server name
655
+ const serverName = await question('Server name: ')
656
+ if (!serverName) {
657
+ console.error('Error: Server name is required')
658
+ rl.close()
659
+ process.exit(1)
660
+ }
661
+
662
+ // Get server type
663
+ const serverType = await question(
664
+ 'Server type (stdio or sse) [stdio]: ',
665
+ )
666
+ const type =
667
+ serverType && ['stdio', 'sse'].includes(serverType)
668
+ ? serverType
669
+ : 'stdio'
670
+
671
+ // Get command or URL
672
+ const prompt = type === 'stdio' ? 'Command: ' : 'URL: '
673
+ const commandOrUrlValue = await question(prompt)
674
+ if (!commandOrUrlValue) {
675
+ console.error(
676
+ `Error: ${type === 'stdio' ? 'Command' : 'URL'} is required`,
677
+ )
678
+ rl.close()
679
+ process.exit(1)
680
+ }
681
+
682
+ // Get args and env if stdio
683
+ let serverArgs: string[] = []
684
+ let serverEnv: Record<string, string> = {}
685
+
686
+ if (type === 'stdio') {
687
+ const argsStr = await question(
688
+ 'Command arguments (space-separated): ',
689
+ )
690
+ serverArgs = argsStr ? argsStr.split(' ').filter(Boolean) : []
691
+
692
+ const envStr = await question(
693
+ 'Environment variables (format: KEY1=value1,KEY2=value2): ',
694
+ )
695
+ if (envStr) {
696
+ const envPairs = envStr.split(',').map(pair => pair.trim())
697
+ serverEnv = parseEnvVars(envPairs.map(pair => pair))
698
+ }
699
+ }
700
+
701
+ // Get scope
702
+ const scopeStr = await question(
703
+ 'Configuration scope (project or global) [project]: ',
704
+ )
705
+ const serverScope = ensureConfigScope(scopeStr || 'project')
706
+
707
+ rl.close()
708
+
709
+ // Add the server
710
+ if (type === 'sse') {
711
+ logEvent('tengu_mcp_add', {
712
+ name: serverName,
713
+ type: 'sse',
714
+ scope: serverScope,
715
+ })
716
+ addMcpServer(
717
+ serverName,
718
+ { type: 'sse', url: commandOrUrlValue },
719
+ serverScope,
720
+ )
721
+ console.log(
722
+ `Added SSE MCP server ${serverName} with URL ${commandOrUrlValue} to ${serverScope} config`,
723
+ )
724
+ } else {
725
+ logEvent('tengu_mcp_add', {
726
+ name: serverName,
727
+ type: 'stdio',
728
+ scope: serverScope,
729
+ })
730
+ addMcpServer(
731
+ serverName,
732
+ {
733
+ type: 'stdio',
734
+ command: commandOrUrlValue,
735
+ args: serverArgs,
736
+ env: serverEnv,
737
+ },
738
+ serverScope,
739
+ )
740
+
741
+ console.log(
742
+ `Added stdio MCP server ${serverName} with command: ${commandOrUrlValue} ${serverArgs.join(' ')} to ${serverScope} config`,
743
+ )
744
+ }
745
+ } else if (name && commandOrUrl) {
746
+ // Regular non-interactive flow
747
+ const scope = ensureConfigScope(options.scope)
748
+
749
+ // Check if it's an SSE URL (starts with http:// or https://)
750
+ if (commandOrUrl.match(/^https?:\/\//)) {
751
+ logEvent('tengu_mcp_add', { name, type: 'sse', scope })
752
+ addMcpServer(name, { type: 'sse', url: commandOrUrl }, scope)
753
+ console.log(
754
+ `Added SSE MCP server ${name} with URL ${commandOrUrl} to ${scope} config`,
755
+ )
756
+ } else {
757
+ logEvent('tengu_mcp_add', { name, type: 'stdio', scope })
758
+ const env = parseEnvVars(options.env)
759
+ addMcpServer(
760
+ name,
761
+ { type: 'stdio', command: commandOrUrl, args: args || [], env },
762
+ scope,
763
+ )
764
+
765
+ console.log(
766
+ `Added stdio MCP server ${name} with command: ${commandOrUrl} ${(args || []).join(' ')} to ${scope} config`,
767
+ )
768
+ }
769
+ } else {
770
+ console.error(
771
+ 'Error: Missing required arguments. Either provide no arguments for interactive mode or specify name and command/URL.',
772
+ )
773
+ process.exit(1)
774
+ }
775
+
776
+ process.exit(0)
777
+ } catch (error) {
778
+ console.error((error as Error).message)
779
+ process.exit(1)
780
+ }
781
+ })
782
+ mcp
783
+ .command('remove <name>')
784
+ .description('Remove an MCP server')
785
+ .option(
786
+ '-s, --scope <scope>',
787
+ 'Configuration scope (project, global, or mcprc)',
788
+ 'project',
789
+ )
790
+ .action(async (name: string, options: { scope?: string }) => {
791
+ try {
792
+ const scope = ensureConfigScope(options.scope)
793
+ logEvent('tengu_mcp_delete', { name, scope })
794
+
795
+ removeMcpServer(name, scope)
796
+ console.log(`Removed MCP server ${name} from ${scope} config`)
797
+ process.exit(0)
798
+ } catch (error) {
799
+ console.error((error as Error).message)
800
+ process.exit(1)
801
+ }
802
+ })
803
+
804
+ mcp
805
+ .command('list')
806
+ .description('List configured MCP servers')
807
+ .action(() => {
808
+ logEvent('tengu_mcp_list', {})
809
+ const servers = listMCPServers()
810
+ if (Object.keys(servers).length === 0) {
811
+ console.log(
812
+ `No MCP servers configured. Use \`${PRODUCT_COMMAND} mcp add\` to add a server.`,
813
+ )
814
+ } else {
815
+ for (const [name, server] of Object.entries(servers)) {
816
+ if (server.type === 'sse') {
817
+ console.log(`${name}: ${server.url} (SSE)`)
818
+ } else {
819
+ console.log(`${name}: ${server.command} ${server.args.join(' ')}`)
820
+ }
821
+ }
822
+ }
823
+ process.exit(0)
824
+ })
825
+
826
+ mcp
827
+ .command('add-json <name> <json>')
828
+ .description('Add an MCP server (stdio or SSE) with a JSON string')
829
+ .option(
830
+ '-s, --scope <scope>',
831
+ 'Configuration scope (project or global)',
832
+ 'project',
833
+ )
834
+ .action(async (name, jsonStr, options) => {
835
+ try {
836
+ const scope = ensureConfigScope(options.scope)
837
+
838
+ // Parse JSON string
839
+ let serverConfig
840
+ try {
841
+ serverConfig = JSON.parse(jsonStr)
842
+ } catch (e) {
843
+ console.error('Error: Invalid JSON string')
844
+ process.exit(1)
845
+ }
846
+
847
+ // Validate the server config
848
+ if (
849
+ !serverConfig.type ||
850
+ !['stdio', 'sse'].includes(serverConfig.type)
851
+ ) {
852
+ console.error('Error: Server type must be "stdio" or "sse"')
853
+ process.exit(1)
854
+ }
855
+
856
+ if (serverConfig.type === 'sse' && !serverConfig.url) {
857
+ console.error('Error: SSE server must have a URL')
858
+ process.exit(1)
859
+ }
860
+
861
+ if (serverConfig.type === 'stdio' && !serverConfig.command) {
862
+ console.error('Error: stdio server must have a command')
863
+ process.exit(1)
864
+ }
865
+
866
+ // Add server with the provided config
867
+ logEvent('tengu_mcp_add_json', { name, type: serverConfig.type, scope })
868
+ addMcpServer(name, serverConfig, scope)
869
+
870
+ if (serverConfig.type === 'sse') {
871
+ console.log(
872
+ `Added SSE MCP server ${name} with URL ${serverConfig.url} to ${scope} config`,
873
+ )
874
+ } else {
875
+ console.log(
876
+ `Added stdio MCP server ${name} with command: ${serverConfig.command} ${(
877
+ serverConfig.args || []
878
+ ).join(' ')} to ${scope} config`,
879
+ )
880
+ }
881
+
882
+ process.exit(0)
883
+ } catch (error) {
884
+ console.error((error as Error).message)
885
+ process.exit(1)
886
+ }
887
+ })
888
+
889
+ mcp
890
+ .command('get <name>')
891
+ .description('Get details about an MCP server')
892
+ .action((name: string) => {
893
+ logEvent('tengu_mcp_get', { name })
894
+ const server = getMcpServer(name)
895
+ if (!server) {
896
+ console.error(`No MCP server found with name: ${name}`)
897
+ process.exit(1)
898
+ }
899
+ console.log(`${name}:`)
900
+ console.log(` Scope: ${server.scope}`)
901
+ if (server.type === 'sse') {
902
+ console.log(` Type: sse`)
903
+ console.log(` URL: ${server.url}`)
904
+ } else {
905
+ console.log(` Type: stdio`)
906
+ console.log(` Command: ${server.command}`)
907
+ console.log(` Args: ${server.args.join(' ')}`)
908
+ if (server.env) {
909
+ console.log(' Environment:')
910
+ for (const [key, value] of Object.entries(server.env)) {
911
+ console.log(` ${key}=${value}`)
912
+ }
913
+ }
914
+ }
915
+ process.exit(0)
916
+ })
917
+
918
+ // Import servers from Claude Desktop
919
+ mcp
920
+ .command('add-from-claude-desktop')
921
+ .description(
922
+ 'Import MCP servers from Claude Desktop (Mac, Windows and WSL)',
923
+ )
924
+ .option(
925
+ '-s, --scope <scope>',
926
+ 'Configuration scope (project or global)',
927
+ 'project',
928
+ )
929
+ .action(async options => {
930
+ try {
931
+ const scope = ensureConfigScope(options.scope)
932
+ const platform = process.platform
933
+
934
+ // Import fs and path modules
935
+ const { existsSync, readFileSync } = await import('fs')
936
+ const { join } = await import('path')
937
+ const { exec } = await import('child_process')
938
+
939
+ // Determine if running in WSL
940
+ const isWSL =
941
+ platform === 'linux' &&
942
+ existsSync('/proc/version') &&
943
+ readFileSync('/proc/version', 'utf-8')
944
+ .toLowerCase()
945
+ .includes('microsoft')
946
+
947
+ if (platform !== 'darwin' && platform !== 'win32' && !isWSL) {
948
+ console.error(
949
+ 'Error: This command is only supported on macOS, Windows, and WSL',
950
+ )
951
+ process.exit(1)
952
+ }
953
+
954
+ // Get Claude Desktop config path
955
+ let configPath
956
+ if (platform === 'darwin') {
957
+ configPath = join(
958
+ process.env.HOME || '~',
959
+ 'Library/Application Support/Claude/claude_desktop_config.json',
960
+ )
961
+ } else if (platform === 'win32') {
962
+ configPath = join(
963
+ process.env.APPDATA || '',
964
+ 'Claude/claude_desktop_config.json',
965
+ )
966
+ } else if (isWSL) {
967
+ // Get Windows username
968
+ const whoamiCommand = await new Promise<string>((resolve, reject) => {
969
+ exec(
970
+ 'powershell.exe -Command "whoami"',
971
+ (err: Error, stdout: string) => {
972
+ if (err) reject(err)
973
+ else resolve(stdout.trim().split('\\').pop() || '')
974
+ },
975
+ )
976
+ })
977
+
978
+ configPath = `/mnt/c/Users/${whoamiCommand}/AppData/Roaming/Claude/claude_desktop_config.json`
979
+ }
980
+
981
+ // Check if config file exists
982
+ if (!existsSync(configPath)) {
983
+ console.error(
984
+ `Error: Claude Desktop config file not found at ${configPath}`,
985
+ )
986
+ process.exit(1)
987
+ }
988
+
989
+ // Read config file
990
+ let config
991
+ try {
992
+ const configContent = readFileSync(configPath, 'utf-8')
993
+ config = JSON.parse(configContent)
994
+ } catch (err) {
995
+ console.error(`Error reading config file: ${err}`)
996
+ process.exit(1)
997
+ }
998
+
999
+ // Extract MCP servers
1000
+ const mcpServers = config.mcpServers || {}
1001
+ const serverNames = Object.keys(mcpServers)
1002
+ const numServers = serverNames.length
1003
+
1004
+ if (numServers === 0) {
1005
+ console.log('No MCP servers found in Claude Desktop config')
1006
+ process.exit(0)
1007
+ }
1008
+
1009
+ // Create server information for display
1010
+ const serversInfo = serverNames.map(name => {
1011
+ const server = mcpServers[name]
1012
+ let description = ''
1013
+
1014
+ if (server.type === 'sse') {
1015
+ description = `SSE: ${server.url}`
1016
+ } else {
1017
+ description = `stdio: ${server.command} ${(server.args || []).join(' ')}`
1018
+ }
1019
+
1020
+ return { name, description, server }
1021
+ })
1022
+
1023
+ // First import all required modules outside the component
1024
+ // Import modules separately to avoid any issues
1025
+ const ink = await import('ink')
1026
+ const reactModule = await import('react')
1027
+ const inkjsui = await import('@inkjs/ui')
1028
+ const utilsTheme = await import('../utils/theme')
1029
+
1030
+ const { render } = ink
1031
+ const React = reactModule // React is already the default export when imported this way
1032
+ const { MultiSelect } = inkjsui
1033
+ const { Box, Text } = ink
1034
+ const { getTheme } = utilsTheme
1035
+
1036
+ // Use Ink to render a nice UI for selection
1037
+ await new Promise<void>(resolve => {
1038
+ // Create a component for the server selection
1039
+ function ClaudeDesktopImport() {
1040
+ const { useState } = reactModule
1041
+ const [isFinished, setIsFinished] = useState(false)
1042
+ const [importResults, setImportResults] = useState<
1043
+ { name: string; success: boolean }[]
1044
+ >([])
1045
+ const [isImporting, setIsImporting] = useState(false)
1046
+ const theme = getTheme()
1047
+
1048
+ // Function to import selected servers
1049
+ const importServers = async (selectedServers: string[]) => {
1050
+ setIsImporting(true)
1051
+ const results = []
1052
+
1053
+ for (const name of selectedServers) {
1054
+ try {
1055
+ const server = mcpServers[name]
1056
+
1057
+ // Check if server already exists
1058
+ const existingServer = getMcpServer(name)
1059
+ if (existingServer) {
1060
+ // Skip duplicates - we'll handle them in the confirmation step
1061
+ continue
1062
+ }
1063
+
1064
+ addMcpServer(name, server as McpServerConfig, scope)
1065
+ results.push({ name, success: true })
1066
+ } catch (err) {
1067
+ results.push({ name, success: false })
1068
+ }
1069
+ }
1070
+
1071
+ setImportResults(results)
1072
+ setIsImporting(false)
1073
+ setIsFinished(true)
1074
+
1075
+ // Give time to show results
1076
+ setTimeout(() => {
1077
+ resolve()
1078
+ }, 1000)
1079
+ }
1080
+
1081
+ // Handle confirmation of selections
1082
+ const handleConfirm = async (selectedServers: string[]) => {
1083
+ // Check for existing servers and confirm overwrite
1084
+ const existingServers = selectedServers.filter(name =>
1085
+ getMcpServer(name),
1086
+ )
1087
+
1088
+ if (existingServers.length > 0) {
1089
+ // We'll just handle it directly since we have a simple UI
1090
+ const results = []
1091
+
1092
+ // Process non-existing servers first
1093
+ const newServers = selectedServers.filter(
1094
+ name => !getMcpServer(name),
1095
+ )
1096
+ for (const name of newServers) {
1097
+ try {
1098
+ const server = mcpServers[name]
1099
+ addMcpServer(name, server as McpServerConfig, scope)
1100
+ results.push({ name, success: true })
1101
+ } catch (err) {
1102
+ results.push({ name, success: false })
1103
+ }
1104
+ }
1105
+
1106
+ // Now handle existing servers by prompting for each one
1107
+ for (const name of existingServers) {
1108
+ try {
1109
+ const server = mcpServers[name]
1110
+ // Overwrite existing server - in a real interactive UI you'd prompt here
1111
+ addMcpServer(name, server as McpServerConfig, scope)
1112
+ results.push({ name, success: true })
1113
+ } catch (err) {
1114
+ results.push({ name, success: false })
1115
+ }
1116
+ }
1117
+
1118
+ setImportResults(results)
1119
+ setIsImporting(false)
1120
+ setIsFinished(true)
1121
+
1122
+ // Give time to show results before resolving
1123
+ setTimeout(() => {
1124
+ resolve()
1125
+ }, 1000)
1126
+ } else {
1127
+ // No existing servers, proceed with import
1128
+ await importServers(selectedServers)
1129
+ }
1130
+ }
1131
+
1132
+ return (
1133
+ <Box flexDirection="column" padding={1}>
1134
+ <Box
1135
+ flexDirection="column"
1136
+ borderStyle="round"
1137
+ borderColor={theme.kode}
1138
+ padding={1}
1139
+ width={'100%'}
1140
+ >
1141
+ <Text bold color={theme.kode}>
1142
+ Import MCP Servers from Claude Desktop
1143
+ </Text>
1144
+
1145
+ <Box marginY={1}>
1146
+ <Text>
1147
+ Found {numServers} MCP servers in Claude Desktop.
1148
+ </Text>
1149
+ </Box>
1150
+
1151
+ <Text>Please select the servers you want to import:</Text>
1152
+
1153
+ <Box marginTop={1}>
1154
+ <MultiSelect
1155
+ options={serverNames.map(name => ({
1156
+ label: name,
1157
+ value: name,
1158
+ }))}
1159
+ defaultValue={serverNames}
1160
+ onSubmit={handleConfirm}
1161
+ />
1162
+ </Box>
1163
+ </Box>
1164
+
1165
+ <Box marginTop={0} marginLeft={3}>
1166
+ <Text dimColor>
1167
+ Space to select · Enter to confirm · Esc to cancel
1168
+ </Text>
1169
+ </Box>
1170
+
1171
+ {isFinished && (
1172
+ <Box marginTop={1}>
1173
+ <Text color={theme.success}>
1174
+ Successfully imported{' '}
1175
+ {importResults.filter(r => r.success).length} MCP server
1176
+ to local config.
1177
+ </Text>
1178
+ </Box>
1179
+ )}
1180
+ </Box>
1181
+ )
1182
+ }
1183
+
1184
+ // Render the component
1185
+ const { unmount } = render(<ClaudeDesktopImport />)
1186
+
1187
+ // Clean up when done
1188
+ setTimeout(() => {
1189
+ unmount()
1190
+ resolve()
1191
+ }, 30000) // Timeout after 30 seconds as a fallback
1192
+ })
1193
+
1194
+ process.exit(0)
1195
+ } catch (error) {
1196
+ console.error(`Error: ${(error as Error).message}`)
1197
+ process.exit(1)
1198
+ }
1199
+ })
1200
+
1201
+ // Function to reset MCP server choices
1202
+ const resetMcpChoices = () => {
1203
+ const config = getCurrentProjectConfig()
1204
+ saveCurrentProjectConfig({
1205
+ ...config,
1206
+ approvedMcprcServers: [],
1207
+ rejectedMcprcServers: [],
1208
+ })
1209
+ console.log('All .mcprc server approvals and rejections have been reset.')
1210
+ console.log(
1211
+ `You will be prompted for approval next time you start ${PRODUCT_NAME}.`,
1212
+ )
1213
+ process.exit(0)
1214
+ }
1215
+
1216
+ // New command name to match Kode
1217
+ mcp
1218
+ .command('reset-project-choices')
1219
+ .description(
1220
+ 'Reset all approved and rejected project-scoped (.mcp.json) servers within this project',
1221
+ )
1222
+ .action(() => {
1223
+ logEvent('tengu_mcp_reset_project_choices', {})
1224
+ resetMcpChoices()
1225
+ })
1226
+
1227
+ // Keep old command for backward compatibility (visible only to ants)
1228
+ if (process.env.USER_TYPE === 'ant') {
1229
+ mcp
1230
+ .command('reset-mcprc-choices')
1231
+ .description(
1232
+ 'Reset all approved and rejected .mcprc servers for this project',
1233
+ )
1234
+ .action(() => {
1235
+ logEvent('tengu_mcp_reset_mcprc_choices', {})
1236
+ resetMcpChoices()
1237
+ })
1238
+ }
1239
+
1240
+ // Doctor command - check installation health
1241
+ program
1242
+ .command('doctor')
1243
+ .description(`Check the health of your ${PRODUCT_NAME} auto-updater`)
1244
+ .action(async () => {
1245
+ logEvent('tengu_doctor_command', {})
1246
+
1247
+ await new Promise<void>(resolve => {
1248
+ render(<Doctor onDone={() => resolve()} doctorMode={true} />)
1249
+ })
1250
+ process.exit(0)
1251
+ })
1252
+
1253
+ // ant-only commands
1254
+
1255
+ // claude update
1256
+ program
1257
+ .command('update')
1258
+ .description('Show manual upgrade commands (no auto-install)')
1259
+ .action(async () => {
1260
+ logEvent('tengu_update_check', {})
1261
+ console.log(`Current version: ${MACRO.VERSION}`)
1262
+ console.log('Checking for updates...')
1263
+
1264
+ const latestVersion = await getLatestVersion()
1265
+
1266
+ if (!latestVersion) {
1267
+ console.error('Failed to check for updates')
1268
+ process.exit(1)
1269
+ }
1270
+
1271
+ if (latestVersion === MACRO.VERSION) {
1272
+ console.log(`${PRODUCT_NAME} is up to date`)
1273
+ process.exit(0)
1274
+ }
1275
+
1276
+ console.log(`New version available: ${latestVersion}`)
1277
+ const { getUpdateCommandSuggestions } = await import('../utils/autoUpdater')
1278
+ const cmds = await getUpdateCommandSuggestions()
1279
+ console.log('\nRun one of the following commands to update:')
1280
+ for (const c of cmds) console.log(` ${c}`)
1281
+ if (process.platform !== 'win32') {
1282
+ console.log('\nNote: you may need to prefix with "sudo" on macOS/Linux.')
1283
+ }
1284
+ process.exit(0)
1285
+ })
1286
+
1287
+ // claude log
1288
+ program
1289
+ .command('log')
1290
+ .description('Manage conversation logs.')
1291
+ .argument(
1292
+ '[number]',
1293
+ 'A number (0, 1, 2, etc.) to display a specific log',
1294
+ parseInt,
1295
+ )
1296
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
1297
+ .action(async (number, { cwd }) => {
1298
+ await setup(cwd, false)
1299
+ logEvent('tengu_view_logs', { number: number?.toString() ?? '' })
1300
+ const context: { unmount?: () => void } = {}
1301
+ const { unmount } = render(
1302
+ <LogList context={context} type="messages" logNumber={number} />,
1303
+ renderContextWithExitOnCtrlC,
1304
+ )
1305
+ context.unmount = unmount
1306
+ })
1307
+
1308
+ // claude resume
1309
+ program
1310
+ .command('resume')
1311
+ .description(
1312
+ 'Resume a previous conversation. Optionally provide a number (0, 1, 2, etc.) or file path to resume a specific conversation.',
1313
+ )
1314
+ .argument(
1315
+ '[identifier]',
1316
+ 'A number (0, 1, 2, etc.) or file path to resume a specific conversation',
1317
+ )
1318
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
1319
+ .option('-e, --enable-architect', 'Enable the Architect tool', () => true)
1320
+ .option('-v, --verbose', 'Do not truncate message output', () => true)
1321
+ .option(
1322
+ '--safe',
1323
+ 'Enable strict permission checking mode (default is permissive)',
1324
+ () => true,
1325
+ )
1326
+ .action(async (identifier, { cwd, enableArchitect, safe, verbose }) => {
1327
+ await setup(cwd, safe)
1328
+ assertMinVersion()
1329
+
1330
+ const [tools, commands, logs, mcpClients] = await Promise.all([
1331
+ getTools(
1332
+ enableArchitect ?? getCurrentProjectConfig().enableArchitectTool,
1333
+ ),
1334
+ getCommands(),
1335
+ loadLogList(CACHE_PATHS.messages()),
1336
+ getClients(),
1337
+ ])
1338
+ // logStartup()
1339
+
1340
+ // If a specific conversation is requested, load and resume it directly
1341
+ if (identifier !== undefined) {
1342
+ // Check if identifier is a number or a file path
1343
+ const number = Math.abs(parseInt(identifier))
1344
+ const isNumber = !isNaN(number)
1345
+ let messages, date, forkNumber
1346
+ try {
1347
+ if (isNumber) {
1348
+ logEvent('tengu_resume', { number: number.toString() })
1349
+ const log = logs[number]
1350
+ if (!log) {
1351
+ console.error('No conversation found at index', number)
1352
+ process.exit(1)
1353
+ }
1354
+ messages = await loadMessagesFromLog(log.fullPath, tools)
1355
+ ;({ date, forkNumber } = log)
1356
+ } else {
1357
+ // Handle file path case
1358
+ logEvent('tengu_resume', { filePath: identifier })
1359
+ if (!existsSync(identifier)) {
1360
+ console.error('File does not exist:', identifier)
1361
+ process.exit(1)
1362
+ }
1363
+ messages = await loadMessagesFromLog(identifier, tools)
1364
+ const pathSegments = identifier.split('/')
1365
+ const filename = pathSegments[pathSegments.length - 1] ?? 'unknown'
1366
+ ;({ date, forkNumber } = parseLogFilename(filename))
1367
+ }
1368
+ const fork = getNextAvailableLogForkNumber(date, forkNumber ?? 1, 0)
1369
+ const isDefaultModel = await isDefaultSlowAndCapableModel()
1370
+ render(
1371
+ <REPL
1372
+ initialPrompt=""
1373
+ messageLogName={date}
1374
+ initialForkNumber={fork}
1375
+ shouldShowPromptInput={true}
1376
+ verbose={verbose}
1377
+ commands={commands}
1378
+ tools={tools}
1379
+ safeMode={safe}
1380
+ initialMessages={messages}
1381
+ mcpClients={mcpClients}
1382
+ isDefaultModel={isDefaultModel}
1383
+ />,
1384
+ { exitOnCtrlC: false },
1385
+ )
1386
+ } catch (error) {
1387
+ logError(`Failed to load conversation: ${error}`)
1388
+ process.exit(1)
1389
+ }
1390
+ } else {
1391
+ // Show the conversation selector UI
1392
+ const context: { unmount?: () => void } = {}
1393
+ const { unmount } = render(
1394
+ <ResumeConversation
1395
+ context={context}
1396
+ commands={commands}
1397
+ logs={logs}
1398
+ tools={tools}
1399
+ verbose={verbose}
1400
+ />,
1401
+ renderContextWithExitOnCtrlC,
1402
+ )
1403
+ context.unmount = unmount
1404
+ }
1405
+ })
1406
+
1407
+ // claude error
1408
+ program
1409
+ .command('error')
1410
+ .description(
1411
+ 'View error logs. Optionally provide a number (0, -1, -2, etc.) to display a specific log.',
1412
+ )
1413
+ .argument(
1414
+ '[number]',
1415
+ 'A number (0, 1, 2, etc.) to display a specific log',
1416
+ parseInt,
1417
+ )
1418
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
1419
+ .action(async (number, { cwd }) => {
1420
+ await setup(cwd, false)
1421
+ logEvent('tengu_view_errors', { number: number?.toString() ?? '' })
1422
+ const context: { unmount?: () => void } = {}
1423
+ const { unmount } = render(
1424
+ <LogList context={context} type="errors" logNumber={number} />,
1425
+ renderContextWithExitOnCtrlC,
1426
+ )
1427
+ context.unmount = unmount
1428
+ })
1429
+
1430
+ // claude context (TODO: deprecate)
1431
+ const context = program
1432
+ .command('context')
1433
+ .description(
1434
+ `Set static context (eg. ${PRODUCT_COMMAND} context add-file ./src/*.py)`,
1435
+ )
1436
+
1437
+ context
1438
+ .command('get <key>')
1439
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
1440
+ .description('Get a value from context')
1441
+ .action(async (key, { cwd }) => {
1442
+ await setup(cwd, false)
1443
+ logEvent('tengu_context_get', { key })
1444
+ const context = omit(
1445
+ await getContext(),
1446
+ 'codeStyle',
1447
+ 'directoryStructure',
1448
+ )
1449
+ console.log(context[key])
1450
+ process.exit(0)
1451
+ })
1452
+
1453
+ context
1454
+ .command('set <key> <value>')
1455
+ .description('Set a value in context')
1456
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
1457
+ .action(async (key, value, { cwd }) => {
1458
+ await setup(cwd, false)
1459
+ logEvent('tengu_context_set', { key })
1460
+ setContext(key, value)
1461
+ console.log(`Set context.${key} to "${value}"`)
1462
+ process.exit(0)
1463
+ })
1464
+
1465
+ context
1466
+ .command('list')
1467
+ .description('List all context values')
1468
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
1469
+ .action(async ({ cwd }) => {
1470
+ await setup(cwd, false)
1471
+ logEvent('tengu_context_list', {})
1472
+ const context = omit(
1473
+ await getContext(),
1474
+ 'codeStyle',
1475
+ 'directoryStructure',
1476
+ 'gitStatus',
1477
+ )
1478
+ console.log(JSON.stringify(context, null, 2))
1479
+ process.exit(0)
1480
+ })
1481
+
1482
+ context
1483
+ .command('remove <key>')
1484
+ .description('Remove a value from context')
1485
+ .option('-c, --cwd <cwd>', 'The current working directory', String, cwd())
1486
+ .action(async (key, { cwd }) => {
1487
+ await setup(cwd, false)
1488
+ logEvent('tengu_context_delete', { key })
1489
+ removeContext(key)
1490
+ console.log(`Removed context.${key}`)
1491
+ process.exit(0)
1492
+ })
1493
+
1494
+ await program.parseAsync(process.argv)
1495
+ return program
1496
+ }
1497
+
1498
+ // TODO: stream?
1499
+ async function stdin() {
1500
+ if (process.stdin.isTTY) {
1501
+ return ''
1502
+ }
1503
+
1504
+ let data = ''
1505
+ for await (const chunk of process.stdin) data += chunk
1506
+ return data
1507
+ }
1508
+
1509
+ process.on('exit', () => {
1510
+ resetCursor()
1511
+ PersistentShell.getInstance().close()
1512
+ })
1513
+
1514
+ function gracefulExit(code = 0) {
1515
+ try { resetCursor() } catch {}
1516
+ try { PersistentShell.getInstance().close() } catch {}
1517
+ process.exit(code)
1518
+ }
1519
+
1520
+ process.on('SIGINT', () => gracefulExit(0))
1521
+ process.on('SIGTERM', () => gracefulExit(0))
1522
+ // Windows CTRL+BREAK
1523
+ process.on('SIGBREAK', () => gracefulExit(0))
1524
+ process.on('unhandledRejection', err => {
1525
+ console.error('Unhandled rejection:', err)
1526
+ gracefulExit(1)
1527
+ })
1528
+ process.on('uncaughtException', err => {
1529
+ console.error('Uncaught exception:', err)
1530
+ gracefulExit(1)
1531
+ })
1532
+
1533
+ function resetCursor() {
1534
+ const terminal = process.stderr.isTTY
1535
+ ? process.stderr
1536
+ : process.stdout.isTTY
1537
+ ? process.stdout
1538
+ : undefined
1539
+ terminal?.write(`\u001B[?25h${cursorShow}`)
1540
+ }
1541
+
1542
+ main()