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