@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,350 @@
1
+ /**
2
+ * Progress Tracker
3
+ *
4
+ * Tracks and manages tutorial progress across lessons and steps.
5
+ *
6
+ * @module cli/onboarding/tutorial/progress-tracker
7
+ */
8
+
9
+ import {
10
+ INITIAL_LESSON_PROGRESS,
11
+ type Lesson,
12
+ type LessonProgress,
13
+ type TutorialProgress,
14
+ type TutorialStorage,
15
+ } from "./types.js";
16
+
17
+ // =============================================================================
18
+ // Progress Statistics
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Detailed progress statistics
23
+ */
24
+ export interface ProgressStats {
25
+ /** Total lessons in tutorial */
26
+ totalLessons: number;
27
+ /** Completed lessons count */
28
+ completedLessons: number;
29
+ /** Started but incomplete lessons */
30
+ inProgressLessons: number;
31
+ /** Not started lessons */
32
+ notStartedLessons: number;
33
+ /** Total steps across all lessons */
34
+ totalSteps: number;
35
+ /** Completed steps count */
36
+ completedSteps: number;
37
+ /** Completion percentage (0-100) */
38
+ completionPercent: number;
39
+ /** Total time spent in minutes */
40
+ totalTimeMinutes: number;
41
+ /** Estimated remaining time in minutes */
42
+ estimatedRemainingMinutes: number;
43
+ }
44
+
45
+ // =============================================================================
46
+ // Progress Tracker Class
47
+ // =============================================================================
48
+
49
+ /**
50
+ * Tracks and manages tutorial progress
51
+ */
52
+ export class ProgressTracker {
53
+ private storage: TutorialStorage;
54
+ private lessons: readonly Lesson[];
55
+ private cachedProgress: TutorialProgress | null = null;
56
+
57
+ constructor(storage: TutorialStorage, lessons: readonly Lesson[]) {
58
+ this.storage = storage;
59
+ this.lessons = lessons;
60
+ }
61
+
62
+ // ===========================================================================
63
+ // Progress Loading
64
+ // ===========================================================================
65
+
66
+ /**
67
+ * Load progress from storage
68
+ */
69
+ async loadProgress(): Promise<TutorialProgress> {
70
+ this.cachedProgress = await this.storage.loadProgress();
71
+ return this.cachedProgress;
72
+ }
73
+
74
+ /**
75
+ * Get cached progress or load if not cached
76
+ */
77
+ async getProgress(): Promise<TutorialProgress> {
78
+ if (!this.cachedProgress) {
79
+ return this.loadProgress();
80
+ }
81
+ return this.cachedProgress;
82
+ }
83
+
84
+ // ===========================================================================
85
+ // Lesson Progress
86
+ // ===========================================================================
87
+
88
+ /**
89
+ * Start a lesson
90
+ */
91
+ async startLesson(lessonId: string): Promise<LessonProgress> {
92
+ const progress = await this.getProgress();
93
+ const existing = progress.lessons[lessonId];
94
+
95
+ // Already started
96
+ if (existing?.started) {
97
+ return existing;
98
+ }
99
+
100
+ const now = new Date().toISOString();
101
+ const lessonProgress: LessonProgress = {
102
+ ...INITIAL_LESSON_PROGRESS,
103
+ lessonId,
104
+ started: true,
105
+ startedAt: now,
106
+ currentStepIndex: 0,
107
+ completedSteps: [],
108
+ totalTimeSpent: 0,
109
+ };
110
+
111
+ await this.storage.updateLessonProgress(lessonId, lessonProgress);
112
+
113
+ // Update cache
114
+ progress.lessons[lessonId] = lessonProgress;
115
+ progress.lastActiveLessonId = lessonId;
116
+ progress.lastActivityAt = now;
117
+
118
+ if (!progress.startedAt) {
119
+ progress.startedAt = now;
120
+ await this.storage.saveProgress(progress);
121
+ }
122
+
123
+ this.cachedProgress = progress;
124
+ return lessonProgress;
125
+ }
126
+
127
+ /**
128
+ * Complete a step in a lesson
129
+ */
130
+ async completeStep(lessonId: string, stepId: string, timeSpent?: number): Promise<void> {
131
+ const progress = await this.getProgress();
132
+ const lessonProgress = progress.lessons[lessonId];
133
+
134
+ if (!lessonProgress) {
135
+ throw new Error(`Lesson ${lessonId} not started`);
136
+ }
137
+
138
+ // Already completed
139
+ if (lessonProgress.completedSteps.includes(stepId)) {
140
+ return;
141
+ }
142
+
143
+ // Update lesson progress
144
+ const updatedSteps = [...lessonProgress.completedSteps, stepId];
145
+ const updatedTime = lessonProgress.totalTimeSpent + (timeSpent ?? 0);
146
+
147
+ await this.storage.updateLessonProgress(lessonId, {
148
+ completedSteps: updatedSteps,
149
+ currentStepIndex: updatedSteps.length,
150
+ totalTimeSpent: updatedTime,
151
+ });
152
+
153
+ // Update cache
154
+ lessonProgress.completedSteps = updatedSteps;
155
+ lessonProgress.currentStepIndex = updatedSteps.length;
156
+ lessonProgress.totalTimeSpent = updatedTime;
157
+ progress.stepsCompleted++;
158
+ progress.lastActivityAt = new Date().toISOString();
159
+
160
+ this.cachedProgress = progress;
161
+ }
162
+
163
+ /**
164
+ * Complete a lesson
165
+ */
166
+ async completeLesson(lessonId: string): Promise<void> {
167
+ const progress = await this.getProgress();
168
+ const lessonProgress = progress.lessons[lessonId];
169
+
170
+ if (!lessonProgress) {
171
+ throw new Error(`Lesson ${lessonId} not started`);
172
+ }
173
+
174
+ // Already completed
175
+ if (lessonProgress.completed) {
176
+ return;
177
+ }
178
+
179
+ const now = new Date().toISOString();
180
+
181
+ await this.storage.updateLessonProgress(lessonId, {
182
+ completed: true,
183
+ completedAt: now,
184
+ });
185
+
186
+ // Update cache
187
+ lessonProgress.completed = true;
188
+ lessonProgress.completedAt = now;
189
+ progress.lessonsCompleted++;
190
+ progress.lastActivityAt = now;
191
+
192
+ await this.storage.saveProgress(progress);
193
+ this.cachedProgress = progress;
194
+ }
195
+
196
+ /**
197
+ * Get progress for a specific lesson
198
+ */
199
+ async getLessonProgress(lessonId: string): Promise<LessonProgress | undefined> {
200
+ const progress = await this.getProgress();
201
+ return progress.lessons[lessonId];
202
+ }
203
+
204
+ /**
205
+ * Check if lesson is completed
206
+ */
207
+ async isLessonCompleted(lessonId: string): Promise<boolean> {
208
+ const lessonProgress = await this.getLessonProgress(lessonId);
209
+ return lessonProgress?.completed ?? false;
210
+ }
211
+
212
+ /**
213
+ * Get current step in a lesson
214
+ */
215
+ async getCurrentStep(lessonId: string): Promise<number> {
216
+ const lessonProgress = await this.getLessonProgress(lessonId);
217
+ return lessonProgress?.currentStepIndex ?? 0;
218
+ }
219
+
220
+ // ===========================================================================
221
+ // Statistics
222
+ // ===========================================================================
223
+
224
+ /**
225
+ * Get detailed progress statistics
226
+ */
227
+ async getStats(): Promise<ProgressStats> {
228
+ const progress = await this.getProgress();
229
+
230
+ let completedLessons = 0;
231
+ let inProgressLessons = 0;
232
+ let notStartedLessons = 0;
233
+ let totalSteps = 0;
234
+ let completedSteps = 0;
235
+ let totalTimeSeconds = 0;
236
+ let estimatedRemainingSeconds = 0;
237
+
238
+ for (const lesson of this.lessons) {
239
+ const lessonProgress = progress.lessons[lesson.id];
240
+ totalSteps += lesson.steps.length;
241
+
242
+ if (lessonProgress?.completed) {
243
+ completedLessons++;
244
+ completedSteps += lesson.steps.length;
245
+ totalTimeSeconds += lessonProgress.totalTimeSpent;
246
+ } else if (lessonProgress?.started) {
247
+ inProgressLessons++;
248
+ completedSteps += lessonProgress.completedSteps.length;
249
+ totalTimeSeconds += lessonProgress.totalTimeSpent;
250
+
251
+ // Estimate remaining for in-progress lesson
252
+ const remainingSteps = lesson.steps.length - lessonProgress.completedSteps.length;
253
+ const avgStepTime = (lesson.estimatedMinutes * 60) / lesson.steps.length;
254
+ estimatedRemainingSeconds += remainingSteps * avgStepTime;
255
+ } else {
256
+ notStartedLessons++;
257
+ estimatedRemainingSeconds += lesson.estimatedMinutes * 60;
258
+ }
259
+ }
260
+
261
+ const totalLessons = this.lessons.length;
262
+ const completionPercent = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
263
+
264
+ return {
265
+ totalLessons,
266
+ completedLessons,
267
+ inProgressLessons,
268
+ notStartedLessons,
269
+ totalSteps,
270
+ completedSteps,
271
+ completionPercent,
272
+ totalTimeMinutes: Math.round(totalTimeSeconds / 60),
273
+ estimatedRemainingMinutes: Math.round(estimatedRemainingSeconds / 60),
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Get completion percentage for a lesson
279
+ */
280
+ async getLessonCompletionPercent(lessonId: string): Promise<number> {
281
+ const lesson = this.lessons.find((l) => l.id === lessonId);
282
+ if (!lesson) return 0;
283
+
284
+ const lessonProgress = await this.getLessonProgress(lessonId);
285
+ if (!lessonProgress) return 0;
286
+
287
+ const totalSteps = lesson.steps.length;
288
+ const completedSteps = lessonProgress.completedSteps.length;
289
+
290
+ return totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
291
+ }
292
+
293
+ /**
294
+ * Check if all tutorials are complete
295
+ */
296
+ async isAllComplete(): Promise<boolean> {
297
+ const progress = await this.getProgress();
298
+ return this.lessons.every((lesson) => progress.lessons[lesson.id]?.completed);
299
+ }
300
+
301
+ /**
302
+ * Get completed lesson IDs
303
+ */
304
+ async getCompletedLessonIds(): Promise<string[]> {
305
+ const progress = await this.getProgress();
306
+ return Object.entries(progress.lessons)
307
+ .filter(([_, p]) => (p as { completed: boolean }).completed)
308
+ .map(([id]) => id);
309
+ }
310
+
311
+ // ===========================================================================
312
+ // Reset
313
+ // ===========================================================================
314
+
315
+ /**
316
+ * Reset all progress
317
+ */
318
+ async resetAll(): Promise<void> {
319
+ await this.storage.resetProgress();
320
+ this.cachedProgress = null;
321
+ }
322
+
323
+ /**
324
+ * Reset a single lesson
325
+ */
326
+ async resetLesson(lessonId: string): Promise<void> {
327
+ await this.storage.updateLessonProgress(lessonId, {
328
+ ...INITIAL_LESSON_PROGRESS,
329
+ lessonId,
330
+ });
331
+
332
+ if (this.cachedProgress) {
333
+ delete this.cachedProgress.lessons[lessonId];
334
+ }
335
+ }
336
+ }
337
+
338
+ // =============================================================================
339
+ // Factory
340
+ // =============================================================================
341
+
342
+ /**
343
+ * Create a progress tracker
344
+ */
345
+ export function createProgressTracker(
346
+ storage: TutorialStorage,
347
+ lessons: readonly Lesson[]
348
+ ): ProgressTracker {
349
+ return new ProgressTracker(storage, lessons);
350
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Tutorial Storage
3
+ *
4
+ * Handles persistence of tutorial progress to disk.
5
+ *
6
+ * @module cli/onboarding/tutorial/storage
7
+ */
8
+
9
+ import * as fs from "node:fs";
10
+ import * as fsPromises from "node:fs/promises";
11
+ import * as os from "node:os";
12
+ import * as path from "node:path";
13
+
14
+ import {
15
+ INITIAL_LESSON_PROGRESS,
16
+ INITIAL_TUTORIAL_PROGRESS,
17
+ type LessonProgress,
18
+ type TutorialProgress,
19
+ TutorialProgressSchema,
20
+ type TutorialStorage,
21
+ } from "./types.js";
22
+
23
+ // =============================================================================
24
+ // Paths
25
+ // =============================================================================
26
+
27
+ /**
28
+ * Get Vellum config directory
29
+ */
30
+ function getVellumDir(): string {
31
+ const home = os.homedir();
32
+ return path.join(home, ".vellum");
33
+ }
34
+
35
+ /**
36
+ * Get tutorial progress file path
37
+ */
38
+ function getTutorialProgressPath(): string {
39
+ return path.join(getVellumDir(), "tutorial-progress.json");
40
+ }
41
+
42
+ // =============================================================================
43
+ // File Storage Implementation
44
+ // =============================================================================
45
+
46
+ /**
47
+ * File-based tutorial storage
48
+ */
49
+ export class FileTutorialStorage implements TutorialStorage {
50
+ private progressPath: string;
51
+ private cachedProgress: TutorialProgress | null = null;
52
+
53
+ constructor(customPath?: string) {
54
+ this.progressPath = customPath ?? getTutorialProgressPath();
55
+ }
56
+
57
+ /**
58
+ * Ensure the storage directory exists
59
+ */
60
+ private async ensureDir(): Promise<void> {
61
+ const dir = path.dirname(this.progressPath);
62
+ if (!fs.existsSync(dir)) {
63
+ await fsPromises.mkdir(dir, { recursive: true });
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Load tutorial progress from file
69
+ */
70
+ async loadProgress(): Promise<TutorialProgress> {
71
+ // Return cached if available
72
+ if (this.cachedProgress) {
73
+ return this.cachedProgress;
74
+ }
75
+
76
+ try {
77
+ if (!fs.existsSync(this.progressPath)) {
78
+ return { ...INITIAL_TUTORIAL_PROGRESS };
79
+ }
80
+
81
+ const content = await fsPromises.readFile(this.progressPath, "utf-8");
82
+ const data = JSON.parse(content) as unknown;
83
+ const parsed = TutorialProgressSchema.safeParse(data);
84
+
85
+ if (parsed.success) {
86
+ this.cachedProgress = parsed.data;
87
+ return parsed.data;
88
+ }
89
+
90
+ // Invalid data, return initial state
91
+ console.warn("[TutorialStorage] Invalid progress data, resetting");
92
+ return { ...INITIAL_TUTORIAL_PROGRESS };
93
+ } catch (error) {
94
+ // File doesn't exist or parse error
95
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
96
+ console.warn("[TutorialStorage] Failed to load progress:", error);
97
+ }
98
+ return { ...INITIAL_TUTORIAL_PROGRESS };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Save tutorial progress to file
104
+ */
105
+ async saveProgress(progress: TutorialProgress): Promise<void> {
106
+ try {
107
+ await this.ensureDir();
108
+
109
+ // Validate before saving
110
+ const parsed = TutorialProgressSchema.safeParse(progress);
111
+ if (!parsed.success) {
112
+ throw new Error(`Invalid progress data: ${parsed.error.message}`);
113
+ }
114
+
115
+ // Update cache
116
+ this.cachedProgress = parsed.data;
117
+
118
+ // Write to file
119
+ const content = JSON.stringify(parsed.data, null, 2);
120
+ await fsPromises.writeFile(this.progressPath, content, "utf-8");
121
+ } catch (error) {
122
+ console.error("[TutorialStorage] Failed to save progress:", error);
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Reset all progress
129
+ */
130
+ async resetProgress(): Promise<void> {
131
+ try {
132
+ this.cachedProgress = null;
133
+
134
+ if (fs.existsSync(this.progressPath)) {
135
+ await fsPromises.unlink(this.progressPath);
136
+ }
137
+ } catch (error) {
138
+ console.error("[TutorialStorage] Failed to reset progress:", error);
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Get progress for a specific lesson
145
+ */
146
+ async getLessonProgress(lessonId: string): Promise<LessonProgress | undefined> {
147
+ const progress = await this.loadProgress();
148
+ return progress.lessons[lessonId];
149
+ }
150
+
151
+ /**
152
+ * Update progress for a specific lesson
153
+ */
154
+ async updateLessonProgress(lessonId: string, updates: Partial<LessonProgress>): Promise<void> {
155
+ const progress = await this.loadProgress();
156
+
157
+ // Get or create lesson progress
158
+ const existing = progress.lessons[lessonId] ?? {
159
+ ...INITIAL_LESSON_PROGRESS,
160
+ lessonId,
161
+ };
162
+
163
+ // Merge updates
164
+ progress.lessons[lessonId] = {
165
+ ...existing,
166
+ ...updates,
167
+ };
168
+
169
+ // Update last activity
170
+ progress.lastActivityAt = new Date().toISOString();
171
+ progress.lastActiveLessonId = lessonId;
172
+
173
+ await this.saveProgress(progress);
174
+ }
175
+
176
+ /**
177
+ * Clear the cache (useful for testing)
178
+ */
179
+ clearCache(): void {
180
+ this.cachedProgress = null;
181
+ }
182
+ }
183
+
184
+ // =============================================================================
185
+ // In-Memory Storage (for testing)
186
+ // =============================================================================
187
+
188
+ /**
189
+ * In-memory tutorial storage for testing
190
+ */
191
+ export class MemoryTutorialStorage implements TutorialStorage {
192
+ private progress: TutorialProgress = { ...INITIAL_TUTORIAL_PROGRESS };
193
+
194
+ async loadProgress(): Promise<TutorialProgress> {
195
+ return { ...this.progress };
196
+ }
197
+
198
+ async saveProgress(progress: TutorialProgress): Promise<void> {
199
+ this.progress = { ...progress };
200
+ }
201
+
202
+ async resetProgress(): Promise<void> {
203
+ this.progress = { ...INITIAL_TUTORIAL_PROGRESS };
204
+ }
205
+
206
+ async getLessonProgress(lessonId: string): Promise<LessonProgress | undefined> {
207
+ return this.progress.lessons[lessonId];
208
+ }
209
+
210
+ async updateLessonProgress(lessonId: string, updates: Partial<LessonProgress>): Promise<void> {
211
+ const existing = this.progress.lessons[lessonId] ?? {
212
+ ...INITIAL_LESSON_PROGRESS,
213
+ lessonId,
214
+ };
215
+
216
+ this.progress.lessons[lessonId] = {
217
+ ...existing,
218
+ ...updates,
219
+ };
220
+
221
+ this.progress.lastActivityAt = new Date().toISOString();
222
+ this.progress.lastActiveLessonId = lessonId;
223
+ }
224
+
225
+ /**
226
+ * Get raw progress (for testing)
227
+ */
228
+ getRawProgress(): TutorialProgress {
229
+ return this.progress;
230
+ }
231
+
232
+ /**
233
+ * Set raw progress (for testing)
234
+ */
235
+ setRawProgress(progress: TutorialProgress): void {
236
+ this.progress = progress;
237
+ }
238
+ }
239
+
240
+ // =============================================================================
241
+ // Factory
242
+ // =============================================================================
243
+
244
+ /**
245
+ * Create a tutorial storage instance
246
+ */
247
+ export function createTutorialStorage(customPath?: string): TutorialStorage {
248
+ return new FileTutorialStorage(customPath);
249
+ }