@shareai-lab/kode 1.0.70 → 1.0.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +342 -75
  2. package/README.zh-CN.md +292 -0
  3. package/cli.js +62 -0
  4. package/package.json +49 -25
  5. package/scripts/postinstall.js +56 -0
  6. package/src/ProjectOnboarding.tsx +198 -0
  7. package/src/Tool.ts +82 -0
  8. package/src/commands/agents.tsx +3401 -0
  9. package/src/commands/approvedTools.ts +53 -0
  10. package/src/commands/bug.tsx +20 -0
  11. package/src/commands/clear.ts +43 -0
  12. package/src/commands/compact.ts +120 -0
  13. package/src/commands/config.tsx +19 -0
  14. package/src/commands/cost.ts +18 -0
  15. package/src/commands/ctx_viz.ts +209 -0
  16. package/src/commands/doctor.ts +24 -0
  17. package/src/commands/help.tsx +19 -0
  18. package/src/commands/init.ts +37 -0
  19. package/src/commands/listen.ts +42 -0
  20. package/src/commands/login.tsx +51 -0
  21. package/src/commands/logout.tsx +40 -0
  22. package/src/commands/mcp.ts +41 -0
  23. package/src/commands/model.tsx +40 -0
  24. package/src/commands/modelstatus.tsx +20 -0
  25. package/src/commands/onboarding.tsx +34 -0
  26. package/src/commands/pr_comments.ts +59 -0
  27. package/src/commands/refreshCommands.ts +54 -0
  28. package/src/commands/release-notes.ts +34 -0
  29. package/src/commands/resume.tsx +31 -0
  30. package/src/commands/review.ts +49 -0
  31. package/src/commands/terminalSetup.ts +221 -0
  32. package/src/commands.ts +139 -0
  33. package/src/components/ApproveApiKey.tsx +93 -0
  34. package/src/components/AsciiLogo.tsx +13 -0
  35. package/src/components/AutoUpdater.tsx +148 -0
  36. package/src/components/Bug.tsx +367 -0
  37. package/src/components/Config.tsx +293 -0
  38. package/src/components/ConsoleOAuthFlow.tsx +327 -0
  39. package/src/components/Cost.tsx +23 -0
  40. package/src/components/CostThresholdDialog.tsx +46 -0
  41. package/src/components/CustomSelect/option-map.ts +42 -0
  42. package/src/components/CustomSelect/select-option.tsx +78 -0
  43. package/src/components/CustomSelect/select.tsx +152 -0
  44. package/src/components/CustomSelect/theme.ts +45 -0
  45. package/src/components/CustomSelect/use-select-state.ts +414 -0
  46. package/src/components/CustomSelect/use-select.ts +35 -0
  47. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  48. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  49. package/src/components/Help.tsx +215 -0
  50. package/src/components/HighlightedCode.tsx +33 -0
  51. package/src/components/InvalidConfigDialog.tsx +113 -0
  52. package/src/components/Link.tsx +32 -0
  53. package/src/components/LogSelector.tsx +86 -0
  54. package/src/components/Logo.tsx +145 -0
  55. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  56. package/src/components/MCPServerDialogCopy.tsx +25 -0
  57. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  58. package/src/components/Message.tsx +221 -0
  59. package/src/components/MessageResponse.tsx +15 -0
  60. package/src/components/MessageSelector.tsx +211 -0
  61. package/src/components/ModeIndicator.tsx +88 -0
  62. package/src/components/ModelConfig.tsx +301 -0
  63. package/src/components/ModelListManager.tsx +227 -0
  64. package/src/components/ModelSelector.tsx +3386 -0
  65. package/src/components/ModelStatusDisplay.tsx +230 -0
  66. package/src/components/Onboarding.tsx +274 -0
  67. package/src/components/PressEnterToContinue.tsx +11 -0
  68. package/src/components/PromptInput.tsx +740 -0
  69. package/src/components/SentryErrorBoundary.ts +33 -0
  70. package/src/components/Spinner.tsx +129 -0
  71. package/src/components/StickerRequestForm.tsx +16 -0
  72. package/src/components/StructuredDiff.tsx +191 -0
  73. package/src/components/TextInput.tsx +259 -0
  74. package/src/components/TodoItem.tsx +11 -0
  75. package/src/components/TokenWarning.tsx +31 -0
  76. package/src/components/ToolUseLoader.tsx +40 -0
  77. package/src/components/TrustDialog.tsx +106 -0
  78. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  79. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  80. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  81. package/src/components/binary-feedback/utils.ts +220 -0
  82. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  83. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +49 -0
  84. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  85. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  86. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  87. package/src/components/messages/AssistantToolUseMessage.tsx +133 -0
  88. package/src/components/messages/TaskProgressMessage.tsx +32 -0
  89. package/src/components/messages/TaskToolMessage.tsx +58 -0
  90. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  91. package/src/components/messages/UserCommandMessage.tsx +30 -0
  92. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  93. package/src/components/messages/UserPromptMessage.tsx +35 -0
  94. package/src/components/messages/UserTextMessage.tsx +39 -0
  95. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  96. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  97. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  98. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  99. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  100. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  101. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  102. package/src/components/permissions/FallbackPermissionRequest.tsx +153 -0
  103. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  104. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +77 -0
  105. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  106. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +83 -0
  107. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +240 -0
  108. package/src/components/permissions/PermissionRequest.tsx +101 -0
  109. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  110. package/src/components/permissions/hooks.ts +44 -0
  111. package/src/components/permissions/toolUseOptions.ts +59 -0
  112. package/src/components/permissions/utils.ts +23 -0
  113. package/src/constants/betas.ts +5 -0
  114. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  115. package/src/constants/figures.ts +4 -0
  116. package/src/constants/keys.ts +3 -0
  117. package/src/constants/macros.ts +8 -0
  118. package/src/constants/modelCapabilities.ts +179 -0
  119. package/src/constants/models.ts +1025 -0
  120. package/src/constants/oauth.ts +18 -0
  121. package/src/constants/product.ts +17 -0
  122. package/src/constants/prompts.ts +177 -0
  123. package/src/constants/releaseNotes.ts +7 -0
  124. package/src/context/PermissionContext.tsx +149 -0
  125. package/src/context.ts +278 -0
  126. package/src/cost-tracker.ts +84 -0
  127. package/src/entrypoints/cli.tsx +1518 -0
  128. package/src/entrypoints/mcp.ts +176 -0
  129. package/src/history.ts +25 -0
  130. package/src/hooks/useApiKeyVerification.ts +59 -0
  131. package/src/hooks/useArrowKeyHistory.ts +55 -0
  132. package/src/hooks/useCanUseTool.ts +138 -0
  133. package/src/hooks/useCancelRequest.ts +39 -0
  134. package/src/hooks/useDoublePress.ts +42 -0
  135. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  136. package/src/hooks/useInterval.ts +25 -0
  137. package/src/hooks/useLogMessages.ts +16 -0
  138. package/src/hooks/useLogStartupTime.ts +12 -0
  139. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  140. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  141. package/src/hooks/useTerminalSize.ts +49 -0
  142. package/src/hooks/useTextInput.ts +318 -0
  143. package/src/hooks/useUnifiedCompletion.ts +1404 -0
  144. package/src/messages.ts +38 -0
  145. package/src/permissions.ts +268 -0
  146. package/src/query.ts +707 -0
  147. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  148. package/src/screens/Doctor.tsx +219 -0
  149. package/src/screens/LogList.tsx +68 -0
  150. package/src/screens/REPL.tsx +798 -0
  151. package/src/screens/ResumeConversation.tsx +68 -0
  152. package/src/services/adapters/base.ts +38 -0
  153. package/src/services/adapters/chatCompletions.ts +90 -0
  154. package/src/services/adapters/responsesAPI.ts +170 -0
  155. package/src/services/browserMocks.ts +66 -0
  156. package/src/services/claude.ts +2083 -0
  157. package/src/services/customCommands.ts +704 -0
  158. package/src/services/fileFreshness.ts +377 -0
  159. package/src/services/gpt5ConnectionTest.ts +340 -0
  160. package/src/services/mcpClient.ts +564 -0
  161. package/src/services/mcpServerApproval.tsx +50 -0
  162. package/src/services/mentionProcessor.ts +273 -0
  163. package/src/services/modelAdapterFactory.ts +69 -0
  164. package/src/services/notifier.ts +40 -0
  165. package/src/services/oauth.ts +357 -0
  166. package/src/services/openai.ts +1305 -0
  167. package/src/services/responseStateManager.ts +90 -0
  168. package/src/services/sentry.ts +3 -0
  169. package/src/services/statsig.ts +171 -0
  170. package/src/services/statsigStorage.ts +86 -0
  171. package/src/services/systemReminder.ts +507 -0
  172. package/src/services/vcr.ts +161 -0
  173. package/src/test/testAdapters.ts +96 -0
  174. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  175. package/src/tools/ArchitectTool/prompt.ts +15 -0
  176. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +569 -0
  177. package/src/tools/BashTool/BashTool.tsx +243 -0
  178. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  179. package/src/tools/BashTool/OutputLine.tsx +49 -0
  180. package/src/tools/BashTool/prompt.ts +174 -0
  181. package/src/tools/BashTool/utils.ts +56 -0
  182. package/src/tools/FileEditTool/FileEditTool.tsx +315 -0
  183. package/src/tools/FileEditTool/prompt.ts +51 -0
  184. package/src/tools/FileEditTool/utils.ts +58 -0
  185. package/src/tools/FileReadTool/FileReadTool.tsx +404 -0
  186. package/src/tools/FileReadTool/prompt.ts +7 -0
  187. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  188. package/src/tools/FileWriteTool/prompt.ts +10 -0
  189. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  190. package/src/tools/GlobTool/prompt.ts +8 -0
  191. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  192. package/src/tools/GrepTool/prompt.ts +11 -0
  193. package/src/tools/MCPTool/MCPTool.tsx +107 -0
  194. package/src/tools/MCPTool/prompt.ts +3 -0
  195. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  196. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  197. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  198. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  199. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  200. package/src/tools/MultiEditTool/prompt.ts +45 -0
  201. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  202. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  203. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +258 -0
  204. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  205. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  206. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  207. package/src/tools/TaskTool/TaskTool.tsx +466 -0
  208. package/src/tools/TaskTool/constants.ts +1 -0
  209. package/src/tools/TaskTool/prompt.ts +92 -0
  210. package/src/tools/ThinkTool/ThinkTool.tsx +54 -0
  211. package/src/tools/ThinkTool/prompt.ts +12 -0
  212. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +290 -0
  213. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  214. package/src/tools/lsTool/lsTool.tsx +272 -0
  215. package/src/tools/lsTool/prompt.ts +2 -0
  216. package/src/tools.ts +63 -0
  217. package/src/types/PermissionMode.ts +120 -0
  218. package/src/types/RequestContext.ts +72 -0
  219. package/src/types/conversation.ts +51 -0
  220. package/src/types/logs.ts +58 -0
  221. package/src/types/modelCapabilities.ts +64 -0
  222. package/src/types/notebook.ts +87 -0
  223. package/src/utils/Cursor.ts +436 -0
  224. package/src/utils/PersistentShell.ts +373 -0
  225. package/src/utils/advancedFuzzyMatcher.ts +290 -0
  226. package/src/utils/agentLoader.ts +284 -0
  227. package/src/utils/agentStorage.ts +97 -0
  228. package/src/utils/array.ts +3 -0
  229. package/src/utils/ask.tsx +99 -0
  230. package/src/utils/auth.ts +13 -0
  231. package/src/utils/autoCompactCore.ts +223 -0
  232. package/src/utils/autoUpdater.ts +318 -0
  233. package/src/utils/betas.ts +20 -0
  234. package/src/utils/browser.ts +14 -0
  235. package/src/utils/cleanup.ts +72 -0
  236. package/src/utils/commands.ts +261 -0
  237. package/src/utils/commonUnixCommands.ts +161 -0
  238. package/src/utils/config.ts +942 -0
  239. package/src/utils/conversationRecovery.ts +55 -0
  240. package/src/utils/debugLogger.ts +1123 -0
  241. package/src/utils/diff.ts +42 -0
  242. package/src/utils/env.ts +57 -0
  243. package/src/utils/errors.ts +21 -0
  244. package/src/utils/exampleCommands.ts +109 -0
  245. package/src/utils/execFileNoThrow.ts +51 -0
  246. package/src/utils/expertChatStorage.ts +136 -0
  247. package/src/utils/file.ts +402 -0
  248. package/src/utils/fileRecoveryCore.ts +71 -0
  249. package/src/utils/format.tsx +44 -0
  250. package/src/utils/fuzzyMatcher.ts +328 -0
  251. package/src/utils/generators.ts +62 -0
  252. package/src/utils/git.ts +92 -0
  253. package/src/utils/globalLogger.ts +77 -0
  254. package/src/utils/http.ts +10 -0
  255. package/src/utils/imagePaste.ts +38 -0
  256. package/src/utils/json.ts +13 -0
  257. package/src/utils/log.ts +382 -0
  258. package/src/utils/markdown.ts +213 -0
  259. package/src/utils/messageContextManager.ts +289 -0
  260. package/src/utils/messages.tsx +939 -0
  261. package/src/utils/model.ts +836 -0
  262. package/src/utils/permissions/filesystem.ts +118 -0
  263. package/src/utils/responseState.ts +23 -0
  264. package/src/utils/ripgrep.ts +167 -0
  265. package/src/utils/secureFile.ts +559 -0
  266. package/src/utils/sessionState.ts +49 -0
  267. package/src/utils/state.ts +25 -0
  268. package/src/utils/style.ts +29 -0
  269. package/src/utils/terminal.ts +50 -0
  270. package/src/utils/theme.ts +133 -0
  271. package/src/utils/thinking.ts +144 -0
  272. package/src/utils/todoStorage.ts +431 -0
  273. package/src/utils/tokens.ts +43 -0
  274. package/src/utils/toolExecutionController.ts +163 -0
  275. package/src/utils/unaryLogging.ts +26 -0
  276. package/src/utils/user.ts +37 -0
  277. package/src/utils/validate.ts +165 -0
  278. package/cli.mjs +0 -1803
@@ -0,0 +1,373 @@
1
+ import * as fs from 'fs'
2
+ import { homedir } from 'os'
3
+ import { existsSync } from 'fs'
4
+ import shellquote from 'shell-quote'
5
+ import { spawn, execSync, type ChildProcess } from 'child_process'
6
+ import { isAbsolute, resolve, join } from 'path'
7
+ import { logError } from './log'
8
+ import * as os from 'os'
9
+ import { logEvent } from '../services/statsig'
10
+ import { PRODUCT_COMMAND } from '../constants/product'
11
+
12
+ type ExecResult = {
13
+ stdout: string
14
+ stderr: string
15
+ code: number
16
+ interrupted: boolean
17
+ }
18
+ type QueuedCommand = {
19
+ command: string
20
+ abortSignal?: AbortSignal
21
+ timeout?: number
22
+ resolve: (result: ExecResult) => void
23
+ reject: (error: Error) => void
24
+ }
25
+
26
+ const TEMPFILE_PREFIX = os.tmpdir() + `/${PRODUCT_COMMAND}-`
27
+ const DEFAULT_TIMEOUT = 30 * 60 * 1000
28
+ const SIGTERM_CODE = 143 // Standard exit code for SIGTERM
29
+ const FILE_SUFFIXES = {
30
+ STATUS: '-status',
31
+ STDOUT: '-stdout',
32
+ STDERR: '-stderr',
33
+ CWD: '-cwd',
34
+ }
35
+ const SHELL_CONFIGS: Record<string, string> = {
36
+ '/bin/bash': '.bashrc',
37
+ '/bin/zsh': '.zshrc',
38
+ }
39
+
40
+ export class PersistentShell {
41
+ private commandQueue: QueuedCommand[] = []
42
+ private isExecuting: boolean = false
43
+ private shell: ChildProcess
44
+ private isAlive: boolean = true
45
+ private commandInterrupted: boolean = false
46
+ private statusFile: string
47
+ private stdoutFile: string
48
+ private stderrFile: string
49
+ private cwdFile: string
50
+ private cwd: string
51
+ private binShell: string
52
+
53
+ constructor(cwd: string) {
54
+ this.binShell = process.env.SHELL || '/bin/bash'
55
+ this.shell = spawn(this.binShell, ['-l'], {
56
+ stdio: ['pipe', 'pipe', 'pipe'],
57
+ cwd,
58
+ env: {
59
+ ...process.env,
60
+ GIT_EDITOR: 'true',
61
+ },
62
+ })
63
+
64
+ this.cwd = cwd
65
+
66
+ this.shell.on('exit', (code, signal) => {
67
+ if (code) {
68
+ // TODO: It would be nice to alert the user that shell crashed
69
+ logError(`Shell exited with code ${code} and signal ${signal}`)
70
+ logEvent('persistent_shell_exit', {
71
+ code: code?.toString() || 'null',
72
+ signal: signal || 'null',
73
+ })
74
+ }
75
+ for (const file of [
76
+ this.statusFile,
77
+ this.stdoutFile,
78
+ this.stderrFile,
79
+ this.cwdFile,
80
+ ]) {
81
+ if (fs.existsSync(file)) {
82
+ fs.unlinkSync(file)
83
+ }
84
+ }
85
+ this.isAlive = false
86
+ })
87
+
88
+ const id = Math.floor(Math.random() * 0x10000)
89
+ .toString(16)
90
+ .padStart(4, '0')
91
+
92
+ this.statusFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.STATUS
93
+ this.stdoutFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.STDOUT
94
+ this.stderrFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.STDERR
95
+ this.cwdFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.CWD
96
+ for (const file of [this.statusFile, this.stdoutFile, this.stderrFile]) {
97
+ fs.writeFileSync(file, '')
98
+ }
99
+ // Initialize CWD file with initial directory
100
+ fs.writeFileSync(this.cwdFile, cwd)
101
+ const configFile = SHELL_CONFIGS[this.binShell]
102
+ if (configFile) {
103
+ const configFilePath = join(homedir(), configFile)
104
+ if (existsSync(configFilePath)) {
105
+ this.sendToShell(`source ${configFilePath}`)
106
+ }
107
+ }
108
+ }
109
+
110
+ private static instance: PersistentShell | null = null
111
+
112
+ static restart() {
113
+ if (PersistentShell.instance) {
114
+ PersistentShell.instance.close()
115
+ PersistentShell.instance = null
116
+ }
117
+ }
118
+
119
+ static getInstance(): PersistentShell {
120
+ if (!PersistentShell.instance || !PersistentShell.instance.isAlive) {
121
+ PersistentShell.instance = new PersistentShell(process.cwd())
122
+ }
123
+ return PersistentShell.instance
124
+ }
125
+
126
+ killChildren() {
127
+ const parentPid = this.shell.pid
128
+ try {
129
+ const childPids = execSync(`pgrep -P ${parentPid}`)
130
+ .toString()
131
+ .trim()
132
+ .split('\n')
133
+ .filter(Boolean) // Filter out empty strings
134
+
135
+ if (childPids.length > 0) {
136
+ logEvent('persistent_shell_command_interrupted', {
137
+ numChildProcesses: childPids.length.toString(),
138
+ })
139
+ }
140
+
141
+ childPids.forEach(pid => {
142
+ try {
143
+ process.kill(Number(pid), 'SIGTERM')
144
+ } catch (error) {
145
+ logError(`Failed to kill process ${pid}: ${error}`)
146
+ logEvent('persistent_shell_kill_process_error', {
147
+ error: (error as Error).message.substring(0, 10),
148
+ })
149
+ }
150
+ })
151
+ } catch {
152
+ // pgrep returns non-zero when no processes are found - this is expected
153
+ } finally {
154
+ this.commandInterrupted = true
155
+ }
156
+ }
157
+
158
+ private async processQueue() {
159
+ /**
160
+ * Processes commands from the queue one at a time.
161
+ * Concurrency invariants:
162
+ * - Only one instance runs at a time (controlled by isExecuting)
163
+ * - Is the only caller of updateCwd() in the system
164
+ * - Calls updateCwd() after each command completes
165
+ * - Ensures commands execute serially via the queue
166
+ * - Handles interruption via abortSignal by calling killChildren()
167
+ * - Cleans up abortSignal listeners after command completion or interruption
168
+ */
169
+ if (this.isExecuting || this.commandQueue.length === 0) return
170
+
171
+ this.isExecuting = true
172
+ const { command, abortSignal, timeout, resolve, reject } =
173
+ this.commandQueue.shift()!
174
+
175
+ const killChildren = () => this.killChildren()
176
+ if (abortSignal) {
177
+ abortSignal.addEventListener('abort', killChildren)
178
+ }
179
+
180
+ try {
181
+ const result = await this.exec_(command, timeout)
182
+
183
+ // No need to update cwd - it's handled in exec_ via the CWD file
184
+
185
+ resolve(result)
186
+ } catch (error) {
187
+ logEvent('persistent_shell_command_error', {
188
+ error: (error as Error).message.substring(0, 10),
189
+ })
190
+ reject(error as Error)
191
+ } finally {
192
+ this.isExecuting = false
193
+ if (abortSignal) {
194
+ abortSignal.removeEventListener('abort', killChildren)
195
+ }
196
+ // Process next command in queue
197
+ this.processQueue()
198
+ }
199
+ }
200
+
201
+ async exec(
202
+ command: string,
203
+ abortSignal?: AbortSignal,
204
+ timeout?: number,
205
+ ): Promise<ExecResult> {
206
+ return new Promise((resolve, reject) => {
207
+ this.commandQueue.push({ command, abortSignal, timeout, resolve, reject })
208
+ this.processQueue()
209
+ })
210
+ }
211
+
212
+ private async exec_(command: string, timeout?: number): Promise<ExecResult> {
213
+ /**
214
+ * Direct command execution without going through the queue.
215
+ * Concurrency invariants:
216
+ * - Not safe for concurrent calls (uses shared files)
217
+ * - Called only when queue is idle
218
+ * - Relies on file-based IPC to handle shell interaction
219
+ * - Does not modify the command queue state
220
+ * - Tracks interruption state via commandInterrupted flag
221
+ * - Resets interruption state at start of new command
222
+ * - Reports interruption status in result object
223
+ *
224
+ * Exit Code & CWD Handling:
225
+ * - Executes command and immediately captures its exit code into a shell variable
226
+ * - Updates the CWD file with the working directory after capturing exit code
227
+ * - Writes the preserved exit code to the status file as the final step
228
+ * - This sequence eliminates race conditions between exit code capture and CWD updates
229
+ * - The pwd() method reads the CWD file directly for current directory info
230
+ */
231
+ const quotedCommand = shellquote.quote([command])
232
+
233
+ // Check the syntax of the command
234
+ try {
235
+ execSync(`${this.binShell} -n -c ${quotedCommand}`, {
236
+ stdio: 'ignore',
237
+ timeout: 1000,
238
+ })
239
+ } catch (stderr) {
240
+ // If there's a syntax error, return an error and log it
241
+ const errorStr =
242
+ typeof stderr === 'string' ? stderr : String(stderr || '')
243
+ logEvent('persistent_shell_syntax_error', {
244
+ error: errorStr.substring(0, 10),
245
+ })
246
+ return Promise.resolve({
247
+ stdout: '',
248
+ stderr: errorStr,
249
+ code: 128,
250
+ interrupted: false,
251
+ })
252
+ }
253
+
254
+ const commandTimeout = timeout || DEFAULT_TIMEOUT
255
+ // Reset interrupted state for new command
256
+ this.commandInterrupted = false
257
+ return new Promise<ExecResult>(resolve => {
258
+ // Truncate output files
259
+ fs.writeFileSync(this.stdoutFile, '')
260
+ fs.writeFileSync(this.stderrFile, '')
261
+ fs.writeFileSync(this.statusFile, '')
262
+ // Break up the command sequence for clarity using an array of commands
263
+ const commandParts = []
264
+
265
+ // 1. Execute the main command with redirections
266
+ commandParts.push(
267
+ `eval ${quotedCommand} < /dev/null > ${this.stdoutFile} 2> ${this.stderrFile}`,
268
+ )
269
+
270
+ // 2. Capture exit code immediately after command execution to avoid losing it
271
+ commandParts.push(`EXEC_EXIT_CODE=$?`)
272
+
273
+ // 3. Update CWD file
274
+ commandParts.push(`pwd > ${this.cwdFile}`)
275
+
276
+ // 4. Write the preserved exit code to status file to avoid race with pwd
277
+ commandParts.push(`echo $EXEC_EXIT_CODE > ${this.statusFile}`)
278
+
279
+ // Send the combined commands as a single operation to maintain atomicity
280
+ this.sendToShell(commandParts.join('\n'))
281
+
282
+ // Check for command completion or timeout
283
+ const start = Date.now()
284
+ const checkCompletion = setInterval(() => {
285
+ try {
286
+ let statusFileSize = 0
287
+ if (fs.existsSync(this.statusFile)) {
288
+ statusFileSize = fs.statSync(this.statusFile).size
289
+ }
290
+
291
+ if (
292
+ statusFileSize > 0 ||
293
+ Date.now() - start > commandTimeout ||
294
+ this.commandInterrupted
295
+ ) {
296
+ clearInterval(checkCompletion)
297
+ const stdout = fs.existsSync(this.stdoutFile)
298
+ ? fs.readFileSync(this.stdoutFile, 'utf8')
299
+ : ''
300
+ let stderr = fs.existsSync(this.stderrFile)
301
+ ? fs.readFileSync(this.stderrFile, 'utf8')
302
+ : ''
303
+ let code: number
304
+ if (statusFileSize) {
305
+ code = Number(fs.readFileSync(this.statusFile, 'utf8'))
306
+ } else {
307
+ // Timeout occurred - kill any running processes
308
+ this.killChildren()
309
+ code = SIGTERM_CODE
310
+ stderr += (stderr ? '\n' : '') + 'Command execution timed out'
311
+ logEvent('persistent_shell_command_timeout', {
312
+ command: command.substring(0, 10),
313
+ timeout: commandTimeout.toString(),
314
+ })
315
+ }
316
+ resolve({
317
+ stdout,
318
+ stderr,
319
+ code,
320
+ interrupted: this.commandInterrupted,
321
+ })
322
+ }
323
+ } catch {
324
+ // Ignore file system errors during polling - they are expected
325
+ // as we check for completion before files exist
326
+ }
327
+ }, 10) // increasing this will introduce latency
328
+ })
329
+ }
330
+
331
+ private sendToShell(command: string) {
332
+ try {
333
+ this.shell!.stdin!.write(command + '\n')
334
+ } catch (error) {
335
+ const errorString =
336
+ error instanceof Error
337
+ ? error.message
338
+ : String(error || 'Unknown error')
339
+ logError(`Error in sendToShell: ${errorString}`)
340
+ logEvent('persistent_shell_write_error', {
341
+ error: errorString.substring(0, 100),
342
+ command: command.substring(0, 30),
343
+ })
344
+ throw error
345
+ }
346
+ }
347
+
348
+ pwd(): string {
349
+ try {
350
+ const newCwd = fs.readFileSync(this.cwdFile, 'utf8').trim()
351
+ if (newCwd) {
352
+ this.cwd = newCwd
353
+ }
354
+ } catch (error) {
355
+ logError(`Shell pwd error ${error}`)
356
+ }
357
+ // Always return the cached value
358
+ return this.cwd
359
+ }
360
+
361
+ async setCwd(cwd: string) {
362
+ const resolved = isAbsolute(cwd) ? cwd : resolve(process.cwd(), cwd)
363
+ if (!existsSync(resolved)) {
364
+ throw new Error(`Path "${resolved}" does not exist`)
365
+ }
366
+ await this.exec(`cd ${resolved}`)
367
+ }
368
+
369
+ close(): void {
370
+ this.shell!.stdin!.end()
371
+ this.shell.kill()
372
+ }
373
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Advanced Fuzzy Matching Algorithm
3
+ *
4
+ * Inspired by:
5
+ * - Chinese Pinyin input methods (Sogou, Baidu)
6
+ * - IDE intelligent completion (VSCode, IntelliJ)
7
+ * - Terminal fuzzy finders (fzf, peco)
8
+ *
9
+ * Key features:
10
+ * - Hyphen-aware matching (dao → dao-qi-harmony)
11
+ * - Numeric suffix matching (py3 → python3)
12
+ * - Abbreviation matching (dq → dao-qi)
13
+ * - Subsequence matching
14
+ * - Word boundary bonus
15
+ */
16
+
17
+ export interface MatchResult {
18
+ score: number
19
+ matched: boolean
20
+ algorithm: string
21
+ }
22
+
23
+ export class AdvancedFuzzyMatcher {
24
+ /**
25
+ * Main matching function - combines multiple algorithms
26
+ */
27
+ match(candidate: string, query: string): MatchResult {
28
+ // Normalize inputs
29
+ const text = candidate.toLowerCase()
30
+ const pattern = query.toLowerCase()
31
+
32
+ // Quick exact match - give HUGE score for exact matches
33
+ if (text === pattern) {
34
+ return { score: 10000, matched: true, algorithm: 'exact' }
35
+ }
36
+
37
+ // Try all algorithms and combine scores
38
+ const algorithms = [
39
+ this.exactPrefixMatch(text, pattern),
40
+ this.hyphenAwareMatch(text, pattern),
41
+ this.wordBoundaryMatch(text, pattern),
42
+ this.abbreviationMatch(text, pattern),
43
+ this.numericSuffixMatch(text, pattern),
44
+ this.subsequenceMatch(text, pattern),
45
+ this.fuzzySegmentMatch(text, pattern),
46
+ ]
47
+
48
+ // Get best score
49
+ let bestScore = 0
50
+ let bestAlgorithm = 'none'
51
+
52
+ for (const result of algorithms) {
53
+ if (result.score > bestScore) {
54
+ bestScore = result.score
55
+ bestAlgorithm = result.algorithm
56
+ }
57
+ }
58
+
59
+ return {
60
+ score: bestScore,
61
+ matched: bestScore > 10,
62
+ algorithm: bestAlgorithm
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Exact prefix matching
68
+ */
69
+ private exactPrefixMatch(text: string, pattern: string): { score: number; algorithm: string } {
70
+ if (text.startsWith(pattern)) {
71
+ const coverage = pattern.length / text.length
72
+ // Higher base score for prefix matches to prioritize them
73
+ return { score: 1000 + coverage * 500, algorithm: 'prefix' }
74
+ }
75
+ return { score: 0, algorithm: 'prefix' }
76
+ }
77
+
78
+ /**
79
+ * Hyphen-aware matching (dao → dao-qi-harmony-designer)
80
+ * Treats hyphens as optional word boundaries
81
+ */
82
+ private hyphenAwareMatch(text: string, pattern: string): { score: number; algorithm: string } {
83
+ // Split by hyphens and try to match
84
+ const words = text.split('-')
85
+
86
+ // Check if pattern matches the beginning of hyphenated words
87
+ if (words[0].startsWith(pattern)) {
88
+ const coverage = pattern.length / words[0].length
89
+ return { score: 300 + coverage * 100, algorithm: 'hyphen-prefix' }
90
+ }
91
+
92
+ // Check if pattern matches concatenated words (ignoring hyphens)
93
+ const concatenated = words.join('')
94
+ if (concatenated.startsWith(pattern)) {
95
+ const coverage = pattern.length / concatenated.length
96
+ return { score: 250 + coverage * 100, algorithm: 'hyphen-concat' }
97
+ }
98
+
99
+ // Check if pattern matches any word start
100
+ for (let i = 0; i < words.length; i++) {
101
+ if (words[i].startsWith(pattern)) {
102
+ return { score: 200 - i * 10, algorithm: 'hyphen-word' }
103
+ }
104
+ }
105
+
106
+ return { score: 0, algorithm: 'hyphen' }
107
+ }
108
+
109
+ /**
110
+ * Word boundary matching (dq → dao-qi)
111
+ * Matches characters at word boundaries
112
+ */
113
+ private wordBoundaryMatch(text: string, pattern: string): { score: number; algorithm: string } {
114
+ const words = text.split(/[-_\s]+/)
115
+ let patternIdx = 0
116
+ let score = 0
117
+ let matched = false
118
+
119
+ for (const word of words) {
120
+ if (patternIdx >= pattern.length) break
121
+
122
+ if (word[0] === pattern[patternIdx]) {
123
+ score += 50 // Bonus for word boundary match
124
+ patternIdx++
125
+ matched = true
126
+
127
+ // Try to match more characters in this word
128
+ for (let i = 1; i < word.length && patternIdx < pattern.length; i++) {
129
+ if (word[i] === pattern[patternIdx]) {
130
+ score += 20
131
+ patternIdx++
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ if (matched && patternIdx === pattern.length) {
138
+ return { score, algorithm: 'word-boundary' }
139
+ }
140
+
141
+ return { score: 0, algorithm: 'word-boundary' }
142
+ }
143
+
144
+ /**
145
+ * Abbreviation matching (nde → node, daoqi → dao-qi)
146
+ */
147
+ private abbreviationMatch(text: string, pattern: string): { score: number; algorithm: string } {
148
+ let textIdx = 0
149
+ let patternIdx = 0
150
+ let score = 0
151
+ let lastMatchIdx = -1
152
+
153
+ while (patternIdx < pattern.length && textIdx < text.length) {
154
+ if (text[textIdx] === pattern[patternIdx]) {
155
+ // Calculate position score
156
+ const gap = lastMatchIdx === -1 ? 0 : textIdx - lastMatchIdx - 1
157
+
158
+ if (textIdx === 0) {
159
+ score += 50 // First character match
160
+ } else if (lastMatchIdx >= 0 && gap === 0) {
161
+ score += 30 // Consecutive match
162
+ } else if (text[textIdx - 1] === '-' || text[textIdx - 1] === '_') {
163
+ score += 40 // Word boundary match
164
+ } else {
165
+ score += Math.max(5, 20 - gap * 2) // Distance penalty
166
+ }
167
+
168
+ lastMatchIdx = textIdx
169
+ patternIdx++
170
+ }
171
+ textIdx++
172
+ }
173
+
174
+ if (patternIdx === pattern.length) {
175
+ // Bonus for compact matches
176
+ const spread = lastMatchIdx / pattern.length
177
+ if (spread <= 3) score += 50
178
+ else if (spread <= 5) score += 30
179
+
180
+ return { score, algorithm: 'abbreviation' }
181
+ }
182
+
183
+ return { score: 0, algorithm: 'abbreviation' }
184
+ }
185
+
186
+ /**
187
+ * Numeric suffix matching (py3 → python3, np18 → node18)
188
+ */
189
+ private numericSuffixMatch(text: string, pattern: string): { score: number; algorithm: string } {
190
+ // Check if pattern has numeric suffix
191
+ const patternMatch = pattern.match(/^(.+?)(\d+)$/)
192
+ if (!patternMatch) return { score: 0, algorithm: 'numeric' }
193
+
194
+ const [, prefix, suffix] = patternMatch
195
+
196
+ // Check if text ends with same number
197
+ if (!text.endsWith(suffix)) return { score: 0, algorithm: 'numeric' }
198
+
199
+ // Check if prefix matches start of text
200
+ const textWithoutSuffix = text.slice(0, -suffix.length)
201
+ if (textWithoutSuffix.startsWith(prefix)) {
202
+ const coverage = prefix.length / textWithoutSuffix.length
203
+ return { score: 200 + coverage * 100, algorithm: 'numeric-suffix' }
204
+ }
205
+
206
+ // Check abbreviation match for prefix
207
+ const abbrevResult = this.abbreviationMatch(textWithoutSuffix, prefix)
208
+ if (abbrevResult.score > 0) {
209
+ return { score: abbrevResult.score + 50, algorithm: 'numeric-abbrev' }
210
+ }
211
+
212
+ return { score: 0, algorithm: 'numeric' }
213
+ }
214
+
215
+ /**
216
+ * Subsequence matching - characters appear in order
217
+ */
218
+ private subsequenceMatch(text: string, pattern: string): { score: number; algorithm: string } {
219
+ let textIdx = 0
220
+ let patternIdx = 0
221
+ let score = 0
222
+
223
+ while (patternIdx < pattern.length && textIdx < text.length) {
224
+ if (text[textIdx] === pattern[patternIdx]) {
225
+ score += 10
226
+ patternIdx++
227
+ }
228
+ textIdx++
229
+ }
230
+
231
+ if (patternIdx === pattern.length) {
232
+ // Penalty for spread
233
+ const spread = textIdx / pattern.length
234
+ score = Math.max(10, score - spread * 5)
235
+ return { score, algorithm: 'subsequence' }
236
+ }
237
+
238
+ return { score: 0, algorithm: 'subsequence' }
239
+ }
240
+
241
+ /**
242
+ * Fuzzy segment matching (dao → dao-qi-harmony)
243
+ * Matches segments flexibly
244
+ */
245
+ private fuzzySegmentMatch(text: string, pattern: string): { score: number; algorithm: string } {
246
+ // Remove hyphens and underscores for matching
247
+ const cleanText = text.replace(/[-_]/g, '')
248
+ const cleanPattern = pattern.replace(/[-_]/g, '')
249
+
250
+ // Check if clean pattern is a prefix of clean text
251
+ if (cleanText.startsWith(cleanPattern)) {
252
+ const coverage = cleanPattern.length / cleanText.length
253
+ return { score: 150 + coverage * 100, algorithm: 'fuzzy-segment' }
254
+ }
255
+
256
+ // Check if pattern appears anywhere in clean text
257
+ const index = cleanText.indexOf(cleanPattern)
258
+ if (index !== -1) {
259
+ const positionPenalty = index * 5
260
+ return { score: Math.max(50, 100 - positionPenalty), algorithm: 'fuzzy-contains' }
261
+ }
262
+
263
+ return { score: 0, algorithm: 'fuzzy-segment' }
264
+ }
265
+ }
266
+
267
+ // Export singleton instance and helper functions
268
+ export const advancedMatcher = new AdvancedFuzzyMatcher()
269
+
270
+ export function matchAdvanced(candidate: string, query: string): MatchResult {
271
+ return advancedMatcher.match(candidate, query)
272
+ }
273
+
274
+ export function matchManyAdvanced(
275
+ candidates: string[],
276
+ query: string,
277
+ minScore: number = 10
278
+ ): Array<{ candidate: string; score: number; algorithm: string }> {
279
+ return candidates
280
+ .map(candidate => {
281
+ const result = advancedMatcher.match(candidate, query)
282
+ return {
283
+ candidate,
284
+ score: result.score,
285
+ algorithm: result.algorithm
286
+ }
287
+ })
288
+ .filter(item => item.score >= minScore)
289
+ .sort((a, b) => b.score - a.score)
290
+ }