@shareai-lab/kode 1.0.70 → 1.0.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +202 -76
  2. package/README.zh-CN.md +246 -0
  3. package/cli.js +62 -0
  4. package/package.json +45 -25
  5. package/scripts/postinstall.js +56 -0
  6. package/src/ProjectOnboarding.tsx +180 -0
  7. package/src/Tool.ts +53 -0
  8. package/src/commands/approvedTools.ts +53 -0
  9. package/src/commands/bug.tsx +20 -0
  10. package/src/commands/clear.ts +43 -0
  11. package/src/commands/compact.ts +120 -0
  12. package/src/commands/config.tsx +19 -0
  13. package/src/commands/cost.ts +18 -0
  14. package/src/commands/ctx_viz.ts +209 -0
  15. package/src/commands/doctor.ts +24 -0
  16. package/src/commands/help.tsx +19 -0
  17. package/src/commands/init.ts +37 -0
  18. package/src/commands/listen.ts +42 -0
  19. package/src/commands/login.tsx +51 -0
  20. package/src/commands/logout.tsx +40 -0
  21. package/src/commands/mcp.ts +41 -0
  22. package/src/commands/model.tsx +40 -0
  23. package/src/commands/modelstatus.tsx +20 -0
  24. package/src/commands/onboarding.tsx +34 -0
  25. package/src/commands/pr_comments.ts +59 -0
  26. package/src/commands/refreshCommands.ts +54 -0
  27. package/src/commands/release-notes.ts +34 -0
  28. package/src/commands/resume.tsx +30 -0
  29. package/src/commands/review.ts +49 -0
  30. package/src/commands/terminalSetup.ts +221 -0
  31. package/src/commands.ts +136 -0
  32. package/src/components/ApproveApiKey.tsx +93 -0
  33. package/src/components/AsciiLogo.tsx +13 -0
  34. package/src/components/AutoUpdater.tsx +148 -0
  35. package/src/components/Bug.tsx +367 -0
  36. package/src/components/Config.tsx +289 -0
  37. package/src/components/ConsoleOAuthFlow.tsx +326 -0
  38. package/src/components/Cost.tsx +23 -0
  39. package/src/components/CostThresholdDialog.tsx +46 -0
  40. package/src/components/CustomSelect/option-map.ts +42 -0
  41. package/src/components/CustomSelect/select-option.tsx +52 -0
  42. package/src/components/CustomSelect/select.tsx +143 -0
  43. package/src/components/CustomSelect/use-select-state.ts +414 -0
  44. package/src/components/CustomSelect/use-select.ts +35 -0
  45. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  46. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  47. package/src/components/Help.tsx +215 -0
  48. package/src/components/HighlightedCode.tsx +33 -0
  49. package/src/components/InvalidConfigDialog.tsx +113 -0
  50. package/src/components/Link.tsx +32 -0
  51. package/src/components/LogSelector.tsx +86 -0
  52. package/src/components/Logo.tsx +145 -0
  53. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  54. package/src/components/MCPServerDialogCopy.tsx +25 -0
  55. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  56. package/src/components/Message.tsx +219 -0
  57. package/src/components/MessageResponse.tsx +15 -0
  58. package/src/components/MessageSelector.tsx +211 -0
  59. package/src/components/ModeIndicator.tsx +88 -0
  60. package/src/components/ModelConfig.tsx +301 -0
  61. package/src/components/ModelListManager.tsx +223 -0
  62. package/src/components/ModelSelector.tsx +3208 -0
  63. package/src/components/ModelStatusDisplay.tsx +228 -0
  64. package/src/components/Onboarding.tsx +274 -0
  65. package/src/components/PressEnterToContinue.tsx +11 -0
  66. package/src/components/PromptInput.tsx +710 -0
  67. package/src/components/SentryErrorBoundary.ts +33 -0
  68. package/src/components/Spinner.tsx +129 -0
  69. package/src/components/StructuredDiff.tsx +184 -0
  70. package/src/components/TextInput.tsx +246 -0
  71. package/src/components/TokenWarning.tsx +31 -0
  72. package/src/components/ToolUseLoader.tsx +40 -0
  73. package/src/components/TrustDialog.tsx +106 -0
  74. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  75. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  76. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  77. package/src/components/binary-feedback/utils.ts +220 -0
  78. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  79. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +45 -0
  80. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  81. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  82. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  83. package/src/components/messages/AssistantToolUseMessage.tsx +123 -0
  84. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  85. package/src/components/messages/UserCommandMessage.tsx +30 -0
  86. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  87. package/src/components/messages/UserPromptMessage.tsx +35 -0
  88. package/src/components/messages/UserTextMessage.tsx +39 -0
  89. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  90. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  91. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  92. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  93. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  94. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  95. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  96. package/src/components/permissions/FallbackPermissionRequest.tsx +155 -0
  97. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  98. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +75 -0
  99. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  100. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +81 -0
  101. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +242 -0
  102. package/src/components/permissions/PermissionRequest.tsx +103 -0
  103. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  104. package/src/components/permissions/hooks.ts +44 -0
  105. package/src/components/permissions/toolUseOptions.ts +59 -0
  106. package/src/components/permissions/utils.ts +23 -0
  107. package/src/constants/betas.ts +5 -0
  108. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  109. package/src/constants/figures.ts +4 -0
  110. package/src/constants/keys.ts +3 -0
  111. package/src/constants/macros.ts +6 -0
  112. package/src/constants/models.ts +935 -0
  113. package/src/constants/oauth.ts +18 -0
  114. package/src/constants/product.ts +17 -0
  115. package/src/constants/prompts.ts +177 -0
  116. package/src/constants/releaseNotes.ts +7 -0
  117. package/src/context/PermissionContext.tsx +149 -0
  118. package/src/context.ts +278 -0
  119. package/src/cost-tracker.ts +84 -0
  120. package/src/entrypoints/cli.tsx +1498 -0
  121. package/src/entrypoints/mcp.ts +176 -0
  122. package/src/history.ts +25 -0
  123. package/src/hooks/useApiKeyVerification.ts +59 -0
  124. package/src/hooks/useArrowKeyHistory.ts +55 -0
  125. package/src/hooks/useCanUseTool.ts +138 -0
  126. package/src/hooks/useCancelRequest.ts +39 -0
  127. package/src/hooks/useDoublePress.ts +42 -0
  128. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  129. package/src/hooks/useInterval.ts +25 -0
  130. package/src/hooks/useLogMessages.ts +16 -0
  131. package/src/hooks/useLogStartupTime.ts +12 -0
  132. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  133. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  134. package/src/hooks/useSlashCommandTypeahead.ts +137 -0
  135. package/src/hooks/useTerminalSize.ts +49 -0
  136. package/src/hooks/useTextInput.ts +315 -0
  137. package/src/messages.ts +37 -0
  138. package/src/permissions.ts +268 -0
  139. package/src/query.ts +704 -0
  140. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  141. package/src/screens/Doctor.tsx +219 -0
  142. package/src/screens/LogList.tsx +68 -0
  143. package/src/screens/REPL.tsx +792 -0
  144. package/src/screens/ResumeConversation.tsx +68 -0
  145. package/src/services/browserMocks.ts +66 -0
  146. package/src/services/claude.ts +1947 -0
  147. package/src/services/customCommands.ts +683 -0
  148. package/src/services/fileFreshness.ts +377 -0
  149. package/src/services/mcpClient.ts +564 -0
  150. package/src/services/mcpServerApproval.tsx +50 -0
  151. package/src/services/notifier.ts +40 -0
  152. package/src/services/oauth.ts +357 -0
  153. package/src/services/openai.ts +796 -0
  154. package/src/services/sentry.ts +3 -0
  155. package/src/services/statsig.ts +171 -0
  156. package/src/services/statsigStorage.ts +86 -0
  157. package/src/services/systemReminder.ts +406 -0
  158. package/src/services/vcr.ts +161 -0
  159. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  160. package/src/tools/ArchitectTool/prompt.ts +15 -0
  161. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +505 -0
  162. package/src/tools/BashTool/BashTool.tsx +270 -0
  163. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  164. package/src/tools/BashTool/OutputLine.tsx +48 -0
  165. package/src/tools/BashTool/prompt.ts +174 -0
  166. package/src/tools/BashTool/utils.ts +56 -0
  167. package/src/tools/FileEditTool/FileEditTool.tsx +316 -0
  168. package/src/tools/FileEditTool/prompt.ts +51 -0
  169. package/src/tools/FileEditTool/utils.ts +58 -0
  170. package/src/tools/FileReadTool/FileReadTool.tsx +371 -0
  171. package/src/tools/FileReadTool/prompt.ts +7 -0
  172. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  173. package/src/tools/FileWriteTool/prompt.ts +10 -0
  174. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  175. package/src/tools/GlobTool/prompt.ts +8 -0
  176. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  177. package/src/tools/GrepTool/prompt.ts +11 -0
  178. package/src/tools/MCPTool/MCPTool.tsx +106 -0
  179. package/src/tools/MCPTool/prompt.ts +3 -0
  180. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  181. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  182. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  183. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  184. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  185. package/src/tools/MultiEditTool/prompt.ts +45 -0
  186. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  187. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  188. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +266 -0
  189. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  190. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  191. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  192. package/src/tools/TaskTool/TaskTool.tsx +382 -0
  193. package/src/tools/TaskTool/constants.ts +1 -0
  194. package/src/tools/TaskTool/prompt.ts +56 -0
  195. package/src/tools/ThinkTool/ThinkTool.tsx +56 -0
  196. package/src/tools/ThinkTool/prompt.ts +12 -0
  197. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +289 -0
  198. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  199. package/src/tools/lsTool/lsTool.tsx +269 -0
  200. package/src/tools/lsTool/prompt.ts +2 -0
  201. package/src/tools.ts +63 -0
  202. package/src/types/PermissionMode.ts +120 -0
  203. package/src/types/RequestContext.ts +72 -0
  204. package/src/utils/Cursor.ts +436 -0
  205. package/src/utils/PersistentShell.ts +373 -0
  206. package/src/utils/agentStorage.ts +97 -0
  207. package/src/utils/array.ts +3 -0
  208. package/src/utils/ask.tsx +98 -0
  209. package/src/utils/auth.ts +13 -0
  210. package/src/utils/autoCompactCore.ts +223 -0
  211. package/src/utils/autoUpdater.ts +318 -0
  212. package/src/utils/betas.ts +20 -0
  213. package/src/utils/browser.ts +14 -0
  214. package/src/utils/cleanup.ts +72 -0
  215. package/src/utils/commands.ts +261 -0
  216. package/src/utils/config.ts +771 -0
  217. package/src/utils/conversationRecovery.ts +54 -0
  218. package/src/utils/debugLogger.ts +1123 -0
  219. package/src/utils/diff.ts +42 -0
  220. package/src/utils/env.ts +57 -0
  221. package/src/utils/errors.ts +21 -0
  222. package/src/utils/exampleCommands.ts +108 -0
  223. package/src/utils/execFileNoThrow.ts +51 -0
  224. package/src/utils/expertChatStorage.ts +136 -0
  225. package/src/utils/file.ts +402 -0
  226. package/src/utils/fileRecoveryCore.ts +71 -0
  227. package/src/utils/format.tsx +44 -0
  228. package/src/utils/generators.ts +62 -0
  229. package/src/utils/git.ts +92 -0
  230. package/src/utils/globalLogger.ts +77 -0
  231. package/src/utils/http.ts +10 -0
  232. package/src/utils/imagePaste.ts +38 -0
  233. package/src/utils/json.ts +13 -0
  234. package/src/utils/log.ts +382 -0
  235. package/src/utils/markdown.ts +213 -0
  236. package/src/utils/messageContextManager.ts +289 -0
  237. package/src/utils/messages.tsx +938 -0
  238. package/src/utils/model.ts +836 -0
  239. package/src/utils/permissions/filesystem.ts +118 -0
  240. package/src/utils/ripgrep.ts +167 -0
  241. package/src/utils/sessionState.ts +49 -0
  242. package/src/utils/state.ts +25 -0
  243. package/src/utils/style.ts +29 -0
  244. package/src/utils/terminal.ts +49 -0
  245. package/src/utils/theme.ts +122 -0
  246. package/src/utils/thinking.ts +144 -0
  247. package/src/utils/todoStorage.ts +431 -0
  248. package/src/utils/tokens.ts +43 -0
  249. package/src/utils/toolExecutionController.ts +163 -0
  250. package/src/utils/unaryLogging.ts +26 -0
  251. package/src/utils/user.ts +37 -0
  252. package/src/utils/validate.ts +165 -0
  253. package/cli.mjs +0 -1803
@@ -0,0 +1,683 @@
1
+ import { existsSync, readFileSync } from 'fs'
2
+ import { join } from 'path'
3
+ import { homedir } from 'os'
4
+ import { memoize } from 'lodash-es'
5
+ import type { MessageParam } from '@anthropic-ai/sdk/resources/index.mjs'
6
+ import type { Command } from '../commands'
7
+ import { getCwd } from '../utils/state'
8
+ import { logEvent } from './statsig'
9
+ import { execFile } from 'child_process'
10
+ import { promisify } from 'util'
11
+
12
+ const execFileAsync = promisify(execFile)
13
+
14
+ /**
15
+ * Execute bash commands found in custom command content using !`command` syntax
16
+ *
17
+ * This function processes dynamic command execution within custom commands,
18
+ * following the same security model as the main BashTool but with restricted scope.
19
+ * Commands are executed in the current working directory with a timeout.
20
+ *
21
+ * @param content - The custom command content to process
22
+ * @returns Promise<string> - Content with bash commands replaced by their output
23
+ */
24
+ async function executeBashCommands(content: string): Promise<string> {
25
+ // Match patterns like !`git status` or !`command here`
26
+ const bashCommandRegex = /!\`([^`]+)\`/g
27
+ const matches = [...content.matchAll(bashCommandRegex)]
28
+
29
+ if (matches.length === 0) {
30
+ return content
31
+ }
32
+
33
+ let result = content
34
+
35
+ for (const match of matches) {
36
+ const fullMatch = match[0]
37
+ const command = match[1].trim()
38
+
39
+ try {
40
+ // Parse command and args using simple shell parsing
41
+ // This mirrors the approach used in the main BashTool but with stricter limits
42
+ const parts = command.split(/\s+/)
43
+ const cmd = parts[0]
44
+ const args = parts.slice(1)
45
+
46
+ // Execute with conservative timeout (5s vs BashTool's 2min default)
47
+ const { stdout, stderr } = await execFileAsync(cmd, args, {
48
+ timeout: 5000,
49
+ encoding: 'utf8',
50
+ cwd: getCwd(), // Use current working directory for consistency
51
+ })
52
+
53
+ // Replace the bash command with its output, preferring stdout
54
+ const output = stdout.trim() || stderr.trim() || '(no output)'
55
+ result = result.replace(fullMatch, output)
56
+ } catch (error) {
57
+ console.warn(`Failed to execute bash command "${command}":`, error)
58
+ result = result.replace(fullMatch, `(error executing: ${command})`)
59
+ }
60
+ }
61
+
62
+ return result
63
+ }
64
+
65
+ /**
66
+ * Resolve file references using @filepath syntax within custom commands
67
+ *
68
+ * This function implements file inclusion for custom commands, similar to how
69
+ * the FileReadTool works but with inline processing. Files are read from the
70
+ * current working directory and formatted as markdown code blocks.
71
+ *
72
+ * Security note: Files are read with the same permissions as the main process,
73
+ * following the same security model as other file operations in the system.
74
+ *
75
+ * @param content - The custom command content to process
76
+ * @returns Promise<string> - Content with file references replaced by file contents
77
+ */
78
+ async function resolveFileReferences(content: string): Promise<string> {
79
+ // Match patterns like @src/file.js or @path/to/file.txt
80
+ const fileRefRegex = /@([a-zA-Z0-9/._-]+(?:\.[a-zA-Z0-9]+)?)/g
81
+ const matches = [...content.matchAll(fileRefRegex)]
82
+
83
+ if (matches.length === 0) {
84
+ return content
85
+ }
86
+
87
+ let result = content
88
+
89
+ for (const match of matches) {
90
+ const fullMatch = match[0]
91
+ const filePath = match[1]
92
+
93
+ try {
94
+ // Resolve relative to current working directory
95
+ // This maintains consistency with how other file operations work
96
+ const fullPath = join(getCwd(), filePath)
97
+
98
+ if (existsSync(fullPath)) {
99
+ const fileContent = readFileSync(fullPath, { encoding: 'utf-8' })
100
+
101
+ // Format file content with filename header for clarity
102
+ // This matches the format used by FileReadTool for consistency
103
+ const formattedContent = `\n\n## File: ${filePath}\n\`\`\`\n${fileContent}\n\`\`\`\n`
104
+ result = result.replace(fullMatch, formattedContent)
105
+ } else {
106
+ result = result.replace(fullMatch, `(file not found: ${filePath})`)
107
+ }
108
+ } catch (error) {
109
+ console.warn(`Failed to read file "${filePath}":`, error)
110
+ result = result.replace(fullMatch, `(error reading: ${filePath})`)
111
+ }
112
+ }
113
+
114
+ return result
115
+ }
116
+
117
+ /**
118
+ * Validate and process allowed-tools specification from frontmatter
119
+ *
120
+ * This function handles tool restriction specifications in custom commands.
121
+ * Currently it provides logging and validation structure - full enforcement
122
+ * would require deep integration with the tool permission system.
123
+ *
124
+ * Future implementation should connect to src/permissions.ts and the
125
+ * tool execution pipeline to enforce these restrictions.
126
+ *
127
+ * @param allowedTools - Array of tool names from frontmatter
128
+ * @returns boolean - Currently always true, future will return actual validation result
129
+ */
130
+ function validateAllowedTools(allowedTools: string[] | undefined): boolean {
131
+ // Log allowed tools for debugging and future integration
132
+ if (allowedTools && allowedTools.length > 0) {
133
+ console.log('Command allowed tools:', allowedTools)
134
+ // TODO: Integrate with src/permissions.ts tool permission system
135
+ // TODO: Connect to Tool.tsx needsPermissions() mechanism
136
+ }
137
+ return true // Allow execution for now - future versions will enforce restrictions
138
+ }
139
+
140
+ /**
141
+ * Frontmatter configuration for custom commands
142
+ *
143
+ * This interface defines the YAML frontmatter structure that can be used
144
+ * to configure custom commands. It follows the same pattern as Claude Desktop's
145
+ * custom command system but with additional fields for enhanced functionality.
146
+ */
147
+ export interface CustomCommandFrontmatter {
148
+ /** Display name for the command (overrides filename-based naming) */
149
+ name?: string
150
+ /** Brief description of what the command does */
151
+ description?: string
152
+ /** Alternative names that can be used to invoke this command */
153
+ aliases?: string[]
154
+ /** Whether this command is active and can be executed */
155
+ enabled?: boolean
156
+ /** Whether this command should be hidden from help output */
157
+ hidden?: boolean
158
+ /** Message to display while the command is running */
159
+ progressMessage?: string
160
+ /** Named arguments for legacy {arg} placeholder support */
161
+ argNames?: string[]
162
+ /** Tools that this command is restricted to use */
163
+ 'allowed-tools'?: string[]
164
+ }
165
+
166
+ /**
167
+ * Extended Command interface with scope information
168
+ *
169
+ * This extends the base Command interface to include scope metadata
170
+ * for distinguishing between user-level and project-level commands.
171
+ */
172
+ export interface CustomCommandWithScope extends Command {
173
+ /** Scope indicates whether this is a user or project command */
174
+ scope?: 'user' | 'project'
175
+ }
176
+
177
+ /**
178
+ * Parsed custom command file representation
179
+ *
180
+ * This interface represents a fully parsed custom command file with
181
+ * separated frontmatter and content sections.
182
+ */
183
+ export interface CustomCommandFile {
184
+ /** Parsed frontmatter configuration */
185
+ frontmatter: CustomCommandFrontmatter
186
+ /** Markdown content (without frontmatter) */
187
+ content: string
188
+ /** Absolute path to the source file */
189
+ filePath: string
190
+ }
191
+
192
+ /**
193
+ * Parse YAML frontmatter from markdown content
194
+ *
195
+ * This function extracts and parses YAML frontmatter from markdown files,
196
+ * supporting the same syntax as Jekyll and other static site generators.
197
+ * It handles basic YAML constructs including strings, booleans, and arrays.
198
+ *
199
+ * The parser is intentionally simple and focused on the specific needs of
200
+ * custom commands rather than being a full YAML parser. Complex YAML features
201
+ * like nested objects, multi-line strings, and advanced syntax are not supported.
202
+ *
203
+ * @param content - Raw markdown content with optional frontmatter
204
+ * @returns Object containing parsed frontmatter and remaining content
205
+ */
206
+ export function parseFrontmatter(content: string): {
207
+ frontmatter: CustomCommandFrontmatter
208
+ content: string
209
+ } {
210
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)---\s*\n?/
211
+ const match = content.match(frontmatterRegex)
212
+
213
+ if (!match) {
214
+ return { frontmatter: {}, content }
215
+ }
216
+
217
+ const yamlContent = match[1] || ''
218
+ const markdownContent = content.slice(match[0].length)
219
+ const frontmatter: CustomCommandFrontmatter = {}
220
+
221
+ // Simple YAML parser for basic key-value pairs and arrays
222
+ // This handles the subset of YAML needed for custom command configuration
223
+ const lines = yamlContent.split('\n')
224
+ let currentKey: string | null = null
225
+ let arrayItems: string[] = []
226
+ let inArray = false
227
+
228
+ for (const line of lines) {
229
+ const trimmed = line.trim()
230
+ if (!trimmed || trimmed.startsWith('#')) continue
231
+
232
+ // Handle array item continuation (- item)
233
+ if (inArray && trimmed.startsWith('-')) {
234
+ const item = trimmed.slice(1).trim().replace(/['"]/g, '')
235
+ arrayItems.push(item)
236
+ continue
237
+ }
238
+
239
+ // End array processing when we hit a new key
240
+ if (inArray && trimmed.includes(':')) {
241
+ if (currentKey) {
242
+ frontmatter[currentKey as keyof CustomCommandFrontmatter] =
243
+ arrayItems as any
244
+ }
245
+ inArray = false
246
+ arrayItems = []
247
+ currentKey = null
248
+ }
249
+
250
+ const colonIndex = trimmed.indexOf(':')
251
+ if (colonIndex === -1) continue
252
+
253
+ const key = trimmed.slice(0, colonIndex).trim()
254
+ const value = trimmed.slice(colonIndex + 1).trim()
255
+
256
+ // Handle inline arrays [item1, item2]
257
+ if (value.startsWith('[') && value.endsWith(']')) {
258
+ const items = value
259
+ .slice(1, -1)
260
+ .split(',')
261
+ .map(s => s.trim().replace(/['"]/g, ''))
262
+ .filter(s => s.length > 0)
263
+ frontmatter[key as keyof CustomCommandFrontmatter] = items as any
264
+ }
265
+ // Handle multi-line arrays (value is empty or [])
266
+ else if (value === '' || value === '[]') {
267
+ currentKey = key
268
+ inArray = true
269
+ arrayItems = []
270
+ }
271
+ // Handle boolean values
272
+ else if (value === 'true' || value === 'false') {
273
+ frontmatter[key as keyof CustomCommandFrontmatter] = (value ===
274
+ 'true') as any
275
+ }
276
+ // Handle string values (remove quotes)
277
+ else {
278
+ frontmatter[key as keyof CustomCommandFrontmatter] = value.replace(
279
+ /['"]/g,
280
+ '',
281
+ ) as any
282
+ }
283
+ }
284
+
285
+ // Handle final array if we ended in array mode
286
+ if (inArray && currentKey) {
287
+ frontmatter[currentKey as keyof CustomCommandFrontmatter] =
288
+ arrayItems as any
289
+ }
290
+
291
+ return { frontmatter, content: markdownContent }
292
+ }
293
+
294
+ /**
295
+ * Scan directory for markdown files using find command
296
+ *
297
+ * This function discovers .md files in the specified directory using the
298
+ * system's find command. It's designed as a fallback when ripgrep is not
299
+ * available, providing the same functionality with broader compatibility.
300
+ *
301
+ * The function includes timeout and signal handling for robustness,
302
+ * especially important when scanning large directory trees.
303
+ *
304
+ * @param args - Legacy parameter for ripgrep compatibility (ignored)
305
+ * @param directory - Directory to scan for markdown files
306
+ * @param signal - AbortSignal for cancellation support
307
+ * @returns Promise<string[]> - Array of absolute paths to .md files
308
+ */
309
+ async function scanMarkdownFiles(
310
+ args: string[], // Legacy parameter for ripgrep compatibility
311
+ directory: string,
312
+ signal: AbortSignal,
313
+ ): Promise<string[]> {
314
+ try {
315
+ // Use find command as fallback since ripgrep may not be available
316
+ // This provides broader compatibility across different systems
317
+ const { stdout } = await execFileAsync(
318
+ 'find',
319
+ [directory, '-name', '*.md', '-type', 'f'],
320
+ { signal, timeout: 3000 },
321
+ )
322
+ return stdout
323
+ .trim()
324
+ .split('\n')
325
+ .filter(line => line.length > 0)
326
+ } catch (error) {
327
+ // If find fails or directory doesn't exist, return empty array
328
+ // This ensures graceful degradation when directories are missing
329
+ return []
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Create a Command object from custom command file data
335
+ *
336
+ * This function transforms parsed custom command data into a Command object
337
+ * that integrates with the main command system. It handles naming, scoping,
338
+ * and prompt generation according to the project's command patterns.
339
+ *
340
+ * Command naming follows a hierarchical structure:
341
+ * - Project commands: "project:namespace:command"
342
+ * - User commands: "user:namespace:command"
343
+ * - Namespace is derived from directory structure
344
+ *
345
+ * @param frontmatter - Parsed frontmatter configuration
346
+ * @param content - Markdown content of the command
347
+ * @param filePath - Absolute path to the command file
348
+ * @param baseDir - Base directory for scope determination
349
+ * @returns CustomCommandWithScope | null - Processed command or null if invalid
350
+ */
351
+ function createCustomCommand(
352
+ frontmatter: CustomCommandFrontmatter,
353
+ content: string,
354
+ filePath: string,
355
+ baseDir: string,
356
+ ): CustomCommandWithScope | null {
357
+ // Extract command name with namespace support
358
+ const relativePath = filePath.replace(baseDir + '/', '')
359
+ const pathParts = relativePath.split('/')
360
+ const fileName = pathParts[pathParts.length - 1].replace('.md', '')
361
+
362
+ // Determine scope based on directory location
363
+ // This follows the same pattern as Claude Desktop's command system
364
+ const userClaudeDir = join(homedir(), '.claude', 'commands')
365
+ const userKodeDir = join(homedir(), '.kode', 'commands')
366
+ const scope: 'user' | 'project' =
367
+ (baseDir === userClaudeDir || baseDir === userKodeDir) ? 'user' : 'project'
368
+ const prefix = scope === 'user' ? 'user' : 'project'
369
+
370
+ // Create proper command name with prefix and namespace
371
+ let finalName: string
372
+ if (frontmatter.name) {
373
+ // If frontmatter specifies name, use it but ensure proper prefix
374
+ finalName = frontmatter.name.startsWith(`${prefix}:`)
375
+ ? frontmatter.name
376
+ : `${prefix}:${frontmatter.name}`
377
+ } else {
378
+ // Generate name from file path, supporting directory-based namespacing
379
+ if (pathParts.length > 1) {
380
+ const namespace = pathParts.slice(0, -1).join(':')
381
+ finalName = `${prefix}:${namespace}:${fileName}`
382
+ } else {
383
+ finalName = `${prefix}:${fileName}`
384
+ }
385
+ }
386
+
387
+ // Extract configuration with sensible defaults
388
+ const description = frontmatter.description || `Custom command: ${finalName}`
389
+ const enabled = frontmatter.enabled !== false // Default to true
390
+ const hidden = frontmatter.hidden === true // Default to false
391
+ const aliases = frontmatter.aliases || []
392
+ const progressMessage =
393
+ frontmatter.progressMessage || `Running ${finalName}...`
394
+ const argNames = frontmatter.argNames
395
+
396
+ // Validate required fields
397
+ if (!finalName) {
398
+ console.warn(`Custom command file ${filePath} has no name, skipping`)
399
+ return null
400
+ }
401
+
402
+ // Create the command object following the project's Command interface
403
+ const command: CustomCommandWithScope = {
404
+ type: 'prompt',
405
+ name: finalName,
406
+ description,
407
+ isEnabled: enabled,
408
+ isHidden: hidden,
409
+ aliases,
410
+ progressMessage,
411
+ argNames,
412
+ scope,
413
+ userFacingName(): string {
414
+ return finalName
415
+ },
416
+ async getPromptForCommand(args: string): Promise<MessageParam[]> {
417
+ let prompt = content.trim()
418
+
419
+ // Process argument substitution following Claude Code conventions
420
+ // This supports both the official $ARGUMENTS format and legacy {arg} format
421
+
422
+ // Step 1: Handle $ARGUMENTS placeholder (official Claude Code format)
423
+ if (prompt.includes('$ARGUMENTS')) {
424
+ prompt = prompt.replace(/\$ARGUMENTS/g, args || '')
425
+ }
426
+
427
+ // Step 2: Legacy support for named argument placeholders
428
+ if (argNames && argNames.length > 0) {
429
+ const argValues = args.trim().split(/\s+/)
430
+ argNames.forEach((argName, index) => {
431
+ const value = argValues[index] || ''
432
+ prompt = prompt.replace(new RegExp(`\\{${argName}\\}`, 'g'), value)
433
+ })
434
+ }
435
+
436
+ // Step 3: If args are provided but no placeholders used, append to prompt
437
+ if (
438
+ args.trim() &&
439
+ !prompt.includes('$ARGUMENTS') &&
440
+ (!argNames || argNames.length === 0)
441
+ ) {
442
+ prompt += `\n\nAdditional context: ${args}`
443
+ }
444
+
445
+ // Step 4: Add tool restrictions if specified
446
+ const allowedTools = frontmatter['allowed-tools']
447
+ if (
448
+ allowedTools &&
449
+ Array.isArray(allowedTools) &&
450
+ allowedTools.length > 0
451
+ ) {
452
+ const allowedToolsStr = allowedTools.join(', ')
453
+ prompt += `\n\nIMPORTANT: You are restricted to using only these tools: ${allowedToolsStr}. Do not use any other tools even if they might be helpful for the task.`
454
+ }
455
+
456
+ return [
457
+ {
458
+ role: 'user',
459
+ content: prompt,
460
+ },
461
+ ]
462
+ },
463
+ }
464
+
465
+ return command
466
+ }
467
+
468
+ /**
469
+ * Load custom commands from .claude/commands/ directories
470
+ *
471
+ * This function scans both user-level and project-level command directories
472
+ * for markdown files and processes them into Command objects. It follows the
473
+ * same discovery pattern as Claude Desktop but with additional performance
474
+ * optimizations and error handling.
475
+ *
476
+ * Directory structure:
477
+ * - User commands: ~/.claude/commands/
478
+ * - Project commands: {project}/.claude/commands/
479
+ *
480
+ * The function is memoized for performance but includes cache invalidation
481
+ * based on directory contents and timestamps.
482
+ *
483
+ * @returns Promise<CustomCommandWithScope[]> - Array of loaded and enabled commands
484
+ */
485
+ export const loadCustomCommands = memoize(
486
+ async (): Promise<CustomCommandWithScope[]> => {
487
+ // Support both .claude and .kode directories
488
+ const userClaudeDir = join(homedir(), '.claude', 'commands')
489
+ const projectClaudeDir = join(getCwd(), '.claude', 'commands')
490
+ const userKodeDir = join(homedir(), '.kode', 'commands')
491
+ const projectKodeDir = join(getCwd(), '.kode', 'commands')
492
+
493
+ // Set up abort controller for timeout handling
494
+ const abortController = new AbortController()
495
+ const timeout = setTimeout(() => abortController.abort(), 3000)
496
+
497
+ try {
498
+ const startTime = Date.now()
499
+
500
+ // Scan all four directories for .md files concurrently
501
+ // This pattern matches the async loading used elsewhere in the project
502
+ const [projectClaudeFiles, userClaudeFiles, projectKodeFiles, userKodeFiles] = await Promise.all([
503
+ existsSync(projectClaudeDir)
504
+ ? scanMarkdownFiles(
505
+ ['--files', '--hidden', '--glob', '*.md'], // Legacy args for ripgrep compatibility
506
+ projectClaudeDir,
507
+ abortController.signal,
508
+ )
509
+ : Promise.resolve([]),
510
+ existsSync(userClaudeDir)
511
+ ? scanMarkdownFiles(
512
+ ['--files', '--glob', '*.md'], // Legacy args for ripgrep compatibility
513
+ userClaudeDir,
514
+ abortController.signal,
515
+ )
516
+ : Promise.resolve([]),
517
+ existsSync(projectKodeDir)
518
+ ? scanMarkdownFiles(
519
+ ['--files', '--hidden', '--glob', '*.md'], // Legacy args for ripgrep compatibility
520
+ projectKodeDir,
521
+ abortController.signal,
522
+ )
523
+ : Promise.resolve([]),
524
+ existsSync(userKodeDir)
525
+ ? scanMarkdownFiles(
526
+ ['--files', '--glob', '*.md'], // Legacy args for ripgrep compatibility
527
+ userKodeDir,
528
+ abortController.signal,
529
+ )
530
+ : Promise.resolve([]),
531
+ ])
532
+
533
+ // Combine files with priority: project > user, kode > claude
534
+ const projectFiles = [...projectKodeFiles, ...projectClaudeFiles]
535
+ const userFiles = [...userKodeFiles, ...userClaudeFiles]
536
+ const allFiles = [...projectFiles, ...userFiles]
537
+ const duration = Date.now() - startTime
538
+
539
+ // Log performance metrics for monitoring
540
+ // This follows the same pattern as other performance-sensitive operations
541
+ logEvent('tengu_custom_command_scan', {
542
+ durationMs: duration,
543
+ projectFilesFound: projectFiles.length,
544
+ userFilesFound: userFiles.length,
545
+ totalFiles: allFiles.length,
546
+ })
547
+
548
+ // Parse files and create command objects
549
+ const commands: CustomCommandWithScope[] = []
550
+
551
+ // Process project files first (higher priority)
552
+ for (const filePath of projectFiles) {
553
+ try {
554
+ const content = readFileSync(filePath, { encoding: 'utf-8' })
555
+ const { frontmatter, content: commandContent } =
556
+ parseFrontmatter(content)
557
+ // Determine which base directory this file is from
558
+ const baseDir = filePath.includes('.kode/commands') ? projectKodeDir : projectClaudeDir
559
+ const command = createCustomCommand(
560
+ frontmatter,
561
+ commandContent,
562
+ filePath,
563
+ baseDir,
564
+ )
565
+
566
+ if (command) {
567
+ commands.push(command)
568
+ }
569
+ } catch (error) {
570
+ console.warn(`Failed to load custom command from ${filePath}:`, error)
571
+ }
572
+ }
573
+
574
+ // Process user files second (lower priority)
575
+ for (const filePath of userFiles) {
576
+ try {
577
+ const content = readFileSync(filePath, { encoding: 'utf-8' })
578
+ const { frontmatter, content: commandContent } =
579
+ parseFrontmatter(content)
580
+ // Determine which base directory this file is from
581
+ const baseDir = filePath.includes('.kode/commands') ? userKodeDir : userClaudeDir
582
+ const command = createCustomCommand(
583
+ frontmatter,
584
+ commandContent,
585
+ filePath,
586
+ baseDir,
587
+ )
588
+
589
+ if (command) {
590
+ commands.push(command)
591
+ }
592
+ } catch (error) {
593
+ console.warn(`Failed to load custom command from ${filePath}:`, error)
594
+ }
595
+ }
596
+
597
+ // Filter enabled commands and log results
598
+ const enabledCommands = commands.filter(cmd => cmd.isEnabled)
599
+
600
+ // Log loading results for debugging and monitoring
601
+ logEvent('tengu_custom_commands_loaded', {
602
+ totalCommands: commands.length,
603
+ enabledCommands: enabledCommands.length,
604
+ userCommands: commands.filter(cmd => cmd.scope === 'user').length,
605
+ projectCommands: commands.filter(cmd => cmd.scope === 'project').length,
606
+ })
607
+
608
+ return enabledCommands
609
+ } catch (error) {
610
+ console.warn('Failed to load custom commands:', error)
611
+ return []
612
+ } finally {
613
+ clearTimeout(timeout)
614
+ }
615
+ },
616
+ // Memoization resolver based on current working directory and directory state
617
+ // This ensures cache invalidation when directories change
618
+ () => {
619
+ const cwd = getCwd()
620
+ const userClaudeDir = join(homedir(), '.claude', 'commands')
621
+ const projectClaudeDir = join(cwd, '.claude', 'commands')
622
+ const userKodeDir = join(homedir(), '.kode', 'commands')
623
+ const projectKodeDir = join(cwd, '.kode', 'commands')
624
+
625
+ // Create cache key that includes directory existence and timestamp
626
+ // This provides reasonable cache invalidation without excessive file system checks
627
+ return `${cwd}:${existsSync(userClaudeDir)}:${existsSync(projectClaudeDir)}:${existsSync(userKodeDir)}:${existsSync(projectKodeDir)}:${Math.floor(Date.now() / 60000)}`
628
+ },
629
+ )
630
+
631
+ /**
632
+ * Clear the custom commands cache to force reload
633
+ *
634
+ * This function invalidates the memoized cache for custom commands,
635
+ * forcing the next invocation to re-scan the filesystem. It's useful
636
+ * when commands are added, removed, or modified during runtime.
637
+ *
638
+ * This follows the same pattern as other cache invalidation functions
639
+ * in the project, such as getCommands.cache.clear().
640
+ */
641
+ export const reloadCustomCommands = (): void => {
642
+ loadCustomCommands.cache.clear()
643
+ console.log(
644
+ 'Custom commands cache cleared. Commands will be reloaded on next use.',
645
+ )
646
+ }
647
+
648
+ /**
649
+ * Get custom command directories for help and diagnostic purposes
650
+ *
651
+ * This function returns the standard directory paths where custom commands
652
+ * are expected to be found. It's used by help systems and diagnostic tools
653
+ * to inform users about the proper directory structure.
654
+ *
655
+ * @returns Object containing user and project command directory paths
656
+ */
657
+ export function getCustomCommandDirectories(): {
658
+ userClaude: string
659
+ projectClaude: string
660
+ userKode: string
661
+ projectKode: string
662
+ } {
663
+ return {
664
+ userClaude: join(homedir(), '.claude', 'commands'),
665
+ projectClaude: join(getCwd(), '.claude', 'commands'),
666
+ userKode: join(homedir(), '.kode', 'commands'),
667
+ projectKode: join(getCwd(), '.kode', 'commands'),
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Check if custom commands are available in either directory
673
+ *
674
+ * This function provides a quick way to determine if custom commands
675
+ * are configured without actually loading them. It's useful for conditional
676
+ * UI elements and feature detection.
677
+ *
678
+ * @returns boolean - True if at least one command directory exists
679
+ */
680
+ export function hasCustomCommands(): boolean {
681
+ const { userClaude, projectClaude, userKode, projectKode } = getCustomCommandDirectories()
682
+ return existsSync(userClaude) || existsSync(projectClaude) || existsSync(userKode) || existsSync(projectKode)
683
+ }