@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,1220 @@
1
+ /**
2
+ * Skill CLI Commands
3
+ *
4
+ * Provides commands for managing skills:
5
+ * - skill list: List all available skills
6
+ * - skill show: Show details of a specific skill
7
+ * - skill create: Create a new skill from template
8
+ * - skill validate: Validate skill(s)
9
+ *
10
+ * @module cli/commands/skill
11
+ */
12
+
13
+ import * as fs from "node:fs/promises";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+
17
+ import { confirm, input, select } from "@inquirer/prompts";
18
+ import {
19
+ createSkillManager,
20
+ SkillDiscovery,
21
+ type SkillLocation,
22
+ SkillParser,
23
+ type SkillScan,
24
+ type SkillSource,
25
+ type SkillTrigger,
26
+ } from "@vellum/core";
27
+ import chalk from "chalk";
28
+ import { ICONS } from "../utils/icons.js";
29
+ import { EXIT_CODES } from "./exit-codes.js";
30
+ import type { CommandResult } from "./types.js";
31
+ import { error, success } from "./types.js";
32
+
33
+ // =============================================================================
34
+ // Types
35
+ // =============================================================================
36
+
37
+ /**
38
+ * Options for skill list command
39
+ */
40
+ export interface SkillListOptions {
41
+ /** Filter by source (workspace, user, global, builtin) */
42
+ source?: SkillSource;
43
+ /** Output as JSON */
44
+ json?: boolean;
45
+ /** Show full descriptions */
46
+ verbose?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Options for skill show command
51
+ */
52
+ export interface SkillShowOptions {
53
+ /** Show full SKILL.md content */
54
+ content?: boolean;
55
+ /** Output as JSON */
56
+ json?: boolean;
57
+ }
58
+
59
+ /**
60
+ * Options for skill create command
61
+ */
62
+ export interface SkillCreateOptions {
63
+ /** Location to create skill (workspace, user, global) */
64
+ location?: SkillSource;
65
+ /** Non-interactive mode */
66
+ nonInteractive?: boolean;
67
+ /** Force overwrite if exists */
68
+ force?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Options for skill validate command
73
+ */
74
+ export interface SkillValidateOptions {
75
+ /** Validate single skill by name */
76
+ skill?: string;
77
+ /** Treat warnings as errors */
78
+ strict?: boolean;
79
+ /** Output as JSON */
80
+ json?: boolean;
81
+ }
82
+
83
+ /**
84
+ * JSON output for skill list
85
+ */
86
+ interface SkillListJson {
87
+ success: boolean;
88
+ skills: Array<{
89
+ name: string;
90
+ description: string;
91
+ source: SkillSource;
92
+ path: string;
93
+ version?: string;
94
+ tags: string[];
95
+ triggers: Array<{ type: string; pattern?: string }>;
96
+ }>;
97
+ total: number;
98
+ }
99
+
100
+ /**
101
+ * JSON output for skill show
102
+ */
103
+ interface SkillShowJson {
104
+ success: boolean;
105
+ skill: {
106
+ name: string;
107
+ description: string;
108
+ source: SkillSource;
109
+ path: string;
110
+ version?: string;
111
+ priority: number;
112
+ tags: string[];
113
+ dependencies: string[];
114
+ triggers: Array<{ type: string; pattern?: string }>;
115
+ content?: string;
116
+ sections?: {
117
+ rules?: string;
118
+ patterns?: string;
119
+ antiPatterns?: string;
120
+ examples?: string;
121
+ references?: string;
122
+ };
123
+ } | null;
124
+ }
125
+
126
+ /**
127
+ * Validation result for a single skill
128
+ */
129
+ interface SkillValidationResult {
130
+ name: string;
131
+ path: string;
132
+ valid: boolean;
133
+ errors: string[];
134
+ warnings: string[];
135
+ }
136
+
137
+ /**
138
+ * JSON output for skill validate
139
+ */
140
+ interface SkillValidateJson {
141
+ success: boolean;
142
+ results: SkillValidationResult[];
143
+ summary: {
144
+ total: number;
145
+ valid: number;
146
+ invalid: number;
147
+ warnings: number;
148
+ };
149
+ }
150
+
151
+ // =============================================================================
152
+ // Skill Template
153
+ // =============================================================================
154
+
155
+ /**
156
+ * Template for creating new skills
157
+ */
158
+ const SKILL_TEMPLATE = `---
159
+ name: "{name}"
160
+ description: "{description}"
161
+ version: "1.0.0"
162
+ priority: 50
163
+ tags:
164
+ - custom
165
+ triggers:
166
+ - type: keyword
167
+ pattern: "{name}"
168
+ globs:
169
+ - "**/*.ts"
170
+ - "**/*.tsx"
171
+ ---
172
+
173
+ # {name}
174
+
175
+ {description}
176
+
177
+ ## Rules
178
+
179
+ <!-- Define the rules this skill enforces -->
180
+
181
+ - Rule 1: Description of rule
182
+ - Rule 2: Description of rule
183
+
184
+ ## Patterns
185
+
186
+ <!-- Provide code patterns to follow -->
187
+
188
+ \`\`\`typescript
189
+ // Good pattern example
190
+ \`\`\`
191
+
192
+ ## Anti-Patterns
193
+
194
+ <!-- Provide patterns to avoid -->
195
+
196
+ \`\`\`typescript
197
+ // Anti-pattern example - DON'T do this
198
+ \`\`\`
199
+
200
+ ## Examples
201
+
202
+ <!-- Provide usage examples -->
203
+
204
+ ### Example 1
205
+
206
+ Description of the example.
207
+
208
+ ## References
209
+
210
+ <!-- Link to external documentation -->
211
+
212
+ - [Reference Name](https://example.com)
213
+ `;
214
+
215
+ // =============================================================================
216
+ // Helpers
217
+ // =============================================================================
218
+
219
+ /**
220
+ * Get skill source path based on location type
221
+ */
222
+ function getSkillSourcePath(location: SkillSource, workspacePath: string): string {
223
+ switch (location) {
224
+ case "workspace":
225
+ return path.join(workspacePath, ".vellum", "skills");
226
+ case "user":
227
+ return path.join(os.homedir(), ".vellum", "skills");
228
+ case "global":
229
+ return path.join(workspacePath, ".github", "skills");
230
+ default:
231
+ throw new Error(`Cannot create skills in ${location} location`);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Ensure directory exists
237
+ */
238
+ async function ensureDir(dirPath: string): Promise<void> {
239
+ await fs.mkdir(dirPath, { recursive: true });
240
+ }
241
+
242
+ /**
243
+ * Check if file exists
244
+ */
245
+ async function fileExists(filePath: string): Promise<boolean> {
246
+ try {
247
+ await fs.access(filePath);
248
+ return true;
249
+ } catch {
250
+ return false;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Format skill source with color
256
+ */
257
+ function formatSource(source: SkillSource): string {
258
+ const colors: Record<SkillSource, (s: string) => string> = {
259
+ workspace: chalk.green,
260
+ user: chalk.blue,
261
+ global: chalk.yellow,
262
+ plugin: chalk.magenta,
263
+ builtin: chalk.gray,
264
+ };
265
+ const colorFn = colors[source] ?? chalk.white;
266
+ return colorFn(source);
267
+ }
268
+
269
+ /**
270
+ * Truncate string with ellipsis
271
+ */
272
+ function truncate(str: string, maxLength: number): string {
273
+ if (str.length <= maxLength) return str;
274
+ return `${str.slice(0, maxLength - 3)}...`;
275
+ }
276
+
277
+ // =============================================================================
278
+ // List Command (T034)
279
+ // =============================================================================
280
+
281
+ /**
282
+ * Execute skill list command
283
+ */
284
+ export async function handleSkillList(options: SkillListOptions = {}): Promise<CommandResult> {
285
+ try {
286
+ const manager = createSkillManager({
287
+ loader: { discovery: { workspacePath: process.cwd() } },
288
+ });
289
+
290
+ await manager.initialize();
291
+ let skills = manager.getAllSkills();
292
+
293
+ // Filter by source if specified
294
+ if (options.source) {
295
+ skills = skills.filter((s: SkillScan) => s.source === options.source);
296
+ }
297
+
298
+ // JSON output
299
+ if (options.json) {
300
+ const output: SkillListJson = {
301
+ success: true,
302
+ skills: skills.map((s: SkillScan) => ({
303
+ name: s.name,
304
+ description: s.description,
305
+ source: s.source,
306
+ path: s.path,
307
+ version: s.version,
308
+ tags: s.tags,
309
+ triggers: s.triggers.map((t: SkillTrigger) => ({
310
+ type: t.type,
311
+ pattern: t.pattern,
312
+ })),
313
+ })),
314
+ total: skills.length,
315
+ };
316
+ return success(JSON.stringify(output, null, 2));
317
+ }
318
+
319
+ // Table output
320
+ if (skills.length === 0) {
321
+ return success(chalk.yellow("No skills found."));
322
+ }
323
+
324
+ const lines: string[] = [];
325
+ lines.push(chalk.bold.cyan("\n📚 Available Skills\n"));
326
+
327
+ // Header
328
+ const nameWidth = 25;
329
+ const sourceWidth = 10;
330
+ const descWidth = options.verbose ? 60 : 40;
331
+
332
+ lines.push(
333
+ chalk.gray(
334
+ `${"Name".padEnd(nameWidth)} ${"Source".padEnd(sourceWidth)} ${"Description".padEnd(descWidth)}`
335
+ )
336
+ );
337
+ lines.push(chalk.gray("─".repeat(nameWidth + sourceWidth + descWidth + 2)));
338
+
339
+ // Rows
340
+ for (const skill of skills) {
341
+ const name = chalk.white(truncate(skill.name, nameWidth).padEnd(nameWidth));
342
+ const source = formatSource(skill.source).padEnd(sourceWidth + 10); // Account for ANSI codes
343
+ const desc = truncate(skill.description, descWidth);
344
+
345
+ lines.push(`${name} ${source} ${desc}`);
346
+
347
+ // Verbose mode: show triggers
348
+ if (options.verbose && skill.triggers.length > 0) {
349
+ for (const trigger of skill.triggers) {
350
+ const triggerStr =
351
+ trigger.type === "always"
352
+ ? chalk.gray(" └─ always active")
353
+ : chalk.gray(` └─ ${trigger.type}: ${trigger.pattern}`);
354
+ lines.push(triggerStr);
355
+ }
356
+ }
357
+ }
358
+
359
+ lines.push(chalk.gray(`\nTotal: ${skills.length} skill(s)`));
360
+
361
+ return success(lines.join("\n"));
362
+ } catch (err) {
363
+ const message = err instanceof Error ? err.message : String(err);
364
+ return error("INTERNAL_ERROR", `Failed to list skills: ${message}`);
365
+ }
366
+ }
367
+
368
+ // =============================================================================
369
+ // Show Command (T035)
370
+ // =============================================================================
371
+
372
+ /** Format skill data as JSON output */
373
+ function formatSkillShowJson(
374
+ scan: SkillScan,
375
+ loaded: Awaited<ReturnType<ReturnType<typeof createSkillManager>["loadSkill"]>> | null,
376
+ options: SkillShowOptions
377
+ ): SkillShowJson {
378
+ return {
379
+ success: true,
380
+ skill: {
381
+ name: scan.name,
382
+ description: scan.description,
383
+ source: scan.source,
384
+ path: scan.path,
385
+ version: scan.version,
386
+ priority: scan.priority,
387
+ tags: scan.tags,
388
+ dependencies: scan.dependencies,
389
+ triggers: scan.triggers.map((t: SkillTrigger) => ({
390
+ type: t.type,
391
+ pattern: t.pattern,
392
+ })),
393
+ content: options.content ? loaded?.raw : undefined,
394
+ sections: loaded
395
+ ? {
396
+ rules: loaded.rules || undefined,
397
+ patterns: loaded.patterns || undefined,
398
+ antiPatterns: loaded.antiPatterns || undefined,
399
+ examples: loaded.examples || undefined,
400
+ references: loaded.referencesSection || undefined,
401
+ }
402
+ : undefined,
403
+ },
404
+ };
405
+ }
406
+
407
+ /** Format skill metadata lines */
408
+ function formatSkillMetadataLines(scan: SkillScan): string[] {
409
+ const lines: string[] = [];
410
+ lines.push(chalk.bold.cyan(`\n📖 Skill: ${scan.name}\n`));
411
+ lines.push(`${chalk.white("Description:")} ${scan.description}`);
412
+ lines.push(`${chalk.white("Source:")} ${formatSource(scan.source)}`);
413
+ lines.push(`${chalk.white("Path:")} ${chalk.gray(scan.path)}`);
414
+ lines.push(`${chalk.white("Priority:")} ${scan.priority}`);
415
+
416
+ if (scan.version) {
417
+ lines.push(`${chalk.white("Version:")} ${scan.version}`);
418
+ }
419
+ if (scan.tags.length > 0) {
420
+ lines.push(`${chalk.white("Tags:")} ${scan.tags.map((t: string) => chalk.cyan(t)).join(", ")}`);
421
+ }
422
+ if (scan.dependencies.length > 0) {
423
+ lines.push(
424
+ chalk.white("Dependencies:") +
425
+ " " +
426
+ scan.dependencies.map((d: string) => chalk.yellow(d)).join(", ")
427
+ );
428
+ }
429
+ return lines;
430
+ }
431
+
432
+ /** Format skill triggers as lines */
433
+ function formatSkillTriggersLines(triggers: SkillTrigger[]): string[] {
434
+ const lines: string[] = [chalk.white("\nTriggers:")];
435
+ for (const trigger of triggers) {
436
+ if (trigger.type === "always") {
437
+ lines.push(chalk.gray(" • always active"));
438
+ } else {
439
+ lines.push(chalk.gray(` • ${trigger.type}: ${trigger.pattern}`));
440
+ }
441
+ }
442
+ return lines;
443
+ }
444
+
445
+ /** Format skill content/sections as lines */
446
+ function formatSkillContentLines(
447
+ loaded: Awaited<ReturnType<ReturnType<typeof createSkillManager>["loadSkill"]>> | null,
448
+ showContent: boolean
449
+ ): string[] {
450
+ const lines: string[] = [];
451
+ if (showContent && loaded) {
452
+ lines.push(chalk.white("\n─── SKILL.md Content ───\n"));
453
+ lines.push(loaded.raw);
454
+ } else if (loaded) {
455
+ lines.push(chalk.white("\nSections:"));
456
+ if (loaded.rules) lines.push(chalk.gray(" • Rules (✓)"));
457
+ if (loaded.patterns) lines.push(chalk.gray(" • Patterns (✓)"));
458
+ if (loaded.antiPatterns) lines.push(chalk.gray(" • Anti-Patterns (✓)"));
459
+ if (loaded.examples) lines.push(chalk.gray(" • Examples (✓)"));
460
+ if (loaded.referencesSection) lines.push(chalk.gray(" • References (✓)"));
461
+ lines.push(chalk.gray("\nUse --content to show full SKILL.md content"));
462
+ }
463
+ return lines;
464
+ }
465
+
466
+ /**
467
+ * Execute skill show command
468
+ */
469
+ export async function handleSkillShow(
470
+ name: string,
471
+ options: SkillShowOptions = {}
472
+ ): Promise<CommandResult> {
473
+ try {
474
+ const manager = createSkillManager({
475
+ loader: { discovery: { workspacePath: process.cwd() } },
476
+ });
477
+
478
+ await manager.initialize();
479
+ const scan = manager.getSkill(name);
480
+
481
+ if (!scan) {
482
+ if (options.json) {
483
+ const output: SkillShowJson = { success: false, skill: null };
484
+ return error("RESOURCE_NOT_FOUND", JSON.stringify(output, null, 2));
485
+ }
486
+ return error("RESOURCE_NOT_FOUND", chalk.red(`Skill not found: ${name}`));
487
+ }
488
+
489
+ const loaded = await manager.loadSkill(name);
490
+
491
+ if (options.json) {
492
+ return success(JSON.stringify(formatSkillShowJson(scan, loaded, options), null, 2));
493
+ }
494
+
495
+ const lines: string[] = [
496
+ ...formatSkillMetadataLines(scan),
497
+ ...formatSkillTriggersLines(scan.triggers),
498
+ ...formatSkillContentLines(loaded, options.content ?? false),
499
+ ];
500
+
501
+ return success(lines.join("\n"));
502
+ } catch (err) {
503
+ const message = err instanceof Error ? err.message : String(err);
504
+ return error("INTERNAL_ERROR", `Failed to show skill: ${message}`);
505
+ }
506
+ }
507
+
508
+ // =============================================================================
509
+ // Create Command (T036)
510
+ // =============================================================================
511
+
512
+ /**
513
+ * Execute skill create command
514
+ */
515
+ export async function handleSkillCreate(
516
+ name: string,
517
+ options: SkillCreateOptions = {}
518
+ ): Promise<{ success: boolean; path?: string; error?: string; exitCode: number }> {
519
+ try {
520
+ const workspacePath = process.cwd();
521
+ let location = options.location;
522
+
523
+ // Interactive location selection
524
+ if (!location && !options.nonInteractive) {
525
+ console.log(chalk.bold.blue("\n🛠️ Create New Skill\n"));
526
+
527
+ location = await select({
528
+ message: "Where would you like to create the skill?",
529
+ choices: [
530
+ { name: "Workspace (.vellum/skills/) - Project-specific", value: "workspace" as const },
531
+ { name: "User (~/.vellum/skills/) - Available across projects", value: "user" as const },
532
+ { name: "Global (.github/skills/) - Claude compatibility", value: "global" as const },
533
+ ],
534
+ });
535
+ }
536
+
537
+ // Default to workspace
538
+ location = location ?? "workspace";
539
+
540
+ // Validate location
541
+ if (location === "builtin") {
542
+ console.error(chalk.red("Cannot create skills in builtin location"));
543
+ return { success: false, error: "Invalid location", exitCode: EXIT_CODES.ERROR };
544
+ }
545
+
546
+ // Get target path
547
+ const skillsDir = getSkillSourcePath(location, workspacePath);
548
+ const skillDir = path.join(skillsDir, name);
549
+ const manifestPath = path.join(skillDir, "SKILL.md");
550
+
551
+ // Check if skill already exists
552
+ if ((await fileExists(manifestPath)) && !options.force) {
553
+ if (options.nonInteractive) {
554
+ console.error(chalk.red(`Skill already exists: ${name}. Use --force to overwrite.`));
555
+ return { success: false, error: "Skill already exists", exitCode: EXIT_CODES.ERROR };
556
+ }
557
+
558
+ const shouldOverwrite = await confirm({
559
+ message: `Skill "${name}" already exists at ${skillDir}. Overwrite?`,
560
+ default: false,
561
+ });
562
+
563
+ if (!shouldOverwrite) {
564
+ console.log(chalk.gray("Aborted."));
565
+ return { success: false, error: "Aborted by user", exitCode: EXIT_CODES.SUCCESS };
566
+ }
567
+ }
568
+
569
+ // Get description
570
+ let description = "A custom skill";
571
+ if (!options.nonInteractive) {
572
+ description = await input({
573
+ message: "Brief description of the skill:",
574
+ default: description,
575
+ });
576
+ }
577
+
578
+ // Generate skill content from template
579
+ const content = SKILL_TEMPLATE.replace(/\{name\}/g, name).replace(
580
+ /\{description\}/g,
581
+ description
582
+ );
583
+
584
+ // Create directories and file
585
+ await ensureDir(skillDir);
586
+ await fs.writeFile(manifestPath, content, "utf-8");
587
+
588
+ // Create optional subdirectories
589
+ await ensureDir(path.join(skillDir, "scripts"));
590
+ await ensureDir(path.join(skillDir, "references"));
591
+
592
+ console.log(chalk.green(`\n${ICONS.success} Created skill: ${name}`));
593
+ console.log(chalk.gray(` Path: ${skillDir}`));
594
+ console.log(chalk.gray("\n Next steps:"));
595
+ console.log(chalk.gray(` 1. Edit ${manifestPath}`));
596
+ console.log(chalk.gray(` 2. Add scripts to ${path.join(skillDir, "scripts")}`));
597
+ console.log(chalk.gray(` 3. Run 'vellum skill validate --skill ${name}' to verify`));
598
+
599
+ return { success: true, path: skillDir, exitCode: EXIT_CODES.SUCCESS };
600
+ } catch (err) {
601
+ const message = err instanceof Error ? err.message : String(err);
602
+ console.error(chalk.red(`\n${ICONS.error} Failed to create skill: ${message}`));
603
+ return { success: false, error: message, exitCode: EXIT_CODES.ERROR };
604
+ }
605
+ }
606
+
607
+ // =============================================================================
608
+ // Validate Command (T037)
609
+ // =============================================================================
610
+
611
+ /**
612
+ * Validate a single skill
613
+ */
614
+ async function validateSingleSkill(
615
+ skillPath: string,
616
+ parser: SkillParser,
617
+ strict: boolean
618
+ ): Promise<SkillValidationResult> {
619
+ const errors: string[] = [];
620
+ const warnings: string[] = [];
621
+ const name = path.basename(skillPath);
622
+
623
+ try {
624
+ const manifestPath = path.join(skillPath, "SKILL.md");
625
+
626
+ // Check if SKILL.md exists
627
+ if (!(await fileExists(manifestPath))) {
628
+ errors.push("SKILL.md not found");
629
+ return { name, path: skillPath, valid: false, errors, warnings };
630
+ }
631
+
632
+ // Parse the skill
633
+ const result = await parser.parseMetadata(manifestPath, "workspace");
634
+
635
+ if (!result) {
636
+ errors.push("Failed to parse SKILL.md");
637
+ return { name, path: skillPath, valid: false, errors, warnings };
638
+ }
639
+
640
+ // Check required fields
641
+ if (!result.name || result.name.trim() === "") {
642
+ errors.push("Missing required field: name");
643
+ }
644
+
645
+ if (!result.description || result.description.trim() === "") {
646
+ warnings.push("Missing description");
647
+ }
648
+
649
+ if (!result.triggers || result.triggers.length === 0) {
650
+ warnings.push("No triggers defined - skill will only activate with 'always' trigger");
651
+ }
652
+
653
+ // Validate triggers
654
+ for (const trigger of result.triggers || []) {
655
+ if (trigger.type !== "always" && !trigger.pattern) {
656
+ errors.push(`Trigger of type '${trigger.type}' must have a pattern`);
657
+ }
658
+ }
659
+
660
+ // In strict mode, warnings become errors
661
+ if (strict && warnings.length > 0) {
662
+ errors.push(...warnings.map((w) => `[strict] ${w}`));
663
+ warnings.length = 0;
664
+ }
665
+
666
+ return {
667
+ name: result.name || name,
668
+ path: skillPath,
669
+ valid: errors.length === 0,
670
+ errors,
671
+ warnings,
672
+ };
673
+ } catch (err) {
674
+ const message = err instanceof Error ? err.message : String(err);
675
+ errors.push(`Parse error: ${message}`);
676
+ return { name, path: skillPath, valid: false, errors, warnings };
677
+ }
678
+ }
679
+
680
+ /** Calculate validation summary from results */
681
+ function calculateValidationSummary(results: SkillValidationResult[]) {
682
+ return {
683
+ total: results.length,
684
+ valid: results.filter((r) => r.valid).length,
685
+ invalid: results.filter((r) => !r.valid).length,
686
+ warnings: results.reduce((acc, r) => acc + r.warnings.length, 0),
687
+ };
688
+ }
689
+
690
+ /** Format validation results as text lines */
691
+ function formatValidationResultLines(results: SkillValidationResult[]): string[] {
692
+ const lines: string[] = [];
693
+ for (const result of results) {
694
+ const icon = result.valid ? chalk.green(ICONS.success) : chalk.red(ICONS.error);
695
+ lines.push(`${icon} ${chalk.white(result.name)}`);
696
+ lines.push(chalk.gray(` ${result.path}`));
697
+
698
+ for (const err of result.errors) {
699
+ lines.push(chalk.red(` x ${err}`));
700
+ }
701
+ for (const warn of result.warnings) {
702
+ lines.push(chalk.yellow(` ${ICONS.warning} ${warn}`));
703
+ }
704
+ lines.push("");
705
+ }
706
+ return lines;
707
+ }
708
+
709
+ /** Format validation summary as text lines */
710
+ function formatValidationSummaryLines(
711
+ summary: ReturnType<typeof calculateValidationSummary>
712
+ ): string[] {
713
+ const lines: string[] = [];
714
+ lines.push(chalk.gray("─".repeat(50)));
715
+ lines.push(
716
+ `${chalk.white("Total:")} ${summary.total} ` +
717
+ `${chalk.green("Valid:")} ${summary.valid} ` +
718
+ `${chalk.red("Invalid:")} ${summary.invalid} ` +
719
+ `${chalk.yellow("Warnings:")} ${summary.warnings}`
720
+ );
721
+
722
+ const allValid = summary.invalid === 0;
723
+ lines.push(
724
+ allValid
725
+ ? chalk.green(`\n${ICONS.success} All skills are valid!`)
726
+ : chalk.red(`\n${ICONS.error} Some skills have errors.`)
727
+ );
728
+ return lines;
729
+ }
730
+
731
+ /**
732
+ * Execute skill validate command
733
+ */
734
+ export async function handleSkillValidate(
735
+ options: SkillValidateOptions = {}
736
+ ): Promise<CommandResult> {
737
+ try {
738
+ const workspacePath = process.cwd();
739
+ const parser = new SkillParser();
740
+ const discovery = new SkillDiscovery({ workspacePath });
741
+ const discovered = await discovery.discoverAll();
742
+ const results: SkillValidationResult[] = [];
743
+
744
+ if (options.skill) {
745
+ const skillLocation = discovered.deduplicated.find(
746
+ (loc: SkillLocation) => path.basename(loc.path) === options.skill
747
+ );
748
+
749
+ if (!skillLocation) {
750
+ if (options.json) {
751
+ const output: SkillValidateJson = {
752
+ success: false,
753
+ results: [],
754
+ summary: { total: 0, valid: 0, invalid: 1, warnings: 0 },
755
+ };
756
+ return error("RESOURCE_NOT_FOUND", JSON.stringify(output, null, 2));
757
+ }
758
+ return error("RESOURCE_NOT_FOUND", chalk.red(`Skill not found: ${options.skill}`));
759
+ }
760
+
761
+ results.push(await validateSingleSkill(skillLocation.path, parser, options.strict ?? false));
762
+ } else {
763
+ for (const location of discovered.deduplicated) {
764
+ results.push(await validateSingleSkill(location.path, parser, options.strict ?? false));
765
+ }
766
+ }
767
+
768
+ const summary = calculateValidationSummary(results);
769
+
770
+ if (options.json) {
771
+ const output: SkillValidateJson = { success: summary.invalid === 0, results, summary };
772
+ return summary.invalid === 0
773
+ ? success(JSON.stringify(output, null, 2))
774
+ : error("INVALID_ARGUMENT", JSON.stringify(output, null, 2));
775
+ }
776
+
777
+ const lines: string[] = [chalk.bold.cyan(`\n[Skill] Validation Results\n`)];
778
+
779
+ if (results.length === 0) {
780
+ lines.push(chalk.yellow("No skills found to validate."));
781
+ return success(lines.join("\n"));
782
+ }
783
+
784
+ lines.push(...formatValidationResultLines(results));
785
+ lines.push(...formatValidationSummaryLines(summary));
786
+
787
+ return summary.invalid === 0
788
+ ? success(lines.join("\n"))
789
+ : error("INVALID_ARGUMENT", lines.join("\n"));
790
+ } catch (err) {
791
+ const message = err instanceof Error ? err.message : String(err);
792
+ return error("INTERNAL_ERROR", `Failed to validate skills: ${message}`);
793
+ }
794
+ }
795
+
796
+ // =============================================================================
797
+ // Migrate Command (T052)
798
+ // =============================================================================
799
+
800
+ /**
801
+ * Supported migration sources
802
+ */
803
+ export type MigrationSource = "claude" | "roo";
804
+
805
+ /**
806
+ * Options for skill migrate command
807
+ */
808
+ export interface SkillMigrateOptions {
809
+ /** Source format to migrate from */
810
+ from: MigrationSource;
811
+ /** Target location for migrated skills */
812
+ location?: SkillSource;
813
+ /** Output as JSON */
814
+ json?: boolean;
815
+ /** Dry run (don't actually write files) */
816
+ dryRun?: boolean;
817
+ }
818
+
819
+ /**
820
+ * JSON output for skill migrate
821
+ */
822
+ interface SkillMigrateJson {
823
+ success: boolean;
824
+ source: MigrationSource;
825
+ migrated: Array<{
826
+ originalPath: string;
827
+ targetPath: string;
828
+ name: string;
829
+ }>;
830
+ errors: Array<{
831
+ path: string;
832
+ error: string;
833
+ }>;
834
+ summary: {
835
+ total: number;
836
+ migrated: number;
837
+ failed: number;
838
+ };
839
+ }
840
+
841
+ /**
842
+ * Get source paths based on migration source type
843
+ */
844
+ function getMigrationSourcePaths(source: MigrationSource, workspacePath: string): string[] {
845
+ switch (source) {
846
+ case "claude":
847
+ return [
848
+ // Claude Code skill locations
849
+ path.join(workspacePath, ".github", "skills"),
850
+ path.join(workspacePath, ".claude", "skills"),
851
+ path.join(os.homedir(), ".claude", "skills"),
852
+ ];
853
+ case "roo":
854
+ return [
855
+ // Roo Code skill locations
856
+ path.join(workspacePath, ".roo", "skills"),
857
+ path.join(os.homedir(), ".roo", "skills"),
858
+ ];
859
+ default:
860
+ return [];
861
+ }
862
+ }
863
+
864
+ /**
865
+ * Check if a SKILL.md needs migration (missing Vellum-specific fields)
866
+ */
867
+ async function needsMigration(manifestPath: string): Promise<boolean> {
868
+ try {
869
+ const content = await fs.readFile(manifestPath, "utf-8");
870
+ // Check if it has Vellum-specific frontmatter fields
871
+ const hasVellumFields = content.includes("priority:") && content.includes("triggers:");
872
+ return !hasVellumFields;
873
+ } catch {
874
+ return false;
875
+ }
876
+ }
877
+
878
+ /**
879
+ * Parse old skill format and convert to Vellum format
880
+ */
881
+ async function convertSkillFormat(manifestPath: string, source: MigrationSource): Promise<string> {
882
+ const content = await fs.readFile(manifestPath, "utf-8");
883
+
884
+ // Parse existing frontmatter
885
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
886
+ if (!frontmatterMatch) {
887
+ // No frontmatter, create new skill from content
888
+ const skillName = path.basename(path.dirname(manifestPath));
889
+ const firstLine = content.split("\n").find((l) => l.startsWith("# "));
890
+ const title = firstLine?.replace(/^#\s*/, "") ?? skillName;
891
+
892
+ return `---
893
+ name: "${skillName}"
894
+ description: "${title}"
895
+ version: "1.0.0"
896
+ priority: 50
897
+ tags:
898
+ - migrated
899
+ - ${source}
900
+ triggers:
901
+ - type: keyword
902
+ pattern: "${skillName}"
903
+ globs:
904
+ - "**/*"
905
+ ---
906
+
907
+ ${content}`;
908
+ }
909
+
910
+ // Parse existing YAML frontmatter - guaranteed to exist after the regex match
911
+ const frontmatter = frontmatterMatch[1] ?? "";
912
+ const body = content.slice(frontmatterMatch[0].length).trim();
913
+
914
+ // Extract existing fields
915
+ const nameMatch = frontmatter.match(/^name:\s*["']?(.+?)["']?\s*$/m);
916
+
917
+ const name = nameMatch?.[1] ?? path.basename(path.dirname(manifestPath));
918
+
919
+ // Check for existing triggers or globs
920
+ const hasExistingTriggers = frontmatter.includes("triggers:");
921
+ const hasExistingGlobs = frontmatter.includes("globs:");
922
+ const hasExistingPriority = frontmatter.includes("priority:");
923
+
924
+ // Build new frontmatter
925
+ let newFrontmatter = frontmatter.trim();
926
+
927
+ // Add priority if missing
928
+ if (!hasExistingPriority) {
929
+ newFrontmatter += `\npriority: 50`;
930
+ }
931
+
932
+ // Add tags
933
+ if (!frontmatter.includes("tags:")) {
934
+ newFrontmatter += `\ntags:\n - migrated\n - ${source}`;
935
+ }
936
+
937
+ // Add triggers if missing
938
+ if (!hasExistingTriggers) {
939
+ const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
940
+ newFrontmatter += `\ntriggers:\n - type: keyword\n pattern: "${safeName}"`;
941
+ }
942
+
943
+ // Add globs if missing
944
+ if (!hasExistingGlobs) {
945
+ newFrontmatter += `\nglobs:\n - "**/*"`;
946
+ }
947
+
948
+ return `---
949
+ ${newFrontmatter}
950
+ ---
951
+
952
+ ${body}`;
953
+ }
954
+
955
+ /**
956
+ * Migration result for a single skill
957
+ */
958
+ interface MigrationEntry {
959
+ originalPath: string;
960
+ targetPath: string;
961
+ name: string;
962
+ }
963
+
964
+ /**
965
+ * Migration error for a single skill
966
+ */
967
+ interface MigrationError {
968
+ path: string;
969
+ error: string;
970
+ }
971
+
972
+ /**
973
+ * Migrate a single skill directory
974
+ */
975
+ async function migrateSkillDirectory(
976
+ skillDir: string,
977
+ manifestPath: string,
978
+ targetBasePath: string,
979
+ source: MigrationSource,
980
+ dryRun: boolean
981
+ ): Promise<{ entry?: MigrationEntry; error?: MigrationError }> {
982
+ const skillName = path.basename(skillDir);
983
+ const targetPath = path.join(targetBasePath, skillName);
984
+ const targetManifest = path.join(targetPath, "SKILL.md");
985
+
986
+ // Skip if target already exists (unless it's the same location)
987
+ if ((await fileExists(targetManifest)) && targetPath !== skillDir) {
988
+ return {
989
+ error: { path: manifestPath, error: `Target already exists: ${targetPath}` },
990
+ };
991
+ }
992
+
993
+ try {
994
+ const needsConversion = await needsMigration(manifestPath);
995
+ const convertedContent = needsConversion
996
+ ? await convertSkillFormat(manifestPath, source)
997
+ : await fs.readFile(manifestPath, "utf-8");
998
+
999
+ if (!dryRun) {
1000
+ await ensureDir(targetPath);
1001
+ await fs.writeFile(targetManifest, convertedContent, "utf-8");
1002
+
1003
+ // Copy subdirectories
1004
+ for (const subdir of ["scripts", "references", "assets"]) {
1005
+ const sourceSubdir = path.join(skillDir, subdir);
1006
+ const targetSubdir = path.join(targetPath, subdir);
1007
+ if (await fileExists(sourceSubdir)) {
1008
+ await fs.cp(sourceSubdir, targetSubdir, { recursive: true });
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ return {
1014
+ entry: { originalPath: manifestPath, targetPath: targetManifest, name: skillName },
1015
+ };
1016
+ } catch (err) {
1017
+ const message = err instanceof Error ? err.message : String(err);
1018
+ return { error: { path: manifestPath, error: message } };
1019
+ }
1020
+ }
1021
+
1022
+ /**
1023
+ * Scan a source path for skills to migrate
1024
+ */
1025
+ async function scanSourcePathForSkills(
1026
+ sourcePath: string,
1027
+ targetBasePath: string,
1028
+ source: MigrationSource,
1029
+ dryRun: boolean
1030
+ ): Promise<{ migrated: MigrationEntry[]; errors: MigrationError[] }> {
1031
+ const migrated: MigrationEntry[] = [];
1032
+ const errors: MigrationError[] = [];
1033
+
1034
+ if (!(await fileExists(sourcePath))) {
1035
+ return { migrated, errors };
1036
+ }
1037
+
1038
+ try {
1039
+ const entries = await fs.readdir(sourcePath, { withFileTypes: true });
1040
+
1041
+ for (const entry of entries) {
1042
+ if (!entry.isDirectory() || entry.name.startsWith(".")) {
1043
+ continue;
1044
+ }
1045
+
1046
+ const skillDir = path.join(sourcePath, entry.name);
1047
+ const manifestPath = path.join(skillDir, "SKILL.md");
1048
+
1049
+ if (!(await fileExists(manifestPath))) {
1050
+ continue;
1051
+ }
1052
+
1053
+ const result = await migrateSkillDirectory(
1054
+ skillDir,
1055
+ manifestPath,
1056
+ targetBasePath,
1057
+ source,
1058
+ dryRun
1059
+ );
1060
+
1061
+ if (result.entry) {
1062
+ migrated.push(result.entry);
1063
+ }
1064
+ if (result.error) {
1065
+ errors.push(result.error);
1066
+ }
1067
+ }
1068
+ } catch (err) {
1069
+ const message = err instanceof Error ? err.message : String(err);
1070
+ errors.push({ path: sourcePath, error: message });
1071
+ }
1072
+
1073
+ return { migrated, errors };
1074
+ }
1075
+
1076
+ /**
1077
+ * Format migration output as text
1078
+ */
1079
+ function formatMigrationOutput(
1080
+ sourcePaths: string[],
1081
+ migrated: MigrationEntry[],
1082
+ errors: MigrationError[],
1083
+ targetBasePath: string,
1084
+ source: MigrationSource,
1085
+ dryRun: boolean
1086
+ ): string {
1087
+ const lines: string[] = [];
1088
+ const modeLabel = dryRun ? " (dry run)" : "";
1089
+ lines.push(chalk.bold.cyan(`\n${ICONS.migrate} Skill Migration from ${source}${modeLabel}\n`));
1090
+
1091
+ if (migrated.length === 0 && errors.length === 0) {
1092
+ lines.push(chalk.yellow(`No ${source} skills found to migrate.`));
1093
+ lines.push(chalk.gray("\nSearched locations:"));
1094
+ for (const p of sourcePaths) {
1095
+ lines.push(chalk.gray(` • ${p}`));
1096
+ }
1097
+ return lines.join("\n");
1098
+ }
1099
+
1100
+ if (migrated.length > 0) {
1101
+ lines.push(chalk.green(`${ICONS.success} Migrated Skills:`));
1102
+ for (const m of migrated) {
1103
+ lines.push(chalk.white(` ${ICONS.bullet} ${m.name}`));
1104
+ lines.push(chalk.gray(` ${m.originalPath}`));
1105
+ lines.push(chalk.gray(` -> ${m.targetPath}`));
1106
+ }
1107
+ lines.push("");
1108
+ }
1109
+
1110
+ if (errors.length > 0) {
1111
+ lines.push(chalk.red(`${ICONS.error} Failed:`));
1112
+ for (const e of errors) {
1113
+ lines.push(chalk.red(` ${ICONS.bullet} ${e.path}`));
1114
+ lines.push(chalk.red(` ${e.error}`));
1115
+ }
1116
+ lines.push("");
1117
+ }
1118
+
1119
+ const summary = {
1120
+ total: migrated.length + errors.length,
1121
+ migrated: migrated.length,
1122
+ failed: errors.length,
1123
+ };
1124
+ lines.push(chalk.gray("─".repeat(50)));
1125
+ lines.push(
1126
+ `${chalk.white("Total:")} ${summary.total} ` +
1127
+ `${chalk.green("Migrated:")} ${summary.migrated} ` +
1128
+ `${chalk.red("Failed:")} ${summary.failed}`
1129
+ );
1130
+
1131
+ if (dryRun) {
1132
+ lines.push(chalk.yellow(`\n${ICONS.warning} Dry run - no files were modified.`));
1133
+ lines.push(chalk.gray("Remove --dry-run to perform actual migration."));
1134
+ } else if (summary.migrated > 0) {
1135
+ lines.push(
1136
+ chalk.green(`\n${ICONS.success} Successfully migrated ${summary.migrated} skill(s)!`)
1137
+ );
1138
+ lines.push(chalk.gray(`Target location: ${targetBasePath}`));
1139
+ }
1140
+
1141
+ return lines.join("\n");
1142
+ }
1143
+
1144
+ /**
1145
+ * Execute skill migrate command
1146
+ */
1147
+ export async function handleSkillMigrate(options: SkillMigrateOptions): Promise<CommandResult> {
1148
+ try {
1149
+ const workspacePath = process.cwd();
1150
+ const sourcePaths = getMigrationSourcePaths(options.from, workspacePath);
1151
+ const targetLocation = options.location ?? "workspace";
1152
+
1153
+ if (targetLocation === "builtin") {
1154
+ return error("INVALID_ARGUMENT", "Cannot migrate skills to builtin location");
1155
+ }
1156
+
1157
+ const targetBasePath = getSkillSourcePath(targetLocation, workspacePath);
1158
+ const allMigrated: MigrationEntry[] = [];
1159
+ const allErrors: MigrationError[] = [];
1160
+
1161
+ // Scan all source paths
1162
+ for (const sourcePath of sourcePaths) {
1163
+ const { migrated, errors } = await scanSourcePathForSkills(
1164
+ sourcePath,
1165
+ targetBasePath,
1166
+ options.from,
1167
+ options.dryRun ?? false
1168
+ );
1169
+ allMigrated.push(...migrated);
1170
+ allErrors.push(...errors);
1171
+ }
1172
+
1173
+ const summary = {
1174
+ total: allMigrated.length + allErrors.length,
1175
+ migrated: allMigrated.length,
1176
+ failed: allErrors.length,
1177
+ };
1178
+
1179
+ // JSON output
1180
+ if (options.json) {
1181
+ const output: SkillMigrateJson = {
1182
+ success: allErrors.length === 0,
1183
+ source: options.from,
1184
+ migrated: allMigrated,
1185
+ errors: allErrors,
1186
+ summary,
1187
+ };
1188
+ return summary.failed === 0
1189
+ ? success(JSON.stringify(output, null, 2))
1190
+ : error("INTERNAL_ERROR", JSON.stringify(output, null, 2));
1191
+ }
1192
+
1193
+ // Formatted output
1194
+ const outputText = formatMigrationOutput(
1195
+ sourcePaths,
1196
+ allMigrated,
1197
+ allErrors,
1198
+ targetBasePath,
1199
+ options.from,
1200
+ options.dryRun ?? false
1201
+ );
1202
+
1203
+ return summary.failed === 0 ? success(outputText) : error("INTERNAL_ERROR", outputText);
1204
+ } catch (err) {
1205
+ const message = err instanceof Error ? err.message : String(err);
1206
+ return error("INTERNAL_ERROR", `Migration failed: ${message}`);
1207
+ }
1208
+ }
1209
+
1210
+ // =============================================================================
1211
+ // Export Command Definitions for Commander.js (T038)
1212
+ // =============================================================================
1213
+
1214
+ export {
1215
+ handleSkillList as executeSkillList,
1216
+ handleSkillShow as executeSkillShow,
1217
+ handleSkillCreate as executeSkillCreate,
1218
+ handleSkillValidate as executeSkillValidate,
1219
+ handleSkillMigrate as executeSkillMigrate,
1220
+ };