@butlerw/vellum 0.1.0

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 (446) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/__fixtures__/responses/code-generation.json +42 -0
  4. package/__fixtures__/responses/error-response.json +20 -0
  5. package/__fixtures__/responses/hello-world.json +32 -0
  6. package/dist/auth-6MCXESOH.js +26 -0
  7. package/dist/chunk-SECXJGWA.js +597 -0
  8. package/dist/index.js +34023 -0
  9. package/package.json +67 -0
  10. package/src/__tests__/commands.e2e.test.ts +728 -0
  11. package/src/__tests__/credentials.test.ts +713 -0
  12. package/src/__tests__/mode-e2e.test.ts +391 -0
  13. package/src/__tests__/tui-integration.test.tsx +1271 -0
  14. package/src/agents/__tests__/task-persistence.test.ts +235 -0
  15. package/src/agents/commands/delegate.ts +240 -0
  16. package/src/agents/commands/index.ts +10 -0
  17. package/src/agents/commands/resume.ts +335 -0
  18. package/src/agents/index.ts +29 -0
  19. package/src/agents/task-persistence.ts +272 -0
  20. package/src/agents/task-resumption.ts +242 -0
  21. package/src/app.tsx +4737 -0
  22. package/src/commands/__tests__/.gitkeep +1 -0
  23. package/src/commands/__tests__/agents.test.ts +606 -0
  24. package/src/commands/__tests__/auth.test.ts +626 -0
  25. package/src/commands/__tests__/autocomplete.test.ts +683 -0
  26. package/src/commands/__tests__/batch.test.ts +287 -0
  27. package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
  28. package/src/commands/__tests__/completion.test.ts +238 -0
  29. package/src/commands/__tests__/core.test.ts +363 -0
  30. package/src/commands/__tests__/executor.test.ts +496 -0
  31. package/src/commands/__tests__/exit-codes.test.ts +220 -0
  32. package/src/commands/__tests__/init.test.ts +243 -0
  33. package/src/commands/__tests__/language.test.ts +353 -0
  34. package/src/commands/__tests__/mode-cli.test.ts +667 -0
  35. package/src/commands/__tests__/model.test.ts +277 -0
  36. package/src/commands/__tests__/parser.test.ts +493 -0
  37. package/src/commands/__tests__/performance.bench.ts +380 -0
  38. package/src/commands/__tests__/registry.test.ts +534 -0
  39. package/src/commands/__tests__/resume.test.ts +449 -0
  40. package/src/commands/__tests__/security.test.ts +845 -0
  41. package/src/commands/__tests__/stream-json.test.ts +372 -0
  42. package/src/commands/__tests__/user-commands.test.ts +597 -0
  43. package/src/commands/adapters.ts +267 -0
  44. package/src/commands/agent.ts +395 -0
  45. package/src/commands/agents/generate.ts +506 -0
  46. package/src/commands/agents/index.ts +272 -0
  47. package/src/commands/agents/show.ts +271 -0
  48. package/src/commands/agents/validate.ts +387 -0
  49. package/src/commands/auth.ts +883 -0
  50. package/src/commands/autocomplete.ts +480 -0
  51. package/src/commands/batch/command.ts +388 -0
  52. package/src/commands/batch/executor.ts +361 -0
  53. package/src/commands/batch/index.ts +12 -0
  54. package/src/commands/commit.ts +235 -0
  55. package/src/commands/completion/index.ts +371 -0
  56. package/src/commands/condense.ts +191 -0
  57. package/src/commands/config.ts +344 -0
  58. package/src/commands/context-provider.ts +173 -0
  59. package/src/commands/copy.ts +329 -0
  60. package/src/commands/core/clear.ts +38 -0
  61. package/src/commands/core/exit.ts +43 -0
  62. package/src/commands/core/help.ts +354 -0
  63. package/src/commands/core/index.ts +15 -0
  64. package/src/commands/cost.ts +179 -0
  65. package/src/commands/credentials.tsx +618 -0
  66. package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
  67. package/src/commands/custom-agents/create.ts +377 -0
  68. package/src/commands/custom-agents/export.ts +135 -0
  69. package/src/commands/custom-agents/import.ts +199 -0
  70. package/src/commands/custom-agents/index.ts +372 -0
  71. package/src/commands/custom-agents/info.ts +318 -0
  72. package/src/commands/custom-agents/list.ts +267 -0
  73. package/src/commands/custom-agents/validate.ts +388 -0
  74. package/src/commands/diff-mode.ts +241 -0
  75. package/src/commands/env.ts +53 -0
  76. package/src/commands/executor.ts +579 -0
  77. package/src/commands/exit-codes.ts +202 -0
  78. package/src/commands/index.ts +701 -0
  79. package/src/commands/init/index.ts +15 -0
  80. package/src/commands/init/prompts.ts +366 -0
  81. package/src/commands/init/templates/commands-readme.md +80 -0
  82. package/src/commands/init/templates/example-command.md +79 -0
  83. package/src/commands/init/templates/example-skill.md +168 -0
  84. package/src/commands/init/templates/example-workflow.md +101 -0
  85. package/src/commands/init/templates/prompts-readme.md +52 -0
  86. package/src/commands/init/templates/rules-readme.md +63 -0
  87. package/src/commands/init/templates/skills-readme.md +83 -0
  88. package/src/commands/init/templates/workflows-readme.md +94 -0
  89. package/src/commands/init.ts +391 -0
  90. package/src/commands/install.ts +90 -0
  91. package/src/commands/language.ts +191 -0
  92. package/src/commands/loaders/.gitkeep +1 -0
  93. package/src/commands/lsp.ts +199 -0
  94. package/src/commands/markdown-commands.ts +253 -0
  95. package/src/commands/mcp.ts +588 -0
  96. package/src/commands/memory/export.ts +341 -0
  97. package/src/commands/memory/index.ts +148 -0
  98. package/src/commands/memory/list.ts +261 -0
  99. package/src/commands/memory/search.ts +346 -0
  100. package/src/commands/memory/utils.ts +15 -0
  101. package/src/commands/metrics.ts +75 -0
  102. package/src/commands/migrate/index.ts +16 -0
  103. package/src/commands/migrate/prompts.ts +477 -0
  104. package/src/commands/mode.ts +331 -0
  105. package/src/commands/model.ts +298 -0
  106. package/src/commands/onboard.ts +205 -0
  107. package/src/commands/open.ts +169 -0
  108. package/src/commands/output/stream-json.ts +373 -0
  109. package/src/commands/parser/chain-parser.ts +370 -0
  110. package/src/commands/parser/index.ts +29 -0
  111. package/src/commands/parser/pipe-parser.ts +480 -0
  112. package/src/commands/parser.ts +588 -0
  113. package/src/commands/persistence.ts +355 -0
  114. package/src/commands/progress.ts +18 -0
  115. package/src/commands/prompt/index.ts +17 -0
  116. package/src/commands/prompt/validate.ts +621 -0
  117. package/src/commands/prompt-priority.ts +401 -0
  118. package/src/commands/registry.ts +374 -0
  119. package/src/commands/sandbox/index.ts +131 -0
  120. package/src/commands/security/index.ts +21 -0
  121. package/src/commands/security/input-sanitizer.ts +168 -0
  122. package/src/commands/security/permission-checker.ts +456 -0
  123. package/src/commands/security/sensitive-data.ts +350 -0
  124. package/src/commands/session/delete.ts +38 -0
  125. package/src/commands/session/export.ts +39 -0
  126. package/src/commands/session/index.ts +26 -0
  127. package/src/commands/session/list.ts +26 -0
  128. package/src/commands/session/resume.ts +562 -0
  129. package/src/commands/session/search.ts +434 -0
  130. package/src/commands/session/show.ts +26 -0
  131. package/src/commands/settings.ts +368 -0
  132. package/src/commands/setup.ts +23 -0
  133. package/src/commands/shell/index.ts +16 -0
  134. package/src/commands/shell/setup.ts +422 -0
  135. package/src/commands/shell-init.ts +50 -0
  136. package/src/commands/shell-integration/index.ts +194 -0
  137. package/src/commands/skill.ts +1220 -0
  138. package/src/commands/spec.ts +558 -0
  139. package/src/commands/status.ts +246 -0
  140. package/src/commands/theme.ts +211 -0
  141. package/src/commands/think.ts +551 -0
  142. package/src/commands/trust.ts +211 -0
  143. package/src/commands/tutorial.ts +522 -0
  144. package/src/commands/types.ts +512 -0
  145. package/src/commands/update.ts +274 -0
  146. package/src/commands/usage.ts +213 -0
  147. package/src/commands/user-commands.ts +630 -0
  148. package/src/commands/utils.ts +142 -0
  149. package/src/commands/vim.ts +152 -0
  150. package/src/commands/workflow.ts +257 -0
  151. package/src/components/header.tsx +25 -0
  152. package/src/components/input.tsx +25 -0
  153. package/src/components/message-list.tsx +32 -0
  154. package/src/components/status-bar.tsx +23 -0
  155. package/src/index.tsx +614 -0
  156. package/src/onboarding/__tests__/tutorial.test.ts +740 -0
  157. package/src/onboarding/index.ts +69 -0
  158. package/src/onboarding/tips/index.ts +9 -0
  159. package/src/onboarding/tips/tip-engine.ts +459 -0
  160. package/src/onboarding/tutorial/index.ts +88 -0
  161. package/src/onboarding/tutorial/lessons/basics.ts +151 -0
  162. package/src/onboarding/tutorial/lessons/index.ts +151 -0
  163. package/src/onboarding/tutorial/lessons/modes.ts +230 -0
  164. package/src/onboarding/tutorial/lessons/tools.ts +172 -0
  165. package/src/onboarding/tutorial/progress-tracker.ts +350 -0
  166. package/src/onboarding/tutorial/storage.ts +249 -0
  167. package/src/onboarding/tutorial/tutorial-system.ts +462 -0
  168. package/src/onboarding/tutorial/types.ts +310 -0
  169. package/src/orchestrator-singleton.ts +129 -0
  170. package/src/shutdown.ts +33 -0
  171. package/src/test/e2e/assertions.ts +267 -0
  172. package/src/test/e2e/fixtures.ts +204 -0
  173. package/src/test/e2e/harness.ts +575 -0
  174. package/src/test/e2e/index.ts +57 -0
  175. package/src/test/e2e/types.ts +228 -0
  176. package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
  177. package/src/test/fixtures/fake-response-loader.ts +314 -0
  178. package/src/test/fixtures/index.ts +20 -0
  179. package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
  180. package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
  181. package/src/tui/__tests__/mode-components.test.tsx +395 -0
  182. package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
  183. package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
  184. package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
  185. package/src/tui/adapters/agent-adapter.ts +1008 -0
  186. package/src/tui/adapters/index.ts +48 -0
  187. package/src/tui/adapters/message-adapter.ts +315 -0
  188. package/src/tui/adapters/persistence-bridge.ts +331 -0
  189. package/src/tui/adapters/session-adapter.ts +419 -0
  190. package/src/tui/buffered-stdout.ts +223 -0
  191. package/src/tui/components/AgentProgress.tsx +424 -0
  192. package/src/tui/components/Banner/AsciiArt.ts +160 -0
  193. package/src/tui/components/Banner/Banner.tsx +355 -0
  194. package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
  195. package/src/tui/components/Banner/ShimmerText.tsx +193 -0
  196. package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
  197. package/src/tui/components/Banner/index.ts +61 -0
  198. package/src/tui/components/Banner/useShimmer.ts +241 -0
  199. package/src/tui/components/ChatView.tsx +11 -0
  200. package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
  201. package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
  202. package/src/tui/components/Checkpoint/index.ts +19 -0
  203. package/src/tui/components/CostDisplay.tsx +226 -0
  204. package/src/tui/components/InitErrorBanner.tsx +122 -0
  205. package/src/tui/components/Input/Autocomplete.tsx +603 -0
  206. package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
  207. package/src/tui/components/Input/HighlightedText.tsx +236 -0
  208. package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
  209. package/src/tui/components/Input/TextInput.tsx +1002 -0
  210. package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
  211. package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
  212. package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
  213. package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
  214. package/src/tui/components/Input/highlight.ts +362 -0
  215. package/src/tui/components/Input/index.ts +36 -0
  216. package/src/tui/components/Input/slash-command-utils.ts +135 -0
  217. package/src/tui/components/Layout.tsx +432 -0
  218. package/src/tui/components/McpPanel.tsx +137 -0
  219. package/src/tui/components/MemoryPanel.tsx +448 -0
  220. package/src/tui/components/Messages/CodeBlock.tsx +527 -0
  221. package/src/tui/components/Messages/DiffView.tsx +679 -0
  222. package/src/tui/components/Messages/ImageReference.tsx +89 -0
  223. package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
  224. package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
  225. package/src/tui/components/Messages/MessageBubble.tsx +270 -0
  226. package/src/tui/components/Messages/MessageList.tsx +1719 -0
  227. package/src/tui/components/Messages/StreamingText.tsx +216 -0
  228. package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
  229. package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
  230. package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
  231. package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
  232. package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
  233. package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
  234. package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
  235. package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
  236. package/src/tui/components/Messages/index.ts +25 -0
  237. package/src/tui/components/ModeIndicator.tsx +177 -0
  238. package/src/tui/components/ModeSelector.tsx +216 -0
  239. package/src/tui/components/ModelSelector.tsx +339 -0
  240. package/src/tui/components/OnboardingWizard.tsx +670 -0
  241. package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
  242. package/src/tui/components/RateLimitIndicator.tsx +82 -0
  243. package/src/tui/components/ScreenReaderLayout.tsx +295 -0
  244. package/src/tui/components/SettingsPanel.tsx +643 -0
  245. package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
  246. package/src/tui/components/Sidebar/index.ts +9 -0
  247. package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
  248. package/src/tui/components/Status/index.ts +12 -0
  249. package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
  250. package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
  251. package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
  252. package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
  253. package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
  254. package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
  255. package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
  256. package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
  257. package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
  258. package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
  259. package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
  260. package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
  261. package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
  262. package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
  263. package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
  264. package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
  265. package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
  266. package/src/tui/components/StatusBar/index.ts +60 -0
  267. package/src/tui/components/TipBanner.tsx +115 -0
  268. package/src/tui/components/TodoItem.tsx +208 -0
  269. package/src/tui/components/TodoPanel.tsx +455 -0
  270. package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
  271. package/src/tui/components/Tools/OptionSelector.tsx +160 -0
  272. package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
  273. package/src/tui/components/Tools/ToolParams.tsx +483 -0
  274. package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
  275. package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
  276. package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
  277. package/src/tui/components/Tools/index.ts +21 -0
  278. package/src/tui/components/TrustPrompt.tsx +279 -0
  279. package/src/tui/components/UpdateBanner.tsx +166 -0
  280. package/src/tui/components/VimModeIndicator.tsx +112 -0
  281. package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
  282. package/src/tui/components/backtrack/index.ts +13 -0
  283. package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
  284. package/src/tui/components/common/CostWarning.tsx +294 -0
  285. package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
  286. package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
  287. package/src/tui/components/common/ErrorBoundary.tsx +140 -0
  288. package/src/tui/components/common/GradientText.tsx +224 -0
  289. package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
  290. package/src/tui/components/common/HotkeyHints.tsx +70 -0
  291. package/src/tui/components/common/MaxSizedBox.tsx +354 -0
  292. package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
  293. package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
  294. package/src/tui/components/common/ScrollIndicator.tsx +160 -0
  295. package/src/tui/components/common/Spinner.tsx +342 -0
  296. package/src/tui/components/common/StreamingIndicator.tsx +316 -0
  297. package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
  298. package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
  299. package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
  300. package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
  301. package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
  302. package/src/tui/components/common/VirtualizedList/index.ts +30 -0
  303. package/src/tui/components/common/VirtualizedList/types.ts +107 -0
  304. package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
  305. package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
  306. package/src/tui/components/common/index.ts +110 -0
  307. package/src/tui/components/index.ts +79 -0
  308. package/src/tui/components/session/CheckpointPanel.tsx +323 -0
  309. package/src/tui/components/session/RollbackDialog.tsx +169 -0
  310. package/src/tui/components/session/SessionItem.tsx +136 -0
  311. package/src/tui/components/session/SessionListPanel.tsx +252 -0
  312. package/src/tui/components/session/SessionPicker.tsx +449 -0
  313. package/src/tui/components/session/SessionPreview.tsx +240 -0
  314. package/src/tui/components/session/__tests__/session.test.tsx +408 -0
  315. package/src/tui/components/session/index.ts +28 -0
  316. package/src/tui/components/session/types.ts +116 -0
  317. package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
  318. package/src/tui/components/theme/index.ts +227 -0
  319. package/src/tui/components/theme/tokens.ts +484 -0
  320. package/src/tui/config/defaults.ts +134 -0
  321. package/src/tui/config/index.ts +17 -0
  322. package/src/tui/context/AnimationContext.tsx +284 -0
  323. package/src/tui/context/AppContext.tsx +349 -0
  324. package/src/tui/context/BracketedPasteContext.tsx +372 -0
  325. package/src/tui/context/LspContext.tsx +192 -0
  326. package/src/tui/context/McpContext.tsx +325 -0
  327. package/src/tui/context/MessagesContext.tsx +870 -0
  328. package/src/tui/context/OverflowContext.tsx +213 -0
  329. package/src/tui/context/RateLimitContext.tsx +108 -0
  330. package/src/tui/context/ResilienceContext.tsx +275 -0
  331. package/src/tui/context/RootProvider.tsx +136 -0
  332. package/src/tui/context/ScrollContext.tsx +331 -0
  333. package/src/tui/context/ToolsContext.tsx +702 -0
  334. package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
  335. package/src/tui/context/index.ts +140 -0
  336. package/src/tui/enterprise-integration.ts +282 -0
  337. package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
  338. package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
  339. package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
  340. package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
  341. package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
  342. package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
  343. package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
  344. package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
  345. package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
  346. package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
  347. package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
  348. package/src/tui/hooks/index.ts +252 -0
  349. package/src/tui/hooks/useAgentLoop.ts +712 -0
  350. package/src/tui/hooks/useAlternateBuffer.ts +398 -0
  351. package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
  352. package/src/tui/hooks/useBacktrack.ts +443 -0
  353. package/src/tui/hooks/useBracketedPaste.ts +104 -0
  354. package/src/tui/hooks/useCollapsible.ts +240 -0
  355. package/src/tui/hooks/useCopyMode.ts +382 -0
  356. package/src/tui/hooks/useCostSummary.ts +75 -0
  357. package/src/tui/hooks/useDesktopNotification.ts +414 -0
  358. package/src/tui/hooks/useDiffMode.ts +44 -0
  359. package/src/tui/hooks/useFileChangeStats.ts +110 -0
  360. package/src/tui/hooks/useFileSuggestions.ts +284 -0
  361. package/src/tui/hooks/useFlickerDetector.ts +250 -0
  362. package/src/tui/hooks/useGitStatus.ts +200 -0
  363. package/src/tui/hooks/useHotkeys.ts +579 -0
  364. package/src/tui/hooks/useImagePaste.ts +114 -0
  365. package/src/tui/hooks/useInputHighlight.ts +145 -0
  366. package/src/tui/hooks/useInputHistory.ts +246 -0
  367. package/src/tui/hooks/useKeyboardScroll.ts +209 -0
  368. package/src/tui/hooks/useLineBuffer.ts +356 -0
  369. package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
  370. package/src/tui/hooks/useModeController.ts +167 -0
  371. package/src/tui/hooks/useModeShortcuts.ts +196 -0
  372. package/src/tui/hooks/usePermissionHandler.ts +146 -0
  373. package/src/tui/hooks/usePersistence.ts +480 -0
  374. package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
  375. package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
  376. package/src/tui/hooks/useProviderStatus.ts +270 -0
  377. package/src/tui/hooks/useRateLimitStatus.ts +90 -0
  378. package/src/tui/hooks/useScreenReader.ts +315 -0
  379. package/src/tui/hooks/useScrollController.ts +450 -0
  380. package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
  381. package/src/tui/hooks/useSidebarPanelData.ts +115 -0
  382. package/src/tui/hooks/useSmoothScroll.ts +202 -0
  383. package/src/tui/hooks/useSnapshots.ts +300 -0
  384. package/src/tui/hooks/useStateAndRef.ts +50 -0
  385. package/src/tui/hooks/useTerminalSize.ts +206 -0
  386. package/src/tui/hooks/useToolApprovalController.ts +91 -0
  387. package/src/tui/hooks/useVim.ts +334 -0
  388. package/src/tui/hooks/useWorkspace.ts +56 -0
  389. package/src/tui/i18n/__tests__/init.test.ts +278 -0
  390. package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
  391. package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
  392. package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
  393. package/src/tui/i18n/index.ts +72 -0
  394. package/src/tui/i18n/init.ts +131 -0
  395. package/src/tui/i18n/language-config.ts +106 -0
  396. package/src/tui/i18n/locale-detection.ts +173 -0
  397. package/src/tui/i18n/settings-integration.ts +557 -0
  398. package/src/tui/i18n/tui-namespace.ts +538 -0
  399. package/src/tui/i18n/types.ts +312 -0
  400. package/src/tui/index.ts +43 -0
  401. package/src/tui/lsp-integration.ts +409 -0
  402. package/src/tui/metrics-integration.ts +366 -0
  403. package/src/tui/plugins.ts +383 -0
  404. package/src/tui/resilience.ts +342 -0
  405. package/src/tui/sandbox-integration.ts +317 -0
  406. package/src/tui/services/clipboard.ts +348 -0
  407. package/src/tui/services/fuzzy-search.ts +441 -0
  408. package/src/tui/services/index.ts +72 -0
  409. package/src/tui/services/markdown-renderer.ts +565 -0
  410. package/src/tui/services/open-external.ts +247 -0
  411. package/src/tui/services/syntax-highlighter.ts +483 -0
  412. package/src/tui/slash-commands.ts +12 -0
  413. package/src/tui/theme/index.ts +15 -0
  414. package/src/tui/theme/provider.tsx +206 -0
  415. package/src/tui/tip-integration.ts +300 -0
  416. package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
  417. package/src/tui/types/ink-extended.ts +87 -0
  418. package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
  419. package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
  420. package/src/tui/utils/__tests__/text-width.test.ts +158 -0
  421. package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
  422. package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
  423. package/src/tui/utils/bracketedPaste.ts +107 -0
  424. package/src/tui/utils/cursor-manager.ts +131 -0
  425. package/src/tui/utils/detectTerminal.ts +596 -0
  426. package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
  427. package/src/tui/utils/heightEstimator.ts +198 -0
  428. package/src/tui/utils/index.ts +91 -0
  429. package/src/tui/utils/isNarrowWidth.ts +52 -0
  430. package/src/tui/utils/stdoutGuard.ts +90 -0
  431. package/src/tui/utils/synchronized-update.ts +70 -0
  432. package/src/tui/utils/text-width.ts +225 -0
  433. package/src/tui/utils/textSanitizer.ts +225 -0
  434. package/src/tui/utils/textUtils.ts +114 -0
  435. package/src/tui/utils/ui-sizing.ts +192 -0
  436. package/src/tui-blessed/app.ts +160 -0
  437. package/src/tui-blessed/index.ts +2 -0
  438. package/src/tui-blessed/neo-blessed.d.ts +6 -0
  439. package/src/tui-blessed/test.ts +21 -0
  440. package/src/tui-blessed/types.ts +14 -0
  441. package/src/utils/icons.ts +130 -0
  442. package/src/utils/index.ts +33 -0
  443. package/src/utils/resume-hint.ts +86 -0
  444. package/src/version.ts +1 -0
  445. package/tsconfig.json +8 -0
  446. package/vitest.config.ts +35 -0
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Command Registry
3
+ *
4
+ * Central registry for slash commands with support for:
5
+ * - Priority-based conflict resolution
6
+ * - Alias resolution
7
+ * - Category indexing
8
+ * - Fuzzy search
9
+ *
10
+ * @module cli/commands/registry
11
+ */
12
+
13
+ import type { CommandCategory, CommandKind, SlashCommand } from "./types.js";
14
+
15
+ // =============================================================================
16
+ // T008: CommandConflictError
17
+ // =============================================================================
18
+
19
+ /**
20
+ * Error thrown when two commands with the same priority conflict
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * try {
25
+ * registry.register(commandA);
26
+ * registry.register(commandB); // Same name and priority as A
27
+ * } catch (e) {
28
+ * if (e instanceof CommandConflictError) {
29
+ * console.error(`Conflict: ${e.existingCommand} vs ${e.incomingCommand}`);
30
+ * }
31
+ * }
32
+ * ```
33
+ */
34
+ export class CommandConflictError extends Error {
35
+ /** Name of the command that already exists */
36
+ readonly existingCommand: string;
37
+ /** Name of the command attempting to register */
38
+ readonly incomingCommand: string;
39
+ /** Priority level of the conflicting commands */
40
+ readonly priority: CommandKind;
41
+
42
+ constructor(existingCommand: string, incomingCommand: string, priority: CommandKind) {
43
+ super(
44
+ `Command conflict: "${incomingCommand}" cannot be registered. ` +
45
+ `"${existingCommand}" already exists with same priority (${priority}).`
46
+ );
47
+ this.name = "CommandConflictError";
48
+ this.existingCommand = existingCommand;
49
+ this.incomingCommand = incomingCommand;
50
+ this.priority = priority;
51
+ }
52
+ }
53
+
54
+ // =============================================================================
55
+ // Priority Constants
56
+ // =============================================================================
57
+
58
+ /**
59
+ * Priority values for command kinds
60
+ *
61
+ * Lower number = higher priority.
62
+ * Builtin commands always win over plugin commands, etc.
63
+ */
64
+ const KIND_PRIORITY: Record<CommandKind, number> = {
65
+ builtin: 0,
66
+ plugin: 1,
67
+ mcp: 2,
68
+ user: 3,
69
+ };
70
+
71
+ // =============================================================================
72
+ // T008: CommandRegistry Class
73
+ // =============================================================================
74
+
75
+ /**
76
+ * Central registry for slash commands
77
+ *
78
+ * Manages command registration, lookup, and search with:
79
+ * - Priority-based conflict resolution (lower priority number wins)
80
+ * - Alias resolution for alternative command names
81
+ * - Category indexing for grouped retrieval
82
+ * - Fuzzy search by command name
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * const registry = new CommandRegistry();
87
+ *
88
+ * // Register a builtin command
89
+ * registry.register(helpCommand);
90
+ *
91
+ * // Get by name or alias
92
+ * const cmd = registry.get('help'); // or registry.get('h')
93
+ *
94
+ * // Search commands
95
+ * const matches = registry.search('hel'); // returns [helpCommand]
96
+ *
97
+ * // Get by category
98
+ * const systemCmds = registry.getByCategory('system');
99
+ * ```
100
+ */
101
+ export class CommandRegistry {
102
+ /** Primary command storage: name → command */
103
+ private readonly commands: Map<string, SlashCommand> = new Map();
104
+
105
+ /** Category index: category → set of command names */
106
+ private readonly categoryIndex: Map<CommandCategory, Set<string>> = new Map();
107
+
108
+ /** Alias index: alias → command name */
109
+ private readonly aliasIndex: Map<string, string> = new Map();
110
+
111
+ /**
112
+ * Create a new CommandRegistry
113
+ */
114
+ constructor() {
115
+ // Initialize category index with empty sets for all categories
116
+ const categories: CommandCategory[] = [
117
+ "system",
118
+ "auth",
119
+ "session",
120
+ "navigation",
121
+ "tools",
122
+ "config",
123
+ "debug",
124
+ ];
125
+ for (const category of categories) {
126
+ this.categoryIndex.set(category, new Set());
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Number of registered commands
132
+ */
133
+ get size(): number {
134
+ return this.commands.size;
135
+ }
136
+
137
+ // ===========================================================================
138
+ // T009: Registration with Priority Conflict Resolution
139
+ // ===========================================================================
140
+
141
+ /**
142
+ * Register a command
143
+ *
144
+ * Priority rules:
145
+ * - builtin (0) > plugin (1) > mcp (2) > user (3)
146
+ * - Higher priority (lower number) wins silently
147
+ * - Same priority throws CommandConflictError
148
+ *
149
+ * @param command - Command to register
150
+ * @throws CommandConflictError if same-priority conflict occurs
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * registry.register({
155
+ * name: 'help',
156
+ * kind: 'builtin',
157
+ * category: 'system',
158
+ * description: 'Show help',
159
+ * execute: async () => ({ kind: 'success' }),
160
+ * });
161
+ * ```
162
+ */
163
+ register(command: SlashCommand): void {
164
+ const existing = this.commands.get(command.name);
165
+
166
+ if (existing) {
167
+ const existingPriority = KIND_PRIORITY[existing.kind];
168
+ const incomingPriority = KIND_PRIORITY[command.kind];
169
+
170
+ // Same priority = conflict error
171
+ if (existingPriority === incomingPriority) {
172
+ throw new CommandConflictError(existing.name, command.name, command.kind);
173
+ }
174
+
175
+ // Incoming has lower priority (higher number) = ignore silently
176
+ if (incomingPriority > existingPriority) {
177
+ return;
178
+ }
179
+
180
+ // Incoming has higher priority (lower number) = replace
181
+ // First, clean up old command's indexes
182
+ this.removeFromIndexes(existing);
183
+ }
184
+
185
+ // Register the command
186
+ this.commands.set(command.name, command);
187
+
188
+ // Update category index
189
+ const categorySet = this.categoryIndex.get(command.category);
190
+ if (categorySet) {
191
+ categorySet.add(command.name);
192
+ }
193
+
194
+ // Update alias index
195
+ if (command.aliases) {
196
+ for (const alias of command.aliases) {
197
+ this.aliasIndex.set(alias, command.name);
198
+ }
199
+ }
200
+ }
201
+
202
+ // ===========================================================================
203
+ // T010: Get and Unregister
204
+ // ===========================================================================
205
+
206
+ /**
207
+ * Get a command by name or alias
208
+ *
209
+ * @param name - Command name or alias
210
+ * @returns Command if found, undefined otherwise
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * const cmd = registry.get('help'); // by name
215
+ * const cmd2 = registry.get('h'); // by alias
216
+ * ```
217
+ */
218
+ get(name: string): SlashCommand | undefined {
219
+ // Direct lookup first
220
+ const direct = this.commands.get(name);
221
+ if (direct) {
222
+ return direct;
223
+ }
224
+
225
+ // Try alias lookup
226
+ const resolvedName = this.aliasIndex.get(name);
227
+ if (resolvedName) {
228
+ return this.commands.get(resolvedName);
229
+ }
230
+
231
+ return undefined;
232
+ }
233
+
234
+ /**
235
+ * Unregister a command
236
+ *
237
+ * Removes the command from all indexes (commands, category, aliases).
238
+ *
239
+ * @param name - Command name to unregister
240
+ * @returns true if command was removed, false if not found
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * registry.unregister('help'); // removes help command and its aliases
245
+ * ```
246
+ */
247
+ unregister(name: string): boolean {
248
+ const command = this.commands.get(name);
249
+ if (!command) {
250
+ return false;
251
+ }
252
+
253
+ this.removeFromIndexes(command);
254
+ this.commands.delete(name);
255
+
256
+ return true;
257
+ }
258
+
259
+ /**
260
+ * Remove a command from category and alias indexes
261
+ */
262
+ private removeFromIndexes(command: SlashCommand): void {
263
+ // Remove from category index
264
+ const categorySet = this.categoryIndex.get(command.category);
265
+ if (categorySet) {
266
+ categorySet.delete(command.name);
267
+ }
268
+
269
+ // Remove from alias index
270
+ if (command.aliases) {
271
+ for (const alias of command.aliases) {
272
+ this.aliasIndex.delete(alias);
273
+ }
274
+ }
275
+ }
276
+
277
+ // ===========================================================================
278
+ // T011: Search, GetByCategory, List
279
+ // ===========================================================================
280
+
281
+ /**
282
+ * Search commands by name (fuzzy match)
283
+ *
284
+ * Returns commands where the name includes the query string.
285
+ *
286
+ * @param query - Search query
287
+ * @returns Array of matching commands
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * const matches = registry.search('log'); // returns login, logout
292
+ * ```
293
+ */
294
+ search(query: string): SlashCommand[] {
295
+ const normalizedQuery = query.toLowerCase();
296
+ const results: SlashCommand[] = [];
297
+
298
+ for (const command of this.commands.values()) {
299
+ if (command.name.toLowerCase().includes(normalizedQuery)) {
300
+ results.push(command);
301
+ }
302
+ }
303
+
304
+ return results;
305
+ }
306
+
307
+ /**
308
+ * Get all commands in a category
309
+ *
310
+ * @param category - Category to filter by
311
+ * @returns Set of commands in the category
312
+ *
313
+ * @example
314
+ * ```typescript
315
+ * const authCmds = registry.getByCategory('auth');
316
+ * for (const cmd of authCmds) {
317
+ * console.log(cmd.name);
318
+ * }
319
+ * ```
320
+ */
321
+ getByCategory(category: CommandCategory): Set<SlashCommand> {
322
+ const names = this.categoryIndex.get(category);
323
+ const result = new Set<SlashCommand>();
324
+
325
+ if (names) {
326
+ for (const name of names) {
327
+ const command = this.commands.get(name);
328
+ if (command) {
329
+ result.add(command);
330
+ }
331
+ }
332
+ }
333
+
334
+ return result;
335
+ }
336
+
337
+ /**
338
+ * List all registered commands
339
+ *
340
+ * @returns Array of all commands
341
+ *
342
+ * @example
343
+ * ```typescript
344
+ * const allCommands = registry.list();
345
+ * console.log(`${allCommands.length} commands registered`);
346
+ * ```
347
+ */
348
+ list(): SlashCommand[] {
349
+ return Array.from(this.commands.values());
350
+ }
351
+
352
+ /**
353
+ * Check if a command or alias exists
354
+ *
355
+ * @param name - Command name or alias
356
+ * @returns true if command exists
357
+ */
358
+ has(name: string): boolean {
359
+ return this.commands.has(name) || this.aliasIndex.has(name);
360
+ }
361
+
362
+ /**
363
+ * Clear all registered commands
364
+ *
365
+ * Useful for testing or resetting state.
366
+ */
367
+ clear(): void {
368
+ this.commands.clear();
369
+ this.aliasIndex.clear();
370
+ for (const categorySet of this.categoryIndex.values()) {
371
+ categorySet.clear();
372
+ }
373
+ }
374
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Sandbox Commands
3
+ * @module cli/commands/sandbox
4
+ */
5
+
6
+ import type { CommandResult, SlashCommand } from "../types.js";
7
+ import { success } from "../types.js";
8
+
9
+ export type SandboxSubcommand = "enable" | "disable" | "status";
10
+
11
+ export interface SandboxStatusJson {
12
+ enabled: boolean;
13
+ mode?: string;
14
+ }
15
+
16
+ export interface StatusOptions {
17
+ json?: boolean;
18
+ }
19
+
20
+ export type EnableableBackend = "subprocess" | "platform" | "container";
21
+
22
+ export interface EnableOptions {
23
+ backend?: EnableableBackend;
24
+ force?: boolean;
25
+ }
26
+
27
+ export interface EnableResult {
28
+ success: boolean;
29
+ message?: string;
30
+ backend?: EnableableBackend;
31
+ }
32
+
33
+ /**
34
+ * Sandbox command
35
+ */
36
+ export const sandboxCommand: SlashCommand = {
37
+ name: "sandbox",
38
+ description: "Manage sandbox mode",
39
+ kind: "builtin",
40
+ category: "system",
41
+ execute: async (): Promise<CommandResult> => success("Sandbox command not yet implemented"),
42
+ };
43
+
44
+ /**
45
+ * Sandbox enable command
46
+ */
47
+ export const sandboxEnableCommand: SlashCommand = {
48
+ name: "enable",
49
+ description: "Enable sandbox mode",
50
+ kind: "builtin",
51
+ category: "system",
52
+ execute: async (): Promise<CommandResult> => success("Sandbox enable not yet implemented"),
53
+ };
54
+
55
+ /**
56
+ * Sandbox status command
57
+ */
58
+ export const sandboxStatusCommand: SlashCommand = {
59
+ name: "status",
60
+ description: "Show sandbox status",
61
+ kind: "builtin",
62
+ category: "system",
63
+ execute: async (): Promise<CommandResult> => success("Sandbox status not yet implemented"),
64
+ };
65
+
66
+ /**
67
+ * Create sandbox command
68
+ */
69
+ export function createSandboxCommand(): SlashCommand {
70
+ return sandboxCommand;
71
+ }
72
+
73
+ /**
74
+ * Handle sandbox enable
75
+ */
76
+ export async function handleSandboxEnable(): Promise<void> {
77
+ // Placeholder
78
+ }
79
+
80
+ /**
81
+ * Handle sandbox status
82
+ */
83
+ export async function handleSandboxStatus(_options?: StatusOptions): Promise<void> {
84
+ // Placeholder
85
+ }
86
+
87
+ /**
88
+ * Execute sandbox command
89
+ */
90
+ export async function executeSandbox(
91
+ subcommand: SandboxSubcommand,
92
+ options?: EnableOptions | StatusOptions
93
+ ): Promise<EnableResult | SandboxStatusJson> {
94
+ switch (subcommand) {
95
+ case "enable":
96
+ return executeSandboxEnable(options as EnableOptions);
97
+ case "status":
98
+ return executeSandboxStatus(options as StatusOptions);
99
+ case "disable":
100
+ return { success: true, message: "Sandbox disable not yet implemented" };
101
+ default:
102
+ return { success: false, message: `Unknown sandbox subcommand: ${subcommand}` };
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Execute sandbox enable command
108
+ */
109
+ export async function executeSandboxEnable(_options?: EnableOptions): Promise<EnableResult> {
110
+ return { success: true, message: "Sandbox enable not yet implemented" };
111
+ }
112
+
113
+ /**
114
+ * Execute sandbox status command
115
+ */
116
+ export async function executeSandboxStatus(_options?: StatusOptions): Promise<SandboxStatusJson> {
117
+ return { enabled: false };
118
+ }
119
+
120
+ /**
121
+ * Get help text for sandbox commands
122
+ */
123
+ export function getSandboxHelp(): string {
124
+ return [
125
+ "Sandbox Commands",
126
+ "",
127
+ " /sandbox status Show sandbox status",
128
+ " /sandbox enable Enable sandbox mode",
129
+ " /sandbox disable Disable sandbox mode",
130
+ ].join("\n");
131
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Security Module
3
+ *
4
+ * Exports security-related utilities for input validation and sanitization.
5
+ *
6
+ * @module cli/commands/security
7
+ */
8
+
9
+ export { InputSanitizer } from "./input-sanitizer.js";
10
+ export {
11
+ type CommandSecurityPolicy,
12
+ createPermissionChecker,
13
+ PermissionChecker,
14
+ type PermissionResult,
15
+ } from "./permission-checker.js";
16
+ export {
17
+ createDefaultHandler,
18
+ DEFAULT_SENSITIVE_PATTERNS,
19
+ SensitiveDataHandler,
20
+ type SensitivePattern,
21
+ } from "./sensitive-data.js";
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Input Sanitizer
3
+ *
4
+ * Security utilities for sanitizing user input to prevent:
5
+ * - Shell injection attacks
6
+ * - Path traversal attacks
7
+ * - Command injection
8
+ *
9
+ * @module cli/commands/security/input-sanitizer
10
+ */
11
+
12
+ import path from "node:path";
13
+
14
+ // =============================================================================
15
+ // Constants
16
+ // =============================================================================
17
+
18
+ /**
19
+ * Shell metacharacters that need escaping
20
+ * Includes: | & ; $ ` \ ! ( ) { } [ ] < > * ? # ~
21
+ */
22
+ const SHELL_METACHARACTERS = /[|&;$`\\!(){}[\]<>*?#~]/g;
23
+
24
+ /**
25
+ * Pattern to detect path traversal attempts
26
+ * Matches: ../ ..\ ~/ and absolute paths
27
+ */
28
+ const PATH_TRAVERSAL_PATTERNS = [
29
+ /\.\.[/\\]/, // ../ or ..\
30
+ /^~[/\\]?/, // ~/ or ~\ or just ~
31
+ /^[a-zA-Z]:[/\\]/, // Windows absolute path (C:\, D:/)
32
+ /^[/\\]/, // Unix absolute path
33
+ ];
34
+
35
+ /**
36
+ * Dangerous characters to remove from general input
37
+ */
38
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Need to match control chars for security
39
+ const DANGEROUS_CHARS = /[|&;$`\\!(){}[\]<>*?#~\x00-\x1f\x7f]/g;
40
+
41
+ // =============================================================================
42
+ // InputSanitizer Class
43
+ // =============================================================================
44
+
45
+ /**
46
+ * Provides methods for sanitizing and validating user input.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const sanitizer = new InputSanitizer();
51
+ *
52
+ * // Sanitize general input
53
+ * const clean = sanitizer.sanitize("hello; rm -rf /");
54
+ * // => "hello rm -rf "
55
+ *
56
+ * // Validate paths
57
+ * const isValid = sanitizer.validatePath("../secret", "/app/data");
58
+ * // => false
59
+ *
60
+ * // Escape shell metacharacters
61
+ * const escaped = sanitizer.escapeShellMeta("hello; world");
62
+ * // => "hello\\; world"
63
+ * ```
64
+ */
65
+ export class InputSanitizer {
66
+ /**
67
+ * Sanitizes input by removing dangerous characters.
68
+ *
69
+ * Removes shell metacharacters and control characters that could
70
+ * be used for injection attacks.
71
+ *
72
+ * @param input - The input string to sanitize
73
+ * @returns Sanitized string with dangerous characters removed
74
+ */
75
+ sanitize(input: string): string {
76
+ if (!input) {
77
+ return "";
78
+ }
79
+
80
+ // Remove dangerous characters
81
+ return input.replace(DANGEROUS_CHARS, "");
82
+ }
83
+
84
+ /**
85
+ * Validates that a path does not escape the allowed root directory.
86
+ *
87
+ * Prevents path traversal attacks by:
88
+ * - Rejecting paths with ../ or ..\
89
+ * - Rejecting absolute paths
90
+ * - Rejecting paths starting with ~/
91
+ * - Normalizing and checking the resolved path stays within root
92
+ *
93
+ * @param inputPath - The path to validate
94
+ * @param allowedRoot - The root directory that must contain the resolved path
95
+ * @returns true if path is safe and within allowedRoot, false otherwise
96
+ */
97
+ validatePath(inputPath: string, allowedRoot: string): boolean {
98
+ if (!inputPath || !allowedRoot) {
99
+ return false;
100
+ }
101
+
102
+ // Check for path traversal patterns in raw input
103
+ for (const pattern of PATH_TRAVERSAL_PATTERNS) {
104
+ if (pattern.test(inputPath)) {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ // Normalize both paths for comparison
110
+ const normalizedRoot = path.resolve(allowedRoot);
111
+ const resolvedPath = path.resolve(allowedRoot, inputPath);
112
+
113
+ // Ensure resolved path starts with the allowed root
114
+ // Add path.sep to prevent partial directory name matches
115
+ // e.g., /app/data should not allow /app/data-secret
116
+ return resolvedPath === normalizedRoot || resolvedPath.startsWith(normalizedRoot + path.sep);
117
+ }
118
+
119
+ /**
120
+ * Escapes shell metacharacters in input.
121
+ *
122
+ * Instead of removing characters, this method escapes them with
123
+ * backslashes so they are treated literally by the shell.
124
+ *
125
+ * Characters escaped: | & ; $ ` \ ! ( ) { } [ ] < > * ? # ~
126
+ *
127
+ * @param input - The input string to escape
128
+ * @returns String with shell metacharacters escaped
129
+ */
130
+ escapeShellMeta(input: string): string {
131
+ if (!input) {
132
+ return "";
133
+ }
134
+
135
+ // Escape each metacharacter with a backslash
136
+ return input.replace(SHELL_METACHARACTERS, "\\$&");
137
+ }
138
+
139
+ /**
140
+ * Checks if a string contains any shell metacharacters.
141
+ *
142
+ * @param input - The input string to check
143
+ * @returns true if input contains shell metacharacters
144
+ */
145
+ containsShellMeta(input: string): boolean {
146
+ if (!input) {
147
+ return false;
148
+ }
149
+
150
+ // Use a fresh regex to avoid lastIndex issues with global flag
151
+ const pattern = /[|&;$`\\!(){}[\]<>*?#~]/;
152
+ return pattern.test(input);
153
+ }
154
+
155
+ /**
156
+ * Checks if a path contains traversal patterns.
157
+ *
158
+ * @param inputPath - The path to check
159
+ * @returns true if path contains traversal patterns
160
+ */
161
+ containsPathTraversal(inputPath: string): boolean {
162
+ if (!inputPath) {
163
+ return false;
164
+ }
165
+
166
+ return PATH_TRAVERSAL_PATTERNS.some((pattern) => pattern.test(inputPath));
167
+ }
168
+ }