From 0062e5d9d969060b1e0407c2039ce836925675f2 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Tue, 27 Jan 2026 23:33:06 -0500 Subject: [PATCH] Terminal-based AI coding agent with interactive TUI for autonomous code generation. Features: - Interactive TUI with React/Ink - Autonomous agent with tool calls (bash, read, write, edit, glob, grep) - Permission system with pattern-based rules - Session management with auto-compaction - Dual providers: GitHub Copilot and Ollama - MCP server integration - Todo panel and theme system - Streaming responses - GitHub-compatible project context --- .gitignore | 263 + LICENSE | 21 + README.md | 284 + assets/CodetyperAgentMode.png | Bin 0 -> 234198 bytes assets/CodetyperCopilotModels.png | Bin 0 -> 51658 bytes assets/CodetyperLogin.png | Bin 0 -> 106421 bytes assets/CodetyperMenu.png | Bin 0 -> 84986 bytes assets/CodetyperPermissionView.png | Bin 0 -> 49050 bytes assets/CodetyperThemes.png | Bin 0 -> 34102 bytes assets/CodetyperView.png | Bin 0 -> 178264 bytes bin/codetyper | 1 + bun.lock | 1228 +++ bunfig.toml | 6 + docs/CHANGELOG.md | 132 + docs/CONTRIBUTING.md | 202 + eslint.config.js | 16 + package-lock.json | 9224 +++++++++++++++++ package.json | 100 + scripts/build.ts | 62 + scripts/dev-watch.ts | 138 + scripts/sync-version.ts | 29 + src/commands/chat-tui.ts | 36 + src/commands/chat.ts | 8 + .../callbacks/on-learning-detected.ts | 23 + src/commands/components/callbacks/on-log.ts | 14 + .../components/callbacks/on-mode-change.ts | 5 + .../callbacks/on-permission-request.ts | 7 + .../components/callbacks/on-tool-call.ts | 25 + .../components/callbacks/on-tool-result.ts | 47 + .../components/chat/agents/show-agents.ts | 35 + .../components/chat/agents/switch-agent.ts | 60 + src/commands/components/chat/cleanup.ts | 12 + .../chat/commands/commandsRegistry.ts | 111 + .../chat/commands/handle-command.ts | 28 + .../components/chat/commands/show-help.ts | 25 + .../chat/context/add-context-file.ts | 35 + .../components/chat/context/load-file.ts | 32 + .../chat/context/process-file-references.ts | 27 + .../components/chat/context/remove-file.ts | 22 + .../chat/context/show-context-files.ts | 32 + .../chat/history/clear-conversation.ts | 10 + .../chat/history/compact-history.ts | 15 + .../components/chat/history/show-context.ts | 18 + .../components/chat/history/show-history.ts | 18 + src/commands/components/chat/index.ts | 224 + .../components/chat/mcp/handle-mcp.ts | 143 + src/commands/components/chat/mcp/index.ts | 6 + .../components/chat/mcp/show-mcp-status.ts | 76 + .../components/chat/messages/handle-input.ts | 17 + .../components/chat/messages/send-message.ts | 228 + .../components/chat/models/show-models.ts | 31 + .../components/chat/models/show-providers.ts | 7 + .../components/chat/models/switch-model.ts | 50 + .../components/chat/models/switch-provider.ts | 51 + src/commands/components/chat/print-mode.ts | 71 + .../components/chat/session/list-sessions.ts | 38 + .../chat/session/restore-messages.ts | 20 + .../chat/session/show-session-info.ts | 20 + src/commands/components/chat/state.ts | 34 + .../components/chat/usage/show-usage.ts | 129 + .../components/dashboard/build-config.ts | 25 + src/commands/components/dashboard/display.ts | 33 + .../components/dashboard/render-content.ts | 41 + .../components/dashboard/render-footer.ts | 31 + .../components/dashboard/render-header.ts | 18 + .../dashboard/render-left-content.ts | 24 + .../dashboard/render-right-content.ts | 21 + .../components/execute/execute-solid.tsx | 55 + src/commands/components/execute/execute.tsx | 104 + src/commands/components/execute/index.ts | 195 + src/commands/dashboard.ts | 8 + src/commands/handlers.ts | 25 + src/commands/handlers/chat.ts | 10 + src/commands/handlers/classify.ts | 123 + src/commands/handlers/config.ts | 113 + src/commands/handlers/plan.ts | 62 + src/commands/handlers/registry.ts | 28 + src/commands/handlers/run.ts | 10 + src/commands/handlers/serve.ts | 15 + src/commands/handlers/validate.ts | 78 + src/commands/mcp.ts | 319 + src/commands/runner.ts | 10 + src/commands/runner/create-plan.ts | 45 + src/commands/runner/display-header.ts | 27 + src/commands/runner/display-plan.ts | 34 + src/commands/runner/execute-plan.ts | 35 + src/commands/runner/execute.ts | 118 + src/commands/runner/utils.ts | 6 + src/constants/agent.ts | 5 + src/constants/auto-scroll.ts | 23 + src/constants/banner.ts | 103 + src/constants/bash.ts | 30 + src/constants/bashPatterns.ts | 20 + src/constants/chat-service.ts | 106 + src/constants/command-suggestion.ts | 85 + src/constants/components.ts | 114 + src/constants/copilot.ts | 213 + src/constants/dashboard.ts | 42 + src/constants/diff.ts | 13 + src/constants/edit.ts | 25 + src/constants/embeddings.ts | 30 + src/constants/file-picker.ts | 123 + src/constants/files.ts | 2 + src/constants/general.ts | 1 + src/constants/github-issue.ts | 31 + src/constants/glob.ts | 58 + src/constants/grep.ts | 28 + src/constants/handlers.ts | 45 + src/constants/help-commands.ts | 20 + src/constants/home-screen.ts | 35 + src/constants/home.ts | 4 + src/constants/input-editor.ts | 30 + src/constants/learning.ts | 96 + src/constants/login.ts | 28 + src/constants/mouse-handler.ts | 41 + src/constants/mouse-scroll.ts | 38 + src/constants/ollama.ts | 32 + src/constants/paste.ts | 12 + src/constants/paths.ts | 96 + src/constants/patterns.ts | 1 + src/constants/provider-quality.ts | 127 + src/constants/providers.ts | 22 + src/constants/read.ts | 29 + src/constants/reasoning.ts | 250 + src/constants/rules.ts | 78 + src/constants/runner.ts | 61 + src/constants/serve.ts | 6 + src/constants/spinner.ts | 78 + src/constants/status-messages.ts | 238 + src/constants/styles.ts | 95 + src/constants/syntax-highlight.ts | 170 + src/constants/terminal.ts | 9 + src/constants/themes.ts | 834 ++ src/constants/tips.ts | 50 + src/constants/tools.ts | 13 + src/constants/tui-components.ts | 256 + src/constants/ui.ts | 41 + src/constants/view.ts | 11 + src/constants/write.ts | 24 + src/index.ts | 662 ++ src/interfaces/AgentOptions.ts | 22 + src/interfaces/AgentResult.ts | 12 + src/interfaces/AppProps.ts | 34 + src/interfaces/AutoScrollOptions.ts | 67 + src/interfaces/BoxOptions.ts | 30 + src/interfaces/ChatOptions.ts | 15 + src/interfaces/ChatTUIOptions.ts | 15 + src/interfaces/ExecutionResult.ts | 11 + src/interfaces/FileOperation.ts | 10 + src/interfaces/InputEditorOptions.ts | 8 + src/interfaces/MouseHandlerCallbacks.ts | 12 + src/interfaces/MouseScrollOptions.ts | 14 + src/interfaces/PastedContent.ts | 30 + src/interfaces/SpinnerOptions.ts | 26 + src/interfaces/ToolCallParams.ts | 6 + src/interfaces/commandContext.ts | 7 + src/interfaces/memory.ts | 6 + src/prompts/audit-prompt.ts | 81 + src/prompts/index.ts | 122 + src/prompts/system/agent.ts | 210 + src/prompts/system/code-review.ts | 115 + src/prompts/system/debugging.ts | 109 + src/prompts/system/default.ts | 158 + src/prompts/system/environment.ts | 17 + src/prompts/system/git.ts | 74 + src/prompts/system/memory.ts | 78 + src/prompts/system/planner.ts | 98 + src/prompts/system/refactoring.ts | 178 + src/prompts/system/tools.ts | 103 + src/prompts/ui/help.ts | 49 + src/providers/chat.ts | 75 + src/providers/copilot.ts | 47 + src/providers/copilot/auth.ts | 93 + src/providers/copilot/chat.ts | 323 + src/providers/copilot/credentials.ts | 79 + src/providers/copilot/models.ts | 122 + src/providers/copilot/state.ts | 50 + src/providers/copilot/token.ts | 123 + src/providers/copilot/usage.ts | 34 + src/providers/copilot/utils.ts | 83 + src/providers/credentials.ts | 32 + src/providers/index.ts | 46 + src/providers/login.ts | 12 + src/providers/login/copilot-login.ts | 126 + src/providers/login/handlers.ts | 59 + src/providers/login/initialize.ts | 45 + src/providers/login/ollama-login.ts | 65 + src/providers/login/utils.ts | 27 + src/providers/ollama.ts | 46 + src/providers/ollama/chat.ts | 138 + src/providers/ollama/credentials.ts | 18 + src/providers/ollama/models.ts | 36 + src/providers/ollama/pull.ts | 74 + src/providers/ollama/state.ts | 29 + src/providers/ollama/stream.ts | 90 + src/providers/ollama/validation.ts | 35 + src/providers/registry.ts | 28 + src/providers/status.ts | 71 + src/services/__tests__/agent-stream.test.ts | 203 + src/services/agent-loader.ts | 222 + src/services/agent-stream.ts | 427 + src/services/agent.ts | 325 + src/services/auto-compaction.ts | 282 + .../cascading-provider/availability.ts | 122 + src/services/cascading-provider/index.ts | 22 + .../cascading-provider/orchestrator.ts | 174 + src/services/chat-tui-service.ts | 61 + src/services/chat-tui/agents.ts | 72 + src/services/chat-tui/auth.ts | 161 + src/services/chat-tui/commands.ts | 155 + src/services/chat-tui/files.ts | 203 + src/services/chat-tui/initialize.ts | 211 + src/services/chat-tui/learnings.ts | 118 + src/services/chat-tui/message-handler.ts | 477 + src/services/chat-tui/models.ts | 55 + src/services/chat-tui/permissions.ts | 45 + src/services/chat-tui/print-mode.ts | 51 + src/services/chat-tui/streaming.ts | 261 + src/services/chat-tui/usage.ts | 146 + src/services/chat-tui/utils.ts | 78 + src/services/code-review-service.ts | 266 + src/services/command-suggestion-service.ts | 43 + src/services/command-suggestion/analyze.ts | 68 + src/services/command-suggestion/context.ts | 41 + src/services/command-suggestion/format.ts | 41 + src/services/command-suggestion/patterns.ts | 225 + src/services/command-suggestion/state.ts | 57 + src/services/config.ts | 168 + src/services/debugging-service.ts | 208 + src/services/environment-service.ts | 69 + src/services/executor.ts | 358 + src/services/file-picker-service.ts | 71 + src/services/file-picker/files.ts | 80 + src/services/file-picker/filter.ts | 19 + src/services/file-picker/match.ts | 32 + src/services/file-picker/state.ts | 74 + src/services/github-issue-service.ts | 18 + src/services/github-issue/enrich.ts | 50 + src/services/github-issue/extract.ts | 37 + src/services/github-issue/fetch.ts | 44 + src/services/github-issue/format.ts | 19 + src/services/github-issue/repo.ts | 22 + src/services/github-pr/cli.ts | 124 + src/services/github-pr/fetch.ts | 321 + src/services/github-pr/format.ts | 199 + src/services/github-pr/index.ts | 43 + src/services/github-pr/url.ts | 76 + src/services/index.ts | 10 + src/services/learning-service.ts | 32 + .../learning/__tests__/vector-store.test.ts | 231 + src/services/learning/analyze.ts | 86 + src/services/learning/assistant.ts | 72 + src/services/learning/categorize.ts | 20 + src/services/learning/deduplicate.ts | 25 + src/services/learning/detect.ts | 29 + src/services/learning/embeddings.ts | 240 + src/services/learning/extract.ts | 38 + src/services/learning/format.ts | 13 + src/services/learning/index.ts | 67 + src/services/learning/persistence.ts | 48 + src/services/learning/semantic-search.ts | 386 + src/services/learning/vector-store.ts | 243 + src/services/mcp/client.ts | 373 + src/services/mcp/index.ts | 37 + src/services/mcp/manager.ts | 297 + src/services/mcp/tools.ts | 171 + src/services/memory-service.ts | 388 + src/services/permissions.ts | 706 ++ .../__tests__/bash-matcher.test.ts | 152 + .../__tests__/path-matcher.test.ts | 158 + .../__tests__/pattern-index.test.ts | 186 + src/services/permissions/index.ts | 75 + src/services/permissions/matchers/bash.ts | 142 + src/services/permissions/matchers/index.ts | 23 + src/services/permissions/matchers/path.ts | 190 + src/services/permissions/optimized.ts | 448 + src/services/permissions/pattern-cache.ts | 129 + src/services/permissions/pattern-index.ts | 198 + src/services/plan-service.ts | 148 + src/services/planner.ts | 320 + src/services/project-config.ts | 558 + .../provider-quality/feedback-detector.ts | 70 + src/services/provider-quality/index.ts | 38 + src/services/provider-quality/persistence.ts | 128 + src/services/provider-quality/router.ts | 84 + .../provider-quality/score-manager.ts | 99 + .../provider-quality/task-detector.ts | 51 + src/services/reasoning-agent.ts | 569 + .../__tests__/memory-selection.test.ts | 427 + .../__tests__/quality-evaluation.test.ts | 276 + .../reasoning/__tests__/retry-policy.test.ts | 312 + .../__tests__/termination-detection.test.ts | 504 + .../reasoning/__tests__/utils.test.ts | 435 + src/services/reasoning/context-compression.ts | 573 + src/services/reasoning/index.ts | 234 + src/services/reasoning/memory-selection.ts | 448 + src/services/reasoning/orchestrator.ts | 667 ++ src/services/reasoning/quality-evaluation.ts | 397 + src/services/reasoning/retry-policy.ts | 548 + .../reasoning/termination-detection.ts | 498 + src/services/reasoning/utils.ts | 409 + src/services/refactoring-service.ts | 271 + src/services/rules-service.ts | 32 + src/services/rules/categorize.ts | 38 + src/services/rules/format.ts | 35 + src/services/rules/load.ts | 70 + src/services/rules/prompt.ts | 145 + src/services/session.ts | 260 + src/services/upgrade.ts | 323 + src/stores/theme-store.ts | 50 + src/stores/todo-store.ts | 218 + src/stores/usage-store.ts | 64 + src/tools/bash.ts | 21 + src/tools/bash/execute.ts | 188 + src/tools/bash/output.ts | 39 + src/tools/bash/params.ts | 24 + src/tools/bash/process.ts | 40 + src/tools/edit.ts | 11 + src/tools/edit/execute.ts | 154 + src/tools/edit/params.ts | 17 + src/tools/edit/validate.ts | 47 + src/tools/glob.ts | 30 + src/tools/glob/definition.ts | 44 + src/tools/glob/execute.ts | 107 + src/tools/grep.ts | 17 + src/tools/grep/definition.ts | 58 + src/tools/grep/execute.ts | 117 + src/tools/grep/search.ts | 52 + src/tools/index.ts | 183 + src/tools/read.ts | 12 + src/tools/read/execute.ts | 139 + src/tools/read/format.ts | 45 + src/tools/read/params.ts | 23 + src/tools/schema/clean.ts | 42 + src/tools/schema/convert.ts | 22 + src/tools/todo-read.ts | 64 + src/tools/todo-write.ts | 134 + src/tools/types.ts | 32 + src/tools/view.ts | 16 + src/tools/view/execute.ts | 76 + src/tools/write.ts | 6 + src/tools/write/execute.ts | 170 + src/tools/write/params.ts | 12 + src/tui-solid/app.tsx | 439 + src/tui-solid/components/agent-select.tsx | 122 + src/tui-solid/components/bouncing-loader.tsx | 94 + src/tui-solid/components/command-menu.tsx | 230 + src/tui-solid/components/diff-view.tsx | 115 + src/tui-solid/components/file-picker.tsx | 176 + src/tui-solid/components/header.tsx | 92 + src/tui-solid/components/index.ts | 25 + src/tui-solid/components/input-area.tsx | 248 + src/tui-solid/components/learning-modal.tsx | 248 + src/tui-solid/components/log-entry.tsx | 236 + src/tui-solid/components/log-panel.tsx | 166 + src/tui-solid/components/logo.tsx | 18 + src/tui-solid/components/mcp-add-form.tsx | 272 + src/tui-solid/components/mcp-select.tsx | 161 + src/tui-solid/components/mode-select.tsx | 148 + src/tui-solid/components/model-select.tsx | 283 + src/tui-solid/components/permission-modal.tsx | 173 + src/tui-solid/components/provider-select.tsx | 235 + src/tui-solid/components/select-menu.tsx | 119 + src/tui-solid/components/status-bar.tsx | 212 + .../components/streaming-message.tsx | 35 + src/tui-solid/components/theme-select.tsx | 119 + .../components/thinking-indicator.tsx | 38 + src/tui-solid/components/todo-panel.tsx | 120 + src/tui-solid/constants/text-attributes.ts | 7 + src/tui-solid/context/app.tsx | 979 ++ src/tui-solid/context/dialog.tsx | 130 + src/tui-solid/context/exit.tsx | 61 + src/tui-solid/context/helper.tsx | 43 + src/tui-solid/context/index.ts | 7 + src/tui-solid/context/keybind.tsx | 179 + src/tui-solid/context/route.tsx | 60 + src/tui-solid/context/theme.tsx | 89 + src/tui-solid/index.ts | 3 + src/tui-solid/routes/home.tsx | 134 + src/tui-solid/routes/index.ts | 2 + src/tui-solid/routes/session.tsx | 310 + src/tui-solid/types/index.ts | 142 + src/tui-solid/ui/dialog.tsx | 164 + src/tui-solid/ui/index.ts | 4 + src/tui-solid/ui/spinner.tsx | 35 + src/tui-solid/ui/toast.tsx | 179 + src/tui/App.tsx | 751 ++ src/tui/components/AgentSelect.tsx | 251 + src/tui/components/BouncingLoader.tsx | 81 + src/tui/components/CommandMenu.tsx | 215 + src/tui/components/DiffView.tsx | 19 + src/tui/components/FilePicker.tsx | 168 + src/tui/components/Header.tsx | 54 + src/tui/components/InputArea.tsx | 277 + src/tui/components/LearningModal.tsx | 75 + src/tui/components/LogPanel.tsx | 8 + src/tui/components/MCPSelect.tsx | 486 + src/tui/components/ModelSelect.tsx | 264 + src/tui/components/PermissionModal.tsx | 78 + src/tui/components/SelectMenu.tsx | 104 + src/tui/components/StatusBar.tsx | 205 + src/tui/components/StreamingMessage.tsx | 78 + src/tui/components/ThemeSelect.tsx | 199 + src/tui/components/TodoPanel.tsx | 105 + src/tui/components/diff-view/index.tsx | 102 + .../components/diff-view/line-renderers.tsx | 227 + src/tui/components/diff-view/utils.ts | 162 + src/tui/components/home/HomeContent.tsx | 66 + src/tui/components/home/HomeFooter.tsx | 55 + src/tui/components/home/HomeScreen.tsx | 69 + src/tui/components/home/Logo.tsx | 27 + src/tui/components/home/PromptBox.tsx | 134 + src/tui/components/home/SessionHeader.tsx | 70 + src/tui/components/home/index.ts | 11 + src/tui/components/index.ts | 38 + src/tui/components/input-line/index.tsx | 216 + .../components/log-panel/entry-renderers.tsx | 172 + src/tui/components/log-panel/index.tsx | 176 + .../log-panel/thinking-indicator.tsx | 31 + src/tui/components/log-panel/utils.ts | 36 + src/tui/hooks/index.ts | 16 + src/tui/hooks/useAutoScroll.ts | 286 + src/tui/hooks/useMouseScroll.ts | 109 + src/tui/index.ts | 22 + src/tui/mouse-handler.ts | 232 + src/tui/store.ts | 609 ++ src/tui/types.ts | 34 + src/types/agent-config.ts | 33 + src/types/agent.ts | 26 + src/types/banner.ts | 5 + src/types/chat-service.ts | 65 + src/types/command-suggestion.ts | 37 + src/types/commandHandler.ts | 3 + src/types/components.ts | 28 + src/types/copilot-usage.ts | 33 + src/types/copilot.ts | 43 + src/types/dashboard.ts | 11 + src/types/diff.ts | 31 + src/types/embeddings.ts | 108 + src/types/file-picker.ts | 25 + src/types/github-issue.ts | 32 + src/types/github-pr.ts | 71 + src/types/handlers.ts | 22 + src/types/home-screen.ts | 46 + src/types/index.ts | 119 + src/types/input-editor.ts | 50 + src/types/learning.ts | 33 + src/types/log.ts | 1 + src/types/mcp.ts | 141 + src/types/ollama.ts | 72 + src/types/permissions.ts | 43 + src/types/planner.ts | 44 + src/types/project-config.ts | 40 + src/types/provider-quality.ts | 40 + src/types/providers.ts | 155 + src/types/reasoning.ts | 467 + src/types/rules.ts | 34 + src/types/runner.ts | 31 + src/types/session.ts | 15 + src/types/spinner.ts | 25 + src/types/streaming.ts | 99 + src/types/theme.ts | 92 + src/types/todo.ts | 30 + src/types/tools.ts | 154 + src/types/tui.ts | 391 + src/types/usage.ts | 19 + src/ui/banner.ts | 9 + src/ui/banner/lines.ts | 13 + src/ui/banner/logo.ts | 11 + src/ui/banner/print.ts | 39 + src/ui/banner/render.ts | 35 + src/ui/components.ts | 24 + src/ui/components/box.ts | 150 + src/ui/components/header.ts | 44 + src/ui/components/list.ts | 50 + src/ui/components/message.ts | 64 + src/ui/components/status.ts | 54 + src/ui/index.ts | 10 + src/ui/input-editor.ts | 17 + src/ui/input-editor/cursor.ts | 160 + src/ui/input-editor/display.ts | 121 + src/ui/input-editor/editor.ts | 230 + src/ui/input-editor/keypress.ts | 249 + src/ui/input-editor/paste.ts | 252 + src/ui/input-editor/state.ts | 57 + src/ui/spinner.ts | 22 + src/ui/spinner/progress.ts | 30 + src/ui/spinner/scanner.ts | 96 + src/ui/spinner/spinner.ts | 138 + src/ui/styles.ts | 16 + src/ui/styles/apply.ts | 19 + src/ui/styles/colors.ts | 22 + src/ui/styles/text.ts | 75 + src/ui/tips.ts | 16 + src/ui/tips/parse.ts | 45 + src/ui/tips/render.ts | 82 + src/utils/diff.ts | 13 + src/utils/diff/format.ts | 97 + src/utils/diff/generate.ts | 33 + src/utils/diff/hunks.ts | 97 + src/utils/diff/index.ts | 162 + src/utils/diff/lcs.ts | 29 + src/utils/diff/lines.ts | 46 + src/utils/ensure-directories.ts | 14 + src/utils/progress-bar.ts | 77 + src/utils/syntax-highlight.ts | 18 + src/utils/syntax-highlight/detect.ts | 45 + src/utils/syntax-highlight/highlight.ts | 91 + src/utils/terminal.ts | 326 + src/utils/tools.ts | 28 + src/utils/tui-app/index.ts | 33 + src/utils/tui-app/input-utils.ts | 142 + src/utils/tui-app/mode-utils.ts | 63 + src/utils/tui-app/paste-utils.ts | 209 + src/version.json | 3 + tests/auto-scroll-constants.test.ts | 47 + tests/input-utils.test.ts | 86 + tests/paste-utils.test.ts | 314 + tests/tools.test.ts | 49 + tmp.txt | 1 + tsconfig.json | 47 + 521 files changed, 66418 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/CodetyperAgentMode.png create mode 100644 assets/CodetyperCopilotModels.png create mode 100644 assets/CodetyperLogin.png create mode 100644 assets/CodetyperMenu.png create mode 100644 assets/CodetyperPermissionView.png create mode 100644 assets/CodetyperThemes.png create mode 100644 assets/CodetyperView.png create mode 120000 bin/codetyper create mode 100644 bun.lock create mode 100644 bunfig.toml create mode 100644 docs/CHANGELOG.md create mode 100644 docs/CONTRIBUTING.md create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/build.ts create mode 100644 scripts/dev-watch.ts create mode 100644 scripts/sync-version.ts create mode 100644 src/commands/chat-tui.ts create mode 100644 src/commands/chat.ts create mode 100644 src/commands/components/callbacks/on-learning-detected.ts create mode 100644 src/commands/components/callbacks/on-log.ts create mode 100644 src/commands/components/callbacks/on-mode-change.ts create mode 100644 src/commands/components/callbacks/on-permission-request.ts create mode 100644 src/commands/components/callbacks/on-tool-call.ts create mode 100644 src/commands/components/callbacks/on-tool-result.ts create mode 100644 src/commands/components/chat/agents/show-agents.ts create mode 100644 src/commands/components/chat/agents/switch-agent.ts create mode 100644 src/commands/components/chat/cleanup.ts create mode 100644 src/commands/components/chat/commands/commandsRegistry.ts create mode 100644 src/commands/components/chat/commands/handle-command.ts create mode 100644 src/commands/components/chat/commands/show-help.ts create mode 100644 src/commands/components/chat/context/add-context-file.ts create mode 100644 src/commands/components/chat/context/load-file.ts create mode 100644 src/commands/components/chat/context/process-file-references.ts create mode 100644 src/commands/components/chat/context/remove-file.ts create mode 100644 src/commands/components/chat/context/show-context-files.ts create mode 100644 src/commands/components/chat/history/clear-conversation.ts create mode 100644 src/commands/components/chat/history/compact-history.ts create mode 100644 src/commands/components/chat/history/show-context.ts create mode 100644 src/commands/components/chat/history/show-history.ts create mode 100644 src/commands/components/chat/index.ts create mode 100644 src/commands/components/chat/mcp/handle-mcp.ts create mode 100644 src/commands/components/chat/mcp/index.ts create mode 100644 src/commands/components/chat/mcp/show-mcp-status.ts create mode 100644 src/commands/components/chat/messages/handle-input.ts create mode 100644 src/commands/components/chat/messages/send-message.ts create mode 100644 src/commands/components/chat/models/show-models.ts create mode 100644 src/commands/components/chat/models/show-providers.ts create mode 100644 src/commands/components/chat/models/switch-model.ts create mode 100644 src/commands/components/chat/models/switch-provider.ts create mode 100644 src/commands/components/chat/print-mode.ts create mode 100644 src/commands/components/chat/session/list-sessions.ts create mode 100644 src/commands/components/chat/session/restore-messages.ts create mode 100644 src/commands/components/chat/session/show-session-info.ts create mode 100644 src/commands/components/chat/state.ts create mode 100644 src/commands/components/chat/usage/show-usage.ts create mode 100644 src/commands/components/dashboard/build-config.ts create mode 100644 src/commands/components/dashboard/display.ts create mode 100644 src/commands/components/dashboard/render-content.ts create mode 100644 src/commands/components/dashboard/render-footer.ts create mode 100644 src/commands/components/dashboard/render-header.ts create mode 100644 src/commands/components/dashboard/render-left-content.ts create mode 100644 src/commands/components/dashboard/render-right-content.ts create mode 100644 src/commands/components/execute/execute-solid.tsx create mode 100644 src/commands/components/execute/execute.tsx create mode 100644 src/commands/components/execute/index.ts create mode 100644 src/commands/dashboard.ts create mode 100644 src/commands/handlers.ts create mode 100644 src/commands/handlers/chat.ts create mode 100644 src/commands/handlers/classify.ts create mode 100644 src/commands/handlers/config.ts create mode 100644 src/commands/handlers/plan.ts create mode 100644 src/commands/handlers/registry.ts create mode 100644 src/commands/handlers/run.ts create mode 100644 src/commands/handlers/serve.ts create mode 100644 src/commands/handlers/validate.ts create mode 100644 src/commands/mcp.ts create mode 100644 src/commands/runner.ts create mode 100644 src/commands/runner/create-plan.ts create mode 100644 src/commands/runner/display-header.ts create mode 100644 src/commands/runner/display-plan.ts create mode 100644 src/commands/runner/execute-plan.ts create mode 100644 src/commands/runner/execute.ts create mode 100644 src/commands/runner/utils.ts create mode 100644 src/constants/agent.ts create mode 100644 src/constants/auto-scroll.ts create mode 100644 src/constants/banner.ts create mode 100644 src/constants/bash.ts create mode 100644 src/constants/bashPatterns.ts create mode 100644 src/constants/chat-service.ts create mode 100644 src/constants/command-suggestion.ts create mode 100644 src/constants/components.ts create mode 100644 src/constants/copilot.ts create mode 100644 src/constants/dashboard.ts create mode 100644 src/constants/diff.ts create mode 100644 src/constants/edit.ts create mode 100644 src/constants/embeddings.ts create mode 100644 src/constants/file-picker.ts create mode 100644 src/constants/files.ts create mode 100644 src/constants/general.ts create mode 100644 src/constants/github-issue.ts create mode 100644 src/constants/glob.ts create mode 100644 src/constants/grep.ts create mode 100644 src/constants/handlers.ts create mode 100644 src/constants/help-commands.ts create mode 100644 src/constants/home-screen.ts create mode 100644 src/constants/home.ts create mode 100644 src/constants/input-editor.ts create mode 100644 src/constants/learning.ts create mode 100644 src/constants/login.ts create mode 100644 src/constants/mouse-handler.ts create mode 100644 src/constants/mouse-scroll.ts create mode 100644 src/constants/ollama.ts create mode 100644 src/constants/paste.ts create mode 100644 src/constants/paths.ts create mode 100644 src/constants/patterns.ts create mode 100644 src/constants/provider-quality.ts create mode 100644 src/constants/providers.ts create mode 100644 src/constants/read.ts create mode 100644 src/constants/reasoning.ts create mode 100644 src/constants/rules.ts create mode 100644 src/constants/runner.ts create mode 100644 src/constants/serve.ts create mode 100644 src/constants/spinner.ts create mode 100644 src/constants/status-messages.ts create mode 100644 src/constants/styles.ts create mode 100644 src/constants/syntax-highlight.ts create mode 100644 src/constants/terminal.ts create mode 100644 src/constants/themes.ts create mode 100644 src/constants/tips.ts create mode 100644 src/constants/tools.ts create mode 100644 src/constants/tui-components.ts create mode 100644 src/constants/ui.ts create mode 100644 src/constants/view.ts create mode 100644 src/constants/write.ts create mode 100644 src/index.ts create mode 100644 src/interfaces/AgentOptions.ts create mode 100644 src/interfaces/AgentResult.ts create mode 100644 src/interfaces/AppProps.ts create mode 100644 src/interfaces/AutoScrollOptions.ts create mode 100644 src/interfaces/BoxOptions.ts create mode 100644 src/interfaces/ChatOptions.ts create mode 100644 src/interfaces/ChatTUIOptions.ts create mode 100644 src/interfaces/ExecutionResult.ts create mode 100644 src/interfaces/FileOperation.ts create mode 100644 src/interfaces/InputEditorOptions.ts create mode 100644 src/interfaces/MouseHandlerCallbacks.ts create mode 100644 src/interfaces/MouseScrollOptions.ts create mode 100644 src/interfaces/PastedContent.ts create mode 100644 src/interfaces/SpinnerOptions.ts create mode 100644 src/interfaces/ToolCallParams.ts create mode 100644 src/interfaces/commandContext.ts create mode 100644 src/interfaces/memory.ts create mode 100644 src/prompts/audit-prompt.ts create mode 100644 src/prompts/index.ts create mode 100644 src/prompts/system/agent.ts create mode 100644 src/prompts/system/code-review.ts create mode 100644 src/prompts/system/debugging.ts create mode 100644 src/prompts/system/default.ts create mode 100644 src/prompts/system/environment.ts create mode 100644 src/prompts/system/git.ts create mode 100644 src/prompts/system/memory.ts create mode 100644 src/prompts/system/planner.ts create mode 100644 src/prompts/system/refactoring.ts create mode 100644 src/prompts/system/tools.ts create mode 100644 src/prompts/ui/help.ts create mode 100644 src/providers/chat.ts create mode 100644 src/providers/copilot.ts create mode 100644 src/providers/copilot/auth.ts create mode 100644 src/providers/copilot/chat.ts create mode 100644 src/providers/copilot/credentials.ts create mode 100644 src/providers/copilot/models.ts create mode 100644 src/providers/copilot/state.ts create mode 100644 src/providers/copilot/token.ts create mode 100644 src/providers/copilot/usage.ts create mode 100644 src/providers/copilot/utils.ts create mode 100644 src/providers/credentials.ts create mode 100644 src/providers/index.ts create mode 100644 src/providers/login.ts create mode 100644 src/providers/login/copilot-login.ts create mode 100644 src/providers/login/handlers.ts create mode 100644 src/providers/login/initialize.ts create mode 100644 src/providers/login/ollama-login.ts create mode 100644 src/providers/login/utils.ts create mode 100644 src/providers/ollama.ts create mode 100644 src/providers/ollama/chat.ts create mode 100644 src/providers/ollama/credentials.ts create mode 100644 src/providers/ollama/models.ts create mode 100644 src/providers/ollama/pull.ts create mode 100644 src/providers/ollama/state.ts create mode 100644 src/providers/ollama/stream.ts create mode 100644 src/providers/ollama/validation.ts create mode 100644 src/providers/registry.ts create mode 100644 src/providers/status.ts create mode 100644 src/services/__tests__/agent-stream.test.ts create mode 100644 src/services/agent-loader.ts create mode 100644 src/services/agent-stream.ts create mode 100644 src/services/agent.ts create mode 100644 src/services/auto-compaction.ts create mode 100644 src/services/cascading-provider/availability.ts create mode 100644 src/services/cascading-provider/index.ts create mode 100644 src/services/cascading-provider/orchestrator.ts create mode 100644 src/services/chat-tui-service.ts create mode 100644 src/services/chat-tui/agents.ts create mode 100644 src/services/chat-tui/auth.ts create mode 100644 src/services/chat-tui/commands.ts create mode 100644 src/services/chat-tui/files.ts create mode 100644 src/services/chat-tui/initialize.ts create mode 100644 src/services/chat-tui/learnings.ts create mode 100644 src/services/chat-tui/message-handler.ts create mode 100644 src/services/chat-tui/models.ts create mode 100644 src/services/chat-tui/permissions.ts create mode 100644 src/services/chat-tui/print-mode.ts create mode 100644 src/services/chat-tui/streaming.ts create mode 100644 src/services/chat-tui/usage.ts create mode 100644 src/services/chat-tui/utils.ts create mode 100644 src/services/code-review-service.ts create mode 100644 src/services/command-suggestion-service.ts create mode 100644 src/services/command-suggestion/analyze.ts create mode 100644 src/services/command-suggestion/context.ts create mode 100644 src/services/command-suggestion/format.ts create mode 100644 src/services/command-suggestion/patterns.ts create mode 100644 src/services/command-suggestion/state.ts create mode 100644 src/services/config.ts create mode 100644 src/services/debugging-service.ts create mode 100644 src/services/environment-service.ts create mode 100644 src/services/executor.ts create mode 100644 src/services/file-picker-service.ts create mode 100644 src/services/file-picker/files.ts create mode 100644 src/services/file-picker/filter.ts create mode 100644 src/services/file-picker/match.ts create mode 100644 src/services/file-picker/state.ts create mode 100644 src/services/github-issue-service.ts create mode 100644 src/services/github-issue/enrich.ts create mode 100644 src/services/github-issue/extract.ts create mode 100644 src/services/github-issue/fetch.ts create mode 100644 src/services/github-issue/format.ts create mode 100644 src/services/github-issue/repo.ts create mode 100644 src/services/github-pr/cli.ts create mode 100644 src/services/github-pr/fetch.ts create mode 100644 src/services/github-pr/format.ts create mode 100644 src/services/github-pr/index.ts create mode 100644 src/services/github-pr/url.ts create mode 100644 src/services/index.ts create mode 100644 src/services/learning-service.ts create mode 100644 src/services/learning/__tests__/vector-store.test.ts create mode 100644 src/services/learning/analyze.ts create mode 100644 src/services/learning/assistant.ts create mode 100644 src/services/learning/categorize.ts create mode 100644 src/services/learning/deduplicate.ts create mode 100644 src/services/learning/detect.ts create mode 100644 src/services/learning/embeddings.ts create mode 100644 src/services/learning/extract.ts create mode 100644 src/services/learning/format.ts create mode 100644 src/services/learning/index.ts create mode 100644 src/services/learning/persistence.ts create mode 100644 src/services/learning/semantic-search.ts create mode 100644 src/services/learning/vector-store.ts create mode 100644 src/services/mcp/client.ts create mode 100644 src/services/mcp/index.ts create mode 100644 src/services/mcp/manager.ts create mode 100644 src/services/mcp/tools.ts create mode 100644 src/services/memory-service.ts create mode 100644 src/services/permissions.ts create mode 100644 src/services/permissions/__tests__/bash-matcher.test.ts create mode 100644 src/services/permissions/__tests__/path-matcher.test.ts create mode 100644 src/services/permissions/__tests__/pattern-index.test.ts create mode 100644 src/services/permissions/index.ts create mode 100644 src/services/permissions/matchers/bash.ts create mode 100644 src/services/permissions/matchers/index.ts create mode 100644 src/services/permissions/matchers/path.ts create mode 100644 src/services/permissions/optimized.ts create mode 100644 src/services/permissions/pattern-cache.ts create mode 100644 src/services/permissions/pattern-index.ts create mode 100644 src/services/plan-service.ts create mode 100644 src/services/planner.ts create mode 100644 src/services/project-config.ts create mode 100644 src/services/provider-quality/feedback-detector.ts create mode 100644 src/services/provider-quality/index.ts create mode 100644 src/services/provider-quality/persistence.ts create mode 100644 src/services/provider-quality/router.ts create mode 100644 src/services/provider-quality/score-manager.ts create mode 100644 src/services/provider-quality/task-detector.ts create mode 100644 src/services/reasoning-agent.ts create mode 100644 src/services/reasoning/__tests__/memory-selection.test.ts create mode 100644 src/services/reasoning/__tests__/quality-evaluation.test.ts create mode 100644 src/services/reasoning/__tests__/retry-policy.test.ts create mode 100644 src/services/reasoning/__tests__/termination-detection.test.ts create mode 100644 src/services/reasoning/__tests__/utils.test.ts create mode 100644 src/services/reasoning/context-compression.ts create mode 100644 src/services/reasoning/index.ts create mode 100644 src/services/reasoning/memory-selection.ts create mode 100644 src/services/reasoning/orchestrator.ts create mode 100644 src/services/reasoning/quality-evaluation.ts create mode 100644 src/services/reasoning/retry-policy.ts create mode 100644 src/services/reasoning/termination-detection.ts create mode 100644 src/services/reasoning/utils.ts create mode 100644 src/services/refactoring-service.ts create mode 100644 src/services/rules-service.ts create mode 100644 src/services/rules/categorize.ts create mode 100644 src/services/rules/format.ts create mode 100644 src/services/rules/load.ts create mode 100644 src/services/rules/prompt.ts create mode 100644 src/services/session.ts create mode 100644 src/services/upgrade.ts create mode 100644 src/stores/theme-store.ts create mode 100644 src/stores/todo-store.ts create mode 100644 src/stores/usage-store.ts create mode 100644 src/tools/bash.ts create mode 100644 src/tools/bash/execute.ts create mode 100644 src/tools/bash/output.ts create mode 100644 src/tools/bash/params.ts create mode 100644 src/tools/bash/process.ts create mode 100644 src/tools/edit.ts create mode 100644 src/tools/edit/execute.ts create mode 100644 src/tools/edit/params.ts create mode 100644 src/tools/edit/validate.ts create mode 100644 src/tools/glob.ts create mode 100644 src/tools/glob/definition.ts create mode 100644 src/tools/glob/execute.ts create mode 100644 src/tools/grep.ts create mode 100644 src/tools/grep/definition.ts create mode 100644 src/tools/grep/execute.ts create mode 100644 src/tools/grep/search.ts create mode 100644 src/tools/index.ts create mode 100644 src/tools/read.ts create mode 100644 src/tools/read/execute.ts create mode 100644 src/tools/read/format.ts create mode 100644 src/tools/read/params.ts create mode 100644 src/tools/schema/clean.ts create mode 100644 src/tools/schema/convert.ts create mode 100644 src/tools/todo-read.ts create mode 100644 src/tools/todo-write.ts create mode 100644 src/tools/types.ts create mode 100644 src/tools/view.ts create mode 100644 src/tools/view/execute.ts create mode 100644 src/tools/write.ts create mode 100644 src/tools/write/execute.ts create mode 100644 src/tools/write/params.ts create mode 100644 src/tui-solid/app.tsx create mode 100644 src/tui-solid/components/agent-select.tsx create mode 100644 src/tui-solid/components/bouncing-loader.tsx create mode 100644 src/tui-solid/components/command-menu.tsx create mode 100644 src/tui-solid/components/diff-view.tsx create mode 100644 src/tui-solid/components/file-picker.tsx create mode 100644 src/tui-solid/components/header.tsx create mode 100644 src/tui-solid/components/index.ts create mode 100644 src/tui-solid/components/input-area.tsx create mode 100644 src/tui-solid/components/learning-modal.tsx create mode 100644 src/tui-solid/components/log-entry.tsx create mode 100644 src/tui-solid/components/log-panel.tsx create mode 100644 src/tui-solid/components/logo.tsx create mode 100644 src/tui-solid/components/mcp-add-form.tsx create mode 100644 src/tui-solid/components/mcp-select.tsx create mode 100644 src/tui-solid/components/mode-select.tsx create mode 100644 src/tui-solid/components/model-select.tsx create mode 100644 src/tui-solid/components/permission-modal.tsx create mode 100644 src/tui-solid/components/provider-select.tsx create mode 100644 src/tui-solid/components/select-menu.tsx create mode 100644 src/tui-solid/components/status-bar.tsx create mode 100644 src/tui-solid/components/streaming-message.tsx create mode 100644 src/tui-solid/components/theme-select.tsx create mode 100644 src/tui-solid/components/thinking-indicator.tsx create mode 100644 src/tui-solid/components/todo-panel.tsx create mode 100644 src/tui-solid/constants/text-attributes.ts create mode 100644 src/tui-solid/context/app.tsx create mode 100644 src/tui-solid/context/dialog.tsx create mode 100644 src/tui-solid/context/exit.tsx create mode 100644 src/tui-solid/context/helper.tsx create mode 100644 src/tui-solid/context/index.ts create mode 100644 src/tui-solid/context/keybind.tsx create mode 100644 src/tui-solid/context/route.tsx create mode 100644 src/tui-solid/context/theme.tsx create mode 100644 src/tui-solid/index.ts create mode 100644 src/tui-solid/routes/home.tsx create mode 100644 src/tui-solid/routes/index.ts create mode 100644 src/tui-solid/routes/session.tsx create mode 100644 src/tui-solid/types/index.ts create mode 100644 src/tui-solid/ui/dialog.tsx create mode 100644 src/tui-solid/ui/index.ts create mode 100644 src/tui-solid/ui/spinner.tsx create mode 100644 src/tui-solid/ui/toast.tsx create mode 100644 src/tui/App.tsx create mode 100644 src/tui/components/AgentSelect.tsx create mode 100644 src/tui/components/BouncingLoader.tsx create mode 100644 src/tui/components/CommandMenu.tsx create mode 100644 src/tui/components/DiffView.tsx create mode 100644 src/tui/components/FilePicker.tsx create mode 100644 src/tui/components/Header.tsx create mode 100644 src/tui/components/InputArea.tsx create mode 100644 src/tui/components/LearningModal.tsx create mode 100644 src/tui/components/LogPanel.tsx create mode 100644 src/tui/components/MCPSelect.tsx create mode 100644 src/tui/components/ModelSelect.tsx create mode 100644 src/tui/components/PermissionModal.tsx create mode 100644 src/tui/components/SelectMenu.tsx create mode 100644 src/tui/components/StatusBar.tsx create mode 100644 src/tui/components/StreamingMessage.tsx create mode 100644 src/tui/components/ThemeSelect.tsx create mode 100644 src/tui/components/TodoPanel.tsx create mode 100644 src/tui/components/diff-view/index.tsx create mode 100644 src/tui/components/diff-view/line-renderers.tsx create mode 100644 src/tui/components/diff-view/utils.ts create mode 100644 src/tui/components/home/HomeContent.tsx create mode 100644 src/tui/components/home/HomeFooter.tsx create mode 100644 src/tui/components/home/HomeScreen.tsx create mode 100644 src/tui/components/home/Logo.tsx create mode 100644 src/tui/components/home/PromptBox.tsx create mode 100644 src/tui/components/home/SessionHeader.tsx create mode 100644 src/tui/components/home/index.ts create mode 100644 src/tui/components/index.ts create mode 100644 src/tui/components/input-line/index.tsx create mode 100644 src/tui/components/log-panel/entry-renderers.tsx create mode 100644 src/tui/components/log-panel/index.tsx create mode 100644 src/tui/components/log-panel/thinking-indicator.tsx create mode 100644 src/tui/components/log-panel/utils.ts create mode 100644 src/tui/hooks/index.ts create mode 100644 src/tui/hooks/useAutoScroll.ts create mode 100644 src/tui/hooks/useMouseScroll.ts create mode 100644 src/tui/index.ts create mode 100644 src/tui/mouse-handler.ts create mode 100644 src/tui/store.ts create mode 100644 src/tui/types.ts create mode 100644 src/types/agent-config.ts create mode 100644 src/types/agent.ts create mode 100644 src/types/banner.ts create mode 100644 src/types/chat-service.ts create mode 100644 src/types/command-suggestion.ts create mode 100644 src/types/commandHandler.ts create mode 100644 src/types/components.ts create mode 100644 src/types/copilot-usage.ts create mode 100644 src/types/copilot.ts create mode 100644 src/types/dashboard.ts create mode 100644 src/types/diff.ts create mode 100644 src/types/embeddings.ts create mode 100644 src/types/file-picker.ts create mode 100644 src/types/github-issue.ts create mode 100644 src/types/github-pr.ts create mode 100644 src/types/handlers.ts create mode 100644 src/types/home-screen.ts create mode 100644 src/types/index.ts create mode 100644 src/types/input-editor.ts create mode 100644 src/types/learning.ts create mode 100644 src/types/log.ts create mode 100644 src/types/mcp.ts create mode 100644 src/types/ollama.ts create mode 100644 src/types/permissions.ts create mode 100644 src/types/planner.ts create mode 100644 src/types/project-config.ts create mode 100644 src/types/provider-quality.ts create mode 100644 src/types/providers.ts create mode 100644 src/types/reasoning.ts create mode 100644 src/types/rules.ts create mode 100644 src/types/runner.ts create mode 100644 src/types/session.ts create mode 100644 src/types/spinner.ts create mode 100644 src/types/streaming.ts create mode 100644 src/types/theme.ts create mode 100644 src/types/todo.ts create mode 100644 src/types/tools.ts create mode 100644 src/types/tui.ts create mode 100644 src/types/usage.ts create mode 100644 src/ui/banner.ts create mode 100644 src/ui/banner/lines.ts create mode 100644 src/ui/banner/logo.ts create mode 100644 src/ui/banner/print.ts create mode 100644 src/ui/banner/render.ts create mode 100644 src/ui/components.ts create mode 100644 src/ui/components/box.ts create mode 100644 src/ui/components/header.ts create mode 100644 src/ui/components/list.ts create mode 100644 src/ui/components/message.ts create mode 100644 src/ui/components/status.ts create mode 100644 src/ui/index.ts create mode 100644 src/ui/input-editor.ts create mode 100644 src/ui/input-editor/cursor.ts create mode 100644 src/ui/input-editor/display.ts create mode 100644 src/ui/input-editor/editor.ts create mode 100644 src/ui/input-editor/keypress.ts create mode 100644 src/ui/input-editor/paste.ts create mode 100644 src/ui/input-editor/state.ts create mode 100644 src/ui/spinner.ts create mode 100644 src/ui/spinner/progress.ts create mode 100644 src/ui/spinner/scanner.ts create mode 100644 src/ui/spinner/spinner.ts create mode 100644 src/ui/styles.ts create mode 100644 src/ui/styles/apply.ts create mode 100644 src/ui/styles/colors.ts create mode 100644 src/ui/styles/text.ts create mode 100644 src/ui/tips.ts create mode 100644 src/ui/tips/parse.ts create mode 100644 src/ui/tips/render.ts create mode 100644 src/utils/diff.ts create mode 100644 src/utils/diff/format.ts create mode 100644 src/utils/diff/generate.ts create mode 100644 src/utils/diff/hunks.ts create mode 100644 src/utils/diff/index.ts create mode 100644 src/utils/diff/lcs.ts create mode 100644 src/utils/diff/lines.ts create mode 100644 src/utils/ensure-directories.ts create mode 100644 src/utils/progress-bar.ts create mode 100644 src/utils/syntax-highlight.ts create mode 100644 src/utils/syntax-highlight/detect.ts create mode 100644 src/utils/syntax-highlight/highlight.ts create mode 100644 src/utils/terminal.ts create mode 100644 src/utils/tools.ts create mode 100644 src/utils/tui-app/index.ts create mode 100644 src/utils/tui-app/input-utils.ts create mode 100644 src/utils/tui-app/mode-utils.ts create mode 100644 src/utils/tui-app/paste-utils.ts create mode 100644 src/version.json create mode 100644 tests/auto-scroll-constants.test.ts create mode 100644 tests/input-utils.test.ts create mode 100644 tests/paste-utils.test.ts create mode 100644 tests/tools.test.ts create mode 100644 tmp.txt create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d0247a --- /dev/null +++ b/.gitignore @@ -0,0 +1,263 @@ +# Codetyper.nvim - AI coding partner files +*.coder.* +.coder/ +.codetyper/ +.claude/ +# Node + TypeScript (generated via gitignore.io) + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory +coverage/ +*.lcov +.nyc_output + +# Grunt intermediate storage +.grunt + +# Bower dependency directory +bower_components/ + +# Compiled binary addons +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript build info +*.tsbuildinfo + +# Compiled output +dist/ +out/ +build/ +lib/ + +# Next.js, Nuxt.js, SvelteKit, Vercel, etc. +.next/ +.nuxt/ +.svelte-kit/ +.vercel/ +.cache/ +.parcel-cache/ +.output/ +.turbo/ + +# Serverless directories +.serverless/ + +# Environment files +.env +.env.*.local +.env.local + +# IDEs and editors +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.idea/ +*.sublime-project +*.sublime-workspace + +# Misc +.DS_Store +Thumbs.db +*.swp + +# OS generated files +Desktop.ini +*/Thumbs.db + +# Optional npm cache +.npm + +# Yarn Integrity file +.yarn-integrity + +# Yarn v2 +.pnp.* +.yarn/* +!.yarn/releases +!.yarn/plugins +!.yarn/sdks +!.yarn/versions + +# Coverage and test output +coverage/ +test-results/ + +# Temporary files +tmp/ +temp/ + +# eslint cache +.eslintcache + +# Node + TypeScript (generated via gitignore.io) +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory +coverage/ +*.lcov +.nyc_output + +# Grunt intermediate storage +.grunt + +# Bower dependency directory +bower_components/ + +# Compiled binary addons +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript build info +*.tsbuildinfo + +# Compiled output +dist/ +out/ +build/ +lib/ + +# Next.js, Nuxt.js, SvelteKit, Vercel, etc. +.next/ +.nuxt/ +.svelte-kit/ +.vercel/ +.cache/ +.parcel-cache/ +.output/ +.turbo/ + +# Serverless directories +.serverless/ + +# Environment files +.env +.env.*.local +.env.local +.env.test +.env.production.local + +# dotenv environment variables for direnv +.envrc + +# Local env files +.local/ + +# IDEs and editors +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.idea/ +*.sublime-project +*.sublime-workspace + +# Misc +.DS_Store +Thumbs.db +*.swp + +# OS generated files +Desktop.ini +*/Thumbs.db + +# Optional npm cache +.npm + +# Yarn Integrity file +.yarn-integrity + +# Yarn v2 / Plug'n'Play +.pnp.* +.yarn/* +!.yarn/releases +!.yarn/plugins +!.yarn/sdks +!.yarn/versions + +# pnpm +.pnpm-store/ +.pnpm-debug.log + +# Coverage and test output +coverage/ +test-results/ +jest-test-results.json + +# Temporary files +tmp/ +temp/ + +# eslint cache +.eslintcache + +# Parcel cache +.cache/ +.parcel-cache/ + +# Turborepo +.turbo/ + +# Storybook build outputs +out-storybook/ +storybook-static/ + +# Build artifacts from tools +*.tgz +*.snapshot + +# Generated files +*.log.* +npm-debug.log* + +# Lockfiles (if you prefer to ignore; typically keep them) +# package-lock.json +# yarn.lock +# pnpm-lock.yaml + +# Local build files +.cache-loader +!.env.example + +# Other +.vscode-test/ +coverage-final.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d4427d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Carlos Gutierrez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0749de2 --- /dev/null +++ b/README.md @@ -0,0 +1,284 @@ +# CodeTyper CLI + +An AI-powered terminal coding agent with an interactive TUI. CodeTyper autonomously executes coding tasks using tool calls with granular permission controls and intelligent provider routing. + +![CodeTyper Welcome Screen](assets/CodetyperLogin.png) + +## How It Works + +CodeTyper is an autonomous coding agent that runs in your terminal. You describe what you want to build or fix, and CodeTyper: + +1. **Analyzes** your request and breaks it into steps +2. **Executes** tools (bash, read, write, edit) to accomplish the task +3. **Asks permission** before modifying files or running commands +4. **Learns** from your project to provide context-aware assistance + +### Cascading Provider System + +CodeTyper uses an intelligent provider routing system: + +``` +User Request + | + v +[Detect Task Type] --> code_generation, bug_fix, refactoring, etc. + | + v +[Check Ollama Score] --> Quality score from past interactions + | + v +[Route Decision] + | + +-- High Score (85%+) --> Ollama Only (trusted) + | + +-- Low Score (40%-) --> Copilot Only (needs improvement) + | + +-- Medium Score --> Cascade Mode + | + v + [1. Ollama generates response] + | + v + [2. Copilot audits for issues] + | + v + [3. Update quality scores] + | + v + [Return best response] +``` + +Over time, CodeTyper learns which provider performs best for different task types. + +## Installation + +```bash +# Clone and install +git clone https://github.com/your-username/codetyper-cli.git +cd codetyper-cli +bun install && bun run build && bun link + +# Login to a provider +codetyper login copilot + +# Start interactive chat +codetyper +``` + +## Features + +### Interactive TUI + +Full-screen terminal interface with real-time streaming responses. + +![CodeTyper Status View](assets/CodetyperView.png) + +**Key bindings:** +- `Enter` - Send message +- `Shift+Enter` - New line +- `/` - Open command menu +- `Ctrl+Tab` - Toggle interaction mode +- `Ctrl+T` - Toggle todo panel +- `Shift+Up/Down` - Scroll log panel +- `Ctrl+C` (twice) - Exit + +### Command Menu + +Press `/` to access all commands organized by category. + +![Command Menu](assets/CodetyperMenu.png) + +**Available Commands:** + +| Category | Command | Description | +|----------|---------|-------------| +| General | `/help` | Show available commands | +| General | `/clear` | Clear conversation history | +| General | `/exit` | Exit the chat | +| Session | `/save` | Save current session | +| Session | `/context` | Show context information | +| Session | `/usage` | Show token usage statistics | +| Session | `/remember` | Save a learning about the project | +| Session | `/learnings` | Show saved learnings | +| Settings | `/model` | Select AI model | +| Settings | `/agent` | Select agent | +| Settings | `/mode` | Switch interaction mode | +| Settings | `/provider` | Switch LLM provider | +| Settings | `/status` | Show provider status | +| Settings | `/theme` | Change color theme | +| Settings | `/mcp` | Manage MCP servers | +| Account | `/whoami` | Show logged in account | +| Account | `/login` | Authenticate with provider | +| Account | `/logout` | Sign out from provider | + +### Agent Mode with Diff View + +When CodeTyper modifies files, you see a clear diff view of changes. + +![Agent Mode with Diffs](assets/CodetyperAgentMode.png) + +**Interaction Modes:** +- **Agent** - Full access, can modify files +- **Ask** - Read-only, answers questions +- **Code Review** - Review PRs and diffs + +### Permission System + +Granular control over what CodeTyper can do. Every file operation requires approval. + +![Permission Modal](assets/CodetyperPermissionView.png) + +**Permission Scopes:** +- `[y]` Yes, this once +- `[s]` Yes, for this session +- `[a]` Always allow for this project +- `[g]` Always allow globally +- `[n]` No, deny this request + +### Model Selection + +Access to multiple AI models through GitHub Copilot. + +![Model Selection](assets/CodetyperCopilotModels.png) + +**Available Models:** +- GPT-5, GPT-5-mini (Unlimited) +- GPT-5.2-codex, GPT-5.1-codex +- Grok-code-fast-1 +- And more... + +### Theme System + +14+ built-in themes to customize your experience. + +![Theme Selection](assets/CodetyperThemes.png) + +**Available Themes:** +default, dracula, nord, tokyo-night, gruvbox, monokai, catppuccin, one-dark, solarized-dark, github-dark, rose-pine, kanagawa, ayu-dark, cargdev-cyberpunk + +## Providers + +| Provider | Models | Auth Method | Use Case | +|----------|--------|-------------|----------| +| **GitHub Copilot** | GPT-5, Claude, Gemini | OAuth (device flow) | Cloud-based, high quality | +| **Ollama** | Llama, DeepSeek, Qwen, etc. | Local server | Private, offline, zero-cost | + +### Cascade Mode + +When both providers are available, CodeTyper can use them together: + +1. **Ollama** processes the request first (fast, local) +2. **Copilot** audits the response for issues +3. Quality scores update based on audit results +4. Future requests route based on learned performance + +Check provider status with `/status`: + +``` +═══ Provider Status ═══ + +Current Provider: copilot +Cascade Mode: Enabled + +Ollama: + Status: ● Available + Quality Score: 72% + +Copilot: + Status: ● Available +``` + +## Configuration + +Settings are stored in `~/.config/codetyper/config.json`: + +```json +{ + "provider": "copilot", + "model": "auto", + "theme": "default", + "cascadeEnabled": true, + "maxIterations": 20, + "timeout": 30000 +} +``` + +### Project Context + +CodeTyper reads project-specific context from: +- `.github/` - GitHub workflows and templates +- `.codetyper/` - Project-specific rules and learnings +- `rules.md` - Custom instructions for the AI + +## CLI Usage + +```bash +# Start interactive TUI +codetyper + +# Start with a prompt +codetyper "Create a REST API with Express" + +# Continue last session +codetyper --continue + +# Resume specific session +codetyper --resume + +# Use specific provider +codetyper --provider ollama + +# Print mode (non-interactive) +codetyper --print "Explain this codebase" +``` + +## Tools + +CodeTyper has access to these built-in tools: + +| Tool | Description | +|------|-------------| +| `bash` | Execute shell commands | +| `read` | Read file contents | +| `write` | Create or overwrite files | +| `edit` | Find and replace in files | +| `todo-read` | Read current todo list | +| `todo-write` | Update todo list | + +### MCP Integration + +Connect external MCP (Model Context Protocol) servers for extended capabilities: + +```bash +# In the TUI +/mcp +# Then add a new server +``` + +## Development + +```bash +# Watch mode +bun run dev + +# Type check +bun run typecheck + +# Build +bun run build + +# Run tests +bun test + +# Lint +bun run lint +``` + +## Documentation + +- [Changelog](docs/CHANGELOG.md) - Version history and changes +- [Contributing](docs/CONTRIBUTING.md) - How to contribute + +## License + +MIT - See [LICENSE](LICENSE) for details. diff --git a/assets/CodetyperAgentMode.png b/assets/CodetyperAgentMode.png new file mode 100644 index 0000000000000000000000000000000000000000..6e9100515559ffcf4f00d6d83733f3a7fb128d50 GIT binary patch literal 234198 zcmeFYcT`hRmp)9BA|fgxQUp{4>C&a6s7P0eNC)Y?gccwmVy8)w-lQrm^iTo;L3(JR z2Z%@sJyMcDLdX~2d1vNb^BdR9{QqUGo1ENp_bL0_bI#t+v(JYo23jmk7-_Cy3hZ9=JOABtEUf&ghgA<7W}lJG8erf_Wsok z982xjuUjuV-wqI8UT-3D6;v7}cg%Qpx3m-D zVoH~DX&FafUaCGb$KxIUo|B7<@j_Mx*&E|?bL{;>efk6ePbQZO?;j~zQRP2*ogAV# zulG9P#xjeU!0;KW$(Kjiw3Uy7&D{1*Qc3*Z$F&VI)`F9>Wq-JynCLm)WwzI+`Ymu? zE1QO@pKcFv&?Nl+d_Swb=ygN>2Y31GAwxrAyx(^iv-qWMycWW8YNIxaPV{B8OZu0J z`a)V#g}cI-{2y>;7n9L`Cq9EipN6*WW6v3t%}DZOtR~73Z>oxF^n5S+`HAz&?~-4o zcTInB$aT&0JUcmuc6?CnlHR*__S1$V9mBA1ZHLyCn|)4N!*|qsm0!I5VZKxgO8z0X zb`H>8r}%ih(DBqn&9cPy6BnmU7x2}(ow@hMMd~^!u?$-=rnJfL9E8x}-L0F-?}oB5 z?i|%l9suK;Bu8{d20TyFaoByWd%%BruMO(rP~f;%U}ZRKQxN;aTt&UhY;B}J=2Ip7 zp#*w+?#V1?iQkE)%hhU)yVbM?9cN!t7nNB>MC>|VtPmSBs^PySmp1&shAVIQioZB| z+^fQqhblcBf|&m0bux365YUvEpUZtvMr_K-cnR2OSqtdxfRheV; z;jYizyWEpbsvG%KPWklpQEGRnuXNDgrQ-NW4UJM~q)y~NFGgea>8y0rRWa(pPOc|( zIvvi{EGE%P)!ec#qkrCUr4{Nh_q}L!MnOZpiM7b?DwL}5r|Q*r{EQ5V*F}oxH#FXU zQgo!1;#cV7KKuInm%Fzw>O{T%L}zinGFm5J`YYGm8M*tg`@LW4MxDNLv@o;Wzd6di zE))AoyIQ*940q>&9sSU2+Mka@u4mJ@be-|N4rQu(y|qN^D;j>K{VcxA=;(~fTbkR7 zmsyHqYBi*uh?y{o@P3ZHRQRmm<5Tvpp(ZkAw3BaIJ|q>`6sUcvaTUpoZ`5ebCyhRJ zl~CetyVQ2kI?h`IQZQ2JQ@}7*xbAaR`VpH$ykAeXR89Kjp-XxfOK9pi0OtVIhy>0a zXzz;}*$Szti(BvFx=QTR>=j-F+gVn-RJYAs-@bbOopD#jvX$TUyXPf1%wCs&zt)k{ zgI#*-$G7Jnat+F|$iSDt@oV~*=x^d)>2J_c>5<&?3U(f)albp&KC z#iVE?eUeYaB@TbuO4vH5qK?-6-yKNoU`1sV(6KHvBXoguk(c}Wd%hq zN-tF=fsG3NjUMV~j>WoqTF_Uf1f>UM;ehc|JZn6s*(`ue^dbB>zwFEAmjN^JXI@Wt z%t#fF%nqLEnjs6ON^noEReG23mb5B)F{NUc_~A;brahW#GPWr_5Hl zR=M8#`iAg^2NtFl=z1G>M7?S~qJFT!&@Ey8N#J|{ozkjOX<#nGbX_l$zAY&tSFNo4 z+g7RQ*g`R~P`S#bf}vNmw+ezMX;*`$R>6AyMas_H8eb(2@-P3zE$qVFZHrt47Z-Yi)f{ zZsf_%v3uL2+joAs{!CwgzIC_biSuh|d1(nn`)rk*XNoz#gq7`|*<0Z=$C4b(X#r^@ ziIFw#5%dU3CbG|T(-L=>n6Gz(MTA3M{HsGz-!g^SjIetQZ_`IzA zu~y4HTX-6ODta=)@QLOIO&KjOL*|)}w92o3@{{u`@3FpOiGI`Rb)En9wOY9q`>%ek zLF+#DGu5Njnbkr|Q%i$gtv9}2jJb)^f+ifsip4?soG#t^uo+wOK^BfJd9Q3! zzohx41xid?w>uyIJiGSMTJ+0jZBb>j4X}un9CR)?u#4+g=Aqp5ltrbuCDGe@s3rb> z<@d=qn(9ZVqb35j0uo%$F}3ZxT89a*_`>98yuZ%VZ>*NfqA}p_1B?G;e`$k%Mox=vA%C|k=u&K_b6_Lw)2e& zma-W4FAXU?H17o7<&raxwsyLx-zg{DiI?S*Q?Uq(4XtQmcekEXnoFG%^h)r+sP0BW z>$-}UXM9p&JIY_e3f77S`p1pCjo*|C8gOLuD&JH(*!LWm{WAO`2S4&7w>400JACBL zOXf5Qf|bhjk8LGTKX&}X;BZ=kt-Y07>!xo*1G1esBS-Y;4dn^704v-0z?K3E3xgvt z-_-E!bqa0XHqamKfeq?SFnrL&jxsG1?YJ^jd1Tjl9t4uaEFO_2S?{oZUUP!pfi{krrzU}BHIzh7QjbWdmNbaVJSGe=RcK7>{zBn{d2bP(?gV7M)kJ| zOwH}^I@Ju-29NUu*%{Gx5)32*=#$HWe&!v(s5;3E9`f~V&nO1$GpFa;&ieXPB9t^E z74p?!vzvnXH@@fA)ea7U^gZGRzwY4d~#t!~Y zPTl}lpMcEN)+9<%Gj1l&1D@;aDLMFfN!UC3yl|2T@$&s6LZuR-L`ix%1=#b4czJpQ zltNT*{No8FO8U=j$s7FtcqG6>^~Q7kC;XZ|{!aYz5_cr-+)z8q&(E*o@93;#^x)Ay z#VP+(Z@2~o_$onZMosPPlb5-a z(bu5TesIqugnD~{A)IHj;b_kx&>UVXW?4Ib3+WGTvg1b1XuKS~&n0kyUxTLT!s93b zNlBn&4##6)lzNoF>$KMb0@u5vu5$Y-G~pLx#E_e9NOD+JI<^XG0WW(ly4M=Ml9SU6 zKo18uzF#N=gdXmw|AvlU(4eBG``>@@3($SP|Kf@s&HqJ+QbPq*6iDU5|Gu6-jbgk` zO+&oD{^Ab%|Dk~tLB{Q6sh3nVjQs!WFAYYf2X_JAl=!y9{A2!4l2kO;1TG*8MSk`< zZZr3FJ^H`3RHBlimb9s?!t4L*rv7Q&g(n@x9~JDt1~Yd_Y{3I^wm_@lJdaQ0T$M&P z=Z@>B1RA$*{nC*=bU?4!P+@VfCtK&Tg3G|Nf>UTuh=)tm{C={2E^)zasa)Y)vQNbA zL4eL+Fvmcgq37W|#+wAbD(YM%Y5;{-2R zh$t;g*jZpm7;IF1NAjNj{50{Rbl|+Nw(gGj*waKKqi5H_lBiKnGH&j|kRNRh!w3r3 zaw9hSFWL>r)yN--BEidwM|w#+2=-~x@|N-n26OVr#tLWhSdU!x0WzRD4YJ%Nugdv1 zu9!R?`lc#)vCkZ$SXuSunc9A#@UXRa7GwP?uph6ge@AoOd&63?&sf@d;obWg#Bz=WFrLQE1p}3*|d(V?Z zIFxp9*7i4oZY%lfHDXl9McTK}hRNMWxe@rvs&AjoQY)~nUM6cA8Lq>^;OYm9R&_so z@}Afn2eMHe&4f|E{BrdIK9O@H>GPCv{F-UMu1CwF)CkX zu)u6&9kz$>BV(ebcl_Fa&x;RB;BZkMuv^o5g!l8Lx?>}gL`+?!%&sGA`fa`(EGMV^ zu#m_Ni;n{bj$Kjg`zPakHc1%J`+S3w&5QP@ z`_`t@RLWTG&;1Yk7g<%!=hsCMfX5TABL57 zA5vBPZPRI~=_q4S-oXbaUR;v?8fk0USsV zfaEEb&&b?T!hykBaN4j<^>WGdbwJa^bY*8Zqw7Ox-+RVyGSt#@^deP4ZpxzalzF{<xBCEjDfWwtB<~wRW&eG zNHY{8kx!Vn0bw>@rX)d!SDTk-Q3DalR=cwa^I{FArv9_NK51EG;=4;(=>l7)zY9iE zY@v8#|A6HmQd)~SYwCw*GWHV_20c)uMjop+qk_<${!fD?{mpNgBU644W`PeDe@OJ= zwLeRWn+pN-8I2x9=^y^;zlIc04Xd}pu1{Mi ze#Xt}OQx^|X3R+H^F}yiTzh^~ELc3E4Rn6|p_aD7>IJzMA@CCGTL zbF5+Nvur=GU073zR!wY5r#qexL?vR0Y5mMTr0zOmiDtRu;F4S}QLJ7+zo1siBingb zITi5e`}gmjS77td`p-YoR?MUnWP?J!*FrB-p&0jte^4Qw7V+kDMb35x|WD*1k@;a!-8Aru!_m z2WB~E+GCBsw{h``gtCq8$_6rZPbIp^%GJ~k^1l#cqgNuil`N5` zS$cjjMHKe?{ke?LjECxNKH!RQoEvM(jwcOd4TJ+oA5SG@zVFMe^%_$zT1g=%<$cRA zmnH*P2QL_(Do6`H+E3t3jEe+n%agDpYQ0_?!|5JXpDiN~i*|4)JA8@8t-*7v)xUj@ z_iGz2C8yucvN=tL;@U8Mr4gh8aRn$Ee#<|gODO|{aq--+k2?q22}7lBjvI|E%@1Sd zddXdCKhF-G$Z-X9b|vV{z4v5uvJf83JALDMbik+TKyUH>&H-suu5RV`2dmkpF8rJO z(Z6I}M}*}v6Q|NSCGeh!*QCWN-o|HBA9A%zPip4x7Ux+>mEj*o zfr5wn@2rNt)6RSCnyd)cSe(m{Bp>wm)PL#Xv$^h~`B;0Tc`E`hdG$7ffmvU|De(E}!IYSGY+TB8WAVKXLm%gR=^gCQ~jNNpyz??al zPh}Ut>rHr=p)j;JtGO-tGxL@)C?LB~XyhoC-J>?jfJbh~Sq~8**MK!Lu@Ll--%Sk# z-wuY5!s%h%!5030VXE1^lR5`!F*k=8xW2Wqc?C_hy)zt8J7Cj8h<$@tftruE66$c# z7v(0$(S&;+O2X7awo^QjfEgLbaH|T?(4h?V0zut~5HfbE&nF2Qa&X*#Zfu|`wRrmQ zL#hdWIve2ExZ%)23~6b1%lonhELNYR4CM-Tm3!Mt&2e~nV)njyD-g1t7IM_+E0hOHn!Fp+w%Cl=--Fw^6XQ#&ZzmQk6Dez>IoII$VTh);1mQ=ct3G zTq+Vv0(Q;R4%s-zgI`}x&G`(gBsGZ563p_{ymyB3c7|`pEb+Dv=X-VlOw}Ty)%FiVPLi}@K*Ewex_}I z?KSe@A>s@=JLmIhc8-^~_regk4V;w)R0J8MOV4K(8`2VG!}3y>C2mW!-L3`~qnl-H zv{WQ>YAjp4!02LLt-38$DN?d@k4pRSv(J6TC1SX=9vjK6gYyFmqPaE3&5;xLkNFU2 zUH~~}xhi?P=$xYYQ-1YDoFQ;y^bOTSG#|0~FwRTwN|7?}(agy9DPcsWHLY)Ysx?M@ zZ+jx1nKX2uhdzMZ+;TLSyZueJxP$C*V7uDBeMVXd_!2I0l$T|Ly zH!RETEmm=7tMNr{x$JW~$QELJg}(|X+hzhtt5pgr#87?34|pUwhBUfrdD5 z@;QKbx`ZPFAf)+r))t_Rl0!48y|pb@%R+Fvv1C#iPRjiFGkGu;c1ka-oQ38tU)#SY zwM*bGK0I_&Yy>8|Bs9jNIlEWyzaETGTc9_{e1Q>NIc4`DdLrIGD#dbU3gb^uc>+oj zi{`gzdkpJug_Cg&T2A=!A3>zm;6;bc8|v#Dg%?vZnX28kl*3=oAAIM#B_c6g_YL(v z(VwIM6K4VuGhR03g-t=~t9_28!)gM(XPK+^>2NHWDwFisjZ^)bG7PbK{DoKK>E$DO*jMVKl^^8QE$>l!n}n zOT<%$pI=UIRN`}Fn{K{4e+QQRTDOX+YIOWfCg%t|CP7AuWqqr!Bua$KwYF57KM9~m zax#m&cUz*;_gP>wvbm5eQfe`v>4E$;iH0Z;K;I7a#`fF8E3x{Cwu>@70Pkr7y@t-n zTy~6jEawGDU6_>qF9!*g%HH#OpYkHy>x+bX`|vi3>vg*B_fu33Hfk<$E_PMHK+hs) zeQ2oj!gt;qs_(vj`!aR(yuyeXOUO97zNB#)Mn9TARqP?JYwwrgQa~b>^sys)<}|>8 zC6SfFbqip_+H>3!SM62<0uY9_H?It5x<=EQ`OJwI3*PncePo+q|BAnX4pMS8D3jNi ztYp8DDQ;wKETk>SviJKsNuNIr7;D{UJHk1OJZ8h%5O@Etr zemprX+2%~Rr&YZ(-+UmiVKDgBRt;FsvKSv5`>7ex({F!ZmdF*t>9i{;iwr!*vU9`9 z4<-lc*a8$IdqToM*}X9lFQp^b=oIa%3hBY_8f)9%9Q1y>9zH}UdD3WNwX!1aEP;Xe zG5!ajNH+@_OFji5Js|ep#y6Z_vD;iu$<}tzfqGP@tU_;K&%rKo9(WgI*|PHEcBT}3EMcTBDIr7cNysTqm7Z8zJJ_~sXPuN<2tU7#JJ_>RW0-RAU=C=H zQ=LPjcvZhUPIUCws=KtxW*-o4lw>TS7EjC86B;AKLGMsD!6eUI>&UKp z*?!s7>ZGj=m4mcj1(~S|8bbG-X4C5Qt1?5%RYDdbbxf^`E9Tg(oZ4%mOXg;`NA@Cf zhfS6pF0B%Sq5REngjQlkKz*rOZ4QfF`#H#nTM4j`Qtg8Abe_JANN@%!!4_z8K6~!^ zwBG60@V&Q~jE(_ma~Ek^C3svN0#a-VIoKN73c8_STQ!ck)YYZ;(za|Jq=*S~_RdAO zXlbcQf?OtQ@Z({)Bs1{oN2s5F);wV?`4Y!P@yK*Ca147omrxRUU#;Q9Z8jzN`7W=b zVQvl}FC}v*3w#ab+3G?%X2#(<$IJ#~(+o5S~Xbsj5mLSf}$ z700$0fP6C3q|!u~t}XugRpkyrlMr5lZ%MHg#SAzyykmb;tUb zYYEe^q#g1;%P5Z5D!H{dZYZ@u@?;L$XdU9`u@kcB&}w}ifE;VJH42Rkg9zYLCZ=Wi z<^v}SZ5!@^xQg)eUXTG}vytRvBqa0l8+-p92UI=hiJol6P}8z;5cK5cyA>C{CL
xJuAbqO0-Pc5-9DTL{N(Unn!~`=t>f@Wg1|96hc9hj zwVg9ITyO~;|ESZ$vFZ_6Ss!&5LBz@XWCL?KF2QjomS!%k>-*^!H5~_cHH~b{on&rr zuqmWuI&UF@CJZ@k;CH@qHZ)?C-H8E{k!zV^A#<|TK843)@=ni_x-&e(48o3Dz63q3 zBUt0&j95#2dBh;a)NP6rw6>DKcd(AUx-f+u^eVb2o8QZFpSI17Gn7#!yz1mmOhK7A00Ua!tV;1%o-sA>aOIhYQvmp3+F?=kq zH$^Ht=%OmEV4A}#92xs1EP}pOQ)-cNPt3?P!){#p%7G`o#55nS?Q-fSuDiw59Ucl8 zzm{1I8wFCW48x44FV{qx&#O^){hy0bg{3R${agxe0jbxt7~*2$YHrmucs7WN#e+-4xM{ve+u+E=sIfO4D7pxu$1@S4-2rG{iyM9N=9 zY)nu?xKgg!zcR*x23o&;aNb8W2fUeD97(A19Ui*)7nJI^&?onH2%=<_F-xB1mt~Fj zJ5qEoe5y3h!{gY-%UgXUhzN2;I4A_8cF@b->I^tFgZu;R?(~!syn=&`Q}oSh+^w@n zC7Ws@s}8)D`W2VJ`iYgy#?8+2%Lp>u3)~m)uvI+eNDB2@=JTRj@B&B(Fz#>g{4{{s zaF*zpzacS=>Z{qkItcB>e|A9~Rjqtp(|H(}p#nR67X(Xy@4*GXF1zA1;47sEmi4FP zmERev`!$NJbV?7h8qX-h>a-xz7v4KNP-`@NHR8x!Acl=1#E+seuCr{xn&N*D=+sRPm}E_YzeROwfn++fE0W zo0%A2x3$JIG`|>wIQ8k~C69nfm109zg>=E>w}&4;dQ3_!sx%m%)yItny0Fe5gW)^- z^VUHXeu5tM9+?&uDs5T=dY_h)1?z0xygtvXG)cKaX~kOH8hwTKN`$G z{U*QGL%iyZQlF2A+-=Om^U#R5f+)sSJsQ6-oF3(^JCdEsp#~~FgmXzGK$~@VE$f~- zF9d>5Hey)&6S0`kph^+piMf9t9ocsiyQg@8|Jf`d+bm% zhWeQ3V4Et$VPpTIXE7xUM!d->72NIuEn!|(pEZeFdtlT9?o_$0~42Ha_t%*5x638FYdIY8?-`tNlA$2OQ=$*Ci@*` zSPedKVG)8#s+qRIa6Z#rKHuiq(gs_DjOZ>dX$G0s9m1i@5=;siyN4l`wxm{p8)V6poxMzoyMq@{g0aqXT?Kg(9~H~p3<@BPx>=4-$=L!;2maXE34|0TF3gMlHG!G;&pOw!d+t&WAS* zR4TH)k1@+jVyJRIcYBhZt0?$kki*L%V=&(y-6jZwVORxBpG$Q$-o+0{fmZP40E$^} zsO{8t67)lx+XNbJ_QesOkalR(PO%1uy{ue)U{kTBPOe$b=RKC)8i07a;V#=ESQSD; zjI_mG>b7YMGDZQar6@yX<0HqGiI~SBG(`2uRbY-q@m@zO1o|s4Qdu>8?PmZHI@zHR z2?*+8oRGZA;R4lwbjG5mexRCbgk=asJZ@Ipw(Z&o| zjHnRYLE92z!*pp{riZ>_69kbUglW4p;KC!CR8hl9X7PU`iH?XW&qU8{xD;nLKc>Y& z2Y)NQD21bk2^rO}HQa$G1oe)f{1z5SaY9*f06W!Z`3He(wV2=9V|!%EaY&42>_b3No_6H{SpHDg6eqXY@d9=9m2QK$w+C_#sR^okYSASa^uWye$_75-P z!E8zVe9TumCit_aYLV8!vldplPK{_bT9G_7tLH3&FOJyR1#_109uYPt@*xGs-eE){^1`fEGI5N1l zw%gXOnwv&AYON(dpLMOpxKZUGWHt01s81RofN&X23FQV|Uf!@Ap{!gOXs*`V_|$%k zuhh-CqXmH7pMI0jr|D3Yi1G5{11!frb$+#5Z<;=RlcgK+4Dm!VvtLqiZUHd8yMi4d zKzq{pt&5kl%Vbf2o@0vTzPTG*)5eDn1vR|NbYA9G{@zWAT5=_TN>A5=*F^OSa{UiT zZH;y)bsA9QF38`T2*fP=%$m4PJ%0yL< zXO#~bW9T$tI^8!vqT*OKDFJxf=5foE&0c|j`^ajW)l5Ai#)YrjG4T%z{EMzIW8^7DrX?G$qH#_nX$7{$cg{0edt1fPj^pU;?VwYnP-KQG5;>e*sKoN}3G6uT8#0{sj$>z=y@sj-wGY%gW6rI=0<_f)x~Ph>r8 z@~`NrgBDz?9KXdE!ir4ND|`v+lZ3RpX7F6G%qzCGk@YE!^nQwPeRYV$wY?C(tpfD& zTIgML@sFB}R^A|4m4&S0CksJ@!- zg=R)Pyv1Q-8DI-%oEzgj2&l5?0jBpCeY>|xI4yJvkxO0@KDax+HunR!n;m($H*WLC zz-iAZ9HQ_e{~iC%fBDRsmHd=}t~#c&qO4{zB4}HAGq{ZXvs}dVmJTG(rIn;sKT9h& z`xAHI2g#!iW93^9ZzIYkap{vb4C~N-E?d^>8YY3QG^J{1^v$WTbOs!1 z>GyCsm|mC(RJ{IM0;5lz=Amcz)Uo?a+L{v5kCehCAu^JD)wrPEMGtD&VRLY>g+_pz zm(R4SQPu}8)XuWV=v6f&A@*Q?(or+h%TUH|4g*%PlD8&Tc|&N}McL$UPu z{-gjSyK>KL1q+LIC6@R#9NbYr9&bq}HvYoX*m{sM4^JRDjPRab} z&nfRvB2*3j(kL~9WMT{mQmb)Fj)g5b8w2I%1tL9Sv-}!aGL-Z$A;wpBY*kh(_UBtT zN3h58Z#{uM-YuhLE}tktk|mvC7stk^2lw37BPAE(wy?Ir`h`oD?ia_}qjbfw)3M>o zWK!nP$%Lx(=0Ld$_7j%!I_Dxwo)Ri^@&GjpyyeXiT>O5=D{2K!S9J$Pi(pJuV@B^5 zaa`+ZDWk?Ev4heuEA&i-g_2Ex$!qw^`OQ?)0zqlax9-7e$=&raZfX@E{c`%oFpJEf9snK?eI z^eEWnJ%u$9gjV!|Y?EO$Kn~!V+0O_GSryD1%JL>2E5F6J#D_qe0x1|Z6dfjQHn zF@-U4bYm*$6rgpjI&%PQ%))49DaVts5^8*`zW{dVt5L(#8wg$q*Woi`GcVo##@>(x zx7fN{rH&zFze^qiAKkSV*DUH#d&;&=vXDX-&@!Yxa}|8k0*Ri!Sl1Gw2VcSNrnZ%| z{CrcSx!Cp!n46W)g0Ut^LVf`^BJ<-dkw<;Muc1&%DBd`Hc7)tLl`34?iq0?Q;A8G# zYzyMEAo4|k!;gEAWo(R>SQqba{;E7fYAvSbxJlz)y13e^}5D+OFY=a})xM6u+TbnsP6C=h&n72w+9i5upm`(azR1Aqrd*>JG-c3e zszBJAI>Fh~mXo;>z3ekLzw3 z$W`!s=qKO`OHjBBo#};emMS%x8VZC zSMK}+5YFsOVxax84d|6C{W{!U?P*LhDB(sF-!NqQ`6T8u`W!2sMfdT0{nV0K%)~9l z_Jr&{pNc1?_^^y2m$|b#(ZpQ}DupY{RbkcSvXV6gVk-^@DK#v`4?Fnu zziG;rKZk8cn!+Mg0~KTNh2u4_L1yMX?EfcvgZvzq^$&F-VVbd#jEq(cXO|4z)1wME+u9 zT{~n7QgMMz%@ay~`zwBrU@I~UE!lf>OlwPgdoI^MzQFZdI*0zFb4FgYi6k~l z1!Uj>AK}%RX=YA}7pqwcuPz44McY;H9JaUlCXfqSAR#xWnghoiTAO*VNogrKB(B0K$#`h__I_J!$DBD)mc7h)` zAR`Cy;WqSy_FlygxKNvRkb9|~K3p~`oTI%oD@$#zhw5(cetVmAVNHE=op%J`>g=cB z$7NZL+oE!vC38u78S6OjgQ1g4g)desvs25P8t^r(b0P2_^sd=a^oJV>jZ^KBO{CL# zFW6c*1@O*7!kV!=SQVwZ$h<>&DJ#{mTSOiahwYHD5E+bszuL8-oG%EM#{uE5cr04y z>NYwydmID0oR8W^&Sl><{{wUfn#PSty1csx0M}E*NVdZwc}Tn7gIUEsWsb#$S2Irs zd)==&w8dePMe+#PnEf~`a%EAyT`YP?TiI*F*zmE}!#6DJt>h7KUs`I_g@aFd`8(YfU%@Pmz>}u-pybJ;Qc;`fAZiY%!$8(95Z~=o}}7G1*Pn#|B!ij6t3a zUV{-T^`OVkn0&4`^@NFGoZxfl`d1EGtA~rK!`6A0EE>=?(S$*>XHd~XaQl=u680kF za_(L4G|KE%OJ?*%zwRF`Ee>JGAS@7Wd@*#ylte|kFn_!ScoJ!xq%fTFGcLt#Su5Ss zHZYfp2XmU!P*JQBxZA7Wp%v_vS&3=WcwycK?gL~Boh@5=ZnMn%HhB!wKOH}_vmyA^ zy9@8!UrSDxxG1Pm>P8&k!|6QKk3MG@lU$5S7j((BzReIRZ+Hb1ST=7@L)|t=kY7+3 z1qsnJcZ z6>M!7Nf}+-G*(ei)Fl+PD9 zV##6B23uW@H%@V_1huFsSXb6Rr{F`N3)52#qQoVf<4%zPlqMX+mUh~w8S;7|m^~U5 zK%k5}?q4S+Cy8|DTX)c$N0%gpt@k!)HLowNFxcgY+nIN7uPlN&!p_SJsb_6rlQva`4(; zqgw0#%=8>!{UPWp0!*Q<@)BDA%=P8K30xo%mFlE?D$E-G(^6W7DDojbw&q?yawc8h znXr>KMN6+I$OdCI2`Mke8%n4H}~6af>X>%k96)kR;WnthGu+{ex5rMasBO&4cP8BH4zGx z42$Z{K!9&<5EpprJ;4t7#n`$#4%sTIM4K$R-)XHV*PUui7O6!4sxaZ2XYH|? z4eZ_YgL@R3!HL2FyAf19Z%2;o;()W}96@lO2q)3*#lrx^S&3V$*`&rydYr~v1`w%GU~_B#4WCGZKQD-Tc56L}+fqN`T9Wv7Bl% zr|bJD)N{7OCa!ZSxpS8O2WO)f%@Sr!=d!nZzWyRLv-_8H<0bDt+Z9hyHj?oyQB}>0 z^imT|fHrCIHb;Cd5`5UNOpI}=5$-2;As@{lDt%n_%G-9He;Xo#gI7HioIl3ry{M(R zVm2|W(RmAbuo{rO+|&?i|3f?ZyRw0I_MF*xQUQERm!keon3L(EFq%p&Z!6?mxO#dqHXew;Vnq*X4tV9jnD?0lT$GI2h|7kv{ zLom@4$0nDprJGp@;*-pQf$6SA-~js_s4Lp(+J_~@f#f~DgnryZaHuooptAsg?esGCT*FWRU(NDbyZrZQ(CXk?6QMOQ3U5u=luroqku(nDGa%x3L0d9 z|4?3$F@-<=y80FmZGK>Ly3YY>^eYWxDk>HnJmF+pX>i)wiSe-<-rdWRS>4N$R@Hx>dTXEnG7SVfbN*#=`~#C^WQy7Ww$2~lv8TJ>$`>`m8)vGT#VuaZ z%z{b7T9^SBqiw58!`!FiV_p#!CC_KOte=g&md=kNQ5ZT3=G6RoFI-og)5+vrY9nb{bY61s?qvAk=l5b+}Jb z&oWW`jyR2S-Mt`&s9w~pSYi!_G<#LV>FlnrMYiU9#*?3X?;b?X%~4vh!pwK|MPz0? z_TYzBR@G(-QQaos`Qf{_6a_Z1Gs>)jdgFN92gCT3a<_sM2>-*;r3>RUl0#unA|R1( zJcpFUgr)ZOS7Mc&Z2N`H1E3(0lQ)O`GKv&^nML6P?ecDZlS`a#|Lrl`x-IU)4q<#9 ztKsbzcM|52tH+acTX-+g2<2-NgiaYm=*Ckx@0{})K@wEe^#VQ@vN zTo(SZX%< zo|^8yVg8@@cx0=-Y}mHXiX#-^_~?I7|9?4n7Qjk*7fh)Q1;lO;f)Ko;`cK(V4H_nphtcK=*m^BkNb~v%l+C|Mkx7 zk7cyK0Wo~6tqhL*hJd_T|2;Eu^abo8@nzT5ziw{*oo!h{UN+QCL!X8ImKn)$A3HNd zV3@gd?!PVPUt8-xF7Go9%!%6k@1y@_%P2f7vokxlGqOL^$>KWN%4>S_a*1GOa=`a%7ns$>T%)gT)CzdZU{)Umw z2m)V2eWfD8kslh=e(5H|cWF++Yxk_JZ8+<*w44`Zxqyj6X53BWMBl$0kdh-XaTFCY7Ld1ADN{WGcn8t?XF&CHLzGL8D-;)Q zGp!n(kz4V|R4LBl=>*nad2oEM1ej?OcPj6pqj{r3jxqllLCayR?ytI(AFw5>k3sAUA`TTpvO1HkA9*zN#GKZi2x>)tH z%qu5;%d?Cs0v_T_W%QolGKi(88@iSoWe|~FSW8NHC$KV^?LqQ**8E$c%Q6jk0MfCc z!BLO3nXBKW&CFL~RkQPRV|EoCr0!$Zlq0|OIka7h9t;KB2TEt}y;vg7?DDeS?NSXS^S7bj+k*ij{Owf>)v%9A z0!b_iul$u%9V6jmPLGcVcBEs>%rJes>cz7{;_U}I|d&=sO zn_oyMsPGb1LebZTt5({{E7=^9V<%nH0=0(XJs`oqO?ra)$r%bdE~< z5(tweUmhqbX*^Fje?G$3$ zgkk=+u#ZXWyAT_f#iV{)+wm6P;3wvDp+)=CkMGG`}GD-13d;r9>WJ z{8vL?hu`frX9;wjDL$ZEJYbd5-Lc{X;KpwF7E)aXMrsMIRz3B=HVL1vU6K3EIbiSA z^FZk<=9x}oKlLvevA@%VLVm-!$oVq>X@qGh?sSbg;ls3;C{aUGpPO2~h$tzij@D*7 zR(0}UvE%lF2ty0EqIKhK#cV^RuUTAIMlZE8{YIXa!gd_E4v63SW>Q~n0QG_QNNFe9 zk!`!;t`|0baqV|vUR9LbvkD0|2&Uu2n(bTW-Cj7V^8IVWJD4hwD^x&83h zsS7tx^2hk^$PzxZn7sInN8*S)(mKT4uH#?%Kh%Q%YK%2YK)~LVshqR}d2OK_FbQ10 z5$t-(fNK)U3a?-l|0an4<$;}n(a>!;VNbNr{@yV}_$+^Xm2OAn=~qebj_c>z44+AT zp6oOtpCUqY-5(UTKgKv*CBbu(FM?g#y%izW8PUjsnri4xVDhiA|9WyZBH<>t!>Zmq zTl?jE>r!iaul)Vn;(n%{nzErzq||bW-9n~x#tJKQ1$RZxWIbxb-G&!_U^#lgeM|{t z_^i18LS$k^PiK^GuHWF25j1o=aNE7`?Q$L7F#2fv`dQGJ(%U8AT%85X`u3JWMhWtl z01qE{GXX>LO1eC3)wp0^m@KPy4k!M@1_;nA?crs6qQ$3tQs@Ahk5ty4VI>mKv``g3 zjH*7(9y5A0M9C0!Uq|NhbjZIrtiNB@9|IyHmDV=Hf9zh|aH{=_wX? z;(-4Y=dj|qr$BnQmf zd6aO1q=lUroXl@(Y8ozncNA$*u`4B6iod5HB6&A08Eo#sOMy$`-k6?o{dMfYgIA=8 zd^-+J@(v{ji8+{aks~n9M6>MtG^EFcQ!tea6j+|g_DAW(u*19QB{>hfn9d&+@{E}! zaf|oDbW|ZhVnikz^BA6(xk3enhwVS+ucn#@r=?v3#6{Ypbp}-75HP@2H^G9#eHz301NmS?(02+7y zbgkHs3N;DIMb6+Qm6}go;pJ%-9HGNL)za=b8_}rI(Jg^v!B-ZN$`{1vN7hHG?0dV= z7m(kwO#2Ge`8}kqt*IGVrK*GQ5pX^zl}#M}Qk6ZzoRUhs4e-#USDzm^a{4Ut_AJou zxc?*XQhWIr(?2-Sv3FhEt1y$~<>g}A)NT?IA9;qGb-BFdwp=dAzYbb$-4pLJ9#1b& zS8BPX(4zh}FltMPyEOn1Uidk>ASwq36hF?fU0%S8U6V5SV<>R&>Hbh45LoV}6?Wc{ zccG}hX`X<2FJT%@ZB*-hziG6wju^vK+kuH5s`ONl-(|aZEOw*iwHnV1_*kW~CYYIG zl#EORq~VH*z~J40-{w;MH62y9+ebqa1(74%Wn9Ze#>r3?(VMvV2dCoYWOsX=Nubqv z1m4@%zSnFpjzZ`Y(NC|!K*GOywC|4|0yE~MTL6LyVY|AY&WaIiztde2KW@7tzDuHt4o#eDo8ji< zk~Flpa`RBTn%>;o>2O3LLo};9Zit03Z2EF~CROAuq@zSa9t9yM;I~pAq6NCE;nZ0v za!NVg?X}=7o3IB84~KX|8bf02re}l`_064v?;bX*s{7-4xwyXs(i0cNT|dPoQJxC1 z(352xRF&~^vVvr}{cF<11{L2yABrGR>my&VWu84(y%`ix zLTL981jL*w8ePVyrKJuJ9=KvTp-$Bhci6U4sa3{zf-p@l+*<70J!sex7S* zyO!-$Abpp$s1#@s&!GkONrVaBv96r#>bg^ji$snUFJ0Aw-Ggvq{0k)-_fxV`=+K(@GWA^r*P+vW~TWOJ_T>E6&vH_Ddf#s zBRxSvb(D^ll4FF|7%85w1+DsW49s<_Z)~69UC|gqkp@&;LL^Rmx2zcHzzj;o<92E) z%3$IbnQ?lqfTWIYt$obT-Qcnk)Y4gfdVSj6dmpUMP2AJEVV+K#4 zq@#(Mv*HpWU)MH2kbY}Hw(pr>RYBbc5&!a?ebQ63e%-$i> zTG@_+1y~^cFgWgG%Q7Tp*X$Y}bz78c7&bkZ7|DUn3^|K?TVX~%8w%)S?dLdGhbF88 zG(pi4UO65yUq?M=UP*U%G>4YP?UX|2K0j%im#}kR$ULmKic!Cl5fyRcn$$?N&SjWj zF)mT2t2)<^T_(MLO>NekhW77E?TjWI03-N-l~#v zV`O7>z6Wxm@sf^TdY`6;imlOy{{F!-{K@oK}@(c-ln|uBGzkq@(B=I!A|SzsTpSKiQwhgTo=*ByBwM=pcsIXE1EW1mG8mX z5^3(FS2Q8MBjQ4OLg^j(UPGX6)t5QvmQ{L<2{W8Hjh%`QO^{lc#h6>l0xj*NovM_> zq1$PHwzTc38fFC%Fx>afC4yI+^I(W_BQA4LdH!uUYb);xAX!j!3F=sWxwx zA!|;u1Eo&S%`jsvYrpL`Cm%%!G#%YfeMBX%${g70UxfpR(g#P8*b_9z96vUMpcECn zEf42hH^VDE+GTm&b6{T0rN38~4YP_}KEFVUG!0Lr8bH2ds$`hbN8B8EVaA zLnM#A@@;4}rJbB3Mr4#1Kmd~4ACgxAzvqGwd|qpGX@dK61;?wPkj)2CSz zJj>TX+qd1C)#(#{hOBw*`ZWNvd%6;+Y);uT(=gB*o41;iVN|qG29nmAN|*|Ecev_m z>{iAGB)s_gn@2zi_v-a)C0&J#dD(>PijX8*s_;;KAj6sxBod$yn|FU;)G#LZ*kZk{ zdRJ1KtGxDvv-5JI&6jLXTgyruWtw4O1QQGd6jIZ!nQv8Hc91AsPQ9FZ?K(8vEx7$o zp*?F*oX+EG@y_g}g(sMuoGVj}lZ{}VygRH|YJ&i=Zu4!2G^%=b;#zOb^!7i?Od?@U z<$D!%b3E_C!HW5NS}FSU8;JnScq;KO*kUy>Bd6-&bG3%f$zojxTtZuk<2n-$FHiN1 zgXJAu>{ma9&4n3YFci=vn{1C`ya7zv{`C0r5XfNwe<=uUPKO7)dl!cCS9f01s{ZmL z&aaOX`J(E^mzDq{L!4@bIYA!iDlckKVxH6N2)C{_^lC1!4l$p+2joI*H5rZ2(hbHQ zj#_a*pd>{GmDG(E)Qip=hSF^0c%BlZloY2Pe#z=;ps!=M1ODil>V_IJp^2q>ZkIc9 zb4e&;g0GnR(RC+E5;QiMD(cJOz(xb(Ws{iKLQ|8oKLg8W->Ga>QC(;bzBU}Z4=4i| zD=UtXuM?GAFLISxRU6)o*$&l_VKw+e{N4!9jmK@bZ!t*RZ6-eh=OZQS<##m~5-JvD zptR?ysvM!P{yvj0A87mZBszDzs-tHH!=@P}edY4KCR+HB@~K@5A&OUq7s(=Lm(^FDhH z$E;tlf{R|lD)4ad$OV<glbDgyNRT354c$3RRVOb3Y`b3t#A;3cQ6kv_0UFSk# zLKcL@x*%jg8q313YafzdTb2(uDR`1t=_QdwpJs)em=B_CJf)Qfq(nw)KAbwLpmX^94f#_T_E>NznO;`#P}nj3Gp&Mc{ls{jcDEXtf!Ej6j&ENbUA( ztIvAnzD_I8&r#782o z+;IvBjm(C$pl{-UJ|6(=^9>L7-loAVrSMed9Dc^A2x0`IhE0?mPl;u?io{w`ynZ!m zUzR36zG&=6tf3_f-(1O1=s;mGjgEknU;B!aq!du{9bG8YWzYMOYSvU)a21_=XS9Y*C*Q*DS8k>~!6i5^CY8h$A?9O2YtuJh}&fY8!(LfCdRV zKOekM#PAV+1;Hvu!B~~cNH0E-B@(tb)!yG$<Q^9=`_UyuyW2N9isYFERp_aC zNM#kZ=sJ{pTQyf~Q77Hdx^mDa-G@pi=T%pG3J<XR<-FMyTdIW(m=bLRBAC)_4`4yV#rTAT=W;*V$PHh%d6xjOi=E(ET`U>^>ju4%bl zgx_%m*Dh9lUu6$9KUnlkk!1J-lHj_d(xGF08zA9i_+^jc8N%3oYd> z*+A0l0GcPUaIb%W9D;GbN-y7$M{nPa7nV?gOil;792x|`44f335UuqK!c9iG%ob;w zorDSJX+-zX#ooRi^aY3?BQOY@3Arpgl3-H_=A^={oIw}KH zi?N0CnT3VX2glsQ81HSu@p?^(_wf|08dh|0aDZ4>CDkl7Eea=F&}!kE+)0N9SQ#I) z+)~4?Tdgu58;&ci{_w3y>L8hc+#w%1no)@&b(K)-p7+t72@z zo(`onAqDIY@xz8bMZ+HYDyxA)janyQkjT_N2VGO&j?$n!dX%B5vSWcky(WZmp?ov4 z3g^mGd+tVkmcT?JY^l2M?xNUX$By7M+jwcGJY0Gv_Xs~HG5W;7OW91TWit8jMcfb>zejtO_W%~Lcd2X$L|8ycod80o3=(uHyzYC>O*%z5xs zypAX2Zf`e#8bN>2ykgvrZB*5${SE$TS1gDWViHY!cyW)A!SwSh@yJRL~+FE%${;;OrP*zf*;_ur@JRh&os0rYi(zf#F0m)o53{ z(0LwsO?sNt=c(6g=|ioE_~qq0It@v+#Gh%qi}`6t4iz+P7vCTQ-p$(t>3n-ySbcQE zim11Vsb{G5PAz@A{6bSh3?IGdB-mV8sJ5AgV~BOCpZ1zBTy02biV0jN5K-Vp*AbG~ za&?bfohPI4X8ZK0GaGA7UVa5ZC?a*C2J*|&%4FAjx#9bVjBf5v!}gjp`;A+XCf~ke%rzqsqPhxRQ!5+Twgv8yikxLV-bx0ig5OdX1E)8 z(E-OIG*I94>63vE#J;$fYbI~h?}5s92_F>1B(mj@H-!FM&KS=Vn=TPaGK^7Gf<@2N z3-c--{sP_BrC)tchiiAb{{8U4-OY`SQ?gSxknCP*v^r~c4>a!O@rudUW-8x?i%Qi3 zO-@^H55})cvDl*kvI>k)S5*XTxDZf?O9$+*GAfz0({1onH5*&8JVF4&7ov=$bDHmT zWEa8126(uQ$EoBJbNA80BP&BF)OJEq&*KRD7e5-f^xg47M177JW-X)*@i{j&5f%=B zGK;u2!AullAu-&N2174~yF)Oe>7YNVYUPcziMMerZQIULqx25;4ciKW9}Z7^|Jib5 zg{{~rUfPE3l3b=P4@vFu(137at}84=%b-BSW8-9q<1})6R4vhFk-R5SO{Ol~ecUyc zByx7UG(Dewr}@iTM>FG5LltEJaet_}tt%v5Rk3apGyHvcGd@^Sy5vDs=<50qVQXzE zdhgd$L&Dt~ffEmYR#T~aEdK)q^UoUIK*fiLaVWFVT`9ocp%T-$ZOzkfVKgHAdD};l zFmwn*s90ok0Eb=nBV=~>$t9iCbk?E8N{qIbS@S?}V*_IXo^gG01tJ%xZ3P-V8SH5} zpYt2!g)3m_R(-{fh*Ax#9elxHQvZl`g+pbzkzk*Os%Yt59VTJWA+_YBYuY+q{EHrg z#oA@G_16~&NL=a=N$m0QvU!c*PX2aA(4k8PSbyLy$;g_Xh#5Y>f2AIwNcECFCFix&mf-5btOWWK-e@}LcV&- zk(H$add9KR=XHi%%@wDI!byIFX7L01J6@U(jU|u02PWP15v3YhYxshG z^aQ#3LlRaka$W|lJE0WSjqm8v--oXEo$GZdVI9vrY|Yw={4u%pG7nLrs%J>x5W;Yh zP6o4{-)i324Y%^WvE)q%E^!>=<*CSS^8{$=Q?c*(CK6 zQEEbgA@oneB5biot>wT6g*3+J=(xmEiV!n-=*`p*U2To7XY zb8*E9g+2nI?PFqMZuJ-(<@kW%&50iI^dQ@d>llf9oj|`5ii*#3V}(8qOXRcYxTSz? zM+}t^sF1S$#ijCvih5=$+_%s1C+6}|1Xo?7*n*6svM@jcO0mXNUET>LX_R%{1(S1G zu28z$V%TDEBIe0y@HZmh%O1r!gDlgV711@L<(siM`X77MgTpBUj20c{OYiZ{e4ru^ z8-gj@yc79y?!s_LLX8RS!pId&hQ^f|3ezSZTS}JvyWs?J5wj2U?`=nspUv_r7)N7R z=aQCmD>Uvk-9xZE-6Ut%13qG6jQOU;CP1+Wke&^O$!O1*lc--Bun5Vpk}JR`lnix=lMh(e6*0W(Y;9j4 zC{%2ojs!c2_DJQ*o$HF0$n?B^#*sz{DF`L{-eZJTkhPz?@W%I;JOMFznl?Gdv)J(> zk2KYy4e#*CK#V-i{BaFTCv9NCnmTeq@pHdc&AHep(W7Zc(;$XC&IjvjFVe7l*E{OG z_LTU%Y5w7ryOy6{*t-f`UGRJ8o>Uj{treRrMCWTHhy7wDu7^{0p6qvHTTY9s5yoCd zKo5g6^!3T`l1=S#5y1;O+;3k-dks4OfM)Mb|DEXZzriCMu>j-g#B@c=u;PWDmv!xE zcb>Q?Gg;vM8|R)Yv)XtdTRsRxHD6x;Dle5%SQvUJo5Ran6ESNq(3^S%H%RE|ml!DT z_Bv3EHaQ3IR>T;LZ|Sd`&&sz8&-Vy%cXj|~0wNx!l-1r(adO-_C{CVJg-hy=qA`Y9)1I3U|@Lf zzogH-MHHIUvikDi+4INw@(Z6n-J5by+kK^5QD9|i@3RYppor_4U7=N?-kaaY)^p9> zl~W@PbIOs%h2(*C_rV9Bxl|4?<7%VYqeX3d=WqyIz$it0=SwFxazKrSAj% zwO7^2s?ie(L9>DtcXZBuGvfrh=n~ac>6kjDQ^`{NAT@9S0@n2?$|;wrvTO7-u;&pw zvme=as$@`>Pq&42A4l>7+RJ?e61R34sAOq|#mW|Og&-n8{LI|LA-s!Ym(=8vmad&B zH}UqSZjO{|3&+U__RXsD$!jm3bx5hHb@M z_D?UVvTa?wcKwt2Y|XtkQSTEXme{KDCmWY!5PPDuAfzWSCY{=aftka6BL$9id>NLBFxSx!Dm{ zqMV&$6}Axq2@vqP)4|jkU2SW`#a2EaQPm6!-Q283>>wHv6fF*^dm0)))O$Bl-N_#_MrLIqgP3EXzRz(^Z~sEwJ+&sM{68>Ky%dDg{ZO)N}G+{ z0?WA>k5$l#2egMo4i)59w}e!i;lgvbtIuA(z;l}!S1j*~LZQsY*6c(VF9|BDI;Pjy z?FIw#r$UoxXUj7ao{Zyp-jY^-o}E84w7;hIG-#bUmKYSD_tD_C1;2rykN!fR4b77^ z4?<&v=p6`cO@Yg*6KWMkA&yIfQu|b&qB#*QdlOu(oFh~c5ZTrKN(dWOX5 z#G6qRF?d9b*h2VX5__fDf<{2lGOw)kS25wkOwXPRnu>~6xZ}==9gY^8jVZFW4@l?Y zY+byc35{rry~rxD%ocj=G;%>&2&@fLRf(o;=~mWMiQgsJZ~x?E9KXbagl+$Fxh^gM z5$sH8QNy%~m0x)wZ6S4f1&c>0Ev#P9Hp(>A64cD0vN6Cma_(~!gL^3XfVdkc=LmKkcf@N zyDjgW+O&@7@OAUO_sy0KS<%M_F}gFtZ~4O1ga)T9x{H>Su&&k~9nh8jyd*KcL>q6# z;h01Lwfad7?b_YJ@%qB_^WeNB3>XZ(r&mSGIk-pZ_*~q7 z{+bTLH+<5qj9Z!0ubwI&2u!rCJ5U@uGYiPy+vt13gBTikN;!GdlX5Sv+_TdY zFuwMw39%dl!%JEU8d@!PmswuVFC>6Dte5p@E|>`Y5NG^u;71(!15%7*7IWj(_Uo9& zbw#O3o~rcSPB<)KL`LdahcCPfIu^5zQO%|&?{45mxqcKXGR1S0k)HC0gD{a+hJF^a zFwpdH6(y@*RJd*aC$Da8>1UZHp6jgaOk7j~-}^g^93Q^~*$4u#B8N>G04s9fPB7;t zW(+M&!Rjs_4T-0e4h30D)XQM23VMya_sS3(B!(M!ByRj}wDVwO&R}Rc5LGPd6?YV| z>$6CeTBvsQmq`BwQBLE5+%zmHQaYr}CxTUrOXu76PbYSSwEJib>PBn|j$~`VYu;?= z$F^Sa;a7Ld0|hs!Pch`E7SM#_w+^I8mX#5vrtTLmOpT)te*OBjvYnkVN6f`iBn5_1 zif6E`G7t(Yue?)e;usZVCZ#N9E&4RnhvpZsx1(*Yo8l71E?q! zO$LBc50*$9cUZ*Irhp*@Zz1V}>OvM$T;Qsulja#jxLucBudaGRC5HIZnf>n7{OMmp z2=wE78zlkU+lXZU3e9xF{SHf7JUuMaRH^O^}oLMKvjIC{VmZ%J{ z6NIbHbX+Pf=eDfk>|69!6v=h$ktE z%F3MO*E^VL{#+zXUFhj7@T8I8(K)(Rs~1;r)eXXoXLL1X!@ffnZ0qdY@Bf<|7Jb)A-2GX z2V4(VsqRWl3$;ZUTw`cK4Q&Z>n`@b+>nvn%gC+zF4%e z5%DQTKY?_7siR`dr(K9PSdg`qHaFvkkWB({m&HsxL}W?l7yU;ukOR^?bCKY?`;joO z<}Hg4XH;XDabz3H^KhWyOKcbqC{o9T=lm*UzbjMvU^~J05?haFKiysY0nzYmu6%*nuVuK2%xUDYe*HL?^tzLb-vF{HMG(92ZvJYoN;JMY54|3N!aJHUpy=pKacdI4J-xp zloH8`Yf&7J6BYGUY^oo`k$YUlB3kV}V^}Viflj61IR=~==bc2I-mQ8%3!02B7WdKCn%*e<|1}gP1u*?ez zE29>Puh4E(Y!Y2~nN`&l|8C3q2Y@WTjh@CFkQKzJ#^IO>wv-yg{u5VB)%~CmaSqPm z5Bn_X_$~3}N1!Mny}SY*$dQ+g6VfnW4?hqiv)-+Md%Xv7tuh_c)(SJH44MexRAZid8S4r5TeWmY635ekqLrj-`4z+aqJ}{* z!Q3mNqCjuvk+2nI#Kmuuv(#;y%VqY6q=+IxrtoF~+nDzdVUvE9wo?PEK}ZO%v0(J+UQ;@N-awM;Zvx2gvHm_TT+I}h%@|O2XzU}BY5_yr%}m51 z^FqSz?9zk_X@f-W0Dw#r+SY#oCEMu_TU+5pS0T>xvbHJ%5)-AKg|PGSvfReD2BJF8 zQ&Zv7!rYz<-`l$;-r062P>U&;BR@|k+k&#-F)aU4xRT~}1EYf)3* z<(>?a^0XnG)s?94nFks`M7&Fek4@d;{ZY|cR1b{cLEat^ZCL1O)6g{;>xLq>C7)R< z4;2_!f0odWtM_Id$&oM6!H;xM=XSUG*3uUKgfHjFJ}Hc`aS>NLn9id)s&@;{RhtoC z(Fa-=jh|pECw01-#ofNRI56+4^xb~t8=E8-!nxYS^>m)S=aHxU;Iq4=6kYX-d3JG2 zC(qc6x6iYjKROwxM!G zSzk{F8*HUy2%79YX^19E8^nhckJ^=v2Mt*=2%Cj+` z6HX~C!5AsSriRoVwbH?qoQV3$iJn;HiI5H&?wK0^WvBN+<}ffH&EL?V>b-l!ni>zKCZ!R)ujT<@UqP;%jfcO~>NgCvik|&o$nUWq z*W^r+`IUp@8;&JN>6prz95CJcV_SUA9|KKJb_V~q#QHyP+8!{ZNJ(Z+J6NgvXSh6; z1yCUmPnkO$%s~G{KYucE6m`q;Km0wc^~__R<(FMviCo=nv3$rANY1M=Pl2*J1DmU2 zJr_T@+_u`+(=Q0se(e^gp%n%WU?}b*5cqNVh!RBilhsifD4avWF<)i+g7W{x=UT4s zBYxSH(TI)nOiZ^1%7RNM%U0D7LwydKVEJy=f*`U(JJuWY!9 zk&EP>bc*m%j@{D4 z)CrYF;Hd2^)nbFXj{nfLvbMt7NTT+_8MJ~TfdD~U$vgdbf~0?$Y=4B%L;#At@fj5- zJ&%aba-|u^7zoB~7bvPgf+7Im#bJC%w=lH3D;BoHuRTCo>ghS7`z*mOv`m=)pdGY7 z49#SPLA70@X0edx#*GKx%*>#~pZE0JHzq5O6Q-=%0sv#V2AGN<0lF9%7*GzkWyG zxghPJH^+)r@S8lvF3Y`t#8N&sh}hPgE=^zTb9*FX@zPVZ4jhcnD6a9N*~ zcS5A4#|nPbn-e{QfXIjJ!}?D&S6tFD)Dx#qGlE$Xk`wu=PbZc%B|5EdYy>+>68_RO z|BWyGU%&c3Ht9RMA-?oLXhTG}3@upR28nS*%YZ_t3ky!bsTm+$;>V|{%+PO$bmS+;M);Gjv_ANLX) zOY@Zxsq3Q~HBH6~euUV-ts-W#mCi9SO`jcMHuqgBb#n2b|0%v1JSEv2xfd+F$}f=N zx;`;h%DddOnI9qTuTB)K1t0~kGXhHAo^P8^1o#zvV6ZPvwIRS?9ehj)SUjIL&O7D+ zYuS>IM*zY{5dJte{-vW#BQ}PI#fnGciSEQ4!9*66LuH9<>lQINc~p&yV+lYuL2+LJ z+QlQJP4#|Dn(zb8_tU-moFY8(wgC1jtV905NHO4lL`wmT)*NsHN&pozZtWWMNt>!tP-s*W zcXxHOVWFRjwv&c7$z&s(6F7z#Ub|O$2EvLG64i)@_0%%Hq~FpyI&*g)zAr4U)Sm9_ zEYGmL{;4S)@aQ>I)~Pb^4xCHIUsHu>J8a(*qF-VCe&!i2tHP=K2~V}w!pZ6avfTop zd#~0uw%_yS_;L#yDptM}(^arYt>Qaq7V(b*cIKi_7>jP@u7s0^cTfu>+Ty~aPehGS z8OTA_^q(65RK9=Tj)nkUHc5Z-&&Xk3~Jk4$PJKQVfeVek)e< zZ~z(WdDk}h-@TpgFYl4V6_A@ck>v74`L`AX^AgD1G{-ry3cq!a|GR1bx?Dd0yJ`RC zNdEVv{o8{4?@9YNzjch=rwGtIDV%lixbews;7=9{^E|gYYD1f+%ZQg0Vz*osW$e8b z_TY_ThfiNctX|-_W(p_9(BEPrtI~XBO5~XH`RemUv^%~k6$&4JTQ(z9HFxk%*uV!z(kRBbX7ZWMLv_UKoiQgJg6M!MtbGUEV&Z{+bwtyu+{or1XS<d9Dfz^-OE-HXpwU~1`tmm3)avz z6k~91I=Ix?F1e3lkfFqZs!Irg?)}aU#aCS?^U+2voifP zMr!(&C~;k8uRu1&ckOw$UG{rp1gd?a3hA(uy4glq+?AwgC6V@Mx4t)fJ10^b@yX;H zZT5HrlbSgM{dZh9pFwSU*a$hX{2R+iIq)Or8gVxk= znl=E!c=G;i-^ehZgoV|XIfZFwfx7Lu?79sIaZL^=jX6*Dpk z*qUX>)oKHP3xzHQ^9pO}sp$?Q&7+)U7vtT2)Hwk!bL1`nN+dT0x9~H}*GDm0#f$no zWn15*EMVol#*c51HrS&oU%Q1zp0P$lBnV-bM)tCM0ba4V;T>xiY zbaeBiR##hXc`5-Fd3gv#+^(>&f*?OKM?HW8l&o=)~JXoa%ei?=zvK?}SO4Zm)LwLwj3GC`AoM z*h#lRwxh;pb}e_-V@`uA3oc*TU6desliN>sv z#gT@2prCN4n7;vC}z5$KvOV-^OOq)M62m;N;@N?M>BnsxJM=wTfJU;yauFsJA- z+m58PKB! zSXr-qV`5L)i^+*}eJEv7ZE=47Et>m!x-EL_i)5sW7Za2l})HMxxF z5+^i&SJyKT&8YQHpql^%<`nXb^q$ik@i3T*L7}clBp2>DCFPUR`RZglD}^YJ3hFbz z(-n*2*F@IU_Up3S%*>>e*43sUm(iK2hAYhc6&{Ykn@hKPy4V9Lbbot#|LqSxXXy&8 zc*Z`)TIymwlW_emBJ%P}hU;0Am<#Q)QooLf8`T->NusBsYGq?8LTY$JN53mDFt7}D z(dJ%>c#QnHOQ^GIv3y#8#qK9F<4b4U#6`uF&D}nn7;llhE9+Z|r>&TeCj{S+7Z!Gg zi2PCN7{?)sLsyWYU$KogT`7k=W6n><@*?e15KM^34G4ekhK*Qa~XdjEb zUBUq@z=97%7&TXP$EEu@rE{U9VoV391_ue;=(A6R4)euoj(6m<_@bg}QUCr+CRiTBYf% zfE%E{;r4NdW^`$3$-0Ix5xVLs8^85$xzgYM%2m-Ia$-JSQc~xfYBV;6Jc6JJDBv6C zy)Lekdfm()92cLIU`D&6F=5e4Y@cAaE2!v(me|;>&BXI^7jrczV{0nKLV@Pm;W;|?twXCe% zXlN-VB{pE`^!0L_$K+s z6vZ?4+nn!FRIID-smfD3ptHU6p7ClA8+%Dk&ysZ`VUoP9N)?T$S$qU1z*!Vjebb~P zy7c}H#>eE*Dz+rw*wZ*IdL8tvx+Ljq$&rvX1LF#5d4jo|>hoW0`pn>Rxm1?-7lNkLEBDl*CpQ|YA%r0XyGt{9rlGhj8X=xn=WNu3mQr)mw&J=1M^4-Q6)QMXePE|Yxcmnor$ndPcMDYR2BWI zRgwBD9zxYlPg}97`4Vd9GgTv2KwaY-{$T>uWcCzKyWUNK_x1($fffa#HY_A(k_5eA z%@+mkkJIbUDRIZI)Y&EO-axN3`PjzTutN2eQ=OcXqHIh0K&MI_3bwXK0d7FMnBXUm zpP6;!{&ZJO!9k&{7t25Q3iuy;v939<7`!%2VhqA?>otGGMJ^=9#ml(P5+q%^qV(t(BzFCa!P&uZR0DlNnLd^5%vf5f5$$a2m<%vP+a^iF*bAN z)jajp-xvJ)2Bgp2m^P2?HmvvBv8sILVpO-$!zjq_68Avy^P`8#1$o9lU;bP_dc_5| z`YGg!lCgKP=d`qOROH85=gzc>-2ethR+*g`>h#%DAv*J7U-WjS7N<@tIYP&2Hy%EF z*TMf3(l9N%ze)@ zLib09va%YXK)a^{pwM;|y+kozfD$c$!B^P%{*y7lkmi+@n%*)0!kvKtmE*{aLAujV zjon)Avri8SNd8J($BAstB2OjrIt6DTESYdgVud%mhpigM+ZB+L*2)akT(u65jdT$d z9-hwP!(NL7+VyHyb&Jh)hoFOE?Jo;yt_qbn>^QyHM+&8J*yA_8!m}005AOED-A)(m zmge#3@@F#$rRQT6_y&x_Ml;1a_{*if>U|qY$XM}T+79nbc)|=tw0wMHJ6GMR9#olLs<=pe#!~S!B zef}aslC|FT&b%|vJo8N0W3aW`%hb*(9HG>b(Q#`U82hexvGaskS0dk5};S z|1>`ApNlWoKLfS;+X04VR^dz)o3=%igX9({)nh&|$uZY8H(gT5qz&df=R!j<9sP!( z?=~rPd?oYq%kP`MeRS{bs&8F5!JsU4b@+hak^!`{P)|8Ue_Ptq)bTP{96@M)hT&ELfJdb?8v9TB}%K>Z1TEU*_$cq z^oiHa-=}G6@zn`ufPb&iLT&MNFn?z=fz9v@p|&^A4=)F=CJK-fG9-X;tr?I-_j+7@ z5aoS_w<-0Pgd*jO35A81O01EvzL{86m^f}zki1ErR0+c1@j;6$yk#W4^vQ)yB{o}g zfQAv@-Ib7?C-ERChV|>|@0Fk=o=#mlJ~X1zDAcWE5kF6n}z0 z>}*eW+$P7hSv+dbdLofHEs-8;rNQ3OUqyPT=ebgY;uU41AM@q^05py+fYhAvA_ey@ z$V^vN)|U$`g@6PConr6g-qp}~0vl{J5YB^pK9Cm~zu%xQVOQN;O=L#@Cbm3ouVbn4 zHjGt-re9;6erb8nvPNoqt7~Z+xlz@x;Z{~S4*senE(RCXz@s2&vVq&|Aoo4yIA`(gR-fJ zVHoMbSob-|!-^d-+2Hphb;tUBzcjw`9Rn)rKnKnhz(lSvaj!$^iJl{#U7t}7i4cp( zGby|hoBIp&2J5>k<(o=eU+SCeM=2EpdFlX>`bXOxbt_LyQnlBHJ!N)2V(6SJ&#yI^=Nr{dhk{k$$qZ}*hSl+4C#NA3;s04|p8+@*|e=-hTK&|7fE zS#noEsLj7o`*4joe>lqsaaNCDFs#Hx7)4!H!Tp%TcD~CtvTSd|&ziya&Z%eI0CzZ5 z(Jd9i_hY!dcbe~!S4sovaH+IoqD(H`)o(hP9I{cz0f3M3W0r6&eh9sCan7kGI}5 zWgdHV>Db*3xG`yWTqO`mblh1%8H1bZYMjpFR6!GalESSH>ppL`(}AAq0;TUZ zua3u5#`7zDss8L+`8jqo2JJ>cv-ofWk@#`gFo@RO&Uf;cA<*#ModgjD2$4*(mOu7^ zNm$-43bqe->{58SK)+*^N*!O7V{P+WOA$+vhi!@j{PJs!sl}{ZW0=`^$WKOm7f6R% zc>mi-(Hq4+)TazzQ*r&F8ul%l49Z)kGw$*)UuNOn9Hz+39@KsZ#5urTOL9{*MK&{tb|sp=T;zPXT}%M8sdK zuG~8>3%B%*Ch(S_o`v-O8WiTPG03JE8J-%CU~56 zEzJ+&vWh6IfpQ5Lh;@aqHzjlz)mRljd&hqzSAn;3b8>ILeRe8l#_Owfqee>Y_rUk{ zSa45w1m^~IpP2E*2hF_!hl4Su{Z$;Z_y;E75c?7)XNR&88E4GGVv2WbT1h^Dge=j! zXC{;Or$R`GzL~sPEl?>;=8EH3^fqR%#As!wA(FQQWtRABt}hTdOj`J)^zuWhB3K(z z(x}hlGdsYOOn1L7Xst?Bfh8~)%}aRyy-=%@k1H5=~NQ zrxV>q{K^(`j2?@z5Hg`5)Vil3pXYIB>LkeoF)NA}8}~*yn~h7gCEthd*`?mkkaoGE z`z%j=I0TgAWS=nTRIs&^8EYUXlSu&J`O=Zn+|9W!K4cfa0tdTSF-+U7i5$$e(9YV! zD<0Ds)2QivB*BB$;TxkLg3QW61Gok1?r~dZLbRg-u`6hAmSfryK9GjP<3^e6%rVLC z9L6c!3sciM5A#d%xoksGBLJM{uB~8XD2T~*umwaJx*w#hdQi4jmIBi{w>WvJ^S5<< zYYo#WSpBrsOW2vJrb}#}KHsi5A;0p4p~M49Z%AK9URqxA0~&$_E?=IozH%|OO5z&m zo|7MC^3oHG>psZ@tS6C(_^jjXowlU*)GtExH)*Kf0CVX+^@snxdwbm7U5NM!tAzd` z%!(^DHR&oK4Dch{d972TUc#T}gbQ#%!bk+-;jl`9$Q&xVt*VF>7y6pf{%k7aEr3 z@<2^Mk1v4W;C`Up@(|nL^2R4;U>9A|C~mCW7cQf^MWTPKJ*uTlan?%R>IIr6BGSss zYLSWgoQF*kfauqh?oGkj(h_)n?cFA7b_ODZmuw+y+GJ|#M`o%ozRw#7xA(^*t}VOT zJuL6;MtN?V6^3M?K_;J|deQcPD%E{Yt>Qv6mK|i!*u9g`O)I$9nN5_9^7V`7OC*=2!h({rnKV)7zP`sL6bxvwR(?;h*R5mh4BO0y3Ex_~DYV}U?LN_w?UyhM z{k)9hGb_tWGxm$n<=(+rIx}ZZ*NKLp=a(25pUke|y&eo4C7=biVNU__-Zk+NW(L8F zt{bPtl}+P?G^Qo20}|ApKk)TU2V60>D76lR>>Mdw0qe z>fR>Dm$H0=`uC2l#m@N25F(Elu22gzm&Et;ATk`&LpWfc?+c5-UpbFxhRehJMMU8I z+AdfsdG>>VkjQPPI@fm%_#V&I){;dZ#k4&GRTB$93B|3C(>8y9V$Rc{g%_2uMZ$IF z!_g8$rOb3Ceq2cz(zjn~)naCBx*AM~^@Q-+g^l1!OY$ZVap|tixA*})7+-RVO@MZ( z-9UEPe@-#~`-qPJ`rGE$s*e(;0S)c7JuFJmS|!A6st`WDym(E?Oq{yQs}e1-yWWm_ zZxudUtRM8N&TDp5eWa}PcuF7zl&s<%)$StZU&!kSm0l}D1#|-aE`$5~B4*!s^PY@& zuQu3x8|qOyK1-S;IdwkJiEC9bExTO^=nzjb(QfY4nKiD1c&X7_L}DkKN^JO% z8d)E3X+x92pw{^NNFxU#yrbg*y6#1$aS^02O&JMNMVcjIo2u1$cACMlNvwo*?T?AY ziFG^lzSGB5;!1aa;vKI4YATjMR~Z)-mE+#6bY=!QNw-A6cNS)=g%eP125jQj4RDx(7M0h9dp|cW78#WExc%7co+NK zI3EzmSno(8P1>q&=#x1&FdNr=r>7M-&wVgbO+s5d+wqX|+WGEsd;V|aiM$6}*cmZi zkIPe`R=sM2bw@}s+@L1t8ONF_lDNnup`ht9jB_Af1!ruV9I%_s8y}l>YhiUEX21r8 zU`{`&%l^+6m7_&FkMRbGlU*xs56y zf;DUW`^n{thkW;(Cf7|o%WWzD`BTPgzjhD&^MzptAW|JWL@U(>`3YS6NqNgjRoH_qPk^#EVZLbjm<^h$Ne8&28+44nnOe}MeB z{kB}#Haj8m|4OL;d-)~!2IJc(UwXmAzvGW0Xs|0Apiy1(_+ta%vrg2EgXX)#Vyg## zcl{hNSV;C9R^I<_VLwUV4hWB<`mNvpI&=Q1Ap5Uh=4Ku6`__vqZ2tSL|Led0>!X7i zL;w!rTd=g~q0yIv4>Z4aAmQ`BxAvd=>3^TvKX1GL-H`uedjD%{|7k9N{`CLbji~;9 zI^mH?G+IAs~P0vEhl>%IP9bUN``1sb^Iei?@W~mul45EtHkWbNA5HC2GvhhUCJ+awQf{t}j(j zVwe~(zmr!CUS{hb9_1?ypm50)npxlk$k@^VH2p7I)pWSyeww$&^E+>mzw?g1OA|wu z<0v|#Q{R7*01}Fa@}~^Zf80UFQO?l6ag_U=A!mVcGe=d4H29&-}oe1o1 z{iPPWyrV)i@}~jZ_@6J0>u=p^wKNP2o*)gu$eQ(UmdyX` z_Wz13{>k|PFO$Y-ezlbE)h-b#bF5G+KI7tY*_I^}dx=3}3l;bWL&*cV%PYsiv^CF| zwI6fC2Ce;mqFXa3NC|!a(T&`a&7y&<1(n=?mZ2dy>~X>hQUgXD%NN#z5~huu z69F>Yx~DOp87U}ej%~vm!O;=N(r9LAXFq!zOKIY_v8v~(#lh3hGR@X6g$D_j3FpTq z#atrnjXOU;CGWAJ&Z@dQ)FHu__*2;{?N!N`qiulAG7!+v-pk9 z2xvUJSm>C+ANM0K&DC$Zv%@J5#KWr)=V6!@28zS+dr_{=c!0bbA_UMeRk4?a!_JVv zLaMBMpt<2S)xmXy*K4|Sjy7I^;?wFvxY${*)NJ%1)&FeSS;D!1P-A$tQXYW_*Wa>7i zt2M~*>`oI>j$S|Qza%NYXy6pi z5HX-C2TX|M&@S)uSSbjw-|JHM(+a4ouQhNospHd-aoAY!SS^;lqcXXY@4TPy0h0=U zl0HH)&RogbUdqAwRc%$nC?55$(=`*DOBq$c?<`PckFBx3@}|k9Mp8vaoKC>@(#A?; zVWXc`gWSmQ5)te=?);Lo=kMIXD&3rBThsokQ<#o+b)j8pVO-JA@SlT|+eMP~ufi~1 z<0$uEryKx@aShFhS)38{y)uR{W%l%=%EYipowbY5u*ljj49WX30NE;rj7?* z@+~qAe&6$3SWYe$R(Z=#=Q#7I2&O^LO#^|OEmZLq-wr7Co4uhUJW(!2@{Lm9f{B~5 zGC64Wbh|O533m%e#!kIX(~4AX52z!9VYnWVlBz(s<~msrfA-JP-G_Dj`fSJQvv_ldk#traMB!^vA=S`VV% z=p}3j z*~>0XUJfRy-&6bC$)6+&PD|4TZ-p*{)WA?Zl&yR_c4wc3Kkv)aY0YS}aiqN5c6cJL znFJSUrKQ!DMUmdyD2vroKdDq5^$VhOv-KN$BHeq?&&$_&eCM%22JEE0RXntQFX|sRSa0;C#f~_aHNK$KJ1=Z{EjG)qv7YRR zN^Vw8BadNp5Ej(&5nK*-KlRnr*d3%JJgkHTHj}YK463$Qp|q9GwoB~3sL_&`-%MWk zbCnt8PtYDxM*?t`st@SCOMjCP6cC$9shhqOwrTKN^;s@|Wi|O?$_h8qkHg~O`s)51 zRrt!4*8ZVUK3XLuz=d!zEmUJ2O50cVcqxk`0ym`f0ylW659*Ad7*iP=3UbR2(->46TrU*wu79n^lu2Gn%?iGj@04t15rMkl?A#g9IV_v<4=Z|K%J$^&W^_~{*abN% zD@03aFh>8aE=*N(Lz-W}G$p0tA(zvKCM5iMT)n*mw2MZ6lf3%(-Zuc~S~&YT8UHO@ z=-`P19h?jzA%AR=?)S}5oWRD99^{#G=cKt{`1He^puoO)ME5d8@?^-t4my^OZEOYY zcXWjAu5l3ji0CBfYs2)$02A%hdig5GD@ESqSQ>PLQ*FcU@bTI!77~AH0rX=AIJSn4 zxizJb$oVElIki={nR(=vWbr@V`WQTZ#{WUbEEXurz&9t4KZ9_n`?`Mq!Y|N~Qy?$r zH&;%wtv*}}YOuk%(2nM&pXAysH7?uy&V!1{9xZLZC5@W4SrMc;0#D{m)Uo?}ug%V%0vZZ>63f&ZI*%+@2d>7A-^LGN>l;StRfZb~Aw1Igga<)^+ z`0sLcr#sMc_&IQT*Ty#N#!H%=0(&kRQ;606WT}~+6Rjf3v;fj_f=W5f8WMM?XTl1h zIn6nmmkcc#9Mrh|Bn8mk(9nJP#=*GpDUZK-c7y9wQe~z0*oPQ6p9arZLmT~A1EtUQ zfRLbNe{J@cBiAgJ2)3l5IevSi_d7W7l7YtwdgP5@aA9J?sDn`q4*u(G3p zD!wbPPn}{$^I0%0V#)(+JI^Fu73x6|B7*lfyuI&e5FqEybwut6_7y>b4ZZF`l}tQm z*ww(nnKusy{#FRGD%pE8|BAGCmPY8soh~fQ(|@n^`a@ul$3_;@gouRr+Jx^=zkrMn zp<@BjKcd2?)ChK)|LqH4_|gV_?7%Eb|0@%Cj^%AhU_@ak10Pqcz3ieRrvU+edz9`z zp8`lLR?7gh(*{vL9uXN?27dqkioyW?3#-RJ2Lb=GHW&O2*isST8C!cuLzx&zNpDP{ z9$cUP zSFw(+Z+XyhCmYMOue|Mt^aZtitn0zx3#xI@RDLcIgri({_=uen%Ico=^%|uCXR9G+7`;v^{JueJ= zl3tGIrrR@l%}1}+vlP*GZ`iB0@Y{9wP-iEKS_kAFaDDb*XgeFH)oUGbV=%KZcbNGJ zROX(P{ajD+Qun5^9rl<~N%DxG9hx(rXkZ_mPY*tLKs|bF(FH);S+kreV^zk!=|>K+ zvrI`!p1;O?p162+BMBWts=VHHHlFV;D!R2P$GNfHtVPpl+p#)&I<`K2Uk1k9tml48 zYwtY?)3)m-JPa|Gj`pJDK4d!!y}ru2U<;#Kdy}>z{U<5r@e|BN7lkIr>$%1c<`CxdPR2D}x1FS`&=$~6dn@0Vl_ff24!^)dz>hKQy1j9i%1)tY?|C}F z*OUPa;L*;^>_H}DKW_Ytje%S)ZS?;!I?;2|>p(Zy&hdHGQ#&Uaw5Z3gZ&CHz{YUOI z)i4f9W#?FryN^r7K}iaqgR9zmOk-kyGCD5Ui-lkuXejJr-BoDbZP|Qy>s7WvIZH_) zJA@?R13lH&9hAOu6F~*k~t&I!uf1(0xZvLahQ`h%)Rg(jNb2cC^e|BzP3^Nq@2#;Lz_OlBD){xt+Y46Hen362x?2{o z7(>xK()sNvrp8L6s6{Ke5)M zO_oAH1}alay$7o7fq7`YLt{orK?|cO#d2`A*zp3?d}`+^-tw&b&ilps`n;g;pNH<^ z6&?%gE0qF|?^_fD#|?x~>TG-WTS7E=IF^5U4!AC zmJSX2zqutH`UI43`-EEE<79N%2yxw>^_jcT2VahIO|PnPt7p=`by8B+9onx`bU@0B z&O!n zl?@f!WKZbSYwNN8Oo_c$YjM`2hW6#6cA41GX5|6Tdq#;56CpS)Y&d z5^PsJ8h4?{#HResA~x}3>|KB&pYz)Nygv)^u*P^MNfrnj_+n$yC2iI9k*6T(P*EY2 zSxUZ*Ie|@kb6_J;DOc3Vw(nN$Q$2;xt;KWCQuZDqzQ_lwUGT|%&!pwZW{M}irEj;b z)n!_Ce-qwN$}Pn?QE8j`$=f{1JHy;*WWqtxDkU+LUR*op&cdCgdFfz{G2PfRN^1I@ z*5mXba4Ohgc~^S$n!LQ>^h=A#&NRvP$-UGW@0qNlp8lrjB_)SP)gP3Ztv%=w$cKrR zPd0`qWa(tprt+)+;SMecqrP+Ksxy!a$ZG-9$S3ibi3mIfi}bp_OM?-*BKiJD{nP+jlgz)vF&l%2GyvZyF^;-hb zLrNK8@SVa@e}LR0^GZ`g)((E)hMij8x8w7 z0aR^6-DefC#XC2jKVp(CH4zMcsEYmST#46@m#97(n4aS2*j*VuQcUYKJZV=BFS`{7 ze!L=;lDa>csw$6viR{fIq@=@#Cah_@rH*KtY^M%PMB%r*DcsjonA!&6H$)QI)x&*> z7iw}HcTcqHL zN#upWr3+A8F)2y|SN_uX zWxI8lj9N5C3GB9VuhGV8u>d?sUgsu_nO{H>VWeL56f5LFAMRcdH4|)Vr1ecdi(zp< z)a1gEZ6ZZzxq?$6!W!d8n3qerOF*};!djA{zgFgx5f=6IR%QrBxbIq$bglEuL-mIb zzNy(7X6ZMk&UtZP2(6@M56C1rlUj`&=e~hBO2<_n+7y*xp~5cI`!<%ht!fRt%aEqI zsLU3+W;w@PlT?Z}5xn!V3ViQE!&kKwc~qQW?=xCN11(6}D7DFGr&uj*Y>~@|f538L za2c%au8>Z@5;zxjJPol7gZKcGD#{3&-}aRE!d8gN7rx~m{is(3n~f;z#g`sDzsBni zsNBk)kELD%vWlJb&_JK*H#%(!u+5Z$3T+j@X`x;ajtgmhThsOR8+pbyC3iJXv-y|Z zrmSsNF6TY{q^TdVcYWQ4;>XjzGO=LAZHwIxt8M-3DrOOmi@`Y zRlUhrWMFz(g{u0xOZG?}f^3!M{`D1TI|h3;?(B zC^47gPd%G_ ziRQ!|s49@upqujhD~d*l+>qfYag;LGiSFFy_jU#~t+nFcm?e!=j7bCdJiMX;~k$g@u10E4DPp1!M? zKq_b|(*t0-*qYJv_O3fGJ?|KNJAbIir{+rZ1zt}Zq2^~JX={)pjk`KpXFqUoNY7%? zOC1dzqp*HUQ$uIT>(bSgh?L`>e8tT8mBpV)DGX%o8go4z8l4couWbhDS>?$H{5-)x zBENH6@dj1X^PZcvLez>#OG-%v$liYUvkEwSx#E5AKs6pcYU7*@OH0YggXv6?r)F>3 z*id^dHyMX1k`xgpMcPSxvvTU8)CYq*&W1*HTVV|JZUv-1eReh+^5kNvk_*AE6VX|D zG^n(}vxAlpvpq~@j}#6x}=LF>BPcV<0nH20r#h;_#Rs0Q0v zKZ}i?ieBKnl#SzWK!Ng+jfX0O@fp)yR{rL^@=@2;!7*_^SWRW~%`vphSHcf+VSnF{ z1tQqAq)AvkY`wh5BgTyS=;8@ron9g%!kQfzi>@0ejw{sXG?iIZ=Si;awSog@w4ot~ zwE{J^(0Sd4!@kSdGwGa9eA)cbixT|+%q?Ma-MmQ2U|{3}~qLOcNaso!=_(NVEM_Nb)|gt_adnNU`?7;+Dg+afaq zK}L7b%H}-p`PgAV7s=;$(J3YhMB3D}`nk+A@hW%nPX*pFzULVXJ!NUj5mH>e{s06D zPuBS<(CeQ=NErY+^(?D8FBA}-MEG*lX_2w^(%Zy6_Z^?0uIdI>N%I=l6 zAq@c$B|Se1{cLxRoz1HZXIVJ*gG_16a<|mMmvlByt*9;n=Gu4atFSu52BPr;k|6Hl z=tD@+#(66z05~?wVc~d}a|ekn)4hU{Mgd1|%*>8W;KlA~x)f0f69+`yq?DTJg|#ng z!fXj^+}_Q?Jf}!xEmt`m1 zfB4$kE|s@I>7_L{mPHGMovR znt|mLYfutNOQw&TMlE3)rNGBC}X*eFqA?=A9*I$A`#~k>gt@#IzR9u)Ehk_G@4OH!MYYiprQssoy@3z79Rl{tc{+MrM%xKanE z#>h180|fzVrB>y=)O{y-2rf1@{wnlb$rxR=Cuu9$^VE368<{nSZbM9uW673=jJ=s| zO|!M#V5Sy9hC5M}kU&rGY-scXba%17cYH+R!cD`GUWan!^v$L!8(_^9(~O-oiA}GL zeC~OS)dJrKW>nR5vfG9zG_L*vmb@hvNJd_Tcx}V7gF}{{#iOyI`{}obayKV#NprLv z%J(0odn3Ra1LT@XSyqp#x(n!^X$`*Agae?6zab*F8tRsRVR`mqco+kdP7TWWa_Iuk z2ww(soPZ9v(Wto7CDGxbhux8AZ+p9ywPm~NY92P$oW0fDA=xVJq#>PMzpq z?1s8h0&;zMPrtdBH?4au%qdXBViRKQK-&vt@8k^T8kZ8i;*xu9FWm7$bgOA>YQv6= z=QPwbLtYL|m8fcg(rnul_vYPG5svSKMT$_jXf=fFoJ_WMfCX$~yWlZrX}Q&e9nHLj zPfa1qfrTjP%izB5VB{QZ&Qf$}eT$nrCCB3$9D5-Sqf2WgZI15rAST4-qK$PgCN zGrHVUR+Va)|8Ex8zpBn82?zJ)d`Zba7vY{^B@pgt&{>gK&@ShX@BIcO5}J;3$-vm0 z!CSs7m{#H!w)rod=vxPeW{au2om;GD`d46nABp+=fLqR_7H#B~97bztbs=NTjoS=y zH1QlRoZfxWeMGVWD*SErSq0X!5!_@IR25$=gY3^sMgfy~+L4tkQoztfL^=AUS+_IZ zZT?DXoT!EI0!2)q5|_`km0}+Zu3yEP2esR+g?lS+b?(pC%wI@M6t7M` zseq*1e9V{|?#?UZj=e(~oN>Z)yo$(AA2>6zBMSg($u7vN3z*yuN@QeMjAJgHvxd=! z$|Q%^hG$F=6hE6;k2bH(iAq9%e+D=2;l@jv2QvP?=W^%uJNoElU#n8=!*pMd6o$6N z1X&iI&C~(6FXsfpXTMi*N#2yQ{A8c0@$6nfYVwzeQk!UDCt#s+YcA?z?44xPt$v;{ z%JOpHG}5Ql?Ctcmz7YBa%|WQv8<8F@0?R)uW$?3m<;3gV0UXM6#wqyJReSTyI1J&L zd*OUpMCDl>_6JN!hOZ>2rKK>nJs{O++1$mf76*>fp|R$smP^#`_Xq?#qOB{(3&M7+ z=a?^p8gEI>b)DF_Gqa7^HYT5xk`b1)w#nl>Plz>@uRbGBT=+`$nB#b*D2uOLcFPx5 z;kN0zt$;GY_LXr8HX;L&qUg0- zkCBkHs)sojRJJy3wlL&7J2Z}8s^vMH1xC-49b76t|D=DFAR%0-_MAa*b8M~(p&31z z$>afbOmWG`Fv{4}w{v)eixOLMu$D$H=HTQJkh2{C>{P?uR%-SHvC^!-)Dt1yqV7Eo zkOnmKhtG2@=c@%?7?n0Hyeb^ukgCbXdd?n%v9kJt?nh~35qh37*IfpnC%H6pgFkSI zh&WIm9gC1B4HG(z-OBs5o>l1y_Ff;bwizG=V!CuWF9J|lTx0vk_?2_GyZsJtB)C)p zZQ6|VznN_9k>_UxI%Np$N`)d0=Jp;Qm0<4!ybW|rc5}0q?}Wlm{h-`ry;m)AA`jlV zErQsc^oPN*0Qo}KDR7`2Wl{zAX7T`2N98)Dj+QKG%5*HqOyU>1T)7XG=XP5KhayI0 zhdz`6=uWlqYzbvVBZ8sM_Ij`i5Zf5<9H-BdfBInO17&wY)DVHP%l!fPIkybqf#mpT za}d8>7^?2Qwe0PP@%y0bP2O9nz(DzB3(`2=``029Kq}d&*%MOuD)0)(TO6bD+hc7E z^}Ik_A@xcJh!pNs-)={ZpDFD*W@twOz^!rJ-cN)YeW@gk3P69!qu-k7zOz6fCJXU+ zc&)%7KZZV4FO*nGCI+(A6c~(EJ3SXRy!X7Ks;bfxO4@(xk+OZqZ~!P-zT2FN0Ub?b zKgj`xZ=3XbQ^UAZ1mp$BAbw3=*^?j+*d`;9l11nQYVl(Z5juo1J6foYNm1;Ob;%?W zNzg}VI-9~KAef=$Puc^dham>~5+XO?+wA_6a(JS`1lESo|5IDe31*+od#EMmyB>4E zyNl7%7*>GJ4&+r7-RUoRM!slqJoo{}s5K3XyW{{a0mecW^8~=b6yFuRbAyCZ?o$cJ z9hym;hG+Z%+VG6+Ip@b*gG(UYC(EF;tBe*`SC_oHfxSKMc}Ew{RM>U5;;P+%_net2 zM)ubRur`-+!Z&l~tg|oPym-^c3>&}UMN6p*0eZIK&X8!7cQYBea- z&oh_H5oHzncT}995ZtiWn5Dv(L$b450CY&=d29qI@0e7x9dbkWNssKq1GZ1z$oE;o ziHL}Zx1P;z+?ItDD|kSINQHL{Nf?S{c;6>Zn?_QnyayN&%{hFFFk{( z>+57(RP@vK6RzM=|CCt8$0`}TI@!e8smNJ=?E3XXJ@2>0Kr}w6z5? zsP`#;wsrfESM>*wa7&n#yQ_th>u^5sKccu?LEw%C8|DuOKmB#a1B^3(!6qyba!4@y zAMd*^4a`Z%ox4jo!RiBmOtsW>J+_irfFfgw=_+SHp|-+sR^fN?l|W3>==t<%JSONR zel9uvW20Hk?{_JPBE?fqheiHaV`$1w*B0CeH#9KiU(!#|u+XS)@Ld~Cs<;d!^j56} zIY6*|4X8yRIyXmlciwKO0$EeMJv^n%y`n#dPr|6jBtPc!=U?@lUHCDj(&cS*hJ?a} ztD^(sLWBU9jS|R3QTQVzY`x({jzh1(by}L!yrYS%yg~`*eoD&AFSaKqu}Y+jh{xR} z*GfuCPJ0ImG6P4q^>_0+Qpb%ycwYe5t}c_FsO7EfPOiJl=+3VsZ4i~(bWsK|MShRW zHoyGWBl&m63KC^q&+!>4xth>6?6tsqek&WPsS~q%4DB{)8Q@RZV++LFOaqJHEO@G> zefa=868i^sL=o|x}M@5J1HKt-m~s4o@uBksF#ERM00 z`$y3f*RW9=9*iK=vCp=SPzlu3I{HA~?uK!HtW4d&3l1TzJXrZo&N4IdM#n|#(N=m(y9UGk3*5Mop;cB&j$chn@K%c zNC3c`P7n&$yS^E?d9vExpkuAlux(djS9W zpic9#_5tbTQ#})ZjWPMUcJR{+y|O7{0C*;v@)x^l_<-G%>#!PK1Zpw)likD<2dVB6 z#w^;D!fhxD!R~cf8ybM1!BjKuiQSxIk-?9zd2dM_r8>cLh%q$?wO09uMI@6a;+WXRIIbz0_D2??NR^ET59| z{S1s##$c~a;PHV1aU^3q8kp3R@n268zW|^uy{7pHkO(+J>H!>8kk=flBxNpk?ONBQ z1|i?z$fp$10GUt+sD*r@Ei1O^cAmqdEbAp;BvPWth_ieizT4YPH%nC5m=6d}y8tdM zsv%@0@#Sg``e>on5n!HI#;KH3U})`nZx|ynZLL;L;XfbAzxk=#A~>{rG%wi5kf;}u zAQde-aR9Q6nlieVce~Fg5_=Sq*`!04fs!qdi_uZM($@iopt`&L2L`^28y5%s?b_<= z9k;Dh*URbrJ}Jm#r;=)2(55fCjxANx0Hb7Oa~_cs0A`K%XSAi=skVx>l$*y7Z*emR zq*GXtW!WqA5RiAbZma`7UE^w@xZoPRb6MMCVxFn-iT?K<${nrF~;8!hTL@U}R1-mEr zZ_6itt!Q(=xzHNj23NrcMQ#RVOpAZ?qO^`-g;l9=V(c4EZ_mcUNA)c`d9*`-&qVZpXD@V3363UX|Y&YYPU z!wli#I`I|F;ZHffTar9Di<(s~o zDivyr67k>TL4rOxu`!wZZBE^QyMuiB0OxTs+z4453s8$C`*i`O{_)9Mw95zEFhrkW z*pw|aPfS3#k1Yb?Q0?e4**=11=B3Ks^r#OgrC;2RWc!_mGtmg&5a@}gc)p?EPPd>v z-q?O>j<`|&v%JhLpW7|#NWJ-_^EHce9CPY!x^Ei1x0c@~z8ljC*%db}tOAD4Z?30z zTcSqlOH)>|-W)3j{Lsoy&vBtIO=Uw!t1Y#E$>RMN!HRj?vCAcA2@#BLt?I`B20?U03n=11k5r?$|=L@Ts2U;a7$y5^p#?2m^@*|76`+t}H@>yc3Hq(Aq z=P+K=Mdx8XRul#ebNw?5iyhk$gbQmD1NOwH{)U6U%<9TX$=xw-KtL9&fWw}cJ+87h zqy`+jgGdAWU&<=7*Z+~h^miO&Fj?8zIaQ4sL>_rzEy0>@+Y4`D|4}2i49ZB? ziFGK&^-RAR-Bl-`Ce(;Cm#lfovc64J63=T81nQIsdU!ojN#?slt6| zq<-LztB?BQ#!1X>H6Glita6%}kF|vcDppooHDGzEC{14tyz^kwO>!kp$??$RWr3Y~ z!Ul+5zmpfP%hJ6Oy)G$vdGp3Mz@{E=Wute1V?3m&%jGYp_iwifo&LdiGCxV6vj6t8 zs<(3n0P#4V?}-pTWUh09Z%Tr7B`EaDp}+CZw!iB}fR=5F7@4!lxt-_<3b^m|p?UCF zKTB%T&RyffC49g)s=j52E|d7qa}{xDv;HGC90v;V$Q6A1^5HmN|L_i%HG#D{;h!lg zv_U7&xcf1(6hm# z4-6h;dKQUF#BLXM<&@=^Oe*?x4|uu;mc)w*M;C291&IA|EuUZ1qI)j`2ui|aQ=pED z%$qB{X2zn8h(wLHCh(ad6>snrEAAHGf`~W&_WA!u6J`s0=t1utG3JF^?PL*zUT6xzz{3g$tvL^LLAv_iUA#3i#zl{9w2X38 z;)6}?>6B~+#0T7-bG$wjvmd=sA%|LQ{qNV-CGbHKuC&Lo~J{&`t@vVjI!bS@At_xh=F34 z(EHSedvt-X$5zs64-!fZzBXc^ea!BMi%om69!^u7$GN$;hGzH1Zf|C*=n z(i;`XOR1|X6q{@pyu}cJC9t2moIL;IGJV%CbWit{vbj&)!pXAIEk`np$Xl1k0)2Q! zO)a`6v6Ukq!+rD#jPLyPKCGErVM&(KjBQ0`t$B}w{&RR7;IkFe#*2QDHJS_qdmiHIoniwSzc?sx*42WK&!MZJCc2ma0}Fe~j4+wSOKH`J`1EDn9m{ zKnSNG&kNZ9vhh9W{nzsH*yEv0;8P-WQDyf8N~|M#doEb0*7f&>#S<{p!=*H* z=@gN}BuwinyGt?h(+# zG*nR=Tvr#6zbF4^5jAgfIs}HI5>*;WpFlrSFhFiMhfPMtOXNF5})W)<9Y~@DRYPb{jr{0%~ZD% z@7NZ^gqc={!gZB38ZG5S?j)G|!l$1eo)hc7IP;T=i1wcmQcd52g507mNiM@m`}Ra~ zz2elp5RFM@S3Kx3B!fKQZ==8YskQ}?^WHmDycN2p!EJWzUGogQ39x4iohxe9>ZM?3 z)Zv{oR_oHW%$US#P0VBTQ`%1qI%JUbEmOj@rSN(#c4iH%DvXsd#Q1BfSI|t2asnY( zwn;?Zm0e;2V_*V?N95!kGEr3uPe_uPO3`3EdfaN9eViBX+EJQ<23RgBM5eyg!-_cx zv1@ir_+2GYaB`N&=Qb}s;4drvTBw9?tQgZ;I+ObV7zyCylKbkq!rpM3#oGoSI}eK= zq*-#+lD&jNy;@>&awso!w%g3D3GgTWj) z>=;L;B*(^eIbLVAk$S_BNc8K?nZscnyxxbr?ou0#w9wH8rxY#9KeF&rg4a8I+PP++ zSm;X=@zx#_z~Sy^zUiB+8r=UNFizw>>IjVgsh0FtfR;1qAlKy9md1o=k`43wI@&wD z(E!X_oK9fKyc+QIZtDH^o61GsD{?)hYCv%V@5!5)k+yxR=&(D34eq%S?Hy`?_5~s` zr;X17nQkmYTG=lR60k|%zTCI>S)ym0&JS_|W$wS1S%TUh0#(M;_`{k20%Su5H>)5a zG3beH8r+gxCsBgy478*KOZW*#CVEBOErSW%);$Iq{?Oe?} zdeg;{wSo_gGW>e`eu2+3k{+WTeM!m zH>QT!>-_NJ>)xX1qr8arcGXl$1S;UbTFSj0<}zv7lX+4Zkz7$WDYsiSma~Q+TKy!S z(y037;h{VWY3AY6lH(IcY*RCK=bd-nh?0FIB)v|r2`67Vok6P*OH9S0UkHyW;s|PK zb%BF*;EaIYcB?hl!=6zHUMtXHOgcV^OE8bg?JOa3cSNC6dOJTY91qEbN{$5_uiXNI zJH1QOqJH&$@5R=c!M6%i>h?D2EF@}g&Uh^Ui`hF@!k^<_VC`c2QhFyyP#oQ&itvdGO7RpSh z7s*Hea;|)ULww=ywFal;^g_gJI%im$TG%RXbE!tARBijv-j(&w#me;}n+UaUC6W78 zAJ@o;s4gd!CH1&3F(l(Pz?mN?&JSR&?C;1KWBshid@ZH^ zN0?sij};Zqo?aP;xOe#qH_^2Lt!0(IfBrlOXP#y$9&$~VmC0wLE8j|ZdRs>8GN&fH2`e(SL zu%qYA&C4!9ksE72{NC+4d97U1?1tZgn~?TJtkUG!ZI%q(Wd9$W#y@c%uii#L8!ugE zy&)2mQRSGsO=Z3np}(ac$g%C|nE@5Ed7gzsa^=o{32{a{Sv?DD##g1(2|`H4U6;dv z+ehC0=wt<}WZ|Pu4N_%K4=D3Zj{k%Y9`4W6WL%|Kk@%MvK;~*FFxojn?0bItuKC3A zYzs285I|{sqiVVF^khmWEV7#DX@2~@?{-7AQ+Y>7MTLK4AK_k2z-l~hwQy7pibfpt zwjYuPyn-aS9}5WKi~k*S?Pk$pFku;LY8r{P;|2g<5)DkyNW z4F@=DVRsf|U%$)CJ|tf7tio_Ix(uFQCOKXM#iZ&P2)gxWKBpqv;JK30F>?`fgSL(U zFefw!sLp^|rNRxT?Xw>Ad8a+da&T~V1wwu4#8<&{ z-|e6mRL?xx5x@*LH_xMJ^9?y(plblOmN2uC)2AD>*A6;~z4do_H$U@@8_EQPLvG;O zgLWybcgejjWXvd6W;`NS&02)>`5#9|j$QPL#;sB_+}_G$rAlhxyf`~HHz~(*N^C`d zFClr;S!Jr({0!g>X^B@lIj;?Aq2!+i{q*osZ8vr~NP~E#38o)=2pA1e^q0gc^ zy{6hO(-1U&r3r2xFfnmQeBK^oQ3`p&reGv@paO)O=$QE1psT?`RDUYJc>D&gw;`sb2$BOZ)o09hiPqieEi_l-;zJRpRupG+=%$sKgJOu+-=Ooc4GkbAm8hysPY9Q7MB@ z-YYT#Why!8#91{SSLZOvtU_4C*R~oyT_80<-sC>(>YN!-cy937uPtZn{Z>>|Q~@BI zQxR`cQ0x6V?_#e($K^S*EXc1b_WK*?#LDZk(P=El%GrLtPFrSUOFbD+pU1}LoDc%d z|EO>2w3?sfK7$F>fH;+le}|jLA2lBvhn5y>?~Qz>r726=ZDX07Ywq2c%Jn2Tt2~C@ zDlvs0e{=^gCf129E~p@hn-Gvi0!0{gem*hct5gAJ%L}iuhZ$#(k6Ke*!pq4?pRscss6OhJX2L|zhcR4zlAL5qtaBUnyUJ^}E?xqb z?^}V7TD$@|rzy?P=boqm_9C@U8X{|rYjXUKeeH#w7Q9qYh;^Dpm1|rO;xg)Zb=)4= zBTFJiY?;3K3Ai#Vd8eb;p` zXvNjIC0@+g2XDyuHt86isR95WYA036Kr{E#j?=A&p0kil>Uz}4iWWz8aSz2e>*FKL zt=#hgQPO`_Fiv)fjX!zx*^S`v6kfZ-nI@ahV53PEUuAVu>j6QLaSeE`JzkF5ohpQO z9e2&zdSb3Je!c^A&S-cp>nFO_O)7e@D6phz__|zWpZ-{k*H#wwq!SB9rr*>4hWRX^ z*(F#`lvU)*chx;#V6{Zcw1Qbh<8s7~nKhKlrVi09*@|K^`O77r)49qCi2Tg4F^flQ%IuXQEfY?4{zhZe36P*wB>t%Fq4JP9iMP@m_ED9@CIJf}t$;e4wV9h$V9Q8(gXo-D*|}PcA7o22sADcDOn$ zav{y8dq}G`;rECSmucg&@bNR9N09#biF*R`Zvd!EW25CU&*U;oL%lz&9yobcs}vkh zjya>zPp;jj4a08(e&U2=)}h+==ZyWUP}7|hVIiwo9aGdet=&CKVnq~*+}w>ivYUos z5JTh}z|bTPp43Rz%<1#-E9ZFO8Bq^3ro7}D6-SF7CIABnu!Zk+U?bt6k&6%gzVoO^ z_`Y%{tWzFlQ^4UH$>n@}=R25tJEND~RHqz1%JpQ30bC6{Ln8QRvO8i{Bluj^QNfT$ z<9@v)F!A8X!gqn`8sGTZh)}r-rdI{qm7smPC@@t~6`IHEb|{4%?}q#?TPo;&?UZ6^ zYkq;am%in!8ES&H?5$?B<@;UK`sFxpp~OaFT|6k~D`%D#eBb_UPU4U!naYgh^`(Vz zf>-unufh^UP%KNRUos81MXK`S#sS$)x^X%>I-Uen80ZDMWiH*x)tM#NX5DdZIq~8I zM5wv0Ni2-#PPs4aaTw=~ z5z`1>@e>2?h|0OkKSjl-fY3pF8?@}+HUZK5;_uY_GAC=(qaY8Y})WHB%sXE0ad%r0m0nA_KGA=EJt>~^V z)llQt%B}MpjqpEKOH|JEF|Rs;Z#qhtHp%vq9|I@{T~|zH3&kpF>tIdVXh{r!=g9ak zW}D(+!ZVWfzE)?ALfO>N((>FH=4K%I{J`05nLqap6RGjk91o$7FO~fGT(Ll@V~{#9lc5jNkT5wXvWn^^@$#-vJ5NrceQy zD$hN|9BolLhNa-#=iQil>gU~<)p=Gh^3fEs@QpAz7egk5GP!Xn&>rcP_ywEEX)aypmY%o0`C;)WKWz{dbmER0m@++Y|t_E{jOCC zBWVlpSc@DW8C)LqfjUO3v=KS-vA1a0+pWY=y~=SLut$nuTzb7L=#j#Jc=TIJ|E?3H zR!-o+1N(#Fe5b4-JEtP*REHCsIl&|ah@-cL2JZ9q$?_ne;8yajhsl?$8*z#G znNy$lAZXD62-_Jr%AT10=$ZBh^N+qaqW zKNJ4oj9U2eJLcTa+%sdIN#Jo-a6ytu}cRGzR2;qJWG$j9<`}gZzs|C0NWnvrP^D5qKk6D@N z*zj&Ob5V!X*m?cP!(=Aiq#33IsdluF4R`(s+`kkG0^zy?OPs7u_wondkV%E8i#??- zLmg~KaV+j{b8DfC!D*B~h7hTD`jM{5Ruy<^+S~h$fzIhw{vIdj<8&wnmtwah%ZFjK zoz2VvWAYj!Uzk3AL&gOXrWYJZA+@r9OeA~h^Fgs1VUPXhVmR<$w16m4*oxxm&E;_f z|HJvmm&t<2D~lX~Zo=G(_Ww7u>$WQ9J((QiY;+s4mE)NqOtT@mw7TB09JS<|9|qCL z64(Dv$LOEy(HKCE`5kA<<$^O6>vrCWwDeWK9g=i=L@F;IyV=+~j!|m`?6{_j-%^C! zfki}APTcAA#pw=2!A8yXY}C|j5eIp&O_ICy$>)QX>1!|5{4FQEtPli{Y6Dup5xxc4 zwYl3HO=>rDR|f#WfOr}10AOwTH^#=uvQ9q|!QA^V0K)yT_2n@KX*m7AP*u-m0X=lqGB z>n{UZ5i#?a5>*%T=6tlt#6$YdxI4#+JDRI1D^Uz-JVSVxZB9{}%WJ0pBQyQ~ebk_` z8^B`n%6)vl|D>SkVd9zTUErn7Y>lMx#d+e6kDIuy9S1m&Ln?>)06kx z+SmqXC-{JIvWG|&O6`{wc1{oy%}V~&xR!sb-3Z^mAv~m$Q}j(fpz5@2eY(Eag%b$0 z0UbbwUKeb_J5K_exT8*l?ljW|V4Xj@d2ZCK$qCrAsbnC~-%rzQ{mi@U4-DIhUUJ|k ziW!^ZkJKn=mFrjJ9(>iN8DlTay%&R`6qgx$!iX*)${3Flo}0keS4{+b+UN)AL|I*O zGoouW>dZk#VuT4iNc%}V-@Sn*{B)b?T8<>*=?V85pY1~xW|im;DRE>f;+D!imH76% zInrnK_j)g0_bjb$uRq23oIa{~GnJ-u|6q0+u6|-VrLfbqKBk!IveNR` zO5`Su=q${)3zJc&u+r`u{_5&&)owcg4hRg&#iF(my&N_Xwo;pMKc3>&V#Fhm-aVfI z{g>N+aPP4_xBRjtRWgBvFu*_$vdA{&WMLSvFDX5$aX^(Mv}UY(e( zU85KJ{h=eSWOCMd?6;`L3o+5hT5_6vP4%Vs(&@m0^(-=SF0#5?*o8+}6JThCvkPxW(&&fk^|Anp1=z36jl z+wChhrQAG;f3p4SyuWg1-yAMJeM33xcbzPF`hmm4^E_zLAUgrtCJyl-V=HNUZ3^{| zO_`X?*qd=5uyg&RS!|=M2s-^H>)?U*Sk9aQa`J#!&$dzM536o9!B*eWg+MFeF;A}R zook#t=^aNH;|!*0e(SgooZqmfe#f6|O*E~{6LwQFr?P6oJ!$ zW=cwpAKek9Ne>*bWOz|MX%$7K=bU;@%=Cen!Gv_R-eyiUs?4_A&uFbL`P3if1wV3b zFyyS6;3cjZ^IK2HglH@wVv$Ead|%fxzx38@7&wckNJuFj`^qSAs{Phi6CX>x8q$PR z5EK5Qb=!4zVK!4{Kkvebh)7firZl9KX`zV1>-hE5b+XP%EeJ2Q=P{=|VE%tF%iaD=-x@%IQ=hLg(toA#3gOZe$Q#+ZD~{Y-f|nS8b0yuO^U z(hpeCLQXK|&NybO{f+Z=ADc_z4*eOuecY+V0tSzBn1_7nhwQ}^%rgowEbHGu2NbFf zFjP;U+n;;?kZ%7iVFh_p-Wadx+$kvF);8C-4nAz>;xxr&tZ#SHL^;I`$KRIl-D#xU zBtE*3UPz(N(bJEg52I@uVPW~6gds8~pi8}xe!R1c^*L*hLqf`;nOwq}!}Y zwj&)Kvyifo)i%W8*Op*`T0RsgD)i)st@{hyX}VWZU=8L^R`~{)hJo%jj;iibm9)Gh zxzNexlD9D5<29{{Ps!l4=d5+fHw@D^M{u;I`Tg#EE#X(!%0IN0HdOLC4$1md+@WrB^#u%27~X5>8RzVqQm99B8rk&aMB>hlY{T#7ZP>72FP{)?S1;E z6)=b;%ixe$y(6^TwKGxy-Sw@*A2b=#A0@8tF%C^S^^aRB7=y}^Po+I1rui%rDm*>X zb9!<*KW+;hyvDwEBU?FI>#I6_jkivT=ROEPTgHNfcJ8Ef2cp-}Y;h_&aDJ!&CcBc0 zfW&$Qw&4q)bXrSD#BYrqt@>u!!jes3|K3vFT5mxXIS$&A-EBq_lc`gOfr9`VbEejk*V82Dt)t_7T#;Wjf5DV?LF1mBj1nTQhw- za@B3#DDngjm}T=Uq@UuCCp!TJtFv8b=}*iGdJFz+8a#IL!^)RvhCY8F4r=CZ%DC^t zLrC+7&b>|%JC#s`lb>|_35>>2Kv38HGUEMde&)5luYZOe+4IvB#S%?+xAfHese8=! zZ41rfuy|}-;KT`3MeMXH$Ez!l|L}uIZk3->@veUJ*(nd4!N2QC-4CCJ@0?|ddo!Pg zT}FSs_7c*_`a;{oI_{XLn@1vuyUbdqO&+WI=+QNNz9e1t#mL=7yhpOVckqt) zYHun3OskU5!f6SvW^t$+wzHaa{=-Y>jHPV$Ef}ZqJX>3p`+fM!0Bvs%y+W*BlaOO3 z>EjwpY%dby7FH`ppPqT*m$>An1Sn2uSvr+ z#AEb?^_=0jVh04|$Awr+S$4hgX}n{N-Q zvRNShnSl78pYEO^I_dseD|@P=!fLvO8o!Jy5}|T+r&2(`aOvEQ$@t{u7lUVsIy@a) zZBAm*Z+yb@4cp%@_AZty`)+nOj_EzaEB4)epIPp;t$^NNFM8aV z{wa$7Z{Qhp?z|BGVl?kb5T9s51=#VkhX(;UK!a(zU|_7r>&q|BrKYW;ZRO-|BI$Ry zxYHvwI3>4S-0|Gwq6jOWNddD!Opj$kcB_%43eE5DlVEU_xb1~k|N9sG^B)p)2S{S( zX)X|7qwm9jvsQPvdW-VjIEG-9+XUBt{=DO?I`VRvde|p}FrGbFZuB5flbf^!jZ?kS zi0>S`EOHqZpR<}b%%II;2=G8X7g}c+sDfy>6KQzYFC6(ecYu*54~Wfo`dcw`)#WX z|C+$e;YfYuih`lENe-8V^h9I(>y4iHYh%dkzSogziZ0>33RCxXBDyY3BrO{Ba=6IM zd=tYU#!kPzQopUzY-=!!JpL|#4%3A7mP5e&osK@4k`4IdK+y4*1x;JusAJeDde7Mr zEr&WCZ@}is6fQrq?xiDlmZ-EbcI^*9Wxf;DkSU59GfT>( z^?%rwAy{y)F#5O+*3>qf^*wOZ^O>Z^(vwuTLMoaC) zW3pM$`NvH&P(L@i;NK7DEqCih%_O-p&g(qPM>*8sM{LNr&&E6ORJWsfOQMOY6SS@}#d>TReSsjGP|NP^rT400{YAl*zykN212f&2FJ zp)^yb*=Ey#`KgeFDf1~FpSp>iUL{P9i^Zlziv*)t&J|{rSS;aQc%ZQgv8TU=>vtlYla?mxH zs3o)LmpXwRoxYe#e~@d)iiC=pXv$t?ac_Kp&y3Y6;FA>kB3lNXUBFb3_An8g0=Slc z0u+o`GlzWYfMNNrT)n0GX6NH+*-Kj2oRcb$f6)vRN<{LiCTy3U;dLckq($vsWpZ30 znam5n53rlRUf|c-=2c=%jUAmvn|{Zf$fmUscMmp(LW0Lk%uCO^1dii_-yR}R zy=I+u@tL>pQGbK?UjR@fh$dB$-(8tXl_we!W2i8tz4| z)6d!k6CE_#fAm;7<~lolKFTaKPaqb|>@wQDGrJR-3B$-wH8@r5Hoe;Ode@u$n$+o} zqT4`WM|+@Yo0qSWO1U{@5BQcPxgZ~7%$~R#^qM)Bq4o|lc4tQSpP2SS3gUrdBJ~PZ zCm^%;O?1D^L+G)Epo=8RCX*58UzOv$tcQ>svYkYwcO9+1olyN7bS6LT{(jHDLnGQ? zi^rDbGv2?-(=>MfELJTF203U^ugAMkug8GC>Z16|`ie8BF!Gs0X)6&E;-NAmulyfY z0DM09O86J_#QhKSl$}e%o~J%R^`U@~>|8keH~x?*%f!$bDQzwmYja$BB&7_E8`+{q z-M9v4gUN)|m&&}oabJc{`r>-`Yq}-!XNxF)tWS%Kt=}twqY5KAG{#ds=N;8Esi?x3 zG!7rMJRahuW^!7Yd#sS^`Dk)CeWZ3GMeI!nxMaKq@v@I@mrx7_vQy9Gw~mu<3ke)G z-9={swN(**8z&sx@GNO_L+hH!t;sQd+9|2$qo^p~jB1Tj0E{l$_^R>Nf6*Xu4T3mW z=M2d#P0AId8v`4Li+GZ{BRo{_-Z>BD^sj9e)V#J&au(sIr=x!C$8pMLD1}f(j6qr` zG;_PzQSICA2cKsZW_{AS>p7vES*ygRD3#V%)pm2!ghQ5uh!hbYDK7t(uQfL_KNH{r zsTsBlV}2}jJ%{?zDO^g2gh@-;kq5I*>W;#oPtszQBNWunr(2Tfp7|kk=Q(zwd=Z>g zoHw*LM2OQs0>9bg;Vtpi9?_;gzBYY!-eysC4>yp|orW27H1)i7tXSNe%oJ!FY$4Ml zQ0Dc$yLU+pr7b(a4dCWv=pjVV{Q~3WcK!)iYAo+OP2AwX43|rwhnX+&5}nA-{Dl59 z^Kjh~o9Q??o&eJf#&N#D{_R2=m1(OLP3CBT?U}uQ)vtY_Pp^p+y@BRBtvDveI-NKQ zPMn;82Ly7si)W1cHa_wfAJTE|PV7RXx|*H+o>EA&?ErAn>nZ}2lUjbX9emnOh?W+u zZA0wMofSd02P}Slkeodv`ZhwKFWK6e1wh9GYtXe?kig{bJ#}Wmr_lvFu%-||t?>=1 z?JZEHa5D5=JOcM4gAThuW7l?eFf0I;^~7>pt`i*GuoxnvrHY`W7+H_-JqAFqV5UzV zrNv10)2~gat$+#S_9YUR?JZAGPHdR!XSSG_lx{=1t4`k%r>8XFk>zlYiy8I1g=&zk z2!G_|eSWUO;FI~0($u-`5)R=LyX&ulUu7~)JHtl069BlO-N`S)x*PM`s4jW}pa-QT z3uha2OS>m+Nh6$Z$g@h4SxcytFgV<+eC<`TdZ>5hPDoOU$RA@q3s?m7JUh^!3OcxA z_hK?i*gOrph}x{r#H8PX)^UqkE{YkW1;D*^1)8Kn=OAm|Im33ar2_zgT4HZJOzpY} zH5Cbpxvt%i^fQ^Boy4`1Und7>5h-J%v)pGw>A`iqD~hMnWBE$B>i=M?fBIL;l{4UV zr534v^PiE)zyH)?S^m zum0*U3wz2}iKU6beydHvCySSCbMgcG8~WM@Z;w}1Jr^5$HQCzN-QB5WerjQ_`TixT z@c;cCfRN_B&P8WD_kk^>$y8HI3x!5(e}M#+i+NntcbQ3`8gmxVOA=he6wlk{Hb7Wy zHl(QgFbKufqx5{>GGwjhv?qQeE2B7~e>xGsoJcQV#((RsM&d~2Yi>(Z35sr;q&9B z%`X9J?2izq?NzrG|4^^hWJPy$n&Xnx;%Zgy&T#MJ(b0yGZ&^IBq6aV zgW0=-+;5~)vTvhTh)?}WPinBY<}F*lKpY&>&P7zKdQ5b={sJ|*g1Rxl<(f02u)e#s z6hXPXu19;;z?^9OaJ-eF{>uk`%-<8T-=ao(xn- zO!lVNcjvVaAS-6B8T?xHtn|kgj?27ML6mV{u)7V{*9)z5gOPgxQAKdaxzI#;u$86W!|d%>q43!r0DV)&(MoT3N+kPSG6 zTh7M~4#}}{^_nCg3rZ6|jW~*d40!6F6(x$#p1wM~6tdyPd+I8MNLyE*uud%J7k%o^ z9HmrGq*(Zadv1?iO&{B0Qo#JN==IgaPHlo1a~}&R0o1~xYTG?btOVK ziVV#ZP>BP#9(;zlNg+OS-gp^2-fQ9z4PZJcJUq!I#YgMc9s*QSP)SfGY76vq?WZ6! z?1CSd&J~N96O4cw#dJ8bQMECnuv4k)4ypUDrfXqnvp#f=)l_!R#xk5*`HCkP*m=)m z|EPwuoj-`QEH8BgE`k`bc2ue{rPUX1`}JV1xG1O7>Jrmqkd`+Q1hg_!Qs}WK{$i3n zLI}-)>W@kt>^#DcVI~^7b$irTCj{TG=_GCA%)7?yg_jGHnjS&-Sc4LIfVM zweFW^IY#`p`&n1sCUHd{E^(N+veHl3*jB2Z_%7BtNd7=fWF|=sY_+-vk{aCbmeG%i zj5{QGzw|98F5+`j*IjU)x?6M!6jaxJp>aL@UWJ-i5F{*RcFze$yB+zz= z&sfYwFstZ9o41U28Su=#j}7lsExVVVSU*xPL;qZeFWzMF&NF0C@)zo+PVvHJ5NSt9uDHo3iA9b;sl*YvTP7b^=Sef_1L`^9>+)8%Q z2WpRm864xr7a&CpOL3tk%crl2BC1Q62mInQVSG^YdIyUfhUyRl?I+Vvu+$~f5^tz= zN*w`dR%$m><2rBb&^Ka|k+4{}IG5pTym73>KW?#9SMpn$)I#;mW$#;-cO%|#Tprfb zS4Ms(V=T(2U%#>o-iSrEF3kcODxjCPlv*X$(pRIYk9}*WS5sWI_6*)(ZJmdj-SDhN zxHK_xCTj*$5Tsx6r1ofCI4@eupQv`~a9Rl5hSIWG%z3V)#pcp50^%h26bgW@icYom zL*FM;&K;1#6@*>&?&JAq7`5xd}7}}whh>sJx0gmj zvMMb(btc-T92WI6e1^#{M4}R48WkX&Ds`AxTC9`Mg_Fse3|Fs>1`6g?HPq*+9{0^I zzJ`sZsPb}37|8jwgfAOK?U;ygris{}?Vz*j?3SASwXyF}U@AJsQQYjaf0NN-;*9yy zG_6db4entB_J6G?1@N$TAFvTT+8iQU4||&p67}@&6rnyveqc1umeaqfG5iW|>TV=_ zN1N0NJ~2{WTSYPVvnxXiGEHE%0s95bL2J7!6ywQ{Y+0JbD zxRfQneMwKkZvt9oCPFlisP<2KJ#rFo&`Q6u!eSCpqVL2KzHPE0^fVFX_^7y?;=#!t z?)qQ8Sag-)XenQu-*w4_ufqC&jn0#kwsmE(7rWeJO*EQ4w+f%VDdFF*(Ej$Mu{$TG ztOrow0H`1dCZA{+DL%r>JvA}ZJe?=zD9g}|+i~o$xo64cMHY@TNpC)^TSjRNSeI6r zh)rDMCOH_ZwZJPsqU>&dQlBdisZpI+{R=MQ9JXL(J-G;&iG^s;!h1UzvcI!V6u+TaRc=3{&X5-Qkh+%mu2d6I6-F2kqGnV+}t)UPJ(aQ~gU zmR4Vb2LChGfy}}$F)xM0c9KcFm|V?(wIC;ZkpDzVC{DtW;masKT{o0eB>dmNGVz+4YurA` z50&0~Pd`Pabj`0rNJfS)kk}qF3$?vq`*&NHw2qz9->|TtAU@sLG3RT8R%FH#Y`1Bb zTG+xCVq+GH{+whkwbZ~kv;kyEbuO*>%kOVI3(0w&a><&fkbdVi2Mg-kx&qHYV~{W zmz9%P>UAn{xn5GaVAdO|Ei|h?-6ar!m}O;oC9Zrs^nIE(@YVMR?%pQtAphNREQ`39 z==t|mpc5|)`CabGv1x6hV_a(l2Ri69$^d5aEXK#v(tWbwW@5~Y&p0_R;kNW$T@zZ> z?V#zEw=RU@Oe%SFBdUXK2Drd@+jqVwB8>47$_m}vvvcT|Z)IWkQ>xpAX_Af2a^8JC zE_~Lqe)~sEoBIqeW)cXd#*SkuB`cOccsG1;qo#h-1tuo(0#oW(xAHR0H zhR{>cCq?pB#a!WC114CU>xTw8n~oJ;n*v;M5}=Y}d<9$yV@+1>jOtwW9(T-k69RVb zvxFo_x*P&iS6AIagg&xABkAo_PO}5Slw*P;g$v+I_J(> za2kV7yF#5I?hl@)rLZ(frW z#rMXWbR$!J@>37&U8gnxCV~mBUS?TgzpFE3ZmZSoLlI}a*6t~G8JA9j`5A~fY)pH6 z?1jUl+P`0!Xm*4{Z2*VsrsL;eh2RPQ3zH&STZ?0n7d?KDDw>{U>51Kbx~E~2(G+|o zV&@aE(4&;}aAxAf6cl+3cWGFB0Sy9ffz(iMrT}9@HNAsMH(-R%M`WgT70zlO) zB(Y>9=%XcHUnqK{8m7KA>KsSOTP+wBDvNkwvn!F2o^l^>E#~a3F#Y&Z$-2v`e5l() z>}@YOQNsEw<=#+tfGxjhXBje4+_`tZ1i*W zz5JcvNN3oF#s$0wz1>D>)z^-U(X7AtOgRnK+q48!jBTSt0q$W}U#{l7bL#KD=4J1x z249UCdq&rM`pa~yf6yKN_?i0UBTe&0`{4ULDE+DKP$@B&GBBXN{aoupW7Cljx^N;?Y5W*pc=<1iPaho2 zgfmxA+^GlZ!o~L{3DHPeJ^RFGAP-GNoD6U#Mv#XhdOEuNP)<}j#dCYS_^EdN_x|Zp zLE{dI{FE(`Ee+vSzMRTbv3J(cAu+05aJP0Dz5B*!(_8+^z)>&Y+5s~(XxsdKG-oNbxan7u;G zk#LIy&JwrT{>$*%{%r6yn4%6GzoOl7=$^ER!p*=ukS&vCflc=zRG{~Vv` zAMgK#tVM!nMB}J?#C42qo*VVgNtx10{NmMFV`-s9ZhidN@t0m7^6&bI82!{>?|ySGH7&>=SbUsRv_ z8hz85o!1NmN@2SAijNJ$sv)bQ^9n=m9Nt z{h*Vht$_HrwJ)^qz}b-zYJ(gxhzAN9-lZat!>BN|*-#S27D!aB)A?@@FA)T-zv^P< ztk-fwBrenD08A#Do(nn#FydXXm6P<`VcoUm!5Slx3nsNfcB{D!m#3-5K4R-|j;M8^ zw$!@*n&pbp)ejn1e(2g=SF2t*U!`3 z7JZ@O^v4gP`J{x~7;hw6SA(h>1xk1IyvheG$+|!GxVNtHsO5es z;8?grVD?ajZb-uSKt?SUwf6;5=YK9NtgFBK^}#3BI&qYmc03=YPy>N3W)ah6j?X`} z{BIHg)aTN8S}L!mh16R#D&muFBTv#{AiYY)&piX!$3JLkp#IMJ@GkN~AVZJv zw-!D%N;P!pNW=*V3fd=CdrsDkc%aFVdn~SyoVk zL$OK~H$ePMs4?}+khr=1OkVg2MS+ME?hw)btH<5ko zx4O{MN1v$Q4m$b%O)RXW0g46)&<|qlhAb2ui%rcYz#t%bpaLEw5I0P#v4C3TVDxv> z9~FFO|JFAVu^M04;Nn}+`LCls!@OLulnm$KEe{ogCfPyvgQa`KY5N!16jBalQ;NIm zprY@W89Kd|w1cUAyL);oz_PdrPka0DMznH=gg#W9KXuU+8>!*7^15eq`8M>Lh#eSq zAlc|rVYOP(sQEG~4pTD08;bVZ6m;~Q_kyQ2xTEykhDwL@M9^$aE|Y}LMkj)!ju8a<}3H>!B_%e&--~4Ha1e z1G_DMsD_KpErlshj?$w*c|U%!oR{H@Z8M-u?R0$-~Yqjdqy>xc5TB}97Vtp73nxgQJT^_HbA8* zNH0O@T?jpahy_%d(tAXj^j-r&lpY{bLa$N+1c(q4AR*+tn7N;K?zx|L?uCE8wZ3o7 zA6C{%xXO9%bGPHzds|51c@KM8x%2e44)v=D(x#HR?>`PKQVg)nIi2hb#TJd9k|Sm> zNHY&tKa@7mZwJz?mvHVs)$PpEHOK~-f07pG82<*-<c+X;NV3gBOiha9Ug^*+ z%e!$4!!+1>D}rx%Tk>;hehx^oz4#N4%*wf(?3}EuN7BGDOI}T(1cVZ$^svF?)`HXTy>vrMBeB*hpiAq&| ze*XPD`CKKv5`c|>7XS+5rB^#(v0?=vVHRzBWftIUVs#H5JM3rG7A}wLD{8 z8MDNsw9*`I&&{s9!Cs62aJZvlBn>}}7NE@V`?nYHn6eguVlxQ37P^z=pK7tD4?VzHt62VY?%IM)|mDb9W2vNcUAkkRc!2fQ)NgKZ9XwYb&1+uX%RY z*)pU}ms!ru0M?~kadG)Ih-1?jAcC}$z-Zm}^oFr{fMziV0D_l}ZUG~USF-|+T7M_V zSaTmxckgFzU*FlsAA#aKFR!KHe|i1p-dh0eW`1R!4x#A=H)gD=5uCD3A~39J&^$Uu z-n<{AvGBYv0(>DK^e4?V_eUz zdWtspyn<|V|FqvFfrA;&-(Mo={N<9BBNVb$$>e?|Va+SF3giY|u-e?>8^R>GuFDyy zy}e>;E+|r%IyuX>GBl5pj-=$FifuzruHe=<-%Svu_Oo>2)U16~$R{SCAv#shymeCG%XZh>W^sv- zJ7sOW=d)ToD6US!NgW*^C0DRWG)L7X?VmAnC?m@eo0ZTxi*A5FRjS9b8r2Z8p_fUfG^VXgJT1`hk@pW0aQW5^z0IsOyU zaD(T8Wy>)Vb!9a=s9Z+9wN+paK+9gFnRdN+BaOTqomb2wHik?}8oQ-y-yp(1)KO`= zRbbQpcA?p^ytyAP0>(D)kkggK0X+kdD;~%5 z4Hi5vZuGhO2ndOj7#*H z4j(}&mxuvq8vyrO+uHJ5ZU6za!{<_f=&`fbA9)|_WcRt&h~w_&ddriZiZ{O>{{#Lx zV`4cX)`IvGE%|mz*)DG|=01Qs0fjh?*ddBS=QHqcYI7G?llp4C9kx+nN{4W;K@7)f96^df- zhtdNKrwMDE6F8t7D#TCTUQiG#`k6_NXN+q0DKi@XUhiRYx18uu%RFo23nV^@nm)Zg zpe+3koSk6~ib<$oYg3cx@4wuAY-?3gV|^J_ZJhuXA0Zdb}V19oGFfD|;9_7;`?-?4w6LiyUgU;&xg#mbWYCCS8-f!hRMk z5B(i1&#n01V0puLV3g|Xr~e%t|IcrOWKRLQQyp6ObGq|02q;K~{rYo_j?TQ!0Gd;| zH|IG0#KHj&{Ch(mmd8KiWCDT5KQH!|RsLtk{-Q+w5yii{qW_$+e_xOPKP!*RcGv}4 zI!Wb9Zh7!^3nQK-rH1kd5n?I}ZOOFEp_@_8kDB^HZ-=5%bJ^hvt4I1$^apJGXcJzs z0R&Vs%uT+C_;4Er^aZ-7O}cApr}K?aDw`0O^c%(7HA=n8EefC}S(~>wI@s4Wekl6V zeH*P*kvoc9b{gscQi1f#Lc=X!xf2N6D_r#0ly*X{v7smk_9PO>`gb-rTMvY=_1IiO zPu=$``WSfoi)1_XmlsyrsvA2TYqd;M8r$Wifon34;yQ+yR^Ka#xQKJTK)?7=KDz=G zaU?t}K-XA?RidfR zS}Z{4_oGFJB15dIID1;;SR%1vgFW=eMF1DrrBc7F6WU|{Y`uLkVcZQD1+e}_Dwf_2 zwGjeeattWyG=JYy+t^t#>H%+GM+jdK?d5Q;Z)I5x%$Tr{49}Q|BNVjFf)z)>Cx<4} z8<_5?E?uR_?Kb7a*Xsl12UL?1(VGT@LovOjetTLiW52PNo-sOwb|_GmNvUo7BK3YW zEEe=zxBHdwmTv0PwC?#ms@0VVnbyCX#x1N0WaGo9i!h1Ku*UUm{C!(TgJXsw^~(So zRuy)Anqp%A0G|BN;d3~hf97QYMDALiwu5^R(3nF_Y+AogTLy76Mk7!h1mVvmdn6k)6`dV;@QV<-< zaI2ZK-4u@EbydWaeKm`g)o4=V=Yh?Nm_kvYe5rVELdV9hR8va}-T1KR&a}p1Wtj^k z!RhL2jrO2!>gy`O)xSk8U!Hw!Y6;SfLYxaur|VhIy7De9u2t-H{6eAW*^EguJ%<-9 zCJJ@&&Ax`;c~a*sOo4$<`&5(>cy@O7Tp%QV+xCsg8|n2l2{?Kkd2}=K2J|>UqUlzt zzgU?Fz^`19ZLTWQ5J#xn>4yJ~JQo-R73+F5zq160-R7M{fT}Tu;vK_fei2?Lx0d$* zDz=^!ohYW_`yJl7r=@ex!ZuxTu~rD?v%$ABy@4yYE$OfxqO_rR>2pOVE-SrA-S82x zNb#B+*%>9_J83^wY272ShAMlox#B4r(`3NJcK(zC|M8Lh`nJFg2p&=mQlQFg#N&4% zc%-ZIXo-Pm2d79rPL+i9!mU5J zf92zUaQ_mW9TLSK$C(ZK;oL=BQ~raP*MaU94~Xah0MN|RTFjWuWwicff|tBtCt3=n z$kLvRE>Z}wZNN1sY(H%{H)n$?F(wfmznbstt{rvbv?~HqQ=OAAY_kmG>ib{IH>a<| zvjS4?zYQxhft>!rjv9*p`Zx)A4Z|iA%KDX&KSv{9vr>nwzzdHbPh?%Nb zqWIPd2)iU?+2DZIQj65d(Z;GNvTOM=Yek8Y*&n_!T758tO{M*yx3x^5{li z%&~z!`Qlwjv;2M~`lQ4dR4jln%u4W6W+mdW@4LQs6F7Q=hvj6Zs>io$smSxrr<)>F zHwd8?_ytNuvfnXw)UC)R^m0-bURxu%ds4%Fk02^YbTjiqA3@-iQ<-jmGWSl)zr4MdH zw7Wx1#3$_-`=VOC`Y`=sK$4UCk(mXyOeiEYKpH3ZH1kw4aA=Xr1quwgg9HN{O#BpL zJdjg$8M{DG%{kkRf8m;Tlm$9@)m52kJdoQHx8kWB56ay7>L4?W*mI^R5$ea?@KvzL zjlvAp!*HbWQF4yt9-)|SOes@UIwJQr z?=7(|SG1j-VPa_a-{|G^zz2pfDEECX21;+Wo|^f8{^6Q@NG!mi7nE{bNCiU4R9ZQ< zvAFkXhi?cP>INsH-kn&8XMrC(iR=__a_X=h0a1^u29j3nZ7OMmY!8ZoJ=_H*Y*CJusa?9SAa;OI$)T)(tqTRSqke2RI+S{y`fChEr&oGr|6lvT9hXwy3M(7`?3*=S3Z zCkxhAH)rwY|?xsj3=)-n$4<8~+Az?$d>_&a#vsG&G$aH2DU6NTJxTb=ZQzlpIgOV z9qcKmQ;FncKn{Ft6lOA%EQgB@<=?CV>JD3>jR`TF{58Z5>e%W~5FUH8#``xVmQ~8k zCaq+LEbBE>x5L4Hj^S!wF-z)gbsu=)cW>nTaZnEaDISUTs6pGge63zHgCoG*GNV23 z>$;?rtEX7LjF8j73d+^gWh_O8N#)6N=Eh(029cY!Cgw3>`*cz23dYKnf~j+TJ4PB; zM!S+Xi<#Fbai%Ic-L*(JIpTtUd5@!8+856fYJm!G%(@^r_-lZdu3QU`DuhY1t&NPN62f-^th_X zr1;?=vUW<`-FS7j81La-^Vy&d4(Q5&>b>hQn(z3o(tS8Vbk&*5#^1kn?<*g}6QvSJ zKEo3&DRd)U#)LHO(;ZlqJH_i9&|n13Vkh6btGu`%_mrH9n^?sLjR9vb)yvFv_qSSZ zaJN1!%@4h+%i z!{B_fmNfI-%<-v_t;kFdukhV<^j@u*+n#2y+KIV%?4CgcXy?`LVlM4rR1`9yhd_Z{ z<|?Q5jnR4rdkX?#)IE3t?J-i&J?y=(rL3E7#)0P*ft+-54*Zr*(jhx-zZA1e zDVh|o3QrVqUJWo5X&DUh^` zHLKSUhGU-sc7rUZ+)GBzldvq+`_daC0k^N;N`Q;0b*bD6^2Q=G?;E{N1(yc9W7 ziZS^EQj!p_a;NY~IE^|6E-ywII(b`AL++D{R6y0Z6hpkTj|sm?260PSwJh_F@1SoV38Ck46n;#ZS8bYOdbPZ<_c6(<~4Lk2(hSx{nC=%E3J>2@-vMn9K0$9!6bq#f*|%#j0sp_NV(v2)6n3<%Yhe zw5ZdO4E~7PZ`*H-M~V5c<9MZCUoeyz&H(3(qWCE71=|*jo3m7_wdaevTo6 z02=I*i~odgYRzwx%zs5E(ww&_rtMj4O|_|VbGXiWZl5HKBtkQLdIK39^Ey}iTFCTy zmIkl*g|hcNR!0lP;(Cdxp|;Rp3{1MgW$&EBkS_9#C7uyWLv^B7*{pU81dZI<1>9x^ zKDm2iNM4Okq>pQX1X5f7gv4 zS1;UaWP6%*_;6CCPQ2OB&~fGg_%D2MXmAbQ^!;{jG;Qk@(Es!lNl9)PE#}&&qb?t5 zKLw_qkW;7{7`Bz-xfkz76miAk%rf8^IMQ|w(TN82l;(>3ItxwEM9?vh|pYAZPsxo7faAX?Kk7D z4a)f)xen1Cu5+P$Om?Y8bPC2a70A|M?iN4(z}<)V^Jk*oy*u-IvgujS@NnydwsV`i zvw(X_ng(JnWjhL?@jV8Wg^k*sz3lx~Z&kKid5i91@+3rEGU&$HBl?ft?cpVS<0de1 z#tw!i=~HvO-V3QdlN+hVg&a!$7R(8mBfaA?=11}bnjWpWY_|)W6cmxvn!zmdmjVhJIM2CiTB!alWp1D z>!hTrtJpk*G-KtBl%G<3eS3#2iN@~ zCy?1<78%sqQ`I*^I%HX$;@Ih{96o0SM2_QtgVe>Z#BkBe4K);)xfUWCx{6%Fe(3o+ zNp?bd(`Eu0q^Iu6DWs~Gtjkf&o{qx%{nJVLtRikm$B5G%Ju&fwusjXGOk%ubBgt&^Pmkv^h(P1tZlt2Lh`-tC$C3_t@9by5^mWUdB_e zUEIRk?s-e&ugfMD2ZY=H@+LAAsCLyvtRf*^f#@%C*$+~_Vv6a~9M43FqU`NT*D+Or z-kVc{TU!}0>=>vo0Kej!eJMax)>X>4=r z30)^kFZY>iIAT3K49A$1`L|@q&wQ7GEknMsy`s%(;!wjHuWo_RYG{;OMB-h1y3}X3q82nbyY}q#wLOnsmy1y9iC~=i`iyKC7hdz>`v3 zAjlQ_oTG*>;TK&A#T@nX^QIag2Y&f2t@!+k7rW!A+apYyq3_ko2i&6K;l)+F^DFsc zc=7Iq4^0u|tj0ym7G_{Qqu_>$LWxei@L+e3?97B(W~5ZP=54+5{N%Z6anWUuytU+L zY-b0wQmgW!-Sbyc8R<%}E#&wXQOTn!y+hx+(QajZTzt5+W;m3&@;t}djGgAlHg$YAogLh9`rvy?zlSsVd;- zCl~VL`}v)U=~{7+`x0ILZcSuj5%-F$5hPt1-9!faE1y$w>0iPEwEE3}uS;k#682FE zdg%dnW)pl9JL8!zYcOA(2WTh;ds8;gI$MHkt?iyCp&;&!?MPYm1FRE>Wf9YX{#9vb z_m=^?8fT%cykw@6ON+-xD35FO<_g+@T8qTAu+jeB~Var!y z3A%dsu^d;xa_Qd3PD;FSy0YQkLVjxOxrH}$fosZ72Qu71CnjfwuFduzx~O1Z(Q>SP zIW8`0rA`sE0-==+d5OPFy@3XOLdW*IeqAaE2*&v-oB4H~>I8$5defpQTLsaTUV{)7 zq1o?S?o?GopqqjX5<3Ehz;VHRx8t)IAZ(1`I zr1qpJw)#gFNH^({CE`RwKCwnk#QRmW{aOG*r1cM3uD&8GDy+&FH9Xj@5W%C5!{DYS zGT*isl<+_-mjmxl@3~FE*r{jCca!<$r**o?kRM3DBc2oM*x$lEgdx#+B$Ho zCHiK`-tDC!z#KATA5fo9Fm3tK`GZD@jNB@GulOYpdp;?FZha~*AOAh6;l^=)mZb~L zPf0PQgJ<(_Rcn>cA)B;C7?{e4WBGPdl;}s!#YA?j_lL}qV8qL$#2a)x%bJ`8KHH@l|I339gHM>k}x@>!eAR-`nk?pJ+le5&Jj!y zoReWRMFsq^@>nsQu_Ilr1GCya2qdKPWl2Dy@=@SDC2qcD6?gKeJ^$&`VJ^ShWj1S@ z>)w^G8yr8xk+PbilOA4c3$x0J>`G}$6UrfA-YjmIRPy_lkPGdkzRk79m~l)U1E-S8 z1MruI_$v96-`SQY8Y%ac^*?Kp(`z$9hv(}u`8-#GQP=kvT53c3Nq)wr5JbU5nflv} z5R<7K$*1By!dV50=EX@e-Co^$x7!Lq&OK?muE*V$%T>U-wM1yG9TaGch>N7mBqhK( z0kbB?L{cupsp6b6k?>$t%M$k1CuCDAwgMDD3zLRFtn+LL40Jd|%+o?#9y|6$TVlkl zq1uw_Kg8tCQPA)JkQb_)5`F;fir&|ROn+!XZIztcsUb>mq`#Lu2sGPkXCUFRgyti0 zN4$g}wN232U zg#q^vvht|~>?S3&-(5Z+?REg;b0WS1lmqxXske#r-YVl**2Mq^fWQvzrO{z%YlA#0 zoJ{{^c;MA@Ah${|A#{36VuKa5LdexSN6W^K(gVB0SU7K% zTwm%M(v=`N+LhDXX}dkz**#dEi}FX-J&D#7w}?-+VbF_;DzLmoZ6U1dRTd@6d3l?k z$xbx@t1xR?&MPv{eL{^n#wnZkxpw4<7zFcgxq8;ii>0EvLl^EAoJ{EBOFbfziw$g` z1b*tND@fbZmE;w{baH&sSX(+i?OcLNCBL;jZeIF0#D_&Wa>#AW>A0e8c8<|u zD4SV!hV!_T$O1PwAw-fo%A)LLA}n#<1|KfjA?UMb-DIbeEBMK&rpUyo|tlu zVfXdM_AsffOfBu2hAE&QNiD%;Zcljfa;tEF`ZJt-O&2Az`h{Q^jwGapm zN!r)e(!mvxbh5ku!i}>C)DL_54Km z=1uXSw?R(M%DV+F^a)4TUB(L&B)(oHfNg8AxJ~TqG-#kNx{WG3PFU=q2z~>zt>F>c zsV*6oQ!*?5O%`9Nen4myS2+2E8si*x$=z`!Di^V_dwXANk1JKnX)an1LSjt zl<}K>j6}JTv8}D;%&7;;CvLklwxBzxCD@DmCqzzfO3;oy>5haxcWyydsNdhWK)dl| z<5Xzjlu?CemhV%^nArQRO}Dvh(z)#U85wSU7wfqlo1uKm@V$WY2 zGUOd{ervqsun~6h={!Uel-th)@c*v;I8Mfaou*OOj~=6o{G2KyT(Nh}_A4nAwkhdr zi`JyvwnXk6Ex$C9I0zftRHtqJ9dGiqchK(UMfGF);Yu2(r^Oh>__Fyd8-MK{&Pn7S z-YJkiBHmf0Bp%&&~el`20aL{AU^eV?F*6 zkpEbaf6m!o)Z_o#>gu%ys@?xC1pn?>NCRB;!kByAsq%cxk+9=JIens`z?t|8f z+*mxa^yYemsG~FZQK2ax%-#3o^`Kez?}$pt*YvKG!I)>|+%iw{x&%!Hbqf77dn%<) z6tG7`XQ_Uj-(Y2iN@Fk>J{?6$9U81n&keIR{+!2l&gfmd5Q32-HX?#>80XQ~c;lKK z&CoyX%_g;GCr_?#+(SFAXmWYP{bj_F+F`7%HXHAJ)~jFHg7CJyt1Ae@`v)#Gs1XFl z1*nF8fLLi&H6o@+VjF#}0+h=oXs&O7|B5QjW22OyEkY7LBRi}#Y^Ks@kCBLpb+m}z z{8ZFVbN`mOlw0FDvpCGsc7=y5RW~Zyn*36nRatV34di4RGJuh3 ze@j%znytrfe7W5XmaDD5BWY!7=3F3|XyYIhVPH&fhj%Gei$+b@`hBk}4;L?mW?E-v z-9;GZ8DR_O>*UJTgs%k`g`iAT8f=RdrO(h?a8jE<_g=37?VAAJ=-!k6$Ts>=%WcjsQh7 z>&}_t12h2zFo&S|-iR3F044?3-`gV&(xh*ixRgB^LK&DIm*bsx+ETtM%Zmi4$9%9nM48~DF2wv&n(Kt$G29} z&CGH$2qTs^96UCCE6?A_I_ynvrxO*Endw@|lypOi+NX7N0O~@6`eo@mpBP?Rmt+1g z-_X+5(((Oxe<4qejLb@mfAo5x^_{dXiJ%v$si_{#Vd~WnTCo%AZjr?a62_xr3UjgN zle;)Z`B1_#rWPiGrKQ(@b9E1R<#+oIZcPPhHt0OU>*BJ?fAN}nr`iIAS78T=v`@G)Ps5;(`g9S=G8icnyq(GQP_xaexL+;8*xHYcctsuq9F z2yI??@(aP{BkGpZgYhCNOI)uPJhaiW`syrBC^Vf?slAn-hf~Zm(1*vJv{JkOA$(C$DfS|i6@1;@_z<$$D~MvcYzV?_ z^zRe3f2|s-sW?s+E^+&k@TB9KagT*>K*Pz)%f_FL1HDTx3uKec#eICe-=80jPfim! zk?#!|uO+1NN1LOJBg-`Ac`(O}9O7iQw8Etzpv%<&zWXLJl7Pr8P8$NM!Pr20D7jok zV0dp{&%|UpY>s8 zbw_?>SYfv-3u5T#dAAB-LY*tG_$2C-he{#&N*L1y*G#WYk#Tk$lD*~AH%~CAJH!Si zxP`s8#hSW`pQ4*0KZe>qNLnqau&+5kE(5i%I(Hfs3yq%n(K63Ir%8Qfd6!^hY#J}i zogs>2FI_EM-$q@y_z5IHs4N}|1FyT6KJFHEsGjgC?!g<&ww-$mmZfBg{MCj2KkDGG zdjupmnlA09A;K!ou^J}`$QiSGY~Bgq$yOBma`@Qulg)Jb-;xPzT{%t4Cv9s70VPDrzJeRY zKrCW=cbNGE0*pRaRdJc{&cD*T%y+$8-);1hpRI61vMZsMo^`S>O2TZFJld=IYHPf% zo!lvjVp8{4Fb1my&sm%Fc!R!b8OY;r2>k(8(QEAR^KVDc1}MqNv957p#M4(dT~!9w zRXcTFOp**9nS3pO(Ytvg{<^C)iWG5`YfWiWS+0jWZnig)YiIxy>hr)9!0CEl%l(i6 z6&gM{;;N??WxW-YF&XcrK;i-gvBE`5oy9tw>}E7I?s=OhJCarp-7tL2P=727qky-1YWh4xr*PEDS=BeaNdcg4PB zb~lk(HGUd1eW#m8#^sBkgADqk=p|=2$7)xcGIM6arE>mw)1bF(nsG9hO&7Ge-3&vA zo%|fqG4XW84r({MpVoq>W?ZSq5!~9?e1=GFbKyVG7sz;_MD_FQd@OA;O=dY%=P5|L zeg;YNg*~bM%{;G&0>i}=39o`p>no29Gu&1=^7E=jSyk%VPyGO^aOYyLru3}M+8n%! zG3WPenz>1BmQc%VQgE4WhpcaVhAgr+Rmf>|BF1-`U50TVEnkX`O`|)EuNGMk7ooN`Tu8U)Ku3CVIhQ zK#d!mk>7>*Cob<}4d;bF4N>wkSofnGD{~h64xPy00FIao4Dl#C%^&Uyw|e)XhafNi zOpP!{IN+J`N1Gs=C4vkAqiobEVb({0t*mKgQxSDUDl z!+p<`8YU4DbX{8F)N?GSqhm5J_d44FP2d6WfPWkJ`t8_P*363K)lyO4Ry5y)hsI_vfOXvqI**NTx?_4dfgYSUZIZpY zeh5}GrN~EeN_{zX*{s$*=ep?ou!=`Q;AuwY0iSQe+$&D>o+>Flz$S~lzlq)vh4@Z@}$5L7}8Kd4<3dzA7i>;tg$XC z5!6s8=e2G>%yDjk;XP}#8aw=)5dFN9kPIq!RKlS00RZ7?tUG?4d0Iu+uJWJG#-Fz4 zub&=@yn}Pi+C%lj4H8l_>ega+TV}GFD{2gJRVvd*D0SV>jvS-=^o(%LC39l&a~6|z3?Nw+LHb;rpPv}@ULsf#_RN$`Wl>7hZXgs#xoV?>756VK!h zYAa9w0`$nDYj%0^?w6P9AIi)7h2KL36~lY~mXPwlYt!%C;gSBWuut&7(dZwmxq0_l zSb)-#+24-&ig&tkx`rlXlZyvl4 zut?)l!|FUojsWgyXSZSZ`-FsqUn;mbwU)E3{=DmG#$Bq$%ayi4vw(IO@4yd4V#fYmdAWiJYmSMF#w^z}4kZ zoj5_vTIT^uj}6qgbY&cNr>?HPN{q-@P-+Ef}sgD);U-pmsfzK2(2=%{ihE z_7uf#C?*niD?Perd^#4#6#4kJIU!LjpUdk>7*I@J#HV6{Z4Tm_dlQjFC?Ycjvj$ zuL{jw>{|!kh~vvxH?~1E9c}%g;7S)z_zu{()c?h4cB!$tJLTl=q%}pJZbKENE1{X- z59^B?6*eu$Jqq&FNEI?y4#m&Zj*#r{R>Z?b!I9|{q<|eU0eS==V7FZB{x!g zfv*{*(-}ISJ+A2he&_O|KG7;G(aG2y2@tY|4WnZY8nW;_dv+8Uv@Can?X|x0`ui=D zuX{Vidy;2DVE4l0WE5`e>P2Pdkv)qSqLr03Jf@`0A{6Tkr6;KVcU{5(>jofv{*uA> z9Iklid)Mt&l$p4Wcz=G_sE_fAaxFsjX!<;YA8@GL{sw$AxBPsZWL|3-u}eTVzEffc zMe{05#Ykz;E~)IS8Cv^JeseAF=Q9HNFZ&oYP)*}H2{20dxZhYf7ZFLVJdsuRq@lon z6<=mKDV!fTpB8IC;jY7r=C8dh%_x*NLGSr*=1gtat23nqe*YD>>&CrTv{*T2(8v?w zQ-8@sMnR}Q51t&o>Ug`Hxa9pRF?wRH$4kuAhNP0-HH6Iey%RLgcMu_&RHLXkBq@2sEbEsnbY%r(dq6bWg!Sfag zh>XxrFApx(Tt}2blQ!%>;u*8G5_uw3NX(Iz#btf(KY|)zyKs+d+P>c)8)Nxa-xwe}6J>7rEQiFkGo(N0N+OZ@^V0YsOVd*5Wgm zYL++KdIVH9lzS<*ZfqPQET~_$21dAY8i%^V9^oEEpL~bw9yG!9WuT-?r)meKk&-t zgPe%Myd3{MA$m$aWL z_us!w#j{x72ETVIv2^-EEp;*yVtw|sXTw(QT?NP%7AJe<`pr?TY(!C!C@6>5B+l{3 zBlQc6(GBNg`$k4sdy(VpLrD1^I7_O+f1$JdsRsXL%rq@;94Dnhgnai~74hp_bUG4O zXHDs8FnBi_VGc1rB4b|N-f5I`9KKr(bVF65WMQmDO)I@K6n}6}#DBy1W6?gH>M8{8 zGP|jpy@7*nqx>)!Syk!$hIhrr%k$2@b!!Ds9EAx{1eXBkIZbu!=WVKqdl3A!kuBZ% z8FkvC#Y!o6(Yl}HDC#IfYel@?9*n<#x^z~5VLC#OI-xnh|RUYzTq;fbh3!i@ zYcJ8`(~}LrK1%WoP8k@WujBj?!*W4 ziap-|3k#zzdm8JKWDJdUe1H_!U8q^XikxqODKo^>gpbfIGST@}|K0nzCsJNj>2hXT zeT)Zo314Vi-VkZY_sKz~KMW)y8r#kBw2H9gN~5%Z$8Ws)*AQ67p>73eMc*j8yCd!) zUrW&ja`XzfL?=ey*r7DGP=`Lx$6b7ULlFi!yDp5JMONO{qMf4 zu2fKx`iTaZ@s-m#@tbvN${Mq3=kLvTjJh_xY;YaFd9>D;Wx*tXT<~J_(Lo2)0lp^a z$kD4Mc#>%`pp?D{*euVjN5v&wy~6)Pn4ffuzqV#uc4*haciwAhWu+nlM*3RZv0Z9o zK&>s$LHRVUs~R@kT|E~m_%1SVnyUk-SrAkDAy(u%=rX=AI+Pk*EnXKJDH=QDHxa)C zkn~(|Z;ykmH=0W~avBvy>~QQA?f3;ZsFRFqWl?Vy)Jw0rl>e?f;5e08u4(2M$j~^H zyvOUzBt#s?*2a3R1ezJwC;OO|feq;=Xe}5&scD zV&*PE*5k)DibZ_RbIL z>t*z-@~OoZ+i#c}4SV4D7|;+V$leEy}r0apVC@KO=9T0=LGA{yxX2IXy?0Kf#nx1p|r|YkNV}9 z%K8$oX(Bq7KVjVs1|7Z%SPbs>_&q$Yg_I~qaQ}$iiNI4{OEX8+^Q@u9?+Zf1y^aIp z(758h11#38sv-+Aua!kgqSefg^fD*f`;x9#Y6rHn7YmM*9(DNcR%9Ut+ePV>(WW0e zb&S|XvpS=ZFE+yQyN?2Qe^_zp?U0yYm*D=>`DK1%+HyWc`fO~;*8PL28=)ZDNQT8_ z#zB|itDxOwAJrE#o(|)UD%6kAjVa`4VZMsuo{nGhvF{E)lIZ6B+<2Xo%{2}rgfw<$GzNS$ZFoTc zWuC*a(F8Visa&|in5)|Jj>TtGB-OQ1_zER?O1^wAFxCvbZ7%NC!C;Wur=wF6Q7L?B z>q}I}*N}y~+6Hwi%8RTwma8A05U@y-S6gP$jwY73npQSGP#l*ZEW30v&Akc0rL|H; zM_{ts5{n<4^8PMj`A?(GbItzvKIabMN!q7d{lZ|A73KcpojU7@pyT_VaiUvX0kLPA zWjk*YP6cAI-?k=+-nDyGTc}nM1^+1?k(c_Goi)0w4sq=Pfg|$0!h!Eci&7L+%li7~ z68W5d8;vYpZ%deNNiI&}J+Gc`-G=;%%zF@gP+#=Mz{$CsHd!`3eJk|$z(2AV$5w_U z<_$jQrG%*+X80uUyZCx{NG9;^1CZn-k3_Z$gwYuEp2MQRn6A32=q8DyV1@8OIW?Y6 zSlQ{wsgVPV<&6(p0DFr#SpqWs_AHN^wp$v=c{Fva@rIpU4v@0@L_6{2m!G)U(2>#x z*C4us|Hto%cphh9=o&qHR=dL5p8ZABj4KCrQGgo(X_yv~?@(Vzlpwj+Sx%1!#pdwEWC8x)nx zx7FhAdZ=YfrWVCoja7ASfRu#=w${8c;5tG_PFnn>tPH4uTV7!-w^{i&Z0+O)#;P6C zU?}dulrg}^7dUcMtVE1o&2};{r>kaGo7F&CEi+$(vB;QDj5e*{k-ah8BBxO3v;MYZ zmAdy8AAcJ+M0P1IFV~Z%67U|w0}h{*`B=Yh%#p>Q_mp?r8r$908V$8q#%H!nKmvi@ zV_Fk$`$HI`(GB&FNWzf9vD&9BW@mxE4E3^wEUwlErUn4E>n88MyB!`L8jT3t)=LEA zDRV?PO=V~1dvymI4@Fc+Np(;gcV1*#+ZfCAI5h0lp&%g8)nqssF~F&Cm9wyL5C@)u z-u-@-x$r~F(=ew_t^bF!?+j>a+qOQ^EP$w}NJnYXJ4hE5m5w02D!n7U1w?EJg46(^ zi4+w=3oSqbsB}W46G8|`2@r~u(95^+-22{r=iPH3e*X+6**j~kJ2{S z_#o9&a;Q)`2ZYqlVkjU5y=ZPezJV571mRKrp;-tnclN!KnN?{J;R?;Djc8dr{BIfcuq<`>a777`M;1z1p&faQ-^f z+#_jKfR5@5Le6L~{)Sr`e`wt(*bIqJ5(2nT@AkI)*d@Vjwb4z?ZemqD1#7UJrNPAJz~sR5P(^lKChjBpY zo_Y0_jdOdlrL&_o)J@2MrK3{ull(iNV_f_Y#h%o&#$amaz#rT0u~c`@ng47+|6F@hzuDL$i_3F% z0U|H9;yO01MD)p27BQ&vZ?{s~5u<7J$-sR`~>Wx$7A(3KItx% z`AG)Gj>%(+JiPS_tCqIv^No}q&fO${bxdI%W6&ayuHW`odI*0 zu)ERL#t0)Hr1tZgM^&2&W32*&jSaF0L>w} z&W`A7Y8HLt=AwWSN^XVd#cLXeWK`OgW#GPB;qE8X<^Fj%qjCk@j$0Q620V?QyCND6 z0*<_zyV@sTqpF6j`lE;X68k=bnz{z~fj6N&8aw8c;cZ${;$9LX#HV=sY*EPCW3c;) zNWVuT@2Q(MwV`*%EfI5i=hot_f1GiAEDU^ zf!xSfKK`Rqvra*!X^o$FOU$z!bd7u)3r>bPOl`6wtXc#EAR$XhLv||;4IFjJ^^b9^ z1*B_F1eW*4Vr@sD)oOkdr_?kxU@e<5CHt}cY)V{(hg-o$;N@)$r}$)yJU6FDjbk$6 zS!BXgG5g-Pj+-?L1ZCd=YTtX{iA9AlOF!h^V~WK= z8`^q7J1=m>!m`2T9l{#{TJ0TGWT)whOs}FG<`ue0GuAlzunYUEl@@2#SWlsoM1nhx zpEHj4UOGA=fb(1J-E0sV3OmrygS$mNWLFqpE4~3Gto^XOtaJ!1k$0Y$W)Eq7pjfBZ z-ql%z%P2t#R=(n2cpPhldc1|g=qq$w19|dw4b83nsIbgMIUS#FLLm$auc?SGdgYVG z8b2mHC^4A~L{MB4GlpP1_`j{;uxXZ^!$TQZvd26*mCJyjr2y-?`9 zuBk-uVLABmQ}3}UnY0)O`7T-a>)#nW1+2@BbFT**k;W%&3Vwqt@hhQNj|0+T5QUb^ zZy4^Q3!djFMMqxnJ^uMT!Zc<%0i0mxRGRy_D}w(@USQKPDM3un!^J<<=eD|eUOjdw zL~Z~i%;77t{E*v#Pio4x1F*eoER!>~9YcBUbJU=%-KmP<0v?$!y@+T4D`=npZ zx&K)E`xhD{(0h4K`P@lX*DlIHc)8=!!>j?eF#FWP`s-mIHLq9*0yB={&#{X@-Q4u_ zjYP1UY{?R&x5YsdW04yxFM^R33^=l-<`PO4O!L z^siY>EbIz)*!u38D)uCL$~Lbv2K35~xO8TjfAENyVAql>CZ3=B{WAe};l;7H$ov@@ z*w#7=lx1lKHvz;-%INA4&W5Gr_X?*Tq8OcIhxqy0)x?3`2l#=T#B`F3Ogq4<#ucA- zP^pL&N%zi!h6{*(qu2nN!1;I2veI1vW_c2lFEO+MttCJSU_ts3YDUIk-k9+bnKLM3 z6FFY*^r5%Hz%X6MI!oqk>v3-rT+x_q`KEf+7RsXFbqkm?ekbDfx-V`JaeTHQCXvnXvZ=UXHarFe`VCLd%W4I3gNkCSSv( z&W#n^_?uSK|LqpW<1f|ZN^Fp(ik&VuIF99jLXZnBF-%9~zRr%f@~GZcPGG2&L7J(l z-7AyTnjgjn)f!81-?&SA2L4qKc#>liY3PRs+>@W;;D@gbiWwYBhoU1t*loRY6*#94 z6PS-LMv62Vb4ZR*sQR{z zy;091eKEPRI7v+~uN0z1nwC8(a4a26iNBR9V^i8%&73%Pk%xO;!Kao?)z-vVbCt&o2DIPc{Fg=v*qG%Q}sl;6BFPJ{5 zW&I&^E1-pC5jwq#iDo zW6}Ve^CjttQDy67#VUz;rUVoMkOfv2i)XkBl|Y8;8r5Pv@3bX; z2w5Nu(zG@daqFQBK(E1w8PX48kl&OBD@cyAKQL=>%QDD6WKlK{Fp%;so&@SbnwOg- z1D|YD>2Kbuf`eYgMA`2^00YOO_<%EwPfUp0#0w^F3q73;ix`sB&!*~!duS8kR`ibe zxh=_AZE*bH(;h%*0ZQZF*8+|-%_@?m{}@%8-beFtCzRU3P>r^zU-R{d^zjJ_5vt?y zGz)#vo*Nf3gE*xc0bE1ls8j)H26a{`USiwyPd$CEkE3*Ia|119s{;@3bO48ABBf$e zybb75a!&)HP@O#*6pwDTx07LOh%6N>S-CXrwWWek^2wg)u&ZPXtk>(p^U> zHsX72WyCpIR+}m;TJ@h=0HP{sOkB*@$HfwnHa0fg-GWI)Zm?q)B}9!WsI;uJ#&l`- z&Q#?V*hk9ibqTY!pwtbSHrjV%x-nx5EA&+}8lTUXMRiw? zQutC~C4a(xHC+^FC=f4nZ{PMVvI{}?%hU?{14*e$cE-7u0u(TeDP_EcT4TuB6YrE@ zQYs=XAMrTIxg6*Qe4f`yBf)^{mX>^#r^s~dV!SoSDVooX$i&<`z)>op#7epfN)MX+ zG%l^W1&EDKQrih5=biHrFY}`g+9MgBp?yqny3)|B)VPtw3vLZxDJVuThYRE4^4?a} z)MNaPk}8gBGZ@cuC!X3j9}EV;R9CbmHBReDVOXW2vVyGvwZRd>4@wD54yWXSeQ%8+ zQ_Klm>xGW3A6kBO+Y-Df*2o}PH*wu=KyuGCXBfoyXtOF~0m3Ig;;cPnMgrK(UoHb( zI!TX!zNGW}ZnY0rzF-oH`T(ZCn1({I@b<#8T*tAYjMs+Y!;Y-RHLX&kwg!E6w{+6w zSWD+@>Bp0jQh6#tiPK$dnia8VHU|$&Uq;(atyq`&`P-KK8@6s4fY3%TOuW||<;0QC zl=)e9lD{tx=nq(he|Yn%t|Mge$nL;R=Iqt19hyLX%uMyw)j7R#uFF5O9({LGtg~1i z(ru9e;-OA^(hRN~kRfx~9^Z!Xq}?3IY#8u8?ScdJ{dvFp?YKrvXLyufdFdkXCzBvH zyWO)=>+dzr{^}<06y8LC0Ok1JwKu1>*Aj0s-;TFWrqFsFoA9jOJXHo%pCsh2(=0aB z#(;wGf4Fx!pMT)nf8yHrYGr$IhWRMkvV&#@}r8>@ouFwGd54 zGv{O@tx~d$?#in6vd^Xh#ySzD3yZF|6QWtAZskL!)usN>jdQPd{ z@{X1es~2IXVtpk3}RnuhR>gvzV_A2#PIlOWJtzrLK#A`WrDD7c~w1<135BU*I^ zOnrIZ3dKPfP;tie6odUC+WavVG?)9PgRLye)GE5C?HRF09SMTIwG_pc9v)EW;-I&< zM+-%d-E%1oz))bG9H|)D*Xt57qzHr@W0pO0x`bPLb)j~Qy(hnE!BjE`y%{V>sFDslf;3(?8KP1 zgb4fg$IB=X=8>Yzr}}R@z58MH6}L>WBfdi;>QRz?kHHJdA$!mZc#+%X5>R?Mqy5Ln zUU=(l>Tgq*F8j}Mnbztl%=GFS@-v>K3ru|S^rjAlWPqN;jN*O=zms(8#;~wkINzkY zo$aGcg$CCQ$IA_wu3}PlvTC9Xn<{%t+<1HVHNc&Zb= z=VO7F+AxU5xkqO^zHg>;{WCMvRX1nU3~Uh+lhJwwq%< z3h?tTsf#uSQYu9K6UWa6Ie^aln>8BI=TA_CoSF4%`%?qp;<1OMit186J~`jWD0A!p z`j-U(=lsFAE8JYiWaoV(yZIaL|5q{E9B6K6diyc__y7uU)tgzsNX#LJexKjq4j+BY z6G-u&F+U(b?xOyBU$_$e>t$HxtYhP;Dck%`l2KQD)y7#_gW$V^Za4v`+akIB9?iwy z_{uA7zhYw*%VNPa`2hkba4*$Psk74a%k1M+r6&~+vy=o%iifAFEj@JZ>~77^Gf%e@ zz#7_yF9kD|Ez&wQjuIq9hV;qoQnsFdX;v^w5U+jbU5><0+!`Qz}DQ7g)l-N#Ju{`7FqO~&Ib z-}p}~s2#x}i>$1Af#rr(OG?*;2qU%UyVE7_o;Au$lpty0x>d_LMp_HJm%k=Oa;j-z z|NQ*1F4p+;{L%W3jL6x3-Pk|skpQ~!Z2{;6?mBPW&Kxf!B4j1vmftmYcZx3QJ=|n< zA->Y@!8fZ9K)Uk>kSE6{BrREf`&-qkf46csDJX^8^8#GZ{hecPYyb_9?vBBN6QM5B z%W>=dwv0xG3qPXYzKvs~zXz#JGPD?qJhcoQr_lndCkwe;Jl{HVS1+iEmi&us*D-!> z+@+&m84|||w6jxgPt$ci$zU@xv;I5VPje<*CF$9bHs7VT0)ImoT8qi-F(nBug2HC`D>AhfbY8KNb@&VjirpGB zuC@SN$8N%FibKtTB2T1G~>6Co5pnchsTJ2C&=6F+_k^CvVtvDge>4jbNNrD$Xw-88t! z5SM&iqsp$cTnHNp9X6TKivOI;W1yC=w``aHgW$D#FMoB$z;cW$H8Dehz|Ht}Q9-uK z6*I!uk$dMsZp||FeKL;MAS))Ue8cTAdztT@PT9a?EXqeR%#*3T&%wzuXMc-$PjvD%aC5~&bZG}*_|dJwMi zZ07=etKx4*xcnPDh2m7Mtu_AOp0t{cH$<}9_YK;W)y%~5T3vOq#EIa2uWfr!*#7ml8yEW*xAkACmy;2el{cg|GAm0Z7me5Aw$!FM(S`&%A4{{B;FK!T&#mESD9 zP;9Kw{MJ5myt_ljhq$yHZUF(tKI^}{cQ<#!5|R%0xlKg{O4za>YSlhT<2_mv8PtU5 z-%hOg=dt zC?@?-RQe4*4{!of6vLyR<-~h9Z*s<8l00K&@@dM)hbjXiU`0jsDi;dF+RrmHGbb#K zh0ZO_fhz?rcMcENNLJ&rz`!-p*h;IRpcOXuzn_!$ufOIWzi>?@TT~w9l!i!yg{=DDtlHn(0Iu+x z_7|eBF5MnJY~yiqbx)Y7(q2iW0VPdymj@H&=H`gtP8s6ejzQ z7~*(GLB_{8EHky%o?KZOpOC=KI5#JLV+uHu(z3zZ`80H~TNn%Cw#Vzf@L$JJQZViJ z3=vs)#b;HGR$mtK$v~B_b0#JNsxtG>x>x>w63Sm3&99f?56^$()AUYL*Ds-kd4Fa$ z<^xWb<;YFzO10LxglqLxX6A21k-)m8964mJ3)+AAw*TX$XfVLRMrA50qT_FV4cpP_-655r^_C-vRsKW5 za1uk4q@-p5rBxF?F(?BcjOh74zWSfRSvXm0xRu^?z@H9EYRbnl zWcz#WT4AzKqEuy6Ud?r@mX?;hf)cBk%o=N@lRN_%)x5pExht^;_r>Mqa_6!5v*>YC zkvr5{4V%H@kDs+AhY-zFjLbq8hjPO@FPpgU=)4rK2!YcSnelQj*K<1EzTGa$)iTb} zSS!o%;JAmDgO5T_ycC@PTASr_awo1MU+#_8hQ27WMBb0df?s=!T8vLhy=rhNPh3ff zt^6QFQMU9^1dWI>huz_Z$NAIdL{(R=c9F9}W;_;IThA{dHZyI{4TGl&NuB{k*xS@G z$F-O7W*m9syE#1!3^ScA1D#VDbln{m^!){PK0XiKWu))b`HopEhyFO-?(7_2FJN@7 z`#xn3 z`@4Dj)K>30!nPUalmc{L78Y`Ufa$B_18$$uPCw?pGem|>tAvav0I#YxYasl z#Mp@jck6)EcVs-0%bt<7ySgRCSC7R%OW@u2eJp!Lh%YWiC(M*;sHki)ZX2B+9Dy&g;hA2r@WgwHC8jxYoN>Ofs8ur3@alpTh#BK471K^{=6L0D$iX^ zWbUlpW{&kb=LWJMObmFN;IH5QA%467Omvx2Pp)*iu~^IA1*gf##u)c9*v)IEmzKGW z01Z-gD)aH%?D%X)Z3hgSGkOQ+MC?a+9n1@TDr_JaKsK||Ug$*pIHR<4J?DiB;`Q5a zo)5@3__eIfx-;Z^Vg4l*p`^li=^NL#323b0U4DV-F^|Kli|MkQ2Ka+2+Sh%5RQCM% zk!>mY(OE&>jQ@aS#a&RW-Qwl#cFttYV?VinulkS&kZP|wi1z?>hWdpq*z~Tz(UGA> z;7C=h^G*gnZly?&IRNatoVo6X7l!hD6hbLPZQCo9CC}iuXw}%SluowKE><>0EArgX zjzIVFNamk-!u0%;ij(5Wv-exQ(Iy|#Au?_#d==_jF>nnb>V9PvG(osv|8WtL9uNrN7su(QiFOe z#va~Bp)U<__&8H?Xj+c)PX73#99dQjAsT;xmbl1UA{>dtfl2!R;C%k|>E)=u8MLM0 zz4_)Jpe&>f{4Ad|}mNu7PW{_1{%UyWNr=-Z4eO}QY0g5VFlB5e(bgUnGl z7MS4e`mTB4k~afK3?R$y3Nou!?}_7#x3A(mYh+G*Q5VKfka9`4*VABTOMaY?q)m>g zRa!Y6rf3MPh;cfu&tBrh@gS0yrv*Klq*@<9!4J*>%-M^~48_03qY&Kd2DauvULX|anUUUZyxb~z^Wq=T-Qv+{RorHi%> zHr=rF%Y!taG$-BG5)(2K%%k&iSfz2R(N@#RDn;Kx!c7EOh7Bo7PKo}8tUH-kr&W-e zq{RDf_^hgZer@3%v0V$~V1daHx{C z#Ik}!nqT=lIr+c(m2;Ly`S6Hb-E;>kXpa3#1!iX+VmdwisZ%c{OMqx7OmeZc&((0( zu1Z%3(;ATvwr!JfE#JWNlClK+99+f zk(pQv-(dCySuh9bij3^E46_UzFD3{N##i(;@HN!4VM*nCd(r0{QmPb?R+Pyy?m3x5>nX8 zbiYbL-5UQ5?xc%PIen_n4R9YrL2)CcL$~Zg*WBOupbFQlqp%fm=a-}PldIifb{?%T zw0bILsdD_==_A9q>2wbF%`KQ+nq{IbniFkHDBM}{=Y04E@LB^d&*)A{yI({J%9Ofi z&=i?)QXj60aPKbRhMA()wtg%z*h`f1JRD&pBGfD>EzkBztuBVmuASiiaV*F{tlt~ril}O-*35_Ru7HxbrMoNJv zYM6@6@;*D$83d~6v}5@Yn_=a>52~tuoo37Hj*s3ecFitSWTnn{clDnZWcnzRoW&lX z9>gB_&HlB3lF#+odi$8QaxkUk{iyh6u|lX;3tKezPoxtoq_O1-opME(kHAaBqbg;@x`FNd?WfuMkaTTqBUiX<^oJLfH7ZlU-I zE|!ogiXu>t^akqXo#X9ADVnzMx@IUii{H`?DrOm!8T6$V%Y9dn1NG2cJB1+!%3*(x zp#7_dczo%8Ea@fOw4t_Kvs|-r3^(IY4APT<-1jBHF>OIoTq{`-GtVZ`DHeTO|;mJ-h>6-NF_~V}7 z<;WB_A8hCx_zrCOEyV4qltcy<<`TP-!ly{UCE0pTt25JlB!xsVXaHgA4iOWq`R?H^ zxWP4o{2o7l?wUka>c#A&u!(0h#a1HZ%+kY3uMCUc4Ki4sg1i?p($aiqgtm!o54ia~ zd#H@+wtZDa8v?(l)l=oeHsZhhAy$2@;c(MZil9TQ?YciQv~aneN%JEkUQT)6NnQ!J zDl>S2xD~Xyguqvy20GOPzEf!=*B{h2V2$_pWM0aChh|CLfbDM++&r_WTAciDiC9Z? zxjoRiXC?zW5U;Jv+mEuUP=Pw*R<_Ytsk0%)IezQew}6OFy6{nTptMwqPQ=c-N%{@C zA<|o4oOvtbuds83o%F9!%YQo5In=pRhV{33c1Qk+DA+T1HC@f>m#J*WbjsKT1bizI zcYg*u%PP-_l_`#o2(B98Xmp1uF2~U`Om*WfhatB8vN1i9_uZS6_w~PhsvC?NzTc>d zTlJr?+E1c2t=oMS<|wggheHSq0D-3CEW_Bi-#;%jd(TWiHqU&QZ5V=vF!8D%djKkA7TQ&%hVIZFOh>giov0 z?1fo}Eutrx%#HGh_B%y~Ei$g2=~Nf|K&uHnpvnH~)|e{yIdXEnwdybdRwe?JJ8>S3XvC?;YwL6pCm;vk|-`3CDe1|=;GlPwcP^;`M zhJyW8mcIsO|IG0Jf~~Z3=<27;NJi&dGTRd#%9QHW+^Daw2yb*wh|w3f!RMNIeKE0C z;QZ>|RLD4c4Jf7!dM~BCId$jUcQ@D8F2hFI>Y2^UnDeh|j$2fJcMAY9^1eTAvEg^J zFVx3vNSZ!@GC*02InSwQPZO=cIX0~e$zN;tJ$Kj88iwAmsVsSh&vJz0J|E}q^BrOH z&a$aAWPI1G#qU)CX%wKw?WB}7Wl_sDfImd$zKzapqe!JXrY-z~Y~pBokjK{~u1nYD zyzZ>iar;tU-kVg;Uz$oeev{(^pgs{rmdFpzIs{jK62CM9uot`^j`KYUrbEE@PI~*i zr1{(G;QurTuVZfpg`~Z|^uGh3|M|YZILFto0Gw<^H<$MJKKBE_s;tsEoa~oQg-ni7 zuM$~xP#GuWV|@G%O}9H+@bMjSk6Y!h8DX&vV^u~1jgelTFOUe+%Ibt)~n24~DI6a6~lH&Ls0uCC@mJ3GLL4eA+hYdzeZ zjQbL_hfmf-G=lHO?rpxwda>A2Y59@rAe*$Nr=-UfrniPx0UuiI_LaMNzSuj+@i>{0 z1r@18@|!4EJRB2N;Joeh6(&Wq^Y+10->CD^^M83X{`Msd&~n5V^+eSNt|tOj%eyR1 zgCqf4(yFT0Ue_f6NCp>m(HvD7R#sMCZ#M7#Xlb(3lhV5;lEm_MxZ)_&=h|+b{%WwQ ziPn}i^^9voa#}` z;#klPO#Qc@C8o$>gbtvZtKlEiP;cqOKU*8h$13y=?Z2F4hosa~NanQ>7xphVohGRA zPMLOFgzYQ4?bKdOC0cG9_6^nF4lEc?-z%L`JlxzXW7*b33uDBM1zy;|d*ge!JyMX$ zU3D`h^qne^jFS#^>`9f#<|+hoUF8$nIbk^&aTQq+m!0o?w_^%id$#S2gg>;vM*d-d z^ha&M$vz?1un(JjCX#x>7A?Aj70Uzeu;Aj*y4|6U5+9|jc)l<#A6m}0I=g#0CN_&O zdC<)k(%gU0^nub(tOPf}zVI2P;*^ z$l#R=cQPyN{dPTeKO~E&$ENC!ClCIKK*`g1kH1CRDVkfDhkD4R2Xu3Gt=~9iExpSL zuz2=Qw$Cn%*j5dq5+~x$JJk&FI0)lP!$Zd2@vBGW!Rm|*h0LxqYtA5_)w9ekvL~(? zd8k?JE0&mkQI?|4qH4}~eIXI38(ESchp)Jn6*8*h$Cqj4IzQ)}dol9iLRY5F9U&$*r^L3dTDf zM(^28$7;Q^ih>^UN*%f?XU$?=Bd#?(SQ8oK{wTJlirqmB61X(PUB@jF+I?!gtt1D1KV zpPe?X&%j-iDTlaLm&NErTyJoBHaD-Si$fmxt|oQIVuW7n%ve+}!gm6_W2BMsJt5ny zFn0#8BcVbt^l!eYGba}oym10*bS{#c>2qp2=#$7qvl$>L$DvV z?Bnj@Np!ebN!fsx$uFdYqG#~yKr?AG`D0*BH#q2T4afiZk4E!z_p_)YG%Lk+LO8vC zj7n{9PtdRS7T%kR=mXn5Fn}u{!U}916r(Gpd%_O*Q3__O|7^V7148=~emY+KY<NIm`DW1AcrP-KeueBT5j?7OTChK&g|6_A;)o5B>JAyX8&(&F(UzUfQr&J??@K}gx z{G-X5o;mY_#J>*miPC7?gU>YbI&oV)1h*WvW5+glfk1D+V*MLZS0AA zU+h`>Wr?+UkV6gIz@o4SN8JfJ_1IyWceU;!m)I*~vgR?VrNtIQKqlzT>RCbZ^nJ3A z;C5y!UduaXY9q=(aeZLA@6G?fAOTNxlN$oqLI? zal&v}jMwU(l+*G;+*N#O`@FwC;DGGbYE8osci0^LYR}Vu_vbo%{LTGWNuebQtjS?% z4Nun>(ReAq^WafJW4(mHSe`9oy^9$1zvH? zWib5{wNlg2NLX1J=cx3&!=$@`3T_aeTeXrtNIo2$8-=VMCN@kQX3A#@?d}YAp4>LN zW&d)GzjknKzLlt7ee$eenv;&koN+iRz98IzK7uHzDIF~mzA z%$Cb_odxb{pRwCiBUEEXVtQPgn&Vk6PUzXBHF&jM-?;ep{va9?4XTAML0%NN@gO1> z*YoJ8GJ0i+0rf`-pZHQ;&}MKqcMDv8@_^~v4+gi8wEYJ#UaHHqYzx$J8Nd$s!~#;J z-yGDj{oeCn7qYujQC9zjT18i}6$=+P$bbWDVls|t0B(K;q^ZDn1ms0$6Vex6oiM(A zTd5t5itFj=$$oH_Hg-50Ihrom6Kwv*{@U)oDt^Og-x$vmh;+%?o5)$-LTf1mbWxCw zZ0`GKFnSS6kUx~cNB^~m4ZZjrrC}t1UlH7%$UlD8SpVa~#z(V=(Bj}&%o2%QHZ#m1 zNRtCRR-od;ILT5FoGgnm{g^JhxEJ)qaj;d+gMf z@hwFgj@2hEVmG&bEEYtN2E{$c%iOG40t&ECJ}Tjs(nE#7q_>;3@EEzv3}8Ig74_8@ zdEEI5@hU*c*w*;PNt-qa?2;czsq0ojD^QA$kBd8haTJ{38-{uLDuVprhisA$Y=Ssx z&uocPGOW&}{e{CT|J<`q6VbeBltzaRCq#bU`qa)suq&WeU>6EdL)4d1jLBr!po|Mr=}EEf z&ASiLet}gEU{^MCHg9XIqDlR)jf+lEElZHkCCSG7IkG+}ByW>y$3@K}WJYqUGAP&Q zhg%LhRmANMl}*>6OrqamDoRw~vhg}5Wy>61bd);|6MSMUEX>0^U{bMjS3E_I7oCar zku{y?mB7jGZ&L#FJg!cP1)N)G3JAJl2|*NN*MRf7L0ni$iaVXUM@H1#3}|gs?^z7S z3oFo<@g0SMfu1Xth6Pipi(xR$Gr<;qFq#4!T|j+&-}TN{ zh0m(qTM*4y!~=B6tI0H=B_rL+Khyn-PP{fUTdF}UL72Yfx=SCOn5`nFl56sp?__L^U;rKGZcX;!Vf`AVL_4Og5-P|ey- zi+j}|LrY;HUva-U)uo4woOc6DvW7gynGOpCu`I$*RUcJ=h6p~&|FvLpp328uu!N(c zdG1C8aznXX%}@{;_iWF+y`$^h3xnx$|J|T}(mXhw0j+_>48?NqK7KwCq4MWc=2CIHeQ-6Hj z2Qk|vdAy%Wv%U$fwyn{#6xnOTCyhzCA1n)Gu)|D-`0u@$hA-hZ(i`Gszq@(M&)P(Gs^9I%n<;SgC=cG#O3Y_~*IYl_->-v#e!?-r;NZ&z(SP0=( z>zZ?qS_wSm7p!%|+_PKhq}9@h2&H}TG|!bK30$4+luDz03N|Mb07>ql)(V;etd>SS z$oKkpqHwzlYb4V~?@N^*PF`|{;A8FU`W*Blwj~{Zg5~M9$aN5Y=0`qyZ6s@{pfHNK z>{c}+W^r=qnBca(9>)!-5zP&S*j=X8ZQITfQ$S!lK6>4Uk!4j4qz_3)gy_F~#cm(N z%+3zJ-P+x@@wRYkJQFH?sub&r4xai(3HPqHGkvyvIh|pj{=w=?X9X> zgeSTY6ut@!UP~0D#nn6+n??$-UHgz`>3b>?ZJLnfvTbFXmC@eA5q4Mkh29t=05JZv z2CijK;D>3sIRs^el&o9GruyStgzc3N-#$7kS))C)7zJcy)k`4M#;5S-=yiq1(p+rR z-bU+vJ%dnX(EWquH{j_tT<}zw%RzbIj(G0XLW6_gZL}*-+pHqR(D$z3L*0W~+L_&i znUXY)2Fsp5R8Th=jeqSh^@5FD>yhiL#-z3S(9|&eEA0OFm`#e)FW%DP*@PqB5@@ri zC@F{8SB0=8?oXLnOVa3$7%K9T`kJ@uFo2hM^l(Ck!xu^zEwgEkCAr0mq-K|R7#`9= z&ZAbOKck+6D|_ttZgVWZsbA-K>J_8&YU19x0Pk(OYv$9(gdff+ho^m!ENDp!`r7(H z)tTc@_ERqQc{J#J;0(Swdc_BxG$2^Z>YSUcG!(R^^Nx}CR(6+bE5xO$1uB8X>izvs z{R;|n-}eZf0==+F$2*Rs%vKfjym3J|E_$+t>ykB$tt8fi!_?dzs#6x@yr(+tJEwzF zGrV}Jg&+EM9~WQbc&B#+vGse2Gg9c?=IFDH*$X15(D#D{0KCJCufXykf4x9FUOJ75 z-uCkUNJ;*NgYpYXymQ3mB|8g_*B{3{TgwT%{z>5H=5$HH{~rd1H$RAYn0jXIFN^aZ zU+S51DCRk|P91xxe|=ku&5`{tqqM03gf?X;tBMMf9I{4*oSdAzy~(1PPEh1?XE*Gl zLqlR+O<^IXi=M}j7j4`-*LM9n|8~B%@162z_6q_6N$D}&+1?>4)=H;CmgW$8CWeCk zOI6R)H>lC9iP6zM@S&0Ot2^e(a4)B~M^tY3fTvE#S57rWR@#&v(tRBr3F7&zO+$qt z85v%)jpfnn1tPe_nBuYAs`TXuvudx%kkv-*wJtghGly8(Z~YFBwtw)$O^41UIRHWD zkL}Tuk5#=-7s>0lt^{~|+o_L^iD^$JReJ4jSoGK&gU8#rKz-4^j@xOTq~%sL@#ANC z@3q!RTWx6F6w>9NR4@*!knMMe_4a6f;jYavI)y7NtL;a0_W*wP)^;Uijh18+t<(1m zQ_K3$dq5qMeiS=3L!On_ZiGYMf!te+cg9rPL^L#&l|7kbXNeD@_YBxj$0ZWjpjKh8 zyvMCWLd<_uvW@vnV#d-&RK@LE0t(+_T*eN!o#+)zOwAn|hze9)b@w6;yR8u1ZmhV2 zprChI5Keh%XnY#wbo`@dSEPO}^fxn$4Pg;(8AYa5q8}F{t!jgJrwcs7L|1%AuTk?l&CK&4D7m$Bg^{d7Hva@~7+_NtO*X{Tl@Xhu@k}3P&;4W+T`QiC; z&SNKNXYdSZ=M5tK`Ho|Up3~o_^M8cQv_jU5Pz^orC`zK?0ZyK+s5(iE2D0$H_U(s* zPvT9+JFnvF$(TY?v%s8MLW>Vheq#J4d2<;ND?GCKeJ4p{f3wKkkdtYBa{^x2i&b{4 zexn$=@fr29;6$ixbRsEog>xg89Y2a#K}Pz0D}C=O+m zW`)CUkgRLD9~5dKNZk>oN**SR`v~rw(XnoqFnjBcFV*`jpy4)HauxsD$7i!Gj!vYl zZFYarFY9p1CLWjy)1cK5tcsNnRN8QTSPPx*Miu6Dxkk!~&Qe3v+96RxS*GdIX79BH zCB$NrH-t@W6r^t4kRnpt~1oa)`&vRXCkz|H=-WL@DtH*dL3bl&FO zK(#npG9|zH-dw(5bcO%b{$Nz$X#inq)uqhBpT|<=nH9uyq$o7@Ii4qto zaF_bLNq3f3BN8L+rL}lXO-F|w&OXGe9J2Tkx8`#Qr;6-NFBT{ep0}*+TGSlqZ09xsMd8cuv{t#&(#@qrZ>MYjmXeKKT<)~=y zEX^;5ZDFqq374ks@Kp6+m9480101}lDT1lTWBJIPv4K7pJS!7R5%!&S!E>)v?cn!I z1LNb3Pi<(E*Q?H+Em9sDV@`wW;1hglQQ>{1tt%l*78X98vjv+T3?SjqN1%`I>o117r2WU5)B0!~H=g_A-_2ogkk3TztT!}u z|CZgnSE_Y|ueZ|&dzDmL(ddVpqe*fQ3sJp{Pv%i1S>MI41F8`{lEwz)-^|tDxA#_m zrD$?~*+I=i{@$n}qc}5kx$v+rILqm>2JICuD8YlH=_EN%ie{KqAbhh`W?<mdk>&v&&wMod^*q9b2l1c~JMI_uQD4my z;6J-_=T1_+cxaS}af=%05(wJQJblm?4H!H7xV1M_vLwjjUEkf|NV|d67k;dgx+tkD z&X1YPLWVS-*CIYqkpfi-qBtbI6dmeY!HGD4XkVccR||v`x|0LmX%8O29JMNIip_F2 zAf|G+>=mPYbn1(nchYM&nCTcwMD`Js_D}n*QCSedlv+PiNt-;X*0-@bXFGcRA<014 zNeJ3_$+(mCYO?b+ywy=yOy)`}^Gt8B4FWT0fJZUikilJ8g!}vU_@}sw5GhH4lg^@H zRz!UBPWNh6*1%Jw8SSR)pc3wa<>B)YGMo!%W>#cSEQF=veCIdKr{@K{0o8#2?DPPw zQEp0rt1r^8?o=g9JKEdz=snpkFtL~_H3QjyZhqr=mPY={r5QXO*;@I`3rM~F_?b$| zC7}06T^C2ebGcY%snI3}CNJ&MIgKT)J- zP^e=O7AiYP==W_b7QQ_LM~gSS*bNVS58IfNSUZZD!UL%p5xSEN+h zR6=M&vhUfckgQSJm%`ZheHoG~C0mHGlYQUDz6>g48DnP#qilmQCdL?M49~~){oeO; zUBCNvf9rZ)ujjwtzs=+`pZ9Vu$9Wv*2`E%kaK(@$zBrO7Xq8Zi%}7j?xAR4nCk_n> zv$n74y6E1bmwiz`R5u?`3GqGyR`v-0RMeoK&WmT58TQMgKQ@l@27l?me=o{<7yuZh zEj%;$-~?R_U9@UNKnU314B9PES#U18!=I=>D<_O|N~fWm@=7ZtHEbt1*Kv$HjSy!P zfA1t8N~H9TFx)$R#{1O6p)*QG&f;PxGBSqdt(a#+@7)mo^zZ@(fBcei>81HjpO{Rj zako9dWHPq@pcmCsSTH5-Z#>X7nQKY>=Hcb|)tvtVVNgPxV%3msY_UGs(Cd6FaD;Zl zR|&#$d*!(Pn`X%b1oxa&RA*nGZIN-0Kl9G#nTn%Y_2;e)@kUcyD{2s)vTFRcTA({8A02li+ zD1l7^FAmYQeVqV>uDh6)bbv?bpL?%pV{gyNmE-)GpFVIZE1USO@UgIc;ZE!&@1Y~{ z0#M@Y*SHtenB}@xDQp5piA~|8EaxY7QuhSD-i3(@+%PibQMi%%{^G zvC~P}A9__9RRTdC;bz!My*g=A+?BVS*xtB=Jjus$vVi$Sff2}(oi}lG(&>Zv7vMN* zd^8)>|Gw+Hd%TW$-9Xe@zR}0Hp>KPyk=KhW6=eEVTCXwb9Y!qCt)^~v78vMSbI0By zb7Fn0i4NHSN(S%8WVBdl`*@R*9j%qkE+5#0&|e*O%mpuz3Oj2v+CpeKK3=+vicHD@ z@lO6b7%C+t(r%u(d74IaU)G6+$KsU|2R*6(3InFMyiQlOW z$>P~LY!97r*eMIg*LqEsMXA3(^|kLYpf`|)ER7GS4OC?eX1(W?WOcNd{rPRdNme{b zErRa*n^PtpIPLmfQ-Gvc-Cy^?OhNjsueE<_k>7HlD%ZVdiI>%boR`~SzPpY09CUgC z0lRw<3rRXW2q;W8w3c-5XV`0Ot4;VsI@6SS>8D^eM>|ad+MFlKj>e^wmBMN7+4Tq| zPl~-O+AwQ|zc0X;VC`leLIC7AI|Rm3!yI$5x6{EVpXSrj=K#yy?7PjKElk%|4LLVQ z#uf)=o+GVZ>l1h1CBoe>OJmz|+XGclZ&r6In=GiVBnUSX52CEBoQQ1leHakaT=Qeh z(BK(YE0rex^;j!5@OK)pWF1>6`c`=mt0|$QE^263nMem&n}MON_*MboBDagGL31vO zu5;QsE!oKz(73a-yYBTr8!EjY<7j3{BRH)2{7dnBU8Lo;R`;^_L3n<$*G`Ff|wGZQE*g*j}0DS3+{n?Gp z(_`Ku=@BFk7VPvE$&>0G&nJHLD_O>!pJlYTBM+-DP*uNN*OIch7Rz6vJnKYe1+v|; z+*Ik15RY=ANw;uVnG{&6no|fTGB!H-M=AfT|5y;Cq=9TWd8<u5nolo@@0DJFd+pay6}s>)P8 zHSc=B*sF}S&&tvFq%1y!SOvS-jt^DG-IsEo3w-$ASAkS6&}2$sRh6#}?p_m}T3cFN zn7nSqU^dnkTw^QIHZwJjbn+3;8sR0AUem?6NCYD!Q}|k%_+47mOYv?K`^1A-MJto? z8T&}z2+-1Kw#S&lZy7o656^3Di=rB?UPYV+dJOg&@iaYQ@{AS)a1BL~Q?^_!6-im@ z47XDs*L_P)8cFo3t?qY>BKLlp*BhXdVw_Ja0k6$ep+WR59`(vW8!r|t>KdiH4Z6w< z@sCfPURbP;-e`FflDa|)1APzN?wN5Pp(UvVXvvg6w_M>MB;o^@>`zho6qMD8Dbw=jx}=06eI>g#XYMNp%ARb_+XP28#;{x z;(WQB)75xRN}-5HOOa+ACGD?qWr*%I`R3Jut$+uj`uksLjZ}CIK@PjabH|`W*R~!B zSwB{?Ey;uJH{wL`!~P9XUH~NBZ+w*Dr4aynE{|tKk=9*)NVy2l$-22jOxwbNl>OZ6 zI-2O`3h0T`K#p@@3xW=;!+a$bFeo5wztt|>Tg*uacu@c8AXCAQyZ21PZ;UzmuzCoX zlTV2%(CR+5pcdy2O$9Yzo?@A1z6-qA?g$Ocl)0XKPf-fB59IW_#1ygXzJJs zRPG4*H&DjEBIzM-f!C9(`F`@(yYLqRX^0^cFeqvq^6KcHKmT?~J8)_@*W#Nw`T6+` z-+pU2|L#r}V%D&uFr&y9JN}sw*ONS0@Snr=~ z`8lmL^y&%~Y6lmQ3MM`u*53Ef#wYYvzXXbToC^kCTFlHVK$?4N!At8VuzJQpa7waJ*|tY=vdck#9AQfYQxytum%lAe!v zVTWC-3Q4kW4RlK|DRW4Qbpz0%-V7XeVWTf0eM(g&UI$1(f{7CHz{13URYi}aH^gWK)n;jFiT(K%2H8pr9aDwE>ATz5gTf-RC175SbQgb z0xib_#4~zV9(`?7UtPuboF$NUR~I9e9%k)-+-Uf9?)d1&p$LUAj)CVCQ(sSnMD<&=@p6PPU|r9A#-Qyn9SjvqfF$q+brMg@9d zlBr_1q2E$XGw<4DZI3+Q^O|md;rdVofZ5~a2G=TH{NdDX9r(f}qw5WF)raMQR@kVM;EPZlFK4bEPSq=%AVTyTin%_=K{!eX#F9)*-+^_ZbUasyv#YK!GmDWBYN}{ zO(?9A`JI@|5O5b;*ViVs%v*jOtg|gFE`8f(;8hkCSdk42yfr*Db4yje#X?ldUT(sV z&)me?w0vCL7fw;==8*M3(=$|0R=Fj>mkN~f|BaSsX~d(iolWgGPI=<4%Fs1R54`>{~@4eAp6AgjjHak}OZoI8? zN8U$5oznMqqDXMcOIiB>vs)RTNT$K8jG1i)<8X)WMZz1**_3QJU1f7Rla*ccKwL}9zOY{Zh##>nyWpr+ca=11OG`a z6zqDxUdqf8eT!u}kJ;&v`G~1$C~UVnVZyo%zU&+rE{`wk)J98#4q%&P$`)g@RXgnq z4*!ElxkYL73b~5(%S-rc)SSGM0&O!X&*|oct>kZQq$%lU+*Qw#*@1QRhk3(8SDQF) z6hz)WF+4NHQO*84!n%ushk$m|W&7u#e0mUb)f6tH(en#kh+pYMe_|z>V^KT{3m6}X zk`y{w+n?Qyyy3GqMUdSXsW+7V{%;=uP!@8A@xyimBUA1at+1`mlo{ir0%Lu&vtw2L z{?FQ+?UB=02k|ML&c(_!A2Y;^@5~D09L|T)%@tXjX$|CZxg_D_i7{6gJWuJf&$b$h zSv+~qnWNrEUSL-!7Jq-A(v1#bwG3=mc(lmU?wAg$iz1JIdyFs zLbEiAH7bjp{iZCBd5R^KOv+9fnPk#1DL4rx&gPB#@CXD6;B46@cKvaenPz-eu^d}k z$H849<`aHj338XjCQq6l@+~l8sDN9aKjeF=g{INaq>H~I9jYXX_*;9tU?C1A9j|;e zU;Xr~OY)}r^gLafqI@?L@cZeg&HR@2H=Hx*;h}arkZUT1Zgotu7G=Q?|Gd3CUh9qR z__JXWV|!lStYagkYe;W%CnuF^xUhx#F>kYI=Aiod%5`9Cr8kHriI2j2dbbE)xHX&zdu+0v!WpQOHN^91&f#Q^l z8GSWH%a0qa)_rSIb_eRysf}w%^!_8)`#Z-Hyd9hw9*aF+D0lyeG9Z$e$=DL=k+{mzrQdP^W!@xMo>@bO|vbxHo4x$>s*csGk1O`O9=(Td z$Oe}i!3Lev3qu4(?9@d=?T#qd=$bwO9V3T1W}2C{YGbMki$BXO9&8=!+xjC~n?})- z6>gvoa2zXwOuLYy!@^~J#-GS1YZLvo>Z8U>9gIBT;xd+YZXxjR^#`$0x-UcscH;sJ zH^=xh5vFBOFH;osa{Tw;lz!;KaiZq6XspL~=~;tdh@>VLe8oy4^E*GMicCFra4m5DQqt)K=B{JHQ8ZwZ*-_R4rrKL=>KbQJ@2p>P zwZBVf{Ea3oA9N1mkJFsv0LQt0VJys8N=9}&(K zC<~D{{QobS{k=~){xYKP17V@c-O>;zfS7#$^jK!bxVHYU;y^kA9~BK>jp ziqYh5qnD=jzC>2+y?Mh~0NV};3_%vR;G$bCwBXZ)i1F9&b=4$~f>UCL1Cz>pMx2C- zB=c=?enb!jS`mC_?fgr>vnETd_*v`85bcfoe1tK9uzoy$-yWTKrRdY2#w{3DJn2LC zmT-LR^N0Z@nsD)wRb-#nb{a`R0BFJCMb!Y1MyyHB*hqgeLcUSvWo=Vfz$UsUBJNu- zWO-J-DXKw!>UVPI1*RlP;3!{W5xRVJG{^Yj_SV^*P;$%Su*QQDh#)}3ELEFmkq+O{ z*xt}gfzta9uLZu1$xQX++2HP)M|C!X&azBrcYvF`2%nEH>RO7>tV(=ACnl7$t$M>Q z=XGW?SKoB%E;;%DstfX(0R?q6S+7P%m8^NgQ#c1s=Z{|3f)5uc*JE&NEAgAdH;0^( z#D6M_I@mw2y8xYm(ka{h%)A2GcI>>p!;!8(Ik$7|tHkWizQC7g@Li^B7_9h=b;b$) zHy{Tm`1;kRuRgGiz;k}{V-n&HP7N2Q$YBm|2(h$c?Nq={D;LkTB?61%%lx?6pG>8? z{V$r#zj7gfbo}4#)0+B&El7{Uddx)S+2wyKLp^)WaMh`GZA7U|>efxrHZNS&n|S#$ zXHM7iG-{fjYX2$`)R1aF)XvX99#&v+tp{Xchlu>2$TgD_JKV{%+VM#O{YBoosnsyn z_(NXLpNRLa-FREOBRKx9^1TaAfmP+kf0Vx#P{v9Yx+e*yJR$V7PJK3M4JJ<~G^sIn z-<+v@sTO5=0^WYrz?0Qd@S8TGKKT{;6ys#U(ex(H${_RV%FPb%9x7Kme!uy1ZEHKs zBzpb_W;jN_n<(X)?s78-f;KQ!nBRrjJK^hQ>?>^w+hGs;B{T3h^hGC#L{#buW)v%F zmT6W={Mv5MK<2o*sksWu$D{%aX{(~kpAsu!GK5Px2WX!J+o=0?6oE7P5W-)V@PAq5 z@XJa5x1YZ3zDybyCnP$T_|`Yh%+}F4jwLzy5EYhBIBOOl$Hl(ew9|foKUD+h%s@SA z7Lt%`S1)pQtHGoG8IbjIvAoMi(&Z$)h~L!!;Ami)rgF2(oYxGyPv>w-R;2H$q_B?V z9JqW|ykV&qPRBZlIUYibW_4n(XGy`eI@mL5o?06bwx<}a*LuC9t6uT0F!L5-&>3wM zOO?{9eaLEFid~Y9wxu@JW1$*%Vp1(x`S!1?=Sb>PBT0cYQ)j)> zfb|Eon(^rUkR<3}7z_9BD0D9hTIdevnIVC`0<1gFEb4SdaXL&X-&<1HL26dn1pgx7RxzSP~zxQ zl$%}qJ5cZ_4om|bGA(ha>BwoWE4=_`F972K$eiv;Mw>JdBZ%4o40AZ)97)co?Z>+M z*6tzx%~tu%5n75UlrK3tW(da{$(eLNY0o2l>Oo$v)wrVIdm?*bp< zm?>d_^2qACaYx2fPBI5nT*d^TVIyILe1`E&U$I*g#`yF&eysf>K&43VjMvnxUj-lJ zsJlbuH-q=KQAxdrIk~G=N;n?Kovpj`p8;wU_teYy!^9p*E5G7h<#KKJkwlrP0L9z+ zLrdBn#g>%KcnfKiV@5N0fW}SCpxd}>UVM2l;r|CFMB(CJIL_d z>nx(H6N0!)SYmaWY4P76dxFoJU`pnFc1@e26`m_N0Va>v32$#2N^fs=7YN1{!jSng zl??*`tP}2g?s&B1bQPBDPwJ)sW%T=|GYROio;MF4e{LX6fm}Fhl{W9*HT`ejl>*}%C@mC zyEIdHTDqI*U$DSZ{42U3LF1>Kz^ceEmSz!pc*h&gMZbM-wu} z#Z~}RR@9&iwEq%-=$!|sX9HFNWvQRjr{`|!JbPkak}hhf&l7%OknfFxAJ$;BOWdOj zbO*PBl{Abt&KWQGjjp|lFAoLKrGjPu?og_DG6WcCGNh2gTn=p(e8`{$&wb_m;6NdG zb3nse&HO*V%b%ZKIMn~Y`oj5dSmjZq;%s|X%g*MfuVPvyzw!T1_Pd2_Ny!I=#rzs- z{)@{~d<;xIfav!1SpABr@fYm(DD6@cATaj;A7}nena#hR_Pa{G7tF9T^uyYL+-V{Hg!g;E;G?7i9NY8dWBQo%d&Zi= zkQ^3qF&1OhZ#Mhqn-@Ufc%X6Xy_m#c)#-wi;!M%4GtVN4K4Q|+T*l)Qy~HUW<(OaP zaQz<@U(^|2%ru)FRknsF4bV;@J$H@oQ^C)L@}=wwCAj@ zk?tMKC54+3^-3!YKI0nn^6HrIbUq*(6kNDUnn zHahgN_^np4Z9;vb`#!k%&hfiZGuGxKuvV6~8Z^W|iP*6$U+A{^i;6zdG)wd>vJyA< zivZ>Nu_GnCkF9as9JSVUqe@r8XA{T)Gp{>)2%lpgKnm}*7R5dp$UFF-izb--twWVX#nj=f>csl5Lk za+k0_@~R2RCw*{8g9z&cWNa%~4vlZFpLaQPMY~lP8`*pA%_?5$Vk#8&iLqHFv8vgQ zwme|zZ}waqW;tl)*9DjVLP7%6!B&doJ01~6_*7V03YgJ!Zv!O)hJ_h#ClY}91RUSp zOszk6n-k4Ut&oZQeVXnrRq2yFTw0Ib{E=c+)O_B+VA7g~&zh&1Z|ztgC?tmO_US*W zZ;#%-o!bq3G1_4c&EHq&Xjpx_ueym3jFB5)Lqw{i$P7oy4)2|d5vflL=*6RDHVMm% zay{W(GEWz1zLHr|Ga|M(jL)g?Co*ijk=6H)iBsoHN?Nqv& z!c>q|71P|%1kn?4)IB(@XSB#sQS!AlQLufRmYa-xS;c&+o)KRPj~0#jRs61k?%D>wb@7s=&xU%|)H*p` z3BT9Cuvm3H_0%7T{T2s!Z`x*#`RKe(EX0kLCDEYEHTsH-!aQ+}RYqlwfN0cV&_xr?w z5?bAZlHqqty7$`NqWwnPKz>7e|C7ZbzS$dMT^Ub*Wd2AXAew|I##I7^~picF-bFmD;+yJHc8?eKZ?`Uc&Ck z)!OH1t8}MUnBL~+nlz<(kUqCWs?eK&2ds~dA<$iz(VOHU5)C_Nwt2~7zGnF*0*_Kka!RBHL%An{;+?iN0R^X22{ZIm! zu?VagiS~zo=D9kyegw05C8oWIsb6py?^3zDx{HD`K~7J+nuB5nPPAD0_xKKRqH6aR z7d74213lXPuRZ*m-~S&$f<@u!GgzRfVzD@n@DSY%Tkm={O7!G)aZJ8lVmXE} zi)Hg8uhK8;4PL9MORyP;+44ifSQ*-FAjVoA9b*~zHqX5t7sIoVcFgyDF--cri< z&zIFPlWwEC$LILOf^VZ6z?q}zaxRdqF$ETf^pqyLsi>)q6^>nffmCjKF5Bg?vVes& z`u9Vx((|~E=!QOuVb;fAEDvg(y4LTM0h#nO^W5DW;<~2~X>%3}xx^B<7N{Rap2kvY z2U<8-z6x8<85l~pjRxtzW%-ck{5p)WU#8xU5+r2_V-#yn2g#ULd6xSj>XAN^ejHuV z>^I1!xgj2`npAJbc4HwC1ns9Nx8g^Igl`fd;YQN}G9^g(Rshb{y0$=@;dZ86tHE~4 z5j6&T_xGT3qc*1s(>NckmD{Wz*8DzO_(V| zJ`qpycSc$zKfgC%bviQMiqNct|FI%Ul&9M1)SyRrxEi!Eqx$sQR7|kbb%g`zACnVV z&iHm`hO6L(o-w(U=q>8W?!cM7ePV`zIlj?HZsx3nJj0x6y;nV0k~l1j(#aH?)_y%TPX~_%-|U6S>vnGCIl?F1=8CvheFgbXn9~LCme&^$7f=FN{(*Z^?bZ6Xg+Aef)OKvzzpPTZ$5YcL; zZyGQj$l-}Rau-f}b-8tmBlG|U>NR#q{679&gz7w$CB<-mgftnT=0(*q7PBda-z%J@ z=jRHTJkDraa#V^%7V}XD^QldAtNF8nap6b?$);&nAWg{oPRymFTHQ{Vw{3Bx>Sje0kFcw#67N__=sI&~7hvn4mdnN5_Nz?jka zJ)pA9&)76%Ky+pa7Sz>%;sf;HnEny32u^(M_$bv^b^trBfpu3oRASiGU%PIZA0 zhAqjelgiKnq=KRY(BO7RLFH{TFtbW5Gy(L*Bhh$t*I`g2_&^A^qnTk~3y~i`@-d_) zFWI{VqV0_$9q?U^wK+ycIc5$>wQ6CqlbiPYd<6uH3;sbs^4N!}wjG$e!!QbRO*MC;M>gT+u9JTJ@+v zWcw5q7sZRj+i8bZh+iFdqTe#j?iRalI4fSTuxJGc1aNYrqM>Odl3F3b73D)Pv_Cde zBgnAD_GqXPNL%P0IrND3QMPL%s~Bl#rm7n{y0SwYnlyI%?HF;S0y^783<%jizzPwp zM4k};IY>bcE+!L6*grlGE$!uTexp)OEm8zcg7z)1E(hhVtKP$~_Tpz@Xb%@&$$3IB zLi^c7>JfA}*GT~+cWAXwGuY`Z){mYjcW%6buG&89r!RprTV#Ci?kwoYSUYS#v2T$n zoFsWE)Z#$$;(^AiPc@pRzewN0;@T^LDvBqK7;ijXv0ax4ccnH|v$e^(zCTU+$$G<& z$EiKIjm8T3sE0Kyi=2=xMRkkeVO}nAZAjGBs%T?ybl~JTxO)le%{UKvWYD))g zh%MKZ+F6I{IXkXQod`30x^-1OR@}8+X7Fp##4yTPRD^uu{7Q8iS~)aS%)_#fQfPbt?j=F`F$RqlcdklQdQc?u19SFfe_rdE#xh;XFhxJ$B{+CyMG&gpR zrKW4s?6yE<{RHa)FL%+gXIcj{1cFJ%X$WP5XwO`-vXv1(7_$DM-mjb6v}POd7Vi_K z*NwA*(JzePB^XS`@psbeNhZ`OH}dJRF*{X=LZ-A`T90Fcs@Ag!qwro+%&V9ltLz-~4XFS!Dm2*bO67>Jg+p%?8(K!E2t@<=P<96jknHCv3TYHMIFg6Ru9wkqtldP{BIX=xa}x&sVJKNu<)pR5C-aiBzn{XmsB6$>ctnS?lpVvm{5 zZyQu%vMK_yz_h2n$-X$2hLEg(WT*C|tr{wAc@B~}V%8vk%S&Opf5Onr5bMuGRS>L6 z;&VfDew=MXPY~;HpumAzvI{-;&a)V)gSwMj88Q=RwnpGP_rZvn$d!c^Q6|W|xFN+J z3n*ce#U@>@b0(SJK`0f@pPZnu+P9?~)l)Z5J}VA1Ib(1bYJYja8G)nU|HTxbcA@j{ zIOsfF#oru@XVZeHkLC@N?-~iph|bVyY&{d4#$?+CrWo2iM0ZB|4#;{1?jNNcXw*>< z|Mp$cFR!(KH>bvjwaKbF5q*rF_il-k4jPD;(Ga8)UO1z?4B`o$HU}nq7G%)fSv4u9 zRMA$BJgVGaQ}9yR^bmo^Wy3mR2Z&VanemELZ(_`BjBH~QB2(uzxkft!0$U87skaR> zBWEPzkw7@1l#f;>d992q9<^ej&FvFi-OLPmFA%z7s*-{i1e01L1O(BJiN`aaKK%}E z==Lro6gt?};G%^M9|iC-YF@9VW3i_z4Y@+4=ANzXdK@uOyS>(l3#r3x`=@soGfag( zpTPUWbqbUFJg2GrpLXopV?ZLK90xX~ZB&w%>FxEmsk?PaM*{hQxGm#HUaAwf>ww3g z%dJVgdE4qypP5KROW zC15%N7Vq&LB>w}5HMSN_3tT-j7*1{3!i2+9=yrUJRtCXQWmx5yOC?xqzD=$zq^} zu3z@ZpSm8>A1Ris|9lv~xFGiEXYp~-HTOuC(gNl;ONp9s(a~DXR%LCt4BPsjM+i-gN>@ZHoQ;R50s3~dQT-cy(pLoI{6O-q%VnK3hkQMwi z@PNicHRf9_J_q!(U(_&Vdu!7yqp-M&`Nw1K7<|=&Ltal@-s}vEY(72(#4(C@`H0W@zQ zNxDP6k1XT7k!B|+!hL9S>;3#Wd_?67_CJP0&;${}nPp!wUdP)JsT*&|8O>~XxA;%m zvug0Wr5&MUs?djv{Ro`L;H>Z;^HhdB7i#^^h`m(W&{rv4qG!p9AeLZ!Nvav zh(5%3{vP#-Sj=SSBnmZUyr($8JCgU8rWN*ubdz z2d;SdicY5*a=tz{rywXvA{!9de5@navT>bse%}IK)QvYRc;KC)@D-6FF{G+^aDD2s|aO@SYw`iU$Sjia5l{%5 z8mqO@{GjuO8a)CQ5 z<~gdpcFrwl@tvXbbhB;Pplp1H$ISQ)`YokZ?la#mmUh$bLpF$qq0V#93&{Luy{1U) z;X8QAgYcq)eY12k5i{>K-g8oV#)U!XWV2h1(2&^}68-+b+ zK6ecp%Of#5+cwbRPw|wI$B_AD&P^(!fRWj(?+TT6Q@kd4oDbN3jGhC!_yMtk7=m*HVasp&7Op%ST%(>AZ4VPt=Ci60mNT?B7{DOdmeqGHXd z<+!v5!tHz)Gthl(%}xqW+ahb84spQ0du^Mohf@B}*x2>r!}f$|Iyt1d#TTR0on}Z z8U7cFz(2JAynIi`jEDT-7ud;v|EgtE;C0bX_kIzL{N2_GMIM(Ybq0HjH&bAH4En^+ z&v@ib7>3UNVrl=+vvI}*cy7M)`!j_NyP}wR=;=3f^`@>H`><7e$tB#Z z0o-%MV2$0Ce;}8ixTA{$F3_ugmd2KX?@o@}lvQ zi#MpLKS@MMq6MZOomm`vnn~c1gK_gU8AG6Pkg=Tfo19Ij%GqtO>caFc$QYy3$)l#O znP?Z^<+u7K;1=-M_1)p$?1=j>r+D~S=1F~7n4*!<)Z)jU(FuKKNzfm1;Nm89P!~`v zSzBSm)d|2^i!K6-Ioe|Po1NbNB}BU3&|Ycs-shonU7DPa8JG5*k0SAng17iPL90{X z%KOn7_i#^o-~%;BU$Ld9Z%+cb0jKbd4m#Q^k^Kn`4Grw-B!2<+c9ohKmm_-fy1zgi z{)S( z?@r$Oa~r>FT^EUUSSLptl$~~=!=@J(kydi@>y$S8H=CQjmR~7@Bqd#M5?lZ3cDx!W zgau4{?B9pXoFfGTq05g9!GAQ5_GAOoVa)90gmdqpT{LZQ?NH5z^L;5%3Mk@H+x9If zg2biWIc=jI8>-5i_o=FfeZtbz)D)DA*2=_wsb$2?pPrDlJMpU({OfXG_dGnK6Rt2r zPv)6F{-EWE{h0f(d3MN6_&a{Bkr0>m@ive~RC^pe(Vi)d^c<3C0%}|%Sg8y7QMMfB zN%?C3Y_cfz{Nej?Yfl)x7tYxXe5A63c}GX^f+hDdAN7{N4;JpO-lMh9ubtSvI>*2dDEJ{}k421M(L@ZLkyV`Y=yS59-8{gd99 zm#%5Rb1vgoa@o}cB+|Je{@S3y@dCnFN^?L$q| z`#!`jHW8ODt48OZYif@*K6rraX1t6^-!}BbpIF-?$DVKk^DUmvoTd9mPV_t$@Y|ha#`e8m5D)$;#5_uy{TOhXg-vW{=>E~W zjRRz)2qO2Z-G4MM`C$UU;d$aEbM>Fi6E6j(@G@bwj}ZQm%=Fz|K;4?X>MeBjADw&y zKx8U7o_S>H7xnSK`^@vt4x^?qoQ2T;^q+tK=HD{DDgj`tOBIgxbNr*#|39O={|>+Y zzPEq1RKQnW9>!({!N-#R8H4I6K-Xe=Rv+5&`w;{1#$ONVT2=eBLc#xN^$RZot6%JV zBJ_6y&OhJy-C+yUmsEO-{eQIj|M#N(-7qQs-;4J5YxqCXssEoZ+SjKRvkx)@wd4c3 zeF9pK#su3H1X^_3_{%AbYPC2m>KfL$s#JV|QMR#G&W36gm|NhRlRGzO4lHVe8`CK= zh#tmM%H0{U8K_wh1&s|p;csMMjax$6g@7LmgwcqVW zj=!|$K_4}8lREb0d6uVpteBXD+B40W%Jf@|4}R~goL96cJZX{`3C`K{xGl)7clM)x z*xJYB#9Jg2f|{W;)xSrpFJ6z>y>JeDpg+Fm-&;_&!qtpcb>89T5l9jsVi4ar#^n|~ z&`7_&wEDgR7mB2f{(j95o6SB5p0tMT`V!n`8%^EAn{PWRH11ApjxfP+$cd;G z&Ut0X`LRt1M6#!m5Nlqi4>@NlU+Zd^YPf8vIv%HzlGabgm+@9O-vzS?0fQUDao zNGdJJu82pZqV(Y@l_k{1U$|hNWB_yEMKu@n_>MQ@5<+xgTjg2L=agdUF(h*kpW`A@HD) z<5T~{%5?>9^}{SV%4N;t>~7#{t1-+qe*t9k$ipFZP0Cl}EQicu(R@F#=ou@H>Z3Is zh`VWL=KvWqQ>RFKrAvooC}7f35jq(q7C2>;l)*vTZ$~2S7p`u#!#+RR1q7YQ>L0|` zfvYCL`CNHD^=MM%ZSMSUKKq`T*AzSq$}`*|_Ncg7fPIClcE%Ix%09QyZwiuRZ(~Vv zg66?nmJ&yPW}WOYi?%e0+%Th0O%_i)V2rjMfAOGYrDUi zN*8QrBJ=LZ!BBhke^H^%C(upun~UiU17xUC`IpDWwp1hN3Xk87k!wSjHc}>zNF{Qnuyj(yE)Rn{eabuSxJVVxd- zqQZMx*5VpMEW>ly|hoyg3}s^{N*btx)^Zx*~JkMx;iu(NmiXj4ypZib{r z2JVm%Y9`a2E7OfzUoN`T9gubpD!s%?W(CdnT^L_UEW1U52b&eVh?k*`=QHSlUNtVl zA%R*$?WGM3_YP}|HL2Gn;3=fAp3gR+6KRhx?C_<_qOFqq&NHma=uKEq4fwfw32m7SUilpSRYOvplhB6$J$anr1U!5NG^eYS-y3KrEW~ zhM}4ASaawtRgpfrp~5tm*=B)~WOsp~g!mD+?X5Y*XD5gTH46Hn@0DzRH{Sa4*kbVJ zjF|6g@p0>6i|VGIE(y0Z4%I*`U%cnb!!M|`H)sy$YmBF&R3q7)Z@(Wx>Qp~fWBv4ov@SY#++geg3l z+GUP2IC%-sSfHFPh6)Vra>T!8}oB`>e-9}BSD#?vd-{x#g*o!$ z%^nfM0RXV;q`gQK{O+jX9GlBkKx(pbR$BLEorGjc8AU(E5gI+1nl!kY6X`N7zgTn6 z&>Gk!O5(G-Qd$M$I7Q_&FETRvn-1$E94uxRSYs&$@~!{!^duJ z5lR6Z$>Se|fy|a7*W{bMO8_KK`0B9i^E%H6ituVR>;kV#bV0pi2$+Zk!3KyaZUVwL z2)XYBUHK5n56q|XKmVQ-u$l~{_h{0XFYa-VXd$CQ9N)r)XlUKuPJzS?Twy! zR9n2?u#k&6;gnOiQL_Mw{8UyAO)?U#$0d%#Bo3;I6C~>VFUs z--u|7dW6Tu)d3)LhO=&b{F=8%gv)uyO3Eju#^j1|y@-|guBphdfjy#cRyHD0;1li= z#?Yf{V#rVU+Y^8$$pd2NcY+~Lwj!F5FpEU`^|C+6CIEzDuCi#pcXp?*FV#_^N73o+ zh?EZH+HVIi*OL?jD^Jl#RdfWhti@X;I?PlP+7(l(Ct!64%E_{zum$VrRcINTs$}nI z6_k@->WiMsdTt(wk0#aG1KWAVg}KNu!<)v*Hve<8OWVM{RqEo~Tc%o>QoOe|tIMDx z`XN1!F&wswX&w~hke?gR>_Tq)My7u-;gDKT1^a*4d&{UO+r8o23sFHN1OW*Jq@<;# zL8PP6R7*>5c&ghLY}%p=*QzhVFPy_OpjkR=xxf5x$&?7B-pq&J& zO1UN4-{fUwwhdefQ0G?VA?%wjtOkPOW|zZ-b8pZj>z_Cq(%ATHrcUdA3HVg5-s(j+ z3U)q)V~(nROhfc75h)(ta9EODkRMSzcR-Nvu0m8PddNslH?knvBinPO9@lls&0vWw zGao7|Y}Y+b!)BOSxaUKd#_pp~Z+f+>J>A|0>*DP(D4en1YlhNr^X>lO0e;Ymh`DQO zjyHat1MneFE}FqkSL3tLsxte}6iz4X!!Nn5OEiP%rl-W4YUTAh5Tl}Azqqdvlv6I9 zey?hrJFOHCq(l3M#n{M5H(WaMZHtS-CLODWPQf>7M0m zUAqwf9{&sD#jKs!1Y|{;&b=^qi;-08N4=@MhU@5-*?;&3BjlnlnQ1r7_j^MLRJf~B zhB}w5Kc*d>NDHYgI37M_gWpsMVN?_TO1Rs-8Ow@%QYg4tX0mNj_=IJ*RS&p3vwUM| zHvw`1T{aDk15}QE-sM*-zKXHok6TyW)D+PTu!X(dfjJ>I(qE8Yb3O#-KUcGV3<6atYQHJrgwc=$;8y=@iUYT9x`xu7n5RS&8ApFFFr(H1G+zv3oE_KkS$E}h` zx4)(&nA|?*v7Tcij2%s_C4GDXXlb6Ws6%A@6yJuhZGjYB`&*p~-?%a7;X_b@h(nnh z4*e8!jC)$1W&f0^2!6jxb+VAC)s0aTUWVxOuTqH_cX?Tt#Qv;M+SVwi<(z(YASgoV zi8f3ZTKTH)+ zdvvdOM2lQkQ#;sOCG8@}bYpKV6o{Eu7ogStw6z9*@2pdoc{^uZPv98#cm44<^!K+- zD~bZ};Q^?H1U(={R81{@gQJ^E`G4TMfAcG)`<=z_E@mMQZfI@%X@>Yr5KtQGZOk~B zkkg{w>td4de*U*X{rkaxgxn%ItK*)K{C~A+{?!}v901F8NwB#!^IPDUa-K!>LmK2R z9OJdDif1!h9xlYp zy`r_8PT|G$lZnmyfc`0XAnXEx>q)=iyVh$wIpklZ$nD^DiMX_>-nugM28Y)dtq;(R zkH1!K9f>hwU}1<73JjBRadF|oW+^Zntl8vtU`|q^Vd#|6w@zyNH|zHI84BVCC?@OD z6K@3ECaV9crG9tTWtuedo!|*HFmt!@E{TxE>(^5hjG>ZKHEe;ot%h0M==Cb^RgYA9 zM{DYsq`DWgZ6c6Nvna zyS}*+_Of1870?bI*QTD*p`OZ2vk-tsXDvHsvVtPv&sq9VLAcr>Yh z)qiz4_m=#!GH^iKwCv74d}2W?Ad*gpC*T8Kj=3{jiRa zOz$%Kd|p5=_R}D>M~_1M2k_R8-h_K#RRgo)6##mQ1VD$mM-*4PZn+F|&&YI7zcXvq z4vZB~1J|El@lv>k7i#fXmlb|LNKOm%I%Y|8c0_!ZqNqT4UpBhDE}l7=;#4XPDyyl=r zM?Vmmp5vNs^OxSS8wIjH>ik|$`t`i7l(S_{7QDS?o@!m}+X%K#*SfM|l%`R8`u%ZNJp5bU@L@EcqQ3#`N|=$j>30 zL+|E@N)*2Np73dKxW6vi5bzWnSo_v~$P4MJV>w&yUZKw_`p#(vE_~~8m|V!qc(pjK zqx1T|YTIwHzqcR$(i@3UB=CiBx;Q5Y?r!6HeyV@-g+A!=d^fUv+3|p+|AX+^TKE(t zI|9f^$kQ&h4a>~V?xJ|=-Nv_ZLLQk$AAY`ZmT^$AOi$nKw__-T5T8_k=^Z!iyr(4? zKWzsOjY>>pL|y$7aSyAR#4v` z$eGhc=V4exvB1@;w!>_)M+dg;A2$|?7#$H^6E4GDmR4`|R&s~SeoaAS-%WC8*dc{> z=ldxl7l8m-J$>l@rrPs&W^#54@2u-{9DlMWhs^3K^7J^`{g>z`naW*S0_&i>S%>;AFYZh!$|ywPM6VS=p#lSIK~H6Q z|Hva~3yZqF5(@|&!a-Oa**J`!O&c#o;0Em|{!4V9(!a<=JJXzdFx%ZD9j{?LEZ3+6 z*_d)@yqmi@>(NMU+Z{SS8RFzVAANPKG6}tks7UBBCgZV=K7)-oVCodQEWC=JK3ik! zy!4fNeKxs!s&KMI(7&D=(Y@29%rj9^1QUZDq~W?2>D7G*GllmCz)##E2TdNc`emrB z?%TK*Q)eap&ed&*;a|60xE>E{D4bj+$obMMsCMR)Rdo#a7eD>b-Nc3pci2{Cvk4uF z+TrTn80i^(TLC>Z$lf(`g5+tn@3%x{bM`3Ayg|jdqm$N9>`?hC3lsM|0HPmjk2&W_=q4U} z^q1~XBtIL5@hGaso;NuZG>}Ep6TLBbRR=f+8BzrIG^c3JMmL4R0+FJsl8M%NK&6Ma z{LszQ=-7e19Je?Mbtu}YJ4dm3F`-W8vyD!hV|Pb5^Ec$r8fQ@3({*2#Vx0v<*wpCc znRm%43PN+0>D+1=8Cc6EPxnQ86EyqnW`6Np4G*aeBqyCrN%nYOt5q3ghw}49UT=Gx zJLenLo}=8RR$%&WEene;2zOM7d#W!L=$CzmW?4C>hqhbosllAP!PE$T|H+yz{Q5mV z_@wob>3ZK}-bsxHh>gTn2psoAk(4bq+tLG_0H@lH7<(C^oop$u;~PBn{^qe`A5k=M z#u8K?+$M(2dn=m5R5jNqGg~$hQ!qHV;j0WQ6?+qBgQ>1MHIOIdd?j^tz^24s_;XCJ z%q-OtL8iAo5NEO&z|^jCqnU&pfVk7pbbbb%z` zsv?alb(7w&bXP}k`pgCG?@!k&_jzFQaOVk2M+QBXb)H^yhQ2gT@i-QZ58>F$BJGOF z74eooB&^|3auK^X$@8!V=+Om-*T!QFua##tD{aWS`~ypYEwcK5p_wmob z&G4%fkFBiARBTU+-lxpL%ti3brfENs1%Z^%^y~l2uTMb7IrhW>t;P zf5O+yE6VadK08g1#ibtU+}YQ)Ujt~Gf|%OV^4;jP3rX0izasoY^Z2-sal3t4-V`gF zmCO01Lm{Tgc}p=xiB!Yc#l01QtBY@Xp~27vN1O>UwtG%%3oY$=nH5HLR$<{s=t+l4 zLd=1(^u#jO7koA3BzyQhH3a{~_o4zg;TPg8M&lvpzSRP7$3P*Z`LX zOEGRlmG{zdP5_EvnQZP7sCV@V1pnBjxH>RA0TN^BY&NE;3k$7>TwyZ0bEQ&V=fiWy zBf&rWRtp7|vGmTu%w8n3hMu+4r^YH?E{(f89^^|sjlL&OnR>U_{B5eYoq(QB(ILyY zE`zrlD3)BnYp5aN>6wm19?y!USZ$tB2)PQr;#+e;2mmcIsj!k?a2o*&&7frxn_k`h-;8&2y5_xGec zMz9V%}9Ony&PNtwBc&AxJwA-mQ;5ThTB!O3B5N<$>OJS?T03Oi0c5mAD9d>dqpyivnH@y(}m@{ z(1)A!1IJ5p+lz%vM6xG4jFaG!XKx8_?vwyY*mGyAYTBV|FgEWN6``v)e8my~qMZ-1yrg*}b-zHIY;I6|DoYlXP{o*@#+ykkN#J#{KM>78jm;Ct$*OImUq zgZVu3<_ZqTZN}Ijt6ng+7Q3AXyAr|G8)F|hSt0(lz8(btI^9d37ic%n>Umrudj3)C z+i#%>;KLY60AFa9WY|4~Zebf*a@`ct|HHGx3q~{A2yTV;CEwV-xpomURIiia>bW2M z7D%Bax=x1s--rEQC+mNw@$Z}ce>ceA5Yqpiv;TWi{y8uIzuqADhIT`diF+}|YTZI2 z?k_&xJ_fq3+u4=P_qlYQyVS`_#>zch+lr$m{FfVmCMQmQj3;_UQN|=adXvI1{elPBZeHDB7Zvl4&w)G zRN4Sb;X@e`4Q*zTt9cW7<*r|A>&=IPw}&Q9XP9QPs&$y& z5IN_$Zb%n|s_PF$Zmmb%w$zQwaND!KE5du=Ns6)82ZebwWSSVuS}yVN9e0tst=&87 zKH=m})otrdpwGtLiof1_aTf>mQJg>Ct}Bv9t9hS(+i+d)Pj$bTdCcwhB_u(Y5lL~G ztCLsVuvuLXdfxByTu2Rsx_9tD1lhm&^@ADKgM>y{SuF1xL;1=W8_(V^;S4O%Ne#z@ z3|~3q^KdE3eC%>3`4+2mX+TM=x6e0RV6(ApzKymjbU>s~#rJ4+dI z-%8%(@o@CPJ|DhSC*rxqX|mD2(skr5y>$$+fg`!3?I?}=tZg9iQtl+P3orB_{lRee z+ch@Y|Dn&re>)_=PdSYd8ed$_H~8#5&ya^j=5dv{g2yG~lO?)a-pKnJ7mc_^ril_!MPBRF>M*t=EN)>Gd;XLUsBn{oL((bBqq42Jyv3 zf=np#Q$HU>pf#9C_V6ov*?Qm+wEkzdZk63-Ai~|`!TCCwhtRqr{tH&K2~EunVD?~_Jb*HR_q+>_7yM;js; zo&p?FkdCv^u*(Yro6QsfofXbGrma(^nzcQ>7#rrVqi|ZGFx;$@TCnc)7}BTd@0QGe zQO|$9?W7n`u%-N(z~~Kl0FD$ zC-Zr#<0~uCGH{Rb8Ho({;+|P!( zBXJJn$UMsn zWm?SXo=o1`+G01+2^*8Ie@Yy%AnE|F*FJ-I)04QJnB>kF@p-R?nBmQ>-}8EIjUW>e za&&R(v=O!|E;#QoT}i$7v_j)ve+)&Wi037RF-n&B_9LO#UdcBmtF1caayBh%j40*Q zO|J5g;=BA$XG;_k`^P-Oa2*ZzzF*Ow668DxJi+ltFh&Tq-~8)*IKF=^!27Cr+_prn z#Ai<@+1{-5Z7Mk|+crW0sofZS&vkpv<{}?S#C5#YuXk3Ke@Z~Ct&iMv+i}Yk(tNs2 z+G}uZ6Krx%kT}=3MgLE=_sm!x`WtFb01cBwgHLaK&G}7Ke*t{Yo;K^Upue=c-r=;- zOXh{_&g|uZ%^S7xJA8V|EStT(L)ILVZh)e%EB?H?3GuoNjCXB!I(E%fF{K=kLGS@(;|qElC|QBAdjbEX5L!1zbwYkH%m-W^684w4sLSC{Hx zR@!W`b4OHrdt#mB?5_Zk`SbNrCw1CsHM!@(vu|*B1?b>ug7Qn%ebaudewl-&+k^Z) zC80p@YJ77Kn9>PRv){N9&_IC_v_J7IX{#tppx1Qwuel%pdVBtBX7QNyPu3#8^8aKl z;YX8(zf}+lJeV~fsC0r|M6kMq{=Rrp0{t18u-nwM3NlJxcA#2yl?aGksU<-U~6HxGoeU4*mbs&VjaOMPIt ztSQ1@Y&{lqm$4B{mP0vy)8vu|ao4Tj9Ja4Jm%JZOb#lJ#)yI%Rqf8eT+be+6(Yell zU3fZ3ZJz}M4L@AiJ`FkCaqD-|Zo=s7lZ!8e;9_lh{Q`8jG~h2OcJ=hBsHpPzbegfL9~8H73`v4w_C5Gr@+{V6XQKWwX*s**V)1(aEEAdXs0Vug z^t;Uf&5a{ZIO%{o;i}9m8n{)c6L#oz*>hS*_&>V*ChjQ!O@9AOVuQu>zTjh!S&bp; zK4zz;rRD2nMb;q(*!kQSD;#8`)BaK=atIXkW?gE3FMyXx@yox+a{-us1khQrGw0mO z+!7T5HnHLtM&WlIkJz3xCqyNsQ1upCmm%ia`8+pbviTrhv=W9!opnkYP`7wFPFcMp zZsAR3RO#X5&={*ns7?ZAb77$fJ+g;mGCk()mX?WRBADpD+*sf7vFjk>Lp#hq4bf;l z*XNn0SvhE`fdX3R8X+pCEM+VqN4zm-L{`at$J5ie5LVDE?-R9`h=*3GH?0f_$hcl{ zEtuI44Ncy6{QLr0yN-pJ1AGR)&}1_1W_0_MKid*ih>4TXH6lG7^3Ar@ylK)4G8H8= zwJVMV-F;{NF(olOEA!Yf1@vhZA0OXn`0&EM4Lxklz9+HMu63wpx4G6@$)##=%zNYH z;Lzc^Jul2jPv=}V!`yPE%q{8zT8l2Xm>ELuN6Y6W$!;Zrk+IQHV~$s2$9njVSBb6V zncu$;hajAWO#_#|T16`dw=2)fi_(-pYxx!EESAsiFOTpgSIhTeI#s;V~ARG(M9w5_+24waEy_|4hy zvpArA0#k9Ksqc2GZh0$n`80!r{EL+@Sq=rzJ#xQ+N@N~r%O>Zj*BSw&D{2u#P+FO* z3(l~S8wS4uK8c>ceQcCtXvI6(Q?-gfSpM6w67?9upIrrujP)JQ|-Ed1kXAWY7M%Pgyo|486XP zyPN&W`*8NQbP-Z`??SPU%7o74Es_(eWHujt}MoFsO9m?<+oJB~`G$_dY17OnkV8iMN8S@W+KMrXQv4 zW%8ttl`i~V-*en#c3jF#ZAb>s+f+5L(kaNR)*uo_bwStjPj=+5)524078A6-`LHU+ z<-NtvXcnhum3C1q)(-|7eXxCQrdz|@=fne9LOM-P^H7etRGWk(y?q?IQzji%tRI^g|~kOl2YFts4d1E4Ho#az?Yab;1>A6?5WR1+{~ zzVLk@_~}7SO?@~-E?#U$a72mjhm|mn{!qum6jG6M+?TNK^z@Y+sba!&)6QnTPj9pF zm?ULwpHyI8h#qT?SVoT@`rE^tvL((Uil0?mDQ8|!nOfm9?Vvkn_wHInhyLQw7V>!@ zocvxTW-WD)-g>>nj9^uG^}(w>I{sEtz2c?Ss`}El78YU%lI7inEnIJv`VV}{o49Lb30-IIKMb0&wuqM4%Mbm?IB4(;o@Hc3 z(fpWgY#=|rm>2zXfmPDxptW$)T(O~|jqPJ?zZ{8ANYdnuT@}~6#3H7oW9+yHdo_XL zH{$Rav-qRoX5eMc-?#QP*!* z#ogH<{|Qzpe`DOYVV|oi^~x^^WQ|M2Cdo)`JBK(t=~E;lZHaj?Kq#VUz(;kep=PF0 z)HaT=!?G67w6xJSDCSMb@Or&Yfr`F`8FBNMC-l$ zd7eiF^=SgD6@j*S(imb%`G#9^cl^pHKpt9I{{vv<2N zb9vXXSf9R{z!+Q*2vq3YE8gIw`rdTsl}K@?<=HEeDa-CjrUv!R&7qxgr-{0Swq9We zj}UIqFU2i&SAEljucp3?mj~-(rU>S?&V=Me1CP1hWA`V#T8FR0K{eGnHp-*npY7+* zenX4^GUr9Y^$sY0Gt*o4e!vN-;P4S8k8ShCZ@aFKE5Gh%^*1q~>mkGB6r|+G5C*(Y zxArNa58U$|7KumDb_E6!t2B#KY?+YN(fU{QLTzc5PCy&^8Qzboi~{xUI{ImJ#M^Yj zbyo|CO=DY*2GKJ+*atc2&XEEV<%M ztQA2iK10FeXVTY4p;BzzOv1c$x1JcEZ>YMJvHL`Dta=7y!EUIl^d8R@`_rotFuh#I zlIRZ%dLc$;3C50Y?B$N}y|;q~^9vI)nL7vkp!AhCkIlGX_yLc~MG)l5GaOy|<8Qov zd=f4BWWlHz@sZzU79vU1TO0>mB|^QPsI0*X=Pil}D+2Mktmvvwx)UC9L!3KSxn(o! zDmpMej}>dvwECae@1BpMS6Eea7}-#0e%{WLXN-cvr-@>H|C~$L^den6Nf&rPtmJu2 zuELl6cvifijb_XT@uOqX#uB*?Z$H!1kcmu&+1gBRiyVW?SZaF=wke1p9IHlwZE6*W@q43t)BvN;1{|FXDvShZ-Bq2dB zn|zP)4WU?gqN9yZ@XWd~R!9edF-WJuQ(I&@ta#iG%c2LJ^&cYaM;--%Xrr3U?<|b; zhapV#cAAv^bpmC5J@1ABE#3Tux8*d{LNhOeXUxo^2oOb&*<%U`Gm2;Kg>|=$%D(rj zoDI6Q@Ve?~+q}jLS=76F#y$zZ7g)yOlnnPcem??(_7Yd(wTlNW=(?Sid>JF^mU=pC z&{kCWU3PZbTaI+A>ZN^H>QjEZrl689_jj|P*=jiCLYA-Y!9$+g99=Fk_GvQq3s&hM znm#7K6`Pt`!LlZD@Czc?L(gCeTHT(tOJTyR@F&<>IDsAYcaRcVfp^6PdZB=9jW|-& z;aX{0+o`OVG723lH{4_F?CxZLCgw+3?`Aqe%aQX3JL_r1jO~-@A_H+hvH)2M^jw=d z54qZkIbA^8KH<01-O_@}-6?jH%sHGbb+E6zRQRS@e2&H@`O>Ev1PS9?KR;d)sOaxDrXWmb=s*R6Pavgj^y${Po}J=+hDVy5Hw#M9S#Ta zI_v|D;g^!hXRa4+Z)#ThWae7;01Fp++Wwo8Cn5xWh|zL2{F}ZIwVT)`QeB%#A|Cl^ zYsbI@?_<8UjJ{9ZcAJmgma_sHXpe7hXUX#o zAZ4Ip*!}0x4XsvGzoQWV-62{=aJ)JWTg>~ccfOle+eMtn5QW3WR!GlfIpFk$|M1R1 z1aT@xV^#rh&3m6tQ8?qeLsGx;N{<}cLyTq1m$S^+p=99>x_-~xl72&+{3&~);%@na zWdyjChf5X~^!al26%A=2CkC-C2pYVw#=bMv?kr14NXMgvNJxPe9Y!?xLe;Q(qfY8~ zEX9AAl?)Oen_)4e3eExOO1`(qsUSAmw0H;DIM|(q0=@S0Glpnl3y95zUP`=9-!jCi`lNAY_a{V%#-?inx`x`#aZ&u(rW z{`HeS>gdlmf+d3bucc4pJv5~1NSAWBRcu8C7sVUHhk#NY4!t#YDJ4=ZKgO)}awf6w zFzPRl|MzBy+#kT;e%#{EPkxFnd^q7e={C0>6be3E(<##*T@32aKJ#te%fAC=X6%qX zEfRg(MIse!=F@6 zIxuz{k5?V&?+G3?Z#9B3xW8cS-5&3Yo`rdx4CbFDLp%i(4-Zeq2GNPR@(a`(cYCss z94}wS%ulruqmpHp(rOzB@+m6!@{E8REPc=d1En&d5Jk{*Efam;ctk;ypj#tP8>|SeYM8?SP%t8E7aL+38uQ$ek{x~HmBUhi#iR*v&gNr zSl%NFT-lD|Lw^>*P7s~JT+nE1Eg8fKn=LjA%d&?wJT~ng`{rhf%Y@Y@=F#Egf+u+H zUCr=_1zY=*md~m{S;9&}yK_fqK%3pk1uSx7a}V0!6|a`w46E$6TzNIqTit5y1z||t zojzOwj#+PAPghUG9q!mN;~5SHHWvJz@2*jB_a%lT)HL-iwUX97#Me@?r%z!=jdo*M zukzFCs^Ft&V{QD zeRU~_Fm@uQmpxDud`;~?bl~H%DiXQp-HJyOT6RyURz1v*#FixQ4sMzdz8HP4g0Wg8 zKdusC(Q0TWu)2V&>kb#j;jdl5-*0|U8)SOXH(L7f>&13wFlW$vdnYI9qmS+vXJ}zL zcqop@8=s^IQ5v)dtz@rE7N!}gi!c!dgi%g!SflH~%4o&`D*WHJ{NjsdIMUt_8ZNZN z)S*+tdiuK~1E9U^WSIBZG4^ML?AtcPG3G3X7w?SHG4G~F6K9q~)%?M??YDp4Sqix4 zlP?e2n>_#g^6&r`Bf0ai-dl22rT@zs&f|RS!Rnvx@%^ZW>NQ3%J0(X>VpM%AX9FhS zG_!BTjpYubb~T>f_<)I8;7Oae!(0O}^cA5Vry2Ygt;$7uC^?feli2uD-EXbws~#UfF;&>wi{*vn*3rt8nBn%yR|->ri&IKxyf8bX?>)%jT1^qXBaga#C!_*=PEv5!1;Vjh59_m@D<( zT~IQ@W=)VT^nDi>jfJ9(_iK7uII$LmI{EP1oOE-JQ#(kFd>n&fkodeD=Fa1`jJgW- z-5A`TZy$#fAl5&%wJ-fIV(O*VhkPq*vqL}+}+a-0f;9<+=CO-MMbVFk{ChkE=g=} zZP64dDFNSo;$6@?`ow0t1hWJJH*fw;vK3khNp#QoUz2t^RSr|a7?9#gLC|Ww7VD&j zMwyp!;Y06LBU2eEweS-!pXVb#0wG15eVHQQy>Ac87lpN7P^cVrq0Q{+PJufLu#)C_ z2RC60CKN8pdSjZM*(+Wd+irD%w&E_>y|XaH>Z((--xb1P7-L#qNFRbMelHC!N|0nUTC?pIGSWH zD(-xkk5>#}m`3cL9~%X7ZgQ=Jc!qWRV^r9I0?*0b#~cL(9Gp+iQUkZ3>6a%ifl1@` z8cWOF8wzl+PhZm)!BPnfn-HHql{R$sLuNO2?6hjHals?Kgrn-_t%i zJb*tFqesSSJnt&|^xWvwc>F2S@$y_o?+HB1uS# z2&q)bw5FW4Ogq-Iup9+>p7$T!URsDvOR~di|G=@eDdefS?K6NFrU1XRiq&1sh3`m6 zfNCBQuOvA;<#wmS(IW{Ci~6=aEWcM&H@-aZj~kqvp~mpQiX=!rpI*gcp?^1S{UpT% z_3`CneQEkAV8x}~7H0ilPB6R-P#+4wab8UWo!y;2Em0gBowIXpNj~F4&De`|{Azaw zJ?aT|am0pLK1TREFd0L?G(IdlP26giKE8CbAHSg6KZYGCCV3oRetfJsiwbdY|F z_w4R&&1CybA90K+!5D*5=fvy>)SmQ>*H{!y0gZuS1F04dX`yH93Js8?#`isgUm)Yj zAQ@_hDvd8V%*~N8;-u@tKayM$Gt*03b-R=Z zr<(?U$rH9X8a|&EEk_!WW_fPVC<696MWUQK(f$kK$rzMHDP4M}MJ7=~-BHUf!o| zstYLBdzsD3`vf2ipst)$9TW6`!QE6rJ=5ts-&Z~(62{%ut8gINl#;&cglO!%xo>q& zd2%s`aALDZeFo@ahQ|i2*=|cg)0$fGKT$zW(C*M2XG*misq4AbeBTU))JkhFve;yx zxldPie-xXXJ_Kbe7^F^tu@bQ`ZkxiMNXMZ_PGkD(IZJv{cWO+w3_kk60rI?xI= z=@kue-9C2nnK(Tygqk$KOYBP@>P?}dKkw5IK|fEov)@&pQ_q+C4bXx2S@g~QGrZNl zfLif(s%=0mMg~F(qX9cAzgpijM`1ZXMJua0q3Gk=`$zbZQT1SBTL1OS*!({K@ph*< zhWojB&pGvjn|kO38wwXiUS9gsRd-CHM=C=C$Rv&@1}cme$T@NF@@fdh{o@u^NEQ4r zZ0^5HlmFH78maJJWd7$pvE`^c4k6F+^<5*ID~XThb1;YtU8CW4!Kz*@GjSzFrh~IP z$bZbau1NJnZOLphP6VDMtbq0FV*#3%s;|g-IXHA<{Ohq9sHtb1g!h_hE*y%%B&n7m zt*s6|!mt-9hoP~gV}u@@ehvikrPoq$q6JTP?8~VYbp7Crbgp%x-0D~Z zQngg5!bPelUy?TU;-*Iuto6hcUwjlu=M)QBwu0_y6zsh|Sh{^w%&p?9<)WDOa4q?z z8u17@vhUuzkYQ{_E@t}yS$0-~P_2;h?DNv=t=j~u*ZvOmg`JeTgv%wrQs!Ygc;NeZeZ|A-2$q+f5#}f^Q9aJs{fKLcSMs3}#MM_xcM!PcWX=tL68FVeS~?X5xMB%tOY9 zzrao!(v(`yPJK8Qgh?2v&{uzq%E~=nU}9V>&5#YEDXeB3g64XPJlq?>VndpJep@!? z0}xNVZLw+h^=5932Kz|uT4$ygC@QqBzNZbwh>)4`rA;2aOrB$~h@v>UxK+>v%j~02 z**fFXJ>6XtnQe0SdtRxRjq<$H77@&xLwv~`6q7KjAw!3&jkS5+8SE#pm`)cik^EDWWDc^%FG+AnHyg?4o}+XhC=6W&XP{AElLt`fKB=GOU( zzio~@691CU&9<9#@3{A(9etrE;pPO?gvjSOR(3cz&24umHDJ*!5{3kbb`X9d8Yz4J;hi049jp?boeUJHZ!$ziBJcP!n{I>JCr)z5;p zEA|(v-wWRDFs@25^S1X|#wUU|3UAZPJ+?3Ky~m|}kZ;paypT=L`q-(3dQ5oh$ActO z1>*!q=c5pUd^kez=2#%&7Di_Fvw)PNLiSfIFEzvp?$~u6hBA$o=4kclkWhq`IP1nf zY}JPH@#(t)E)L!cazQV#c}B;%DPu|3$fBZvSi-C_dA=#f(ybr&IZlnbYmFrZ(R^lB zxw~WBegxpxTa?!btL9*A`t;_@V*u)-q!YlqQGXEfQ3028TaY^lXsQaR0oZd%-jq#; zcgg$el(4M^mx_Y7NRu4zjT?(g-swAU=pb;zjuWq63n99m08b$Y2()lIPuyT@Og}8t zfXE$(=?m%9j#2KZh=w>V^N5Yyw_EFK-Roh(=*EgKKO18q+^|-s)*54pckEqy)K}As zsCu%q+%o0wOS4Kb#yYjEc7J72twy&y0q5*)vjcBs1sShq@kKCigBEMYJ>n)EIM3Jm z`yNDxv#me{XA6AnPF`9z(&Y23fct7$CoZDhB|JG&i7>`yz;^WW8{H#2$mU0JTlrDu zngWkk73hOS@^}@$;b8U%qA{#ZjE~Q;A>+m^zBVv7h5xd3fDR61>I)Y?j{NHIl}a?> zyMiWUi$_-P>f_$K=00F)e!bLg(4BL6KgO*0Tyb-XagC8f^y?xM@7CvslsuxTp`=7R zojONkgZR^z&J-_MLmBCI5>HR>hTu6szxdcYY0syQPft-@liZe&!!RbCj0i9)jlD=Q z?R-FibL^jFm^_|$3OlCUO=uC*8l*`NXogld;D zHs>ZC)jrqHcszc6;aBsgr%);G2Wv_v9pYL&?!FEbV}YDB4BBKf%MQbLi{0jDPd0|3 za#~sfwadqoOp+t_?7qt@mH&Rt^N*X4+@Q!mlDljb5|r){`6%zI1q&UVQxG<_rN?V)Iz;z0k!BQyR9M zqbI?ZS-w+UeTrNK@>~7Ahje2t*YuTR{#}+1Wi+A$uF8RweccXmmcXl=f;Pn>9 zS^pOV4kQkVpKlC`Decf9efWnyLqIODcz!P13E%gHWcSP*`AZz7My#Duyu43dd^#oP0g;j>`nr`(NAq=Hj4e6IgT=0mjYXEX+PGS4dQ~Lxx0xZd)otV?s9cXP=5cG zvz%`>^v&o_WkEy1dwWeyUm&=%isuS&Z!|G1Vaqs{2xa0TIxs>6VB!gYt_KbjHe7Wd z@NslNyj6bSz9gCmX*5o!%T~>JP=eG_e9i?UFTIvCNSCF(Pd75INgI#M-A9c_~drro=M9 zd5R*=#)~6D^W!@{B~EG{_4x0Hd53ju>y*7i4SQrqyxz^CBjS&5i(wYdp` z74v1ONJm!<^FeR5{t)fueK1FMnfA3uUUj>_zC_^H<;E_)2DDDK>&<;yAt4?9+S--# z755tZ?^Msz(ug}}h4)A8r|L{{nez*Gvd-{k(ov?YI`K_SRC-I2#(P5H@qXSQtx#{{cfc-(VNqs;-{j>(ZW4y z121dT!y5QB&z2jP*I`2186oM%dK*`x*Af;kZI$%1WL1SbFq^qxZaoWxN+%FdwsIx` zoMzCBFDPgGZQJkWs+Ep<*EvZ8cD#|>ruHPv zJj=+`w5z6@*XkW6ADze9`4$&C2v4}N$&Aqt>~+vCF9NXJsC$qCb_d|H)N+n*uiEmy zC~3gViu8Je)WebvB|P2GaDU{@Vsi22>ZX{V7irtz6@(heW9ZeoUXs@g4&-E1FFU++ z5YU?yb{*QMV&W8kvdm}<*jY-tnl#6kpXqA~c~i@xcb>3a6eg_ZFts~7IC5vn$N4FSUBQ2<0*E<948G0&f51|CQMN6_5vo_*#5sC@6R zLMX8MvA+6F?)yk~VYy5Ej?TVR+pqA~UMSva>&WI$f($II`;aB((3|TfDs-i(QZ^!X zKlRt77rZZz8b9nmZfmL`aERU56fZ6>FEhI6()1D6QE-k6;Mj^BCN9QozMS0ulRR5_ zFaYGaC?+XC;4<}XR2_Wpd^sjBbH}E2~t7jjn&sw+wrNvgT|AeHMO@8;UX|` z|M=crAds$~ROQ-4K3S#jf4*FKQL7j7lewSB0La-X2pu><7*AXAfx`NQ>t*{aCXj$w z(x|ucd9PefTi8@d`VgQ~%e{V9CiZL~Zgn_T{T+T; zyBU~vHc;IbHY56V$+$LLbo!2z)^(a!DiJYZj==nG(3wi+A?+#bNigx;%MY+(D=>wd zYw&&!h6j*zuo?%yoZ7YG3*HyFN0C_-#_9$HXU;lC*(2x9r)Ej2gaDaH z;8!7ex|W2J@i29n@cbQ|?TW_0vLXI_q@YKRu>3)yEwsH~m*?4w;Dd|+zt-?wJjW-( zZiCf%V%xWN+B^3?<33E-*ep+e_Jos@W4Sbij8^=PIf`7EWs}k1V~#cS{u*BG?o=;y zb@UoYlNWxtHEvrP%q#Y7KPccWtY}5t?+>b{>Z>cuAxyhv0cSt&VZO?iUIP&Mov=_5 zxW{Wa4<)Vt38Po;JE?ZA$n-(T^kECXK6hovm*>w$F-;rVpH7(lq-CReRM9t#%})$C z?z0u&`Ch+uO#8xuLoUG7TbM$Z$@Y`D<90eu6#TH}2`?PfuwJ<$f5@&Ukwn6;Mm1$Q z!Y(E~G%8E{9D&_+0&c-OS{NW=h>d@g$Oj)CX2Nr5W$cezGPwl)R_jN+;lTz+?)y^4 zSF_GJqW27n3{v3IAcPN@*Yp1F+SgvZo!BD0E0p2aEA=H5$V?1|zWQa8T_j=>~ z$>b~Rpmu$F0*Tz5JA+4uX*C8_Rb}$EcOmx&r;v2Jz0ojkQ6jfm#r@?c}d=H69C@u4TCgCB%2&J zS|`Z|U+*xgc0Tk@{V*RE--jiQu+fMo4*Nnm01(h$oMx}mv?%yX)V+@<86kVY+gIq2#r3Vi;TtgZ0qNRSQAPdUHlnT7nEkK>q}WphY_RtuMOWYp1Us9z2==v zq$o9_jAC0u58QO&DoZ|8>*YAjQ`n5AISvr*+mi+qy=BP5P|0^1-LvP<^-7lI zhSKluu`Is+x1KyTH4%R+hI*Lzwb_xdsDxPa`%p0n8+!BvO7!gos?1x1^rSD+CA~P-R6?XV0kGWnp!W06_Q6z(_=)5k8SU;+3#%d)7bjUUwtQND$BBWX>bRAzKa z{txO_EXgm_I*{NqPhLI|D~Xh(7eS`1l%XU2>&9~B%Bspr|8{(i~L z)jmUhlm!p0OaGiUfdG%Iwh!HI0kIpilLUReAyO^6z|_SJ!67$g&F8~}5U&|{JQ(0l za(?>)8bo-0>AADgi+!pVLmicP$8~&bw9e+!REZ zN=)(v>EUD<9-z9f>kHp9M$}4M8QG6sEN|vAC;#;l!ygr^&g<5x$ek4RZn$dkcc&rz z!692^>A0^3miB$Hc(GaZf=L@?~W$1wcTdh6aZpW_(GNE``631u9!&e``mEFLBo=v7{sf(s|!`r@b0nwFv2_V_lf;Q%da|c2vmm-%9YWnAVAqtzbg@vjyf9N@ z+E_HzLTw}YkQ0ztEe0vhGwjWcoX(wGq~*o%b5y~Re|R+bK0m;E+IO#*353^{iJvW+ zSj{3vsj+SYDT6}WyZZ;biVUTx|AC_Lt2@E_rwN|#Ho-Pueiq3q-DP9kVWdIX!Rucd zB{Z%Bo3k%@x-L=K!wUyoZrtZ)%F~XVlH%;=8fk6x{hKFGfz!h*oPo^ELf;6dE0AbK z=rd~d3wzekZK}1S^rsje?*9r@Bd>rUQR!y8RN*2C2Sz87zhP>_74>}D%mkofzyFqW zvf;zj%BMG@HVrhqhn{cH@2Nl2W-1N)krmw@Xm9^cBEtCExY*~2g0$bOZ*-t}=T)u| z<(KvgDg-#y-kBC@Pk*ol2%!k~KjeF%lPr`UdF&{jqXl(w%@GFHcy^<{La}kLkX91_ z{ocJQLT^du^rw>xAYi-N5m5pn2UAw8VE1x$*<}NslcKLdZ9f{Mji4~%v*&-G7rcxb z*EfbYH+=$RHpXkNCzc`apxGY&wFGDUz$iDmO0{Ax2Ga6sjoTuK3&`qvrpz*(cUuf# zf?{wd#Ll1>tj4ez>vi<+ODjKl{R@;m13=lcjzPbm zY=l(Vor?ILYbvE;OESE_0M3}E{RC$hEv5XW3zgBqIEFFEd96ja6R<){;0U=}>nPHq z)otSQrQnLIp(nozbCm#1Vp`nX5__d3R&ln%BW`Vd?M}s?ir!1j6KnT+Q`l410d@18 zgf>kd>pVIJ@vIie&PsjrnEm&ZMO_> z+d;;B#E|25xRyhu2A_SsyC_AYme8b4)xCvh4aKI7sRMS9MGQL??rT*`)F_~q2E$Pv zJER%ptah2ygvq}I)fm4#(4xH|on~sAWr|hT)SerpYYNevkl&^XKrZh@*gqcZs9U+L zjw1BG_@m^#zx_T)nHwSh+G50;nzs5Ez)kb705^bfx2v&TL7aR}dIr>p9rZ>ER<{Jp zzyW6anI2OF4~_tDiU&9qYeXqe>`pvfmM<<6&#)T=ntN*)_Tcs%>fc8)Nd5+|cQ$C- z&httEXJu%1*N`fbWQ5*1H97*UAhY(zKjd=VPnWwJBwf5p!q9fDx5MNC$HTzKmqvPt zx0J331PHc%V6j9vmIf%jQ_-tyWEATihLZk15*flNZ))-uTj~7IUE23P;V<0`NW812 z)-7P5!yxAA71J#|wx^D(>e=+;-$qj@0|5E%jozm%0~P6RzD#^eo| zH|XCIcq}ttmBcKXb^M2Jvj4)TzkLKq0d*{8`PILNX(Ck~Nv+vGB6oT%s=9i6iKh0i z8|6xu_iUVmY^#oAwz6VO#B4bJPpaV`Kkv)&Ye1vP)r3TgE+a3W&_mYs*hKes@TZ{h z!?hTybaclsTRT>$s#l7tr6=vJ9IW;7_>ne3HBl)bVEpoj(#!K+1~zu~*4P2X+<18$ zk?fldV{INtYujF{Amdx%l;*P$;G?WXVAMA22Yc2cWZ=oZE5>6{iIZ?&fd?2uVM@ z=a%$(y&rfYV^h?zk+c;g_s%cik*NcZBU%G)=2-RI;PF86KSd(LPL)mUNPZlT@#p z(9?Zq>7Spn3-0DD_fxs?ztn3>ZZxs1t$woZ)=GUe`uu6g!dVMECgU?f!Fh69R5Np6AgiT6VzWy_y_tgK3Txrd zEfwv8tDVdb8$Xx3G~>aaKoUA$Rk5|M!mf$!p^F5pB(!wOs>lZ1_XBTkstQFIhE_j) ztYmZo7sPUFKAZdTwMgT*_%-LXDl65f(1cVHf_14jj12)%`_ZM(>R0-AnhawX6_9v9G*R)pkG)Qwoiw_vOcn6IpJkk zH9(y`J`WRIlxg|6y!DFa*kZ79uA#zs6V1bRKP7=%T@H_qhMZM@C>-3{`daOKbwJ8X zmluz5$jjF$7JWtH*vf(I&sJ_a+khr?jK3TRUn!=Y(-L2@5PA5;Y1QT|z~t+t2ReHN zyP0#?1dogQLdt#wej+yM;*g(z_a}L&9j_~iiE?Hd8(_>q5+*Mpt%f>Q!*5-_bu~V=hAFtDs3P~`ZW*ELBiAgK z+;$guMnL%b;j=zSEXJtG{CnXuq1OgEM>VkQ<)5ZdE!s@U;sYO=nZygFlTVCBo1A5> z^txM?itcilZ|&Pu8QfIlb<00kCnyXWl6_{H?^5t=p1C-EIi~RUYT@lHI!%C(49)=y z{O6Vu?GtF{1#YBj5Ct*WKks#TvRVbbF=}CMz98#yp?S&iTm#K5%X&II-Rwwt#MePb zgK%NEL_=<~pDP4AyKMOt-JW4@7R2~uAMM-oNV`9mtg{y}`&#P6Vfe8LAJEDWVq)Do z&`5q^{L@RC?HG2Lj0<=8hN7K^8WnhWo4T@iHp+ z8*|i)S}itE3oxL53kz(o=Kea~*$Qx2QTJ%o z2ex(}zD*;i8S9ARwcE|{Fe9MB`SF*sJ||WO(|^3hgPW`Qp`rkDd~b8Q2#>o*Gc|(U z5aTrJxm&_3(3}M>x-`M7?nd@uax7_t6m72^nDqHkJR6$OWT=R6>Hat`d)^;;zk|4l z1i|*`0p;$)!Xlc^;dMSetHUB=tHc;!R}ymCxar?OjFm*1;B|y_o?NXMmAA-Av^US(4-44TyMDi z2N^!S!Bk+Y;dzNL48UAvyB+Mqs#rB(Bd3PfaU}Bno=oT_yleA^N(5mgdn}KCdry-a z5eX@RfcWa#q6wB}HCXR#knimYD{=kdXiC(0f=N6ki20WI+Q`KmjKJc|<8w>XZVzqv zx5o8?JhgnISPIk0UC1fXK#L=5z9ey`l{snr-|r zZ2Mj)ffD?CKy$kq_t&)0D`z+&b@xR;iygmdGRuczlHwMLTR)r`9f?kL(HxqY9^>bH zyu+M_AhjDD7>x$1^MT>vuV21+o$R3#9c{G`A&OCzApA`5!Lle(f>iL&m8Zb5sdp;x zSMv-Xg&D(OyD#O+f3O=*yzywD+E&P?5Y9)ao6q0Lu2YkqNfUT!E{G`-jg>I_>Mqt6~v7sVoZ)DhrT$IcU&j@HYO{Y zZJ8~kh;nMp-uEV9vI(By1)3v0$kZ>E3)Z;-_kxI1Ida87AADz~}trOi?iNrJL2@shjNLNy^XBM zd|V$La3jvnm~lee-8w>G&RR(Mlt`_EY}9wAZ(JvvEW_3M^L-+M5m%ZgeP>HQEAfw4 zXkX*aO}O6!^Ta_K-ZnPf5L1$jF@?0B@J@4W$V}TvBZ1IEnD;^BsM*xb4%tV&js!I% zE0<|Be1tYL#dNs#t`EGYobN9h_gs@MHN_}Z$qVKm{GPjW*qPu&+n(o9WFB!B*X5BoUp9TSK2p7V#!nkL*;*VVAWI|%Re`f zKVlT6Q^|esA-HApnD~T?di(`s#*O`Ri1uZ9qm#l{YQBzFq@QIc-%t_Xf|WI;!zvKdNR8TlxG?o$FRvSfww_U?Ed8x2!D3YrUb8*ox7wf z(*DowCn$UG7h(xN!I~>kJ`;=|6Ep}CJ=vlVZX1`Pn0K3oKTUF0hfsQ+Ld83 z(=y)x57-ECC5w9K4|Wvw?_%vNQ|#70tV&P(+Nr+dPyE7i_NE|VsBbbmp@yR}&?hB( z%@vQ1G0Cp&_L8GoMd(3R{~AYr_PV_)P0OI2#%uU5T$%$hFw?9Bu zbAa^V2y{_#=EW53LzsUlq1fDRti1#$~bA&eAqIOH)-Y)2Aw4Pk}rtVrEz`L>6y zA6={_LXt*+FPt{+9LQc;1%75%St0L2=8itvIB>cEaOvf56NM9YvqQLH)!%wk3Ld>V z7d$AKZ2H=|wr;L!U}Xzyc(u++KUl}UwwLHcLYt4s%=}KS#3G!M4swQ01cY+j6w&68 zb*z2=tKhx(8I)r+lJudowfYWQ#lld6x_jT{A2-$X`=9Lf|L`Hz)(Q8LE@r*LYFLio z>9Yv_z^;p{OiD7ZFTbzMvjplZgsF$L@a8sO%(pR#@EO)WGXaadXWmpArR5Gk*sSQa zE8M)3Wn}dgz9l`#?o8 zrT zvW1yE05}ETm}7D{?4)q|53K+{ZzIxu`6}I*V^aoCOOpYwmiAvvo!!df^w z&Y}}1xwsX&1lALpo6o_0_5-jKWcLfrfb<1X-8uFvdnV z_A~j-j0gVW_lr(HO0KFf9? z*}O}9IS2B*E8bk13Drv|HU>KF>~c=iPa6U?aD?01q)f><{8Ig_I&C8ovOxTuQuel; z^%>CHh%^)C^QMf~8zoZ(3b?5f_Ib=c7?mmU{11A{RYDw(|HvzP0$Ln%r;kQ$xu7z& zA2Thpv~@25>596~VOSkm)RJnqd;PY<=rOYQxYP(n-tA3!jF6JjzVmB+eJPiEvRDn> zKrOU_kD5$+^7oot6Y=k^l$M96+0Siyq&C^!XFMzCYSI}F1K-4TJ$iZ1$9_|x+3u&= zg=YbSmo;=}>a%B7h0}h`48`yHiCA<Jja`IV<81KoNQHT+4A9P|0ql zHS>)oY!#b@8x#)HmR;P%k3}j!uB);n=|t(CBiob>LHhGmZFulIkF$4GrOV8(fql~= zMNHM$YovHDHytNY)%MA4dq_|tj&eo>d3~0DD!)9Wap|Q?fR7@4vqJNbszO*+_ z_Jk3Zh(9em!-<|zZyPmE2H&>AU%;jIdm%S6;;9Y`-t_wyYrl}hOjvJ6gnmJM);Swj zeR;T%JXU#jNh%$NK_y@)yxYI1DqH2{Nj4wete50XcQj3v(b;FKg0CVP+jzB0CPjQb zOz=Sv>sD3PFEttVb`vc=_+Kh0qdJG;Pluc736?aI6oqD4uQquHgOH`__V|c$tQpf{ ztzeo9bCJDt9l{;qEj3AMke^Qf8`{&_!rb_;^r+ux@#hbbQ z1~q*j-TL%~U%Of~I?8n6cv<|0RY_vfa=re6AL!1et{~|O0+yE%$69tL&@O96`=P_xRsCz&-tyZIn$<*0C=}6pDTI% z2fnYb^1PJa%LQqaqrks-Cf|38->p%`|1fv_uRO#5418e`0-}D{s-6~br=Rw%^Gtz& z%%%VS^%O#88aD{4we=^_D9l~sx6Y_fpR&mC81yLr7Gn4p;rB1Y)J_TzW^hs_ch|oo zs@mlUK-=}2ms_KPPm(VC{&wAdy+g3w*mntc^LfFHnu+~(L!bAz2A+coxZh~&`_U~t z5O3TotR{b?vh7Bly^n61J)HhP0yKP&AVpBvrkQ%oV3!AjmUFmJW-I)eGu(-go}FsY zFy@oyWj_N=9C812guNC%k^NEA>&#^|J|fbctX4r{i!y%a9!EjbGrHT~(Z&`f35D{qr|FEhQwBfI*4TmdZ~SG* z@KM(ZC^o8n{!I0Tg}b%>{==HVsELH^tRn8aCPIeZts7FU53_39Ug}~-62xvss+!En zcMH)T>kUxv%v3sle_5$4lq1t(ps{Wox2d`m{(m!ZYt(j9=2j<#2dQe&QlPEZ{9W(Z zKZ>I=i*9#JpeS@$rc=gPAvy;j1yn3KO+PKlr;}LsvT?;iBH4+3s{y@oGaTB#zIz?} z;o$0Qf!!yYWxYbYPfGRAiqY`HF$`|qpvvc-%dA_<<8GHr1)9m?-DGZfsXNcs#c&iU zc6B~IUR8qZ@S+mtmxlGA3QtczcIW4Nu^Lr;QR97Xn{I@zwUvwT0eD1=+KCXCO{&VO zf6Hz7O$z6d=yV#UkoY+0f1lL<>*qJGslS`S|0IVaaS=ECy+ z2sd80QqPhio+Bk78N-=LrKRJ*cD7=bXkYKxdipfQLV|?lXuo!!=A(3-M&Nj0Y9w1) z*}f4=T%+EefHYMs5y3j#)<^NiR6wE0?@=+{lL{l8;vb|e?^IvgkQSKwH~YAnU@o3ztZ5O{ekSqudSC?CA<$afr4KZ$PZ=| z_tbq1POL0;wZBy-U7ytI@+3Qj(IRDFwW9 z6ZZYNI)1cA(A)L9sbRmExD1uagMD4gA#t%*D;JaGvbQau*w!o!AHka`6+Wr~K8Q!7 zj`!3-Ph(mca&kENSF(W?;y&IH`OsvoAZ;hl7p~P>kDr&Sq z%2RSTyUuIFyLL=QLek#QKW-?>`6E(`SUok<%EFqCfp^BN^V`twJ{6dN;{e#0mVbBrP;(mV@(;j9}tJrDS3(X_wzib=jvGK+UK?Ub#bSjFMl!V~v=_b)FOHJ_(ZO%S!6&SB{W?+3rE3hMoYyN) zPgDp?U|=9yZE5{|xs|FA+4jipw)vxM#*wCbkXM({?NQq>1Ll}$#DR-qyD!Bp!mClY z4D&p-e;(jR&qACFx4u8?k*07h5slDxoL4$~b}aWT>w*;%S%e=8zA>WSCr8=CqM&w* zAJMlB6_Mc&=Pktb?m~pwvLjt>aUTTd`>qocah$>;f$5|gnH;Q<@$Otz5kyHYY= zYM^5d`HePVOrM*kTF~LwmRdho9YPzYSv6keUd)yr&#%>hw&V!iMiQB7gY#ADlRGSq z%9Yldl$Fr3v#mYE9H|6dP7=q;SFq_xpt#8V=chTDO1|J*1f>%aus;;njZ%?>xD7~= zr@;Hg1KHgdAD9ST|2}zIzzYZ>u%k)aV~A)v+`z*_yyEcre@G zoi=(r#CQ_E~rcI5w~57W$QNDTO!OpN3f9-hwn=oY!S%^c1?a%|@@ojrc^15RcyAS@lZu;=OY(PsD#O|>pXyo3WfW+dP`ES8t-|B)+ zcr?MsCF9u$_Lk%{Wd-Yk$fF8<$Tv6+4NbH)-n?kyE%m#(@G0~y%g!h2<)GfqhQMit zy|bhnAKJ7oL#YxJ-dwAz1SK$jWp<(Vcc001HgZdU@AYHynqG-0%{-}fzrO#^K(FuW z#<;5pr}WZ;=XK8w($M<3UI^tyttA0zPNjDzDf*i0#xa8VUcB8-8Qc2%G`>~5kHDg= zj|4H_kY*7&j+|X-^gIf&fe%wQVi_L~GGS%EDaMLt;!SCh+E!XW$<dsW4<98y-B1E6!>+ib_7&2828*)`Xa^}KS86*p7do`QAu}84j(IzU#Gn}K z;1kC69Pd+`*8OGv^peHA`AvjW2O#*&3+B2wcJ;JunwHHbfVeWUIw>{^2Lo=v8JGa1 zC4W=N6EM`NfOE|P=Hmt;Ryd%Owy?I=7t-Dhq8Po<7!tAFn<|!4UbY;PO*aCaP)yX4 zo?lt~Zu+CNz$ub}>I+!#=$lNjKoD<#NZfMwRMJ?EOD$pLO3*TzSLl-ytv}E|ehyg; z6;&XfNLu1_yoOGi1~djzb5I6WMFwIc%J{O6h7>r~7{+|;vV<3gyHdZ7mB%L2$$}3K6H~@`%XuS3sb7xeW~RX`u)cY z9h{2>{6M_$Av-WLh*K=076r5Jo$)M+6^o?$2s1+8s z&L3wjyAYp)n|57~%BX=Q0|Og6aj16LH($>MTe%oXP0+a{`j@xrKR$7+L?@jm2^zyZ zial9Jd*GZ%FIqY`#UmRFxb!NQ=R=1h2gx$<<4eFda3R&VcT%k<5Td_%;23(Okx%_5 z+q+rwx9Y}$XFzGpL6-;^gVmla?aW$SHH9w%U6VF{@Vu(B&G0@yU^?@DvSYsxkBJa? zRpabS>PdO5@EtQX-}bPyZuDn^V;pw=5H?J)pON{H7!Z=clmbTRQa^)QsO%5Xyhoq9 zZVVq6p8(0Z41zgfUavjlOo4Nd(ySy}NM}>H= zhkXXz*;B}3Ga)GJk7aMF)%N%ubk1WYF?L`~Wu5I_Z`O??2sArotF6LrSRzKn3?5f$ zoN!s)F)o(ks1Hc&zVEvin3UQET`B;eXN z;ax|Tu9=Ye3#jJbG%j0jSJ`DR`hvkO)j-#N9u`HlyJZdY2q{Io1Gh1f;6B~3Cl!$a z0kwA=6_=6`uNI@Z7$Fr<1S-{CtN8N22GG&&*eTTuLD=y?ktj&$@a1*%Zv_Pb`P2X` z5u%)*S7$=`9+p@NZ{#}_pQt6{%dXQ@k|S?&L;79m-k!W3)yqBS`i(Mjv~sUuVqkO$ z(8W0nLbwbX6X-vH9@M z1P2R0G%r!FJ&#&`GK2ZByj26Ln@XjAteaoIZ*OVvJ>^FXk@h4J)!_bT>WoN36TF-l zWQdq}@21AVPt-%1clP+lJz*aoY{rkgjUc8z^SJ0vUT(x}#>o*QV?$*jO)H!V;&VTa zEIU46kK_9YOWz&@*Qc&yJD{)9K%4)?6wXQ3EdYfp{{+;m7~^5IX+L+}s?~+R=B4m+ z`)-`g+3hS9IBkCd>=ZmCI6>gKcw^&iI4Y7?fsI1F<7yzkKPrguKr%cH1 zvN5!dMLUe!V~=PWVbs9wUuBc4!z8Ojy?|gN(XYka(;Cxn`EbRhRj<(0twie1eiryi zZ-d~V?nVu0I{ap-U6em~o43#FN!L#{#yY#8cVGG-mo_7AWI<){GaZlCHKVF@pJ$7Q zluHaNjUn0kXcpa5Co6HY`3Pzcm+{FZ)=1})`T@6Mo)#=M|g2(vZozG$z7{NeW~B-#oq4wgBZ~(6oBS*hVkf?&CBZugMb-27^t!Ta_#;3ZJW5vfh01=cUS39i5GVn9Xw` zz7AsV3ovx(pmye->ucR}?-yrRYwSn2F>ncch-TjE3hZQI#FHIYoLpQ560v!cnfr=b zq+UN+mffK@_LHVFV!Z3auZPu{rD^hzcH?!e`_guop#bt{By99Hydp;u z&NwUTPDiK$Vpu`4|zTwarQRf0P0I;Y>ii4|1S)dL0^wQFupP? zOud^2H{!?BmGxNOv3cMddbSv8`&!_2pNE~T)0!pja$QTQ8!5?rTNLS4+!91C#i&?OLZZD4`p!yccyiaBwX6z~B<7w@QG#=M>i zi@R-{C!lTM(r^13oE>*p86 z48z$L7r)p-Z~G>mew=9(1DY?eDuyI8~vF4WyQQz>wW+8!oz&*sz+FA zg5{M#F;0Qw$Yu?RXM0|mYf08WyFkZn@BH2$Ey#hq7q9o6m+qzrr)~-CgvrJYv*h|BC6B1Aug4RgpuzG3@%15hAz7hS4{KDuBV@a z*B`u+dc`LPa__n>{|-B&Zb=TA#z#!3HKaE7eA0fXi>|6S1;9)9tgSjD_a5O3j|G2??B?eZy@M@BU3w z)O%uApB)Hv&?J9Li8V?#$S(hJJ}^eO8?LD~nAO+vaa*}bOe8}fN>IahQD zLJV-e=a707Yk38}_RQ`Ciqm5*{{3p}sL9fGr`pPMT;nmQLQxnX`c%r>2jE$EL@N zsVPGh(F-BAdLw9r15&mcS_xT1_hIT~V-B{p2TB~p%8);GZXL0ti8zc`< zHo6w-UZ(Fr*~IcLjIU+wC(Fx{S&kjHy>>lPP|zGdxJ+IG&9-k+@M#R>S!~8&j=l0c z-~Sww%4I~U(BxUnm_I;|H=oh8_3z}%9t=u)Nz^5``j}AEA*bZ0ASUP#vJKqOzFd{f z)F&6R-bh^?e z%V@gTFP1^?lPOMHK|Nwx#3t1Jycf@b5&*UBY;^lf=48jYJ@yUk`v7$gESh=FHZ5U} z`5VM(r%meL&rRQyvs{<669>9vzKA5;kKWrnZC--!89?!6?I-*AgTqv5VU;Vs#Z;%G z{p47smgoZ8*&|jhfk#oNZFSmjduSKYU{aeBD-nOf3h&d>!fa7cS*zq zzYULt$8kej&*hHJ7q08tP!?PIgxjW<;Bozt%_{$AgIs+C0R#<$Y(HCu;#bNHkHhw2 z?~<2^b9;Pb6vFg8JEZ%&18pP$efe#M>f1>}HM7SNNxM0GBKewhBCDD4nvo;lzlDl3 zwdrbMbh^0Z1cq!IVA@WaRuD;~`lj?YbJzKJoB>^54o>EzFbg^k(ZeHSgSOGiLu@wF z3#z&1Qe^+V$?gB_+3dT17uYO5HoqVK>snZT5(rCF9nK%F)4qu(nd$%7XWw_SqbIDz zb(S@I)_90pNaMPH(Ol|Q!999@+rS%}DwvA$$kmmI66VB%C-89v2s+w-^B*5mv6MT! z_$EQ}k8RBS<#)k{_wYs&_{0IN`@hxQJ&X&u=*$;RY`=Q8`j$N_3%>mEKkx1TeI2)M zQqCmIa1~*^YcEk%?`7^?|9}78|MU0yb$u6XjqV^7|IcCn|MwVvBXNUrrjD_U-btC| zCZVg{yC_f_E@=&rm2|w=9!5TH_0Ds+&YbD>&C z5YhCNwA5sSwdt18YQF;S6pcA0Y)b?tC~`d+4Vy^Q%%zw-{LTOq0k&~X_4Ym6T+K}n z?>g9OkygSU{n70Bg0>)$Yt2)2od$2$mF7*SovZfH<)Fuy;1v}cF98QU*mGz`39ev z<8m#rXA2@8)|J*HKzzb!ceeD_ZaiY_pxhidWiafzAir4&WuZsc_eB!%4p;X|$UjuwOLF7r((x8n$K z1X8#SXqM7?ju~H3=V-U`TtKT0s*!9KhE`sIMi;Pty7KQQvV69@uI28;%S5x)m~jiC zV@?|zUD({B(IRx$#@;?+cdl}&6*k_SV82j>y0~on4%PH4t(N`(F!O)X-k!M!Bx5U0 zXEoKqKc07-nA9awwzW923qX~%u49+e_z%-%uLsZ6{xb{ULV05t|1x}&H1kys`TpXL zcWhA|s6W8QttsYUj%jT6ZmhiHqF>Dyz&<^g|TYg4MzG>2?+Cx9YksNQ&^uCQ^O zd~seb_UE6kEuHbB>M!jGqK=wU?oNE0clk59IK`{ZE;g`{7b*5Yf**Q4Xzg^nNXYJU zdo}-2>$p(g>oWttyd$rb4X@Zy557TFb=UTh%EH`EOPUt@eUBED@a$)And!0u+7`V1 zBJ!KceXBtk6;U$#39Bee^w7u%4Y<^(dC9&kVFKEa7XfsPN**~@Tiadt`GCIY@AgO) zImS@T)fK5VtXy`OS~*Q3axZNnS(e`!#rDh_<`!>EN=TkeEC&D` zo@+Vx>{lZd2 z5ka@8C@7#PhzKZEI@s6(0Ra_h0i{VtdP@*d*+}msp$XD^3sM5u=n#4cfdEnh1PBmF zfKcvYpS|~c&iUSVi{CfCaqk`D@K**SS?gKzna^y$-<(S>^o}_?L_VHLt&XOyiM(?v;Yrn&?x#lExi2}FMDb0j!JUGqNX}dn75(67pI3OT@{`}@ zrw&haxWU#gwKS|5Ykhn)UO*K4@@mIGmdYc8FiCcMTcSaZg#M63HYrC2{ek&Gja!lK zBBG~<1`y`xvLgv2rls6A56magazzX8=D!ntipaeo0i5RXSDw4u5@l;L#p4}u6*jyB zUeOiZOye0q{Qdo6&JF6sc9=uA=`G>jT-cB%yx~#BYM!-}guUjya*YwH^bik>WLdwh zXftrH!U*%RwaB8KOJD|*UjCukvb~S=v=rIG=xj6H*cVS;Nls{48wxBt|I2v~E)Wv~zU3WjL`VxcA|tDjIl{LH2IDz+-) zu59w^(Al>d$imrKLI!7r1_3Cdq<>O^Yik)cheQXRUfuY{^0CLB(=-EHQ*PZA#bog4 zQH;R^P-zgU;1^G^Ea}k*G`O|4Qs9FuLM+=gU0zobCH|I(H)CH-aENPYelc8&Ni9%` zKJ|cvZXQ@(&neHhF2>xU$`y>5%lB!#3huA%6z#9&oXfQz%Z|RUOS6rzuBBb+e-cSE z#7MWkY;n)V_`Wy08-{SCH|SRue{J(JOSe!K=om7bq+W48S4om=`_SuJ+r7+eNWoyI zskU83ftFu;8!8?ayhjc(u7U=A8fNpFW>NhQcg4nVSVDXmh7@Q*a2pt}g%8KvN5r{w zy51*zU^f-`v7Z23$sK0E#*DVllTA^D$yVCI3$y2scUVrx4E96e7Mctj1Be4T;o-^1 zZMS+P4A%GQA~L;cmt}LyRYusB%g$YqbuzepWs*@%_LDZ8c>&u{oS$0|Wi*iqn~Kp2 zxX7n&mdT0cd+#`cD*Pg|+&uG|EHzPNarqlX5%duLgsRF!vGBchLtsg^KBz^oMH+iO ziS)GHDf6|xKPu~DB^pk$sm89ApDSk~+aNnh^%bL*BE1J{ZTWFYmUZft5JG6Ny{MAO$HzXbjNl_owni>f=6++?f zC0Dq~ZCE1pOA+~=cXoK(Z48O~2f4-{h9NFc`7sj(q@e*HOTA_2EWRS9kOCnyQArq8<>-?q?3arjO*B2j z`p&uay^TySGR{`H9dlCJ zzg8!BpMTWkYhI|H2GkrCyA#uyFN%})LdDjNbRr}Dk zTtsk8%{fUrC^~5*jKfPrnzUUl?|>P%)SO+tUlI0{ee3a)ClQ@pb?3#SKlJ5bHWU_o zz-o^JZcQAdSX*P$64q>#b1WaKSI&Cq2RvL;WvE3u+Qf|*N)IY4LD=pfA7c1Q9ZRWT zl23gn!cKE`9&jQvQm`io#2hH%#T$sjWrOs%@q+9A!zCc`4eNn=SB5HGW9g#N@WGAC zn~8I^kxkZ?kL=+&V%oh?pB&E*KK1Zh?l?%((CUj{_(3`AAE7{mmw(?Hi@4IEQFNCz zP)0ID<;vRFu~K?@xmmw=WlqUdj6C-<7gGS?sGJWT8tYq%f55VyUEd4c;^!HsWZH`I z3XAoRC!set`xsZOXGa=0IoTO|1dOM%@G+D9rb-=n4(xCU2X?f>hX5D8G^UQ8usH89 zJyoHV>n1<%eN}Qgx$nS-3>(gq8B_9P^7J8$S5k_l=Q+u#7kY?%uVr{#Q-LHEVQK0D zw|uoKGy!MY7_lQ2-)?GKiktJk>EcYD14dDiYh|Tb`+8P7HrZ*{T*W=Iw7QC#02jg) zgBj?V)R5WY^g_$eLM&6BZnitanL_4J{iv5w?Ky=Az=(-gJ}k=V>hu?Q0-X`DaoFM&uRvsB#7he z6WN!2BoAl=l)9$$$|is0LeG*YYqP=gy7mAGN%Z33_Jw6lrAngHCT$yTa~?yIFNZC# z@eG?4tifYqPq#Hr-}54jiPUGYd(^6i``CHS6%^PmNQTc(xWZ(J4xX3s(xx~##(p{EfonGj$Shx67T244s;AYrRVVAFPk}*6s46{)18F$MV93sz z7c3$1-5h}sxR;Xf^xq0NCwW+{EdyNJErJ-3zI(7MCx_|fOdivH>)v+Aqf;4DEXiim z?(zzRl&y7p!gX5iW!*JZ9FPP~&d1^?sGXEf;#aI~X?lS^bmxooLZTV%wVZ(0g!}pX zZ+{z_qRL8qnapZJw}f>Nr`EP{A3hd$G3na`p*t4|`U2_v5#Iwa8?#P861CEwBUFlJ zY+HP8=vYR80O==d2*?Urx#-qBFyPixnh9QnuY8OI6%ivHMc}DcED`);nH@JN}Uw>0#iApXKZT_w0+!@?fb9wph<;F2Ab*Z0_jM{5X?c{ zZKP)d`Vpp{OO`b&&$qwsNSLH-Yg*|JV+?d#pCF*ptATI#kvtxtR~L%V9W8gz%(^b6 zcTO_;eWG){BmDYlqd0|&9b)Ox|`zMn%~oPTgVc=yEPDkD`$un9kFlxSx}@8Q!)R*3EcTgoLY0ec zC{?Y`M>xAGPYMIFIl|r<^fR(qGU63tps8u_ny5gNAI8PEmWb<_e~lR(DSc4@ltmE$ z=BM5ijlYcAQArt%`tIcX6Vfn!!!l_(u}QvFrStF!uT{}-T;0wEz!OCUV{jJF%^$iu zSnID`J_LcPvwi)E%O3DQbuA*+A0l4nLWy!;lMk*gS3oi77;E#($jE5pIB2)K2$h4} ziO2RO+dnp9EaQ*lqqV&~e*@-Kr9*c395ddZT9@{DD|;b$`vM?byJMY!KP6n9J>fFF zRlRK9y8QOm`*^Rhl$Gk9t5s*LoSKc&tQ%6nIpO<5h9Dg-||Uol?| zSDWQI7dM-r0o6`AJ-+w&hZz&;f9COd{r4W8J1Xk#iz2ebv&TvvXbbffJ}MP zpOMQP-p3^+UktXDh)lOf&4eR&*KreKH)N*fR~95^$YlHXM}O+ZWBPk?dZ@y%0?hAC zJ~7!lLC~SJ+yP=BQFdF)ebuS|Jf;49^!obmq}NJke`=>*DqlZ%c%Eh2?djv6viL#| zUpCa8@*daU7wGlprT?$GnLCALFWymtQYBk}znyQTw-}E{Z72NQ5f*b|qnq>QiZBd; zaFN1cgt;)SJLeITAy|(x)EB0mZLAhP^3%BX22e;&-?Ur_;50^UD~lM_$dM#imT4#| z+kH}*iVSpmuJ%Pd?d_Y7*ZPObHoULJ=D6Si*h;22547hyKELukdxkS|^cO)?QRwqI^y<|Bk#H0k!^@#()POnU$PG6JnGGK=cfcMpe zptVXK65LK_+a^h>h*0IKlR(KP#}-De0;C`|9%-Nf;|t$}l<9P5K%p01z<8L*=*@e} z)pE4cwCZmN^$4WJ=ZwPYTbxAO$)Th`G5ciy@+rPz7nfkbZMTu?WQD$@qy369#CR-<11=Cj*uSRB8>0HmY*-yKwu(wf)FK{pz1!nctw2 z+j6EW+!O^w3(POG-yOMUfO}#kzIjg0NG4PuLc4SCu?%>wSG!UGJ4`1M!Kv zx1RILLFVT&t)WEDsqTp^{!}-26|>n-yW4e++|c1(0ud%@W$))|XOGe` z*0P*DeKKLxk>wbE4LWVgv;2_&1F~oJ*p^21{Zxlds{p{Hu`aHHH8W=MgK!>V{`2$G zrGrv8JJ%7RN5fpD$7FjL1JS-{aTbE>r?7N7=M`>j6#s6k@@8;6~StW*nV zd7EInxypL96(m$EN8JG)wIP^gc|u{bY|9WK1x zmQhz)*1?z?)0)7ngR-1Pb}$xE;H9xmAg|eGBmi_l=NEY14bKyOI*!ap5H_Lf8!b*S z+YxHI2iss3_r`QuZ@*=CkIyG-8{bbcb()nqZ97Q_I~n`!PFm#qws2Y<5S&6O0n&EY zODvD{`{>nk7;x5>*7n7TyAPnezHs{3YbBzt;tUWSJ15A-hzKER5>C0*)oyqgEvuB2 z)w^2OyXxabX|*S$e>|%tM}sH~%O9&9_Hz#tNw#&MRk^(=V^hdNKs&uF>ZOF^7h>AE z|6yD#D&@9^eMJ}O6t_}#*w(A6!X?xT@#sj9U-)yWVW3QLTzD#}WdOHZn@GCqQXt9Y zSc|lnFzD799a7x zb0n}FI0LvwS1R)*pI_rBnG(;U+qYt;%|TE`WtKbuRruF-v+SN1>5RfJ%?9vqYy~AyaYxy z;E*IQS$IOn=t;t2MBOO0tjtBKYtCodXQgpxVI`-kh#Wcrz(Y|r{2%W#{F;WIp|9E*~uq>`0(7GIwqV0P+3!?6;`s`H}km@geH#wa#K> zn}}=sDZl$qUEKrTDq;%b_(qn<4OtR!|z;>=2jprgE`0-v08QHA05d(71x&5I53;BlCe0vKtPO7Iiys(j`25@aJ209Uk}~54U~q`L zpn%O}IPY!aCcp+ekH$FQDjlquf#5dswM`^{d(`&YCRms~SHE4NHzV$GzbXIr&Nmu&h5;PdY25Nw9k)Zu zp&a?dZHJ;F9Mr)W*dTi)D+Lt@YION46OZ%&<_<_ZWewgU3ht^U=fl@#VHtF<;g@w& z-bn204Cd`c=2LH+?St+MU(Pu@E|Ge#&6F)Pq_tC6n7Jr6uD$P?pH{M>x_+bdIIoC1 z;OBtMJWYjP)3T!ZvUS7jUQU}lkn-s3=Nqb7v(UBPp4t*n03tEd|6Dr1>4SiCW`@Mg zdxkyY+R8~7a$^_|O`k!t{OE)@`<*jbS&eBypj>^i?T$-LSU3rq9GFSfQqxttb46mMj?{vRc(daKI zcR1aRQW-kGPGl4Rl?Q~AI5ak8rk_N^E_eboL@P*6@*&OohedygGSREbI1mTz{HWs7 zKW5*tH8}i`CmGH(6d!l;a(e>NS6YG>TSO^+;<@#Uq~~Mt*tugCo)cUpr+%FHm|7o9 z<5Q1w7pY5vG{)5T4Xj-hj}v#&BE7VGQaNO~@s9OfPrHj_UFnE<7DD2aI!xI1%<6{B z)Nkyu&TjI-!AHKwEk3+0%#lZTKN1ToPjoczwN2ellzLXW<0d~_p?|7=cL0ibabG;q zxMsV-a?@qgOQFccC{53}sFP3I`pHO2Py@ia?9%B8_t{L(EOL)TM#@P>JZeMPn3~T+ zxUp!Wk3x#2?ln>28?ecNwhl^IwoWOy33$85371{#$2HBF8IJ5#cPPPBJesB`Ust6$ zH1FoQjNeE^aGlpH0OqV^W@J~)mbJ9Ox9U z+8*t>4So$|>IwFv=@~|RxYx|rx=gduKKpVt5awB6ye}(FMg%v!yjEs|T#&2zWuojw z0I%Xgsl8@YyfY|9+wp>>NmBK-Jt)-4h`>~{BB{ONDE+Roo8F{a`>Dg5?Nbkjs2Ji0D~b-O5K>ArT~7flmc3- zK;~~(XFj4lQm<~ZZgA3xG{_K z{mil5`$^7zd3ujjSqncwInQOPSZU(Akzjy<6kn$2(8d>CVSi)KnN5LILHfh2M9wJZ zJrGTt?{QrGTbebmRzOQJW?Q1#)$U_Jpfu;v^eJ$KjLOwb#U!$gQ%syyUw%l*!c&ZH zPR_M5|3@XX)0;AT;+=Tmk<$10QG#+E!5=IuAQ{H`!DV{LN^EX+&CtH|yk**>IE8w= z=7GKh=9!+y21X|pjf1>+xF%2GXU9Q0N-p93kM)2sxiry5EdF7Z@f=N&;+2siseBb& z{78(Wh)V8TEzfvCFKL!E97g?0ApRm3eh1A^Nln2KiO3W~j!NRMl_v8qI;|>2H2okT zcN4D#kNPr0IH%15>6avR56ADkHO(d4o}*7pgukSljbAH*hP0gdvx@L{rJZZY->XD` zg@aKkq~{e=wb2$3d3Tu0snxNl3e8e`9-{y>zB|VYJ&afKWK>Jv+auwU(=5;5a|(@M<7; z{t0~99*txbvrxNYn`4xf;erQ%zB}r02YX{%hONYs)wSxX9nKH#>to!AAW_kiQKdc4 zu!F-_Nau!6Vf)E6bAnq<7#IIX?SPs-eUQXUO%m?TWrMZN5wE2u0|kYM%ExGy-)<@g zT;w9!n&UFvD6|ZJqakk^i1$77Fcv>xM>>V#0E*Bu<69TsYHUcsQoz#2#JrlyLEc*^ zOMnnpj?-Z~m-R(wgxdKKNU!HTG?FBBEh;v4j6q8ZYE$2*pwP{WdHRNJoGrnJqg~0+d38pYr%6G z$d5+sjPEa>!tp3beY$t-^XVy04U0s{Gg<+=61aXKy<3@_Z7Y4tg(*PN`SU=bJxpCP zhjwR@7?2P5u)HCVLE&<265>OvIl5Qu13aS$%@{iC>8)@PfgsET}F9Y-q<$A{Om!zMCLI_8qAja8GFz596EfVU*G z-;oy7+rtPA4eqFepb!c?nLl@Sd##srKB_FzGb!&R?_7qPP{j zY&!jva{Ef-w);)%jzpuh%MOC8HX4|;Z3MxDO`C>}JuuYjqgRpiqGBmb+|1)rje~di z;^@=GvlC(fr7phjn^6-udUkOgmREASYID8`QiMI?2c(FTGnMgo7`&)3QnLa3^9}zE zw&yh638%%m^IQ*V|4J3%D&@81lU47T=hWR7qN+TrQrr}Y7>NWIY23@lD9sYgXCO)f z9}`70NK;b^brFcYk2Lj`!sBl8>lJrwX7+S>binvs6*G z`m6BnH4l>{NxBBK`fJs&@meL{=amxhqv;%lBe8jvCOgm<6dx~pNjmIi;_g^kG*Ebr zU3vgXp7YT1Yucg!KBQ3VCY}2}@D|2q{WbG-SM zzmoT^FxPXh>6L|#c{mIDT{vQHN?KJ@*Z`UTk^QNe4xKi`cksZRLKkID`>D zSlP^VdZ0LO$jckJAEf!e*fL{T!1F?F z#7_U*B3Vv08;&jeE7n$2X%86;{Cd|bsJ*kZrK>qR@uo|EZ5Y<8$kbt`9U5cbtT>rAHVZyJfsLf_u6hA5cu~VlD};@Nf7v>Vj2Nd8z%d{A$qVQR`k?c zIwppfU0htAGD7?a<7TtaAyKmlb{(UKS7;s%Q9MwqdSB=HM4(RhhPJuAS7h`-h5Quo zHKzGH;+k;O7bh**h9MNTd@1CH!C;ZgLHE77ut3-`@e~#&fO4j)#ABuS#mmYlj~M8H z6=k?V%s!@!I=)g=s>if>?nh_-+SkQ{rDDZ<&}&PBw4gD+ZVKi!tQ$=%ai9SZqXR}o zX+PThC*ni5d$6?hq~qUUuHUatPfq@-f4^jYwEJqbZm)A0iqAnwNs0*qh2}@z`0GOZ zxoZ!5b8AE03HD~ah!la?sSM}&_ETqXOnFq=83`4UaQwRb&$ZN-?jgSfx~l_i5jFSK z*Pzbx`A6Bp@3}9pL4e{b@!4LbcmFqSh*o|a_q~HvCS=C<7)x?N!t-6*G3YUS&*%UB;Y`M4x{dnj zukICDfl_QMexbm5ear!9cWOx$MEh*DKLR{AJNQdO*Kc#xeY>Px#Nx{*vJDg87da z|IL8@M?wChApdGI{;yOJFCjbBwI4K<|GTFUS|a!@>-opYp;l^gwd*pPT*f6QftqX>Lxgcz|skkPbRd-fno-o=ec)3m2|%x1rE`MaO=;dc89Mi1B|$XV=u1!+!~?4Ob0^cm5HB< z@Og_504(*>|Jk&^WDRWE`<|6nzq|lV^}10r(IHR1UHzJNi4gUjk(Il00s=p_qr->G zrKDSdc7+IvYPf}s%}~dMwmM+Cn)ef6>_v%^+z@=KwSd+J1$EK`tt+>6^o%bYtMN_* z)$$*2o~H4Biqdt!e{`s*-R-2S8eP7_uONS{PwZrCtbkTZhJ2uVa~w^=aA15ct=qbV zv1c1BGVnU!di@?Z@1sX>ZM9GE{0RPP2PkiENW!;^%|ql#!lhl0r(jtjo{e;xn4#SA zR+mgsU^tKjFn;8K7ib(IIQY+ou8T&fk&_9tgfL80sl!PO^fkCx{?Qg0SrULjGX5CWw; z4!~@!Um;_JEyMYYm_KI$#NbHFVs6am+cZK`7C*Lq6CGKzSoxUw)&Pq|f^)C7nY&v) zuJ)l#4fo55Nr=Uj^cDi8B(4t9$GLZ6f>+z)1<5t(d$d548X>a@(eE$Yv1=Y}00VcT zlP{b4Y`CRv@MF(nuSUKHVfu7VQ9;}&#O?80RdJ7S{+h5HioLC|RREjMCet2+_VdUH z9_>JTa|rxe+WH`6w$?dtC1aH&m5{9%iEX`qc2~19Wakru;D%GdYgzTvPv`c*Ltf%f zJzG>q9@6CMlU5Mt+k@S=A~!W|MtsMyNZ;{Gbl=NIvkRgCR%ajfWCF&3eK5=KIVMC# zg=L=Q=n3}aYsXh3yslG?|wM_g>12$Kc1_qzfTM(u--c~bqENsssOz9dwmsx zf+FoRUd9@sB6J*Au3agnYCOoQ@*y(KdP!Xuy(yVZc9SbA7pHtn=C!({<;?G5fs~iE z@ho(@#}8hJosD}66H1O8_V*RMsmHfbdG+({_E=Ec`g?9z0Z%;kyLAh!Ez(GLsB)=X zaDgj&+cER05kGz=?yT;So0ymb10`t&&o$rf_MdO@d2sb^%H6EyPT&jzBHrh@i-| zV*O{EOm8QF?xh#v=d7)3E5XPjT$!?wWho%KeOVjhND_3d47p`aP2TpR!Wll z#3G-J&lHlZ-KrM6a88n#YWccn0~ki3=m?dq)N@B-t3qMC(YC+@4k>oev`;)uXd~8e z0@7stGftce50u|40IHi4{9)qX*BI^qx9;HnKFP@q9FcYDH5 zP1ng6+m_|$<`ny1!E|`&)z?SbPgM6iJCO#K_$xwd!;{W$qKxKg66`>sbe!US5>ZM7 zpv{vV5}D}upcZQ{shgLYv`i|{hnpM)!Fd{jMy$Drq$Ji9aM|jtxKpTLx6j730xeO` zsuEqXq9 zC9<|=3Q4ld@p4K~ylW!$>tgh?$Sr-?Fh(Fa+=eM)F#OGH&t7eP^>rUwZM>i>{RC||ZTJ0$tfR*-Uoxrfao#Z+ z4UtSuHy2)@#bsVl;KD6=Sg^#Ic6*^4DtzxBG{-GMZ0N@%h$)s&dz7yFIdA2!t|98R z?n$sF$~m<1*o*>Xa4&oGX)Yt@`gFN8y}8QW^%{0$FE9+n{&);@AVjOAfLBQi4-5-p zy}I5Jp9QMITVu^$8Q~?T-YCgHMdX#XCsv<|6ABl>GR`Y$lQ1`5LIYd(+SI#$8rQf=|xiDZqrM?hGhD^${Efxt8p)YLm49L#y{|j8$FMrL_ek#m&sVcRGuu6 zktqN=pnco8!p;uLo%%k@$;afbrfOTz03tQVQG==Y0{giSpVWW`lt^P=pOnt6sHEWq zPX|)GIpJL;=@ff|=gMa{vckF30;$?5XB8j}&x(sG ziJ}=7G=b(PFj;P;@OgxbIZJ67j@GBIl}pNqr=ib^WAiYhc{$SrH&n}XChcO13a7+i z|AuQtzBR(Km8_q9O?_|>Ls1epD`%PLMC!1l#; zlN}nFFiGMQwg>Li>bwIcC$#iJ4Oo_MRCM!ZHs!-SLY`b)I2y0)z)F6nr1Na?fv(^tr zb=liLkpD4vCEo{_y8`#vIr#-UVypmhXXTX7!4^sUsumz!0WeObDn$$pF<6;wSSKie z9*xNNGpLg(wRmg2szn1bdY z8TeWA_eRa<#31=NV1lSTz*a3qGeE%4i%XbN3v7+1+|Dp>Lgoh00C_G<&%nsMt4fmG_Fybau**)Majht5Lhd$!H>}{~_7M2V74B zbP7WXm*k%)viyL0UV2%;#(`v2xqX^kcg$*@YiKa)IYb-RM#7h+NG3SW)YqNG$KwpI zT;A=nQ(LIh*pRoNpLM3OTrzK+yN9gx$q?(8x>6P!?Ot!$jli@^u?{07^*0lxdL^#D zU%bfGN3d%X|Mc?2d5l0cD1aeL%`KFi)RR*-BvZM5D&HhJxUAtnau{U{-RQb*r8 zDzk0$Y1pWp=W54THHe`>g7(nPg}yy;0hpYn@p8nQ-B*uy1O)1w{!j!S=zd^mPru1; zS0!)tpyPFx|8MY59oBiE;n?Y_FQqWCAN$byMwU2FYb zQ&zb-BA>@)Uz5IWYpS&q3~5cCtHnqX4J6Vd0QG=(YpVZZI~Lpfy=uKGY>-G(CDmJq zvq4wCRSvYm0&(>oM9<&ZvxhMbXwh&keWt#%xHh7hOUl=_)3X*wm1y2CUig5esp_(X z{xRLG+3lREs*2i5%<0!O32frXIzZFLjJwNQX%-N<7Q`Q7ywg^hLoh`iGgz1@rt2|Dzhlh4pb9xpkY5JHa zx4~tWk^*YX+Zx?6Cw*PrajCL$WDgpA&S!RQN8JrvRIjJoA=?%9)N6tukTrpAGbqMH) zlSVl_Y#b%V z)jI4Ns~-s#mn9d{>8@9cmR2orcxjYU4R*8%k=b9f7d;kq)-Te9!YZY>8pz*KWNb^w z)Ii49bp@_7$O)O>O;TzK)zyb2+On8rZJ8el6ayyXKF3Kb zE5I+|Xri56FQmP;_oAs6S|`eGjL5X^}vtNlb0l%=jR^6t$f-HFjWn*E;~7C4pflccnJ`jK!?g zD4Gd9wFZx+cP-ychA5^C%<#xx_Yb&H=CwmAP6#C{*~sAUvp>Iya`P}uH2R{38)7y0|Wp!1X%rnC1p`E1{H-FO%`1M&^MW>wSFH&FQdO+^23Ar`QHPs98 zPA{9!Pis{Io@XYfEz(e&f``>`aIjxY@Ky}24eMinw>X~f)7fE=b_}#}zYQVs!8)#b zue(G3bsXbf&Mh2z9{=&HW9^8jx+HcCpM~GjDf-gpX#%qfW-_t;C*Co8R$Tl7PYBzP z@jIa`mAuF)M>n=ZRJ)RT^zX+$djk>aq|=cRB8%g)4tKp;Y9Vlat06{n8Nb6kH?|Y2ZYD`y z?-n#B+PUD+vqeSXnMWPcr0mpAbI8IdC)`>a+$%*mU8fr@at6lZ?y29lbrGXx$cpqs zV&~R9Y0)ExmNx}()6^|s=phAqUy7iyXgEjf69vY{yHDbMJhSRlLLfN_uZFozD#GooMY(p*jvNjG$FDU=EyB0Qv8PkKk8JbZR# zrlQRl@Df*??VgGGzH@~Zy*;M6(pa>{FLSp~q*6rtknmCfK6sN9gPK+$+>JDJkQ;HeGr%x)dK zsF!a~SHbER zZGu@FTsD;eQZqqEA3SGQ3S;EbXv|&Y$hhLxG_k0dQ;>UoqW0S5e8@9~6T=jil=j+g zC5kPxQOA)}IBs`^Sh$)lT_+=_h@}fLZA#X*W34^>N;oHaZwm)B+L*)Pr60HkBtObt z>W56(L?mdoECwVL3My?X)IXf4LYe4j%HKX8x2d?+21HCfC~nTzED0c%9C7VJ3mtZ4amZkFgt#)2TR=Nh?GT%XMAi=A zYmgS#3h3|sdXf3UaT!A5*+ts9nc3v`KABcC<42dg^6t58ex>?$;;f)!d4vLw9Lq+R zbC`B$fXre|*6iHKeWiiWAq7FLBpUCCu}{fOn@LJb&O%SN>%{iN>`p#n_S0{jY0mhO zQ}MkWCi1>d8^ie6LP6AN!U6D35M&Wu3v<|BHGfxZYR^?+rp8GzeNw$P(R=qvvCHQX zS6atbl=Xv9x5=I5ZySsG)rt3}BTUk_ZHk`4fYLSN;p4o@FaO&Q9f!{;K}0&>ikCY& zd*#E;Os%AcuZJH`Tl?}!Ac-}%6+CP=$yHm&Fx_#& ziP87^UZbxH4EoL7xqy_YX@Zn5p6XE&KPQ!y?<(@nFtI^V!fyJSo zDNExtzvb{XCC{m;iy^;23@e>qloTRwTa2jM^LQJE`3zQ@nXqYxX%!XdKV}r}Ha zV~r@@zNpYBW)Y9l)1cdtNYO^wvhv^`*IG4&o=tjYFuZPf!cR#vQE_*0wRSCxXQtY# zEb|m*Bzn{l^(GnbW$(tufYvZ`*}XV?ZX1^{%Ar>@`m4_|ZesP;1CK)2@}d_8*rKA* zj<9-id|_txttuPr*7qbXzkpFs3orGZLP~$rAU>C7KW$K=RVLDrT+~mKluJg8kgUUK zO}mx933OS@jV8ZMS^U*Q53HGv1PQ1#Qv}{q@^?y9aAI(6Wc*^pFS}28-bWtJSs3)We)nY&xN9*u zY4p^mT;>VccYaH|z?`VrrDbu3MNK18uPF6Hzs$`gV!dD1OdFn|>D^V07d;6!RCEf| zSVNoI22#56%1s+(PmVi3JN!+^7!%rh3!yX$UR_A>+j+c*ODdN;#D=M@z3w*9PQc7V zjRiVcfQ?qfSIZTcpe|8eRJ0FX-*lbc%6KL_rUY4eWPtKj@wWZa+SiQ-zrJW@gJZYq z#3W~Cv~^U!CQYL~Ca+KStZI?$+hb3WGLy!mXok3F1`}e7>BwUc;Xtd8fon7b{#SdM zhpyX=1+GhjIZbQ}wnhr>%IrekT!$(GRlBk)i?!!*b31Epb(T%)3y|_>GM~Y_D?8ye zNmnzYLQ|f{lDx4K<&U5pc-1oWg!@k2Jzje=wzkkt5+S>XL$?#Uj-|LN=oMc>OJS!O>e|0Y3*7F}zuG)8cj8L>AKcqp#$4eK_(|p2KKW^ni#- zLY6v@{+4~v)#pu`Nm_E`(T|5!9}h)%K>NDu8m*^*uDp@?^?v>bWZ-t=Hf7m4D3uC0YVDaO! zZ%5|WENjqiIAS4stkGyeG?RLMW8}3}F}Ygfzqed}7&U)q#>UB6Pu1yJAzL9@KkZcm}zq7gjyq#y@^sgYd$yGC__PeUu zRC+$n5o)Nuc<**tcc_%%-=Bnkx$Nr2>s!+W64R48U`lp<&eLMiTabxk=W_jGD*ky< zf3Ho-Mf7?cqVsM(>7kk5?Jt=r{L3x=$8ZI>!4(zd;vz=Z_&?6w-^u=u%Y-sA(l}e5 z?Eewfjvt>Fq8qu){CDg4=jT)A1Gem**Z})K|MTzm+y4ZxCN*V``}^Hu-mYJP$BqnH z+qZ~Q8G$vO=rWG_yLJ4F)Fni`jOi}prr2}Ja!H?N^<)(BM1CB zRFuEfaf}PrsP0#Ao|b^5lVPeE=~`Edfv)-4ZI97kzr1Dj>Cu%*21N6%W|GHm)6xKC zM+Xe!`;j&&s~=>tzlx8etaP{B3pta#gAs+s)M5QD*^j!IUKhFXp|i>p-@ZCseQH)o zaX6|FT#t1f-C5H^fX}2`YTv62R|DQks%|nr6wfX1*){)Et9CRylCNhkY=-q_exPXc z^-kqsdJiEg6_>pX-Cpgi7FE7Dg}Yi%RwllkqU8H#xu_);2wtPkQis!_1!D>9(s&e%piw<3M5doTw+lA zk|Xk~=C{6@U_yr)5C*Ahg9^0`#rz%{l$t(<-*m8hr|rEt{B5;=aa$(BBd_}cq%dW< z=YuQdbH;_UbHSa*)^^sI<(uW&VK*c}XQcVX(>=r%d=D$fe*r~!Lo(`9JF?j#jirS=lBC%Tw11YNDQ zwA7XeRYHjfRkXI0kk~_QB}jt^N$}o2?>TekJ@Y)}pXaZ)`RksX+;i^zeZRlY{Vd;e zgT=sY!;8wanMsu@t2vP)zB2TGK|;L{x{X5^OaMmPn_-&ZsMml1f@8)D@%opX3M zE%l~Y46o~pP@a^L2Q>&@wBp=Q7A&5;GK!XFm+V(+mvrT{$NN>6-?2A@?p^x#R1;ls6}V{qQ|g)jIc2^fHy~8Y=X3tPy(L}ag-@I-@~&-aPmDjZjX*4 znZ!we{JAG_ZSezRbqBYB*fe%@0c3lLNE!C8S@xTPt%F z$V?UvRuubn!xhetvHQZ*@6`;QsIZ>e8m%UBufOd86;6qnHo0si%iT(e@Nd zf3__mTzwbRkj6ZVUG<#(;}hVEw`Uj-U&Rj(CPMUUp4{&^?vR=$O^uK}7l-B_zoMt7 zAX(Q9vZIiltTE~l3FjyL)}Cn5bGPbrL5#X?p=V{7(Xg7#(Nu6Gnn!@sI`rE$BWkii ziEBqG4Bftd7i0yU$$_U7g?-8nGOKv9vI$-;d#Bbi8ECs&LVk}7rv}o+(txsi;yVuG zT%hegq=FEG)b5Nde;e?L4^EU&)ic4*{I_^miMyf!A$$Wp6-XWowU?0^FyQyM2LPD; zDHZ%_W%?N#dk1cnI^DY@ub;>xTX{Wbz6m~;mUK2OA#izlN^zT(C;w?P?o-gr?gZi7 zi8o0;dW@Bi3=|8}$->pN^ZdnC#i>po2zpCWP9K5~BG@{~rOAW!$z+G$K;E%m9Q5&^ zX|5N?%V(_FUgO6svkYyuRef`c;2A=AXjLC>6N_^Ko9>A%4;Yz|+}ErgICojgKcP%k zp+9g|{jIr$@hbyYHSGcqSFx6&n`Mz8*^k?ZTwvf%p4yib!Ve zdq>sp6r0Sw!){p#bNrv+6ulm%^2(hVy-#K$|c2H zY^>J!enneOVrG&TlOWT0cr=0al6<5~EVQ8t6n+ksm!bMrIYC-5_}(ZPgt4}bTvLQ- zM=UKiz`RwW@%adYUp%dSMh`A^C#K|r^_0_;rPk!*>}|8 zsDhQ1G*WQo=a5K$44+;Vpv+`Z)~8DK4=mK3aBI8_HikacbeSH5d?wkOQtc{FZ;O1Z zn>#^ZWciYALrZvGO*CpN+VH}Gx5*~e z#|8rzb1HbY5lu`-vXZhZW@8bco_PACyT&0N6kG$D39}-wM-C`nPCmQef(Q^eR;8_t zX^5^{V^@}mt(8HHiFug6bv3Ak>(%0)Q)+_WiM670CwXrxkIz4)AY-mtcoTC%Y0n!^ zBoI~49MmxL&S{uTz7oKxP6>%&>O9Q4%qPYv5`$42FY=1y6K$D6olpl?N*iDsi><*l_| z1|5e>#QI6Okn&cza;wFrIJx|CQZ)~>wMr$fu`l#@=BNzT%U1X>%@n)`Q@$FweQz)~`BVzV!ZgP&jPIK0 z;?c;(CrZ*Zh!O+5OkMT@zTx$4aP z=ikuPVGa%_D)RUCAq#&V3vYv10!uo8K)6(=tZ~B;Cy-3^ENz;HG))IaX5xlCSD_J> z2jkU!l$6^%Hq<3s?!rJ_t#LguG?d{5i=S}UFa?2@j4FQj6Gbb|VBKf~I0*R#XQ2nM z;1Yysjvlga>^zBEWpK|PShdo)Uk4pXIA%+fdD^8?;;j6Ej2E%wU zNUa^MT)|sa-QlhjG56`0Kk2;Jdd-SG?$3P(v!0vQowBrj1JLZMM|q626#VHWU~VnB z_?kECBF2Yvgnk5(?5snajU^4tw62xjjU80lNeal%$;6)NWr$2nRH@*hogCty1VNxYZ>y)aTqF%NRl;-?uv zSL}g3^$ZYlXp{bXT};4l~wTGwI|@MRK=I|wf6-ld?zj&e=dA& zfXO7^K5Uk$TaX;OvXoAms;AbdHNG=k7ZS3587t9`n|CCxMqQDtqu!1`Y7@KqH4S_5 zW>6r$i+)p-TMejEGmR^-BPoJpROEP;$-hFc8cz=NSFQ4FkonqJ8_|9>^fAwfap}i5 z5;RH%)mkglb)5LR(I{NKe6A;{K5>-h>6s{Xw*_>wk+*LsF98Nyw_U}=b7XkJ`)~&} zWnM!Mkdbj**pqAVl#yPqrP!p-yTbU6O{_lQm*DC`7`{C%uTRzXz&pll^@F#V2O_=| zzD!lzA@9}~RUJBAGS+-&+~6+5{*0o@m}_LBAf&mIs$lj(U@LwRy)>RN6v!_3W&}l9 zMjjgik}a?!_G3ppS(Mhfy)^UCEg9=)Y6x;h)-U~!M&s@~`H{_$5peZqnV3}`TN2$H zHhi7$&s+0Zonf6FsHaq~kNS`rtQtAH36#lL<@;Y_ph&dhaNsCCy-Ywl>)zHPW;vPd zMxS-2)<&VD;wNAAR+rswUkuo5&b#5>CttSOyN zuzifFoDwybP%EN@w8TqV>Lv~Tc&x~0qGX;!A9P=xJ`i$4{SZ#S@zi-#Te8Eld1iP5 zOdauXsPEM=R9S`0zCO@IhBmT>6(|4*1tIuXsjZ@YS)u)7H!Qw9@g|9e#{2+A* zK34kS50?}SEW-A*YyyOoev#EgFI8uy4FGY#;FSOr=k1BL+7~`7{=$eBWuU8Tjv{bG zS_)2i>XFm$U)D&UWg8C67ox~c5fKZ!3{Anyz2*3by81tq!kgvj6ONyR`rXrj9~Fr2 zB;J~9Epuvn4AB)-fqpX>_o*D%y~FDUMX}+yekxvNjVH5p*SK+aZS-iPG_&`-wPxlk zym&ab5KYCOl*)JpwPuWFExhN0thHMa#NX5O<3Cu`RsL9SY zss_rd%)XM}ciPU>C~WOhw=n(GT;(oF&USsZA@Di}@_PlM_QL1HjSSz~hf;t=%E^wt z^RX{UX51YWmBW6J=EdpF9GU{CgLTI`NRK!<^$0 zS9)Vt5`fHA8HpJd#io8gOf@~z!M;-4p5&*3o*&N$SfJ1TArR;81YaEhawYIIDQF#| z8!~aGZ@za)@`wCMVmc-G0Q&Xpvq|n!MvI^dvjF4Ivb7A8coVssK#Oe-Hvpem+CbpD zq;QF|pkMZ&VlGtPr~cHxl@BP+}+rNHudI|_`atuYFf8ExG z2iCx9<9L|5mZn|MjwbFU9|}Qk=JvA*t`Ags9ltSHBXNWPV!!{H+(a+|Avl z7^U@)ezZpmn54U#M%CCHm-+etuiXs-Qq`ZnKurFV5dFtv-d1&Q55vP^a{u42;hTcZ zd@nGvxh+`yz^2b2NCV~wF@It6<6KI>pk0`Ve$?hiPhn!hh%*-*RJ>7A)Gfg0lJS*u JMQ0rE{SS_(b5;NV literal 0 HcmV?d00001 diff --git a/assets/CodetyperCopilotModels.png b/assets/CodetyperCopilotModels.png new file mode 100644 index 0000000000000000000000000000000000000000..71f870d76d69af0aa2f5f0f7df6553e78cc3f3be GIT binary patch literal 51658 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 literal 0 HcmV?d00001 diff --git a/assets/CodetyperLogin.png b/assets/CodetyperLogin.png new file mode 100644 index 0000000000000000000000000000000000000000..243704d972116ff66ee0bcf67c5c69c45517cfe3 GIT binary patch literal 106421 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 literal 0 HcmV?d00001 diff --git a/assets/CodetyperMenu.png b/assets/CodetyperMenu.png new file mode 100644 index 0000000000000000000000000000000000000000..216cfc46d0e013472ee4097f78fc73835741f241 GIT binary patch literal 84986 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νE?!@sXEm%;eJP**R0x&ce?O^!X6ms)<-Wg8 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 literal 0 HcmV?d00001 diff --git a/assets/CodetyperPermissionView.png b/assets/CodetyperPermissionView.png new file mode 100644 index 0000000000000000000000000000000000000000..5f136592a6578420f4fcb87cfb1a7d8e2d5bc1ea GIT binary patch literal 49050 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%0A>im9GR+i&3MuID}*S-<` z7pgy<-x4Q>ytn%&I-#4Iq<<6qFVycDi6{S?k3Y?i32*Rc)Bb}B0*e1z=WNm|?*C-t z3xUFoZ&&|8oGj7*t+V2F&d-lG+RY0ru`c0G&;0&Nuz%+6y&CjVP-j5TKOm|V{(V1o^J={wg6C zCo9V($wjx4*f94l!3M@ILzdZPiGuFjiMcG)-X%hzn`^rVu#L-*cek^HzK4e^Ulz-n z6MiXOJWpNRE<>J>jmj=}DY9Rdh)W}WF`8Tb#KbQ{((xMf&W!c;Wx+HXyuTW}Xw**6 za2cYGC-({eEwW2`5VXiAOC+}oFo~qR1i8Y6CpSGg?8Cnm`9DOHK@0IwY4jDD;%C|1<;4mJ%fVF9*FEz_DBd_6=)U!X8?dc^Y zWmbOsoy$advm~@X-TTn{vt0BI&_D3y5gsx3=8=q;UbaNbGtu4^L7X+l;4&fy$JzQSuvVLLRx86b3&?d5 zVF$3@p$FoH;f6vAo-Hu<-cMsDN=o(bBD&f7Ta>|Bk zDp4@m)r95vgJkmPW_d-YPiF_;NUPDR z2sHof0BtsKyXcVek=MjzMVat07^l(KsYC0;+u9c0J@{8+l>KJu?M3c-G{2<6XZz_2I5H&uyD|NZ`Asewq?;xb7_RP90RveS@aEz zee>ceL};$YzmZ1xw=RQP_H@=cCW|kN==t4Pmvv~#*9V;nhW%=EcNrx+D%_qlr``ob zk5ImTQ|D7E>EkRlXXz6i58fqBt3-or*)Jp-V$VhF%j>)y+KkW`6vbN2`fJPnpA_S5 zJ(V6ttN{UO%BTXQ#sdnSiXP{S3(D%Amo+wWzVzg}jw-Y96o2ap-es&MUnK;;b+QGd zqvW4R$mh3?C~5O@%edLI`i#Lyz&8VNTb%{Z=W@r-2Ie4J-|atYeY9N8P)dnF5qcQK z#GV#B`)GFj(@1wVVl4OpgT&Rv`8S`b``UJQ4qV}tPJ8BeUui&r5O}B#AB&7Th_&GJj;pN}oWOT+K5QTMVcGo2XmXo} zQHZQG>Zd-O(#vDi5ZIS*f@Wy$wGvGFaCOF|b@fZCOQWsf{!g{?QdJ$3H%g_zaQdE| zqM7)L!l3Wu^P2SzOYOf{KvI=|8^h>E5|_c7L>cbbz@FDNo``YCXo09kxj(342y^owpAEs+`2xo7rchq~T7p(Hg^o}LilOhkaMiQhLz`q7BdM632s7^8rg{o0x50TcZP@tZ0y zy+A}^N!H-jz!B1kf@w!Ki(j!{Jy4rpDUYPpw_ek^ZD|7lG+CP6PL-Z$Fe4cNR=5CN zZ27e0u^y}DaZn|A~P=O1=*R^9}ElNvj+8Qn|wt_d=p zb(*btL{B`=j6WJTi`nZS8#SoOHGfNfcZFBOK5dSr^8RG9@mu2Q({I?Zmvov(4LP*({C_VzOEmDH=VEB%t*K2I6A2bZc%%A2B@SsjIN zZ@!~X*-^y&twvQBglCxd$Uyy+#Gmz6+ZQt(z&O{a;)fC*Db>(*d2cmpX**5e8co@v z8Qs-PoKwY+!Vpme2`AVWHqWMb@$pm001br^N5s(5ISkPlT8`G)Aj!FUfvEU`@Nu9`yjKi2l)3|Gj$*71>`OEDa!`}P3B#mf676ixh-PeS0+mAltBEvtYYW~%xg;WV3ZGYDf60SJrln4L|sPIM^;kR#Z6%Q`4Qq0 zesIsCIdX8feM4#mdSUwoZfY>YZs$0|NY3}J*Y7P|u8(SMaR0=mO2kkVslPxlZesVs zA7t}ZO(I=eO`5irJz8qUD~KZZk&VKz`UJVW@2!o|o0UbuqJhCqBA{;R)`xxDXgjIQ z1xN~KT&9C59ckUm&HYMTy_8&Amw7@OBnKTV9rGg%k<;!StY|Z8%tu?t|C1aifrhkb zx3^4p(2~(B@f<4Y|E*lnhs4K5WBH~KApe~~e*uwIR-M+*Q700sfTl=CWRs`*`|bV) zX@CY-`^4yyE|9P^~V>?{k z*i!{LXdH#%YBPNZrTP8p#>U`b8t43!GgExgj3wrP@Ol}(|=mFgme`1aXU?_xyA(023}k$-UKSbe{;>!DZA;Ejr& z911eX1kVbClGS=r-QN)+GnQn$fi%c?P(*~LfoT^!P(I=-Q*KHkk?FTOopB7PgCtC= z5Y_V9%wTiSOx9dKCwdq@eX<_(BT2dS>&T9iVrca)_IEI6!qW@e(No$AKg-y7UcEQF z;-!gLcvR_t0_Sd8$oD^nv^5YDB~qw?DnYsdA80qngG=><`(BS zyCn$v`2jNIDCu}jY*dK)G^8{uFzw9%05!tcLv>i7d*&n8lLL09Qk9zTrdMUu%{KI+|A@y(q}libjK(f1?FN10 zB*byL0>vxpkR&shM_Mm&HtrBGeg06;v$K_rW}vJf&0?a#sAlebf(-2wAfkHym8N3f z@M}?8__;os5k$vw)JKVlC-&FxdDJy#RSE5gk_tWMP zvUD>?)hU%}#!ra4u;HuQ^w<6t-XGs2CW{a1={1!tZvDO6@e>3M{>jp6 zKdLZY2GBD{H)!zpUh%Ntr@G!|$H_ez_}|1bH^9h}*ME<0<2H`G}mm%yd`0VGljl(~3Gp9Rv`u=|;|Ca231pWVuD=1M{iuQbWtC6h`LS%3n-4X!Wv}=`TFO_2Hwh1$CPc#biRCVrZsh?wG?*ZoOsruPa2Yu-#2V1AiC`_>$KSKu-$t&NmVl~HQ z-^zg;cl!lJPb7qmSEnNOK`kz!9ZTA~)?rQT;c>iK)W0f)ByU3lw;^ByT49-`j_1#l zc)0UEQO2L29;y~ewDxU4If?zHy_O|h_T*k$j$}&JI8Sh<2>WLGNgOGit}Nu_Zd&TR zlY$J+2U-=T0ZpV&*H;@Y`vM2@&EaRTpBEY2Qmr0|04hmVnJ@S6)gi9K2KvH~(utL% z6mh#z(hNs>ZK4!u_0=zT5sNAS@r!M*G1ozOP5|zUsFPQd+xEL=`nl!D+*3R{<4GS{)ojh7RBa*(hN#4@%~e3eijIYv(JDtFZ`Wd@Lo- zAyF}4?2Wl4R_*=jfo;GA#04D^q#(JoMt!ijDz$y4V{O99t6QF^cu1rCh>9=YpDLI` zHEE7XU8!Eu$f*e{&8<`StqqFywsm=tS7p|++8*j)h$Zy8kf$Sc%UfUXDH5S{Sgf1V zF61=b-1t1_y1ww~W9__WbP)7L;sR)kc|`g+dxM5kP=U24E#*|%eV!`?=s2`BwYC&> zbv_^bEM+y464=A(JM;Ac+P9=O#(aVtbx6538J9ZHS}9|{g^)aj7R;zdG|=>#kF>K2 zQ%d(n#KMj1Gr>?aiC(J{xUQomPDr9Y-;uLz;dp zm6R!Px3c|69+~&S#6ZNJ^^p2e>~FT7Fn|*Ie8}6OdG~{zXr_YFxk}?N8J->sHe;_^ zq!W75#=mf+%uXd4*#OOYBF{PttK8;wHKwc2yida64p9j^5+1#S zdai5x)+Py+FDmZ>n|>XxaJfo{X*-?8Wj69v_K|h*M6GFAlqwTGQPS%3MSLUOe7zp8 z3%8NPk)4J@4(r?^K-5)gp`)jmE^L%yQSMZ^9?ZZ>jizQjhz*_(Dc%&WJa z3A_vsJ)s_XLQl3ei#6NJwgo!b3Wt(8D%(-*FHT&wm@@%FcJG=NJ*~MU^34mxhDD*+ zhZ~1jZ&xpDOGByW#=D@g4lF>|$fZ4=*Y|yyaji&I1ZR&>Vu$s)Ig-Ga*S)uAsS)~c zzE!qkZ^CLgk;ViG?6?q~i�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 B(FF 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 V!Ndu^cM15Dd@Ltc^vJ;Xe*kO96bk?V literal 0 HcmV?d00001 diff --git a/assets/CodetyperThemes.png b/assets/CodetyperThemes.png new file mode 100644 index 0000000000000000000000000000000000000000..320cf9575836abb5b9c065e05b50950147685591 GIT binary patch literal 34102 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ℑB6z{VALRx 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 CV(wf3 literal 0 HcmV?d00001 diff --git a/assets/CodetyperView.png b/assets/CodetyperView.png new file mode 100644 index 0000000000000000000000000000000000000000..735529cf9e8005e7c1de62e943156f442cb5ce16 GIT binary patch literal 178264 zcmeFYcT`i&_CJgu*d9bhML`4`h?FQ*2n3WS(gmf1NH0l{5JE>pMMa7rz4uP2iI4

(0IhgcHE1NjdSBD*Fnw=9(l|`Mx~p&x{;2$y0DQq)Y;9`iH+^SyM&}8M)COb z?Q8Uch*xZ34@9>_9*Dm9bjtD%u|~d_c;oThAC_zu;^!sa zzPw7~t|=}iKl}2gi}~WpXXYYKVXbFsY9>9M*-V;hKr9Ba#XeTNYRpy6rdD{{?9wV( zCnhSgXfBiE=)j9JRh*NTAkl9x^79`(m6oy(S%Y31d`qlL9%0b;_shgKwiT_}vW;HF z`zcQ8y^6Uy$8C1KkCScm#Wpo*{=G8b@2@o@cQuPXR)^@J9+HIuNVqz+!eA%_w-0y{z^HTCVc1WD^co2otdTlU40py z!p@)h9e$08Vy%J4yfrSS7wnUs??StKVtZEXKm7HmbOLZGW#OX?^O}mdTHDwBAF&tD z|1O*@y8C#RPp)<9(v#i4NRAp+F3Iid$77cq4@v6l7Is7t4Q9rq%!DucsD}rPGIM??|NBNgPWW{mRD(=vyUU z14i_uuetM8IeEZLuCW|REh)WO0LMP}#cy5SbL%*)i$kvCdaku0-ZuAxk%h8qtJz|I zXJl+eucj1fWzqy32(swYyau1}(>59m+4Y4hdOxJY6m^_^4X=hFnsghAn?q z-+6CI(h#`Z^AcNf5PEog7Q8(Y_{z1&IoKjFmb`n}U);($SZ9H)?%bj50O*)Y6))Sv zD{K`R*6;2@CnGXBsstW_%K6Ji?>n`%T5O+y#*}YIlqzOpo5WobcgH-jm$-#^;IyOl3+rMqf9+i_5jm1?5z`-b#%| zsbR8N1IDgWN&>huxYIUK5H)mee;zdV$Y9e*TjEOY9H5qgQ?()jd}5 zO6Aqbukf$8?bC}N$ci+cx%STOo7}gD&t%rw*AJ~%q^GB6rpH?+r>jBgtqZ2EH0XMV zduKg(cwOd9C;(y)zAol27#X0q} zGUpU-t(sxWxAl${Ip)t-d^5RaI$7ak;P&=??y`x6sT5og&O>E~n_n#REbn<@U?OD< zDxS(yF3rtgt2)H7p-nlIm>ThwD`8hU>cP4c%gX8NsJ~4l6Av6~QxyAOF$| z;KId)WP(b|O2|dxgTw+t9Iyo%h zT$O^eD7A>f!(kIsU#y=twPF!c&XvQ~Kc^TC9Oti}DAdfyre-)%8+ekCn0Ev0CmuAK2iF~f^7zw#rU95k^5 z;5(7zlf;thUliyk_0PzJc0BG+m}~5QysU-L^qPi|d$ z4ws)cZPaa?4ZOU$+v4q9@ib%ozW+|Zxj?F)xWBf)S)l61;HJ*H+r~v|J>6z8u#IPq z?+5xj^hL$@#To&8-d^;c_-_A^SoW*zr5u-!q;kIJ0KWVoyq{fhkLM+K`0Fp=E5fe? zYvktbi=Vst{({<1R1H+6R*C)```O)!xmtWW@)}JW8?*I6A_^NmUi$#8H>x;BG9Vlm@VsL8wa!h1Q^o)QiswMCXinH|??U?2fACbSNo7M5Q)3x)Pi-_}OzFWtJUz2LF zG#7KPev~CB23paI@2X)Hj&xZh4`-HrkbOWr!`sOd_eB|vO~uCO<|bY0H0$A8L;&Qh z8ts>c*|T2SCFUeOPl8L0+cY`v{5Zb&-bOqpK}Q^DwuHQ8Er*>%z+3reQ@7;C$1E!( zt(Xv-p2q0=6<^e(rp? z)@qx$s>QzP&hWRwd3}|FlLbHp2Za*~Gt;w81vz@v<=LLD9s^@z-U0s4cQD{cv`uAI zSuw$2N(wJk@mIMgQo4B3np&V#-`Lz%TUhX<)>oYpA&3e9S|f+NR4QK;W*%f9Y4KvO%l(9XL)7SUBUhg%5!j-3+ZJQ=zGQIyUnY8 z4HGzmJBd=Tl3jDpssD6|J>q&q=6>onnbLYF0vK~UT2@F-*)s4$K>6o$?lzN3lZlfe z;24iJmDNyeZEL~Y1T?Xk0?Y}_UCi(595VT4^14XGfG_VN&>M3NN4T6z5Yic)YVb?9ApUs8;ny|o)=*{i;u(8QdOV|ug)Tney60&Stq zKYjA@5Fu+NAbL}+0uEw}{oVwxSN+^f_Z^`CIgW7*0kOdTRp+VTV8Gh+HfxmUHqU#Z zTgv5r`8&mXh~XKl{F6iR;kMz#m}{43gl13Twy%+50IjbLI}C4SE-QnBoHtj#z3uOa zBAv+OX+DL!5NfeiG@DhA)Uy?2bYu!GCgY^!e#7>ppfABCyY7toIja`*5#AJD-mB5cGNZ?{V-Jana{P7{Vw=Cq zR;<7#ES0=kR`gfu-udLKzq9-r>6y~`FuV$z+PpLyJMxgxW9}EHa}9Oagg?F9wqf)1 z5Eqb9`EA+^a@)GXHo>-Z$$4boc^J2g@ZX1V#h3d2$l7dX`?7z;L%t68*-#JyoVa_~NLaBF0<|5xd8uWXM0N^@@fUGbiY zx{l7l+r+`!$q52;h5CH6nm&NYhbG)i&3(-E^^_c-U@3b?=rboNKkxy9KDdhRrsSt`^&d5q4$^;T1Fj1Hql%A*%2jiHBVl!@x0A5E)NQHTS3$>xg@u*9 z9i5dPX=wec`N1!htFAsiUP=G}0)dc1$VfrGT>y6!6%_%ur2*2?k_R;;Vg3*wdp}7C z?Akv+@?YQ4aDq8_yLtJzK_S9_f7kvQ6z-#P_3Gap{rB}xI-UI7{--Ah>|et=7$D&9 z62Kj)+kpT6_CZtSzjKw0-29w8Ei~M~2kbfM19V4DT3Y!Z4gMcR|I_9FYHI#JO{L`( z{x41cSJD5y>0_9aw>lJj(5VmTe`NEoe*bsjzZxn7{vP}P;>AB1{g2!OPJ@ms1O8iT zpyO3p*T&h{)Yx=1?wR@>S|J`uHyy=quWOr|pHG|)Tocw*J8NuOr4^O>&Gi}7c(cv? znS}qQ$d1REc!ktt1^y^WkvshQa_>(F|HuF5JP;NYEaGY>!v%{B`I27$5!2PI!>&EI zDzkTpa{ug{_~DP*9#l5<2%CFe)HS!BT1t)l@cr5!2|Jc6Y|fppZ%%UJjuleWYCPc$ zox|jh^yN|nJDy)6rGosBal^=l{X|)}63b8xZiC)ex~s)@=>tsR{(^lex~!Yg($y6ZJrB6Gxg1O66s5Nr^wr&3F)V`eD$dS*lcQqRrJ6#EqH?e8+a~?E@CUORQ!Rtm3i=~XVSsM^^#~9 zDf`dtj#}Y?`aRQgwl<%s3c!t3p{f{u4zu|0X@Kd^`Gi~I*|$eGUqzLb2B>;XDDAR# z+e@N1`&;k)8LyU3J&^Odv;4U#`8VkzxaYd@-n4N-)@F zstWcw_+?i~^oi6+r9acpTeo7}Tb|`Ux2N2r-h&6&{Qc~tMY)`p55?Zmseh~`Y+pk< zN)|HBa<>_I{{G^~_|6DS=qiG0?1z4^G!!4#$hf|=A3ErGS+@>Dgf;X=&OKr8&9%Wtac@B zBO7c$;B;~D)ZVAl|EUrGN4*ZNu$x~Z?So$~v*7E|Eb#Kru?8oPg@XrSHLJD4(DY;) zVDR$*t*YhQz@gIuaVZM6cA-|%?dy!f$wk|y(39xi#JsKQ;MR@54Tb{skDPZqQcdaQ z$phmu)^4|U8RHbpHRXU#*Gp@L4pfif=T&1Wc)H@31Z4M;8}$w!5s`5*pUDnD_eqj9 z8`@Exu)x99YO9k1Lb81`3gs~sbkg1?gYbt&B>NU;F4rwC>gM=&pg}>e;9TNpw~BA~ zm~?TUY9EceyLItEq0k`Wq1T-~d?N}EGYIXH)% z9zJ&=xl4KV!hz^p?D*CuP&=@U?y-W}OLbawWt7Uzm`Q-E1pJQ#aMbaku)9|-pD#j&T6KrQnT?Y6RESX59}-|GEOFsZ`E+GF7|}hsdih8~`0~{AdlXdPA|x~vAG#8) zGg3%-7Z=*3!HRJ+)IMcj1KVb{1APf+62KVM@krV@ey$h{QI+&Ko|;!zuxp04+exg+ zyBXXSU~>OIgfst~G1<@9joDAlQJ)j0YAU-_PTnFYr5%hjqj#>Jllq+5F3u#pjINim z_3jr!+C8<>?bl#aHq9bAi9-CR8UT77z#?tcvZ-O$s`l!OuLk{;(?)V`n$+Z zT1;=EyV29ewua5?Ca7TXI8pwx2YVn$M)M_~a(L>Ba)EY9uzvfcl8hANKa$U=j_^YE zebUv|#oyzY_InF{aFjj_UEW>%s~V_1Qq!DuSzF&1XUxnb`z*FN!F* zW*gZzN2_j!WNA1durxU7s5q14!`vYZ1(b_Eym3QD>OcxfdKS#dJ;-VzHQ*(KTaL<; z-0np=bp@|@&8OhRpSicAua3Ur1T!$9&bLCGFAqn{Pdrb>IDw-rU6}5boqfE>75Eml z9&XY{i@036O1OWTg>=fYbFzq=^;ByHam9F=ruuXJwZxwq{%tiXeFqgH6&VdV698Pgt4iG;lXFZj?T-3lPnxKLO=yK{d6r@Y_v{G4 z-H^xP4fnvvqK1>er;Hb6`?2C6Whq;4hYrabNqhPkVXXK}PnOkEQ<>eU zzFv<@lp=VuELvX()-H&c-ZAxPn1XIZGNZ0OP_uB16LbB?jg$bTx!DN_Y{c%=u2aU%!!bD zPF8#XdZna-f>|aBwG`qK{f9_JDXcYur#s6wSYMX5=IXuHB?xdHKrL1IIRTNX<9Fm6 zoUDrc&2a=#Z$(k>{t@6C$%(Oug#ayrb9cZ>=r<3b+d}DRjeV;YMg7kl16!B`pZS62 zms?;A%-rTU<87*m+$QaRi$9CmZ%<%R-GBx+UYPJDxYs!3%|nDFkEr+v-lL)n&P!M) zFI!&xGlR#*{#dxmbH_XINz{?QbotXS))Q$_aS<5+TU=!;S7aNR&qG)Kh0OHpHWB#o z&MkB>xN_JP4@!4)&?Gp+_Mep0Kv)cOm*anGM_BA0V}3xd<@<6{MjY2Db5&22#BRDi zaySmIp;#6b&C^!6Ng8YGPXND`_s(~j?z$K3T z2>?1!w9Z~<-u&sLb2@&vI+UNM_)9Egz}d)VV8ioN zL6nY9Ono^OCFL5VMIO4?;szLynmBO4p^yV>4@MDHP}F-3+#_}1JqIx{d=c!1-goVd zGqfCzKXfXldcte@-vnfoszc=~Z{Kt^Ay2jRRlUXw>jRTiP^|iC?q7B7ldFm02e6~y zQy*L3r}Iy_EKUJ!WAc=Dzof2KOA-`2|7;eiWiyVsDra+=%j@w^_=S{wmMxU=ml0Kq;zGn7JZ*@V}K9SrJe+j>RtbW zWpCK!lX=$OZt^osx3Pm>`RhC;|JVsscY$iwWguw!xS5^wEnl4o9-+kXnas05-xJ2n z28Wy1V!7FqqAyEv0)0E@FP>me%0p5(yT>|KUW>C6>T7%}E?pdI#?-k_Yj}H%$9*Mg zZuGr!%V7V{yb2i0|RM1=}aIxSY_!!!ta7BH=QKvR|ZYArWxEz zeqBZ-@pg8;V7!E`sD5mg{Kf?jX-e+Xg_Wu7xHA*1*wq>ByB z+z6#9BjpzAsPkvLKKOS#B8->6yq8RqlZN@mw{dSe?G24ey9{=H+Li(ZCd+mp1@598 zXF_(E%&fyG>guE|SE|QkBhF|~W@obngty+QI<^(`+cn!oo(mM>@8_{JR$5s;X(y@pj8v$8G@O8a6|3-q24QfTck_&bBa1Le4LECRv zB=MqTc}PNo?vu_h;StZkc%!Oja_^gZM|W<~6eAAq7s{c(&GLh5rE2znfGq5)9!{sM zm+Ga{eSf$$Z_n+8QBgH6gm@>TB$;_36Whc%R(nYFE&({b-nUsXV*`os+Y#govFj7s za%xyNbLo!G1usmN&Sslo!Zs34qjh;;D)lgLr}oSWY4DPUf5?=fxRc^~I+)Pw&?L^6 zOp+258{ABchD|8;@sHU})_L9%kmu|8R^s%fm|~@nB=csA+PFkwG897}i!fDw_-$id zTsJGF8^NLe?tA89N=9+gWqs@DJ9N%y=lUHpDL@}k*NDP zpZqy3j&;P5RH-6j>m22&D@_xg_8%H_p5a(ZF1tm#%VMNCZK=?^CoN!zE@D+4>px!X@QgP3Dfv?Q9NB z)&vB4!W7I8jH-hqIp|&ktsaajZxBQ-w9<#P z`&5FnvB2l4D>AC4aXN5skM64YK))~9jVJ6i<5vTg4E^Em-A;Li@$j^l`(a2-Bb733 z9DS)|fPQG-Z8#v^mDF3ykd4ZK`q8!G(oEE8g5{rGJco{-Z<59)q^o8(xO%ynvyu9z zEw3;>(`WlO!U1sPkBc%gK?{@m8~Ad~l$3Of(`pB?NAA^`#$cBMF6i>r(q(Krn^D-m zQRqwU>_Yl!q~H)U@uET>*Ws_2YbNnp)P-&bxsuL3SYb zSu-_={mV<^_Qlr9^QoTZwpwFG_dVf=-BhVo!=$pT04?WTDqv%hh)8Yene_hII+G+| zyBoes(@pA}&Hz3HQ1avFOYK0C+?I8}+0}G?*W$!O7$+L`UYpIQhyxO4zh>NdS-Zwa zOy5{Qb`fIs)DnZ0b==1*L^Ix;jBkxdSAC@OtZCxgl9;PR_Wg;ewqp$P3f_~gFwnDG z=}EkRR{?yrdi=_4()$loshhL@SK+mMMHyg433PBBk>YTmfm6Qda>Hm)U0WX{r17zg3Z*Ur0HME6Kotgpo!h@&>{gA_ z-uE7V*A+)2OHEomli#?x?mIiu=`m9LGeH4D%%s8YTw%vw9VoIoqWDx^H%;E<-LF73 z1>i7I!KKIQmUvIU`XdHs-_9bj#(9iXgJ>Uosz*9|4T614OZ}mXGw~Uuv5QUB8Shb9 z5jT+c)GsndYhJJ_i=u8Lk}h8rQl{p6S~c949Rn}g&BX3H_c~3C+Qsxv)S0*_&Mgw= zg<^ut&hL?e6^$Ov_5$~D2qzK)UL9ggisXJ^_oa=au{sR0@)TQB`E_O$St~volKRyf z-Agtf1e_y=$>2AcfF408!87ABLCFerfGR`iEOSJVrfD=cf=)~}{)tqj+AZfK(-8@l2hM--aG!jZf0 z-&t3R*Um5^&O(khJ%(*o66aT-Ay!H$1k`w`i8iXjQfzpC!zE>Xk?gdod6TNLf4^a4 zT@;(yZ@1}$uX>tkR%u*YIHkJBTQZy-*5fxP=g+Jb3OGF;kmeT*t4x*G?EAdnyOy}F z+e4Y130XPvsK&!=<%97!;-~sP7)8Ko9q(btxihDrGolp%TXC%KflgnCn-krT&HQOH zwpZHA>F+nSJKom|I5DOQ78>1u&n<-q24cX=lDP!bE#lJixx^u;yPSLabylG7R@*Ph zQIeGHV@ObL_*Y`!LIqDza`Vjzz5w#32JE`~IN|n+O@vdAVe@4wRK9P{(7Vfb`C~1>nMviDg#`F0CW~^}@b!Af*GfC{MP@w_sV<@;ZkF z-G-*^u8r$L4F_2SUMJX3&{|_4x;JURDK{;Bu(o=BVeC_ZK;--m&v;iPZ&yg{ug`=& zFhEz~T<6@r<&}MylY(2UneOynP~_BR7rWSWsCnw*cw9m=2$Q~04AOTT*CqMGt$Zm- z5QO2)aa$r!V;%HH(aDt?`|`*oqNWo%u<3XCl}Da0Gem>riyi{1A-0u&Ee}6Vm34ob zNtm3jtRDsj-+yz7gGZQlA9-WIiU#C#8w^udD*A5PZ-h8Wc2i{~o^D1Fe_Yq;^s%H0n&fT|=k%j_^m_ z5mIYYrASJ%#_C{6g)DTa<-}dPx@Q>@@-dEas{uhf{zYlci<7e~c0GNPgryj<7u%@6 zUo$G;Nc(!OT5OZN;Ezm6TugFe2ssDiTtoNC1u6bff31$|^DA`F_wIWhAj4|Ksh7C( zRz=7|^dQT%wP8FKxs_Amh4_e-u}?)Oiub@hCu(d!DGeP zmZ|iVN)l4MsH5Wv&S~vS@J3z=w5tNwmjH?)_+wu0^?y)!=CW&vmYGkHfxHt`RiuNT z6Y9Uvc9xGc#}T3if1f87Je`5;L^X`f^n?QB$0#1+;fuB4r7i)5o}^%r7cyNo&+_(m z-!gXmM?#E4Pipxe3@$fhI-`&N5#%D-NRJImvboZ46I@`;B=E)f^IOqY#H^A4N$N~` z`=rl!FU4d0?X+d^EHrd(lzs^wjW^qPE3rTCu=e>4UU9n|`iX0cEIBH=N886T;sH$> zzEHd+s8CWKW^rXX$m!b9qoHKAU{}3t5mwmZ7~TPlR$ZyHiC-;UO2l>L!S@6e>a!?o z(Av47Op-?Opsm`g%4(^Dt)m$DZBB3`eV}Q5k|_UuJfhiv^dQh(_19TwU#qjg$_-vs z358}YA;L&G%&cLF2dM+W1zFvo7+n5E3}7K=aE9kU2#`m!LWug_ixfl~!}V#N7LXa= zbO#!+^SHzYX^XIpJ%Z5tte7e* zMDw`S+)x5tnUuOTF?C|&!{bYmTi_(X+~tcRJMgZi3aW>lU5J0MkQ?!Hv@(tEwD3k2 zX=jz$(2zA!S~4TAQF(DtD>3;+zv#5BMWxUY{1v0|!`frAYi8V@!}Pl8IF0>A?eL^_ z{Y)_eVl`T_PCv5Hd$B6SR&D0&aOnOX$D+!#d85xw?)+N8yshE2(|Ou|ZM{@e&8$}Q zO%cXMgRMHecvnh8_4BP-3HhXvW_f;v9hev%p~jy|nZqF59oT6-9zA${Wp`B|)QMzl z9p?}7VHG8>nVC2Pr2B_2L)#r#dj#oC<3kVT|r0_)&m5tXrkoG$sM`~}f-Qsg{$&yzaOC=eHf0Fx6`*0;# zKh|sYPPe5!`wME0PUxX?pQb?ko~5dGptSm8YqMZtIk8S(sSdJ=tK6dT%e!TaL-uO3 z^xk{h0<#DTD@vX-8vY{FLht%?fd3|5W<)%^b-r7&$K@k~IZG5VGFGh}u^MoHzrkE_ z1o4b2=Ivg-yS&yOzoSx>X=Q>XOHw{&B1JPy)dSE%61rpkAJro=c%I!a-1q8J)f7tI zTmL=PVym$0{>dz~v&v&(age7Ys#%FZz)4rZxNboOH?wzl*XZ4|)LN&W7V(9?$UW`% zJSzz?i_P&Pcn18!EY+?Cw5>#O+=26V#2|w$$Yx3mrXa;Kc_cCcd$E;E& zZQ&Dy-HAE#Y>ZypJR_>B(37xvBvf;?Y5oqc8pf(HIRgZKKkLa9>B_4_-pWGP3!pnM zuq3LTxp_PA)>{*yo5Y)Iu=#<2bP1N!({Vz0=} zy4)pt-fG;Ndh)}cd$?)K(7SW`c+;SjHcR6^*gy~uKP@K0Y2`IfcccsP3JJHFtUwpY zTl+Wz$ha=Cyr?=dF@^A?t0*n$ z+neh$cAiF@Cz#P8v`Vzzc$R|w9r@1P|1zc_C$)m@EGvCNrop5Saap1J>e6&#Xm?_P zQ$GgS7iA11*O%Y%kgxwWrcOKSxjFl-4zfL7NteGcVwH~oQDqfNwDe(-H1(meZ*^eX zM)SWT10Oyw$-7OI)~}eOwLl?16;n3X4zPRiCVsQ%4*E-qJjf_U4*#Qfh%I|)`(6cI zh|r=3KB2pJ5Hx6whHX}@-=q3QXUik|Y#*2(3A|!YDH+jDF*(?_K&|w%4y8?2!dI^) zP;Xosu7V374S%+AdwbdWotjBzUjEH~UGBmrLWG~uxvACJs94)`qWO*W_76H%P!Ue= z{W)&C){6!&c<+pIk~+lE)57rOB+lST&W!zx2<(sEm!o9|Ys@C$n3xO8IGs8hbk(SE zU-8n9rZW~>U+y9Ukua_HH~z*$i>~xB$(n%0Epyy?pFMiW9`DYMMbY<{hV`LyG(l`g zNjU&%uWq4=Kp|9ALGW0tAJNQd?&}6HF2PEpC2a3;q}C+aE^i`#U(KDE`)=pR(AWge zUql>uKa9hcPm)d<)B)L5O?i6Qu29|<&Sck$Ev=@dKH;1K*&zNJ72H^quw9^*RND1P zV=Uc=*!q?hbCy0yaTjFbRfY?pb|ZN4WvL8xp%e+yZnKz3OXoi8(R|VP*2YVPB=Dqo z>*XDd$?2V(-C5V$i@|okB*6ShJRR<)$iyI0%!z`{(5NI$0h%A_^HVh#05Abl1;D{1%4+eKG=g5$&k)}12A@$b z3$Sn4rIDgEg%|@%05V$E&3xwYW9T~S{kYt6bc`)q$wdc~kq}hqfstk9f`oiokq2|V zpJ)JhVY^>y&S67UtuDEaqrBQ{SL5)P8y6nXw?g=tECFz`aG9`L2JD_4M_})283Ws! zjP7BUEru)xHtXXWQS?l8@Gyw~w_^9o>=Gw&(%@o;+%~BF@D*~um_o_B)t(ZMl9OV@ zwU}kE-tyD`W>e+p8CM7hJ)DpJNEPKCOTX(>iLMS?Z7OLVA0t$Hyk{_1inAgZ{er%i zsPsAGP@a`c(P?TU>z3tMhBSGkE1nXFGW99_)P``K?mcmM^r;c^Zs(6}ej^8l9sBATH*-HTc+D8!=#h6e zVI`5&&KeOwjCO2b!k3jB1&R-bLJss;7IOT)_}Y8412~(Jdmr3ciM%hW`~3IGP5-CW z_=y5=cj2`9OxfO9YH(&Jy&zOSMY*Z5nCnA!a~ zQs|>rJ-NUqdfziyl+X)lF+oXCH~RHJ_H#kd$~|YkA=5ZZy~#Qw-?XP=qCEx~7#d$U zA25;2rAXSYv29{I!JZ=^0AYCP`9qmJDL_cXD}DZ=ylC0)J9YQ2;#NF2EL5X}+qdMXpS1 zJXbl*n94*>wcI@do0f`-h8*ngq%vIR;W0AW{@2c9RmU{Uk|@Uu6Zor?77UROp8qts zxF6cQ!B|S7ac~7-d<$zpQ9t&`;dKA;yn6-Y#hyfekpShCx)4|xU-oF>qR*e*gVj)m&ERV7pmXZL$P%;=H)K~2Q$?f-In`f>VUKxY@#_UR+}3dNLq z*^cgy@xT%Kn;z>1UFS-PPp!_~iFRjzcq%E*BCeZ)qboj>5H+b%sRNh4>ec4js zme9NfL`_b0fDp02Q*mtJGOxaxBV6ZRXhR;H->WDz$~hi_uiW<=ZZ2IHCE^DPi&?VS z22euI3}C8mdL_FUeHzhUL9qd%XfHeEPD$Iop9D-_N^y-`K11E9@~8t|Ay-EeJ=i@) z0SWodQ$zXa>?)^V2;5fS?M#iE!zSx_7F}f|Q=RF`RyF;IC_+rBZ#n#OG9#tlmU-AF zOE5m#I^06;XoGs%`Rb)q*-`*?dFdV`mk57L)yeGs^Q!gU+7fx)EusVrg&h%F8NKzLE-_#7VnzbidKMlH7X*jkV55n0EK5&3xmw(_gM<;34CYavG|=6BNQjFebcHVTAfl%-s&UR@ zSc|(yGS;eVLZ<-uVN$J3J)-L+=1hjnFeN}2!3{~pxw7sQEf1Dt(8ImH42`zeJuZ4L zqxZCsYjnAJ*g5$}Z*Fje-VdjbTW)E9ydLmKdoO;G$iTJ@jhHX8J9 zu5|Zfh>d~WaA0xp@W$GB1%?(#8l%l}L|`HH-dhCi(HpGDZ;}0%&m#_%_4Ea2DFJob zC_(9R%awJFghDR;WmPV44Z3w~Znv>u|E9!2+&hh9(~+@6kE*iLCo3}|Utg{5&fD8K zSZ+cN_C_?JF1a(I=TnIya_0R9hQxE<&tJqAU~J=KW1Pu@3Bg6-(qz>h*_b@k-iMJm zEtK{Lhv)d6BRupEcBzi+Ntb_iY)05}Z;jYUkyAr}$Ge-*zs$Lp`?pReH;&0Iq9_bq z71rD#)=nPUa6CgtA$i;wuEPk>Au;nBtrf@`|K`Z0)=3KK%CEIRxcVVn;P_N5kaUhB zdE)j~wflI7z?WJ2vmSLD8WiZMPP!&g2S*a-L-_RkL{rSG>we4 zk8Vi+&6Emf?C!wF)&=&*JSkbH>pk+%Coa&Hp8mDjOZioMMe+{9&KebftGg{gs*_GD zgUjAaXW*#=dn%5*F15HDTCAPZ)XmLeRxU#$*lu!Xh~baUkTn3UEF%NMQuwnk0xEWb z-e8~4ps|87YyP1xR|vIMi%pwQyz{d@gXW*k)dIZBZ7=G{qwy$~LE;`?$h}PF^110c zIsKlb!fH0Xx@| z4hILWndIk70%p1eC9*e><+d?kA+#s6NY0P~)ZBSZ;KZL;C!vUoDB@*;H5r^Hc|;I% zu-|I<3bazaxF_0cS|GY)@F`XHfsCkv!YX*A-fnZ!hF%~9eU!x+1(vxTfY(Knmsn4&)U}7?S8BW*NZ&WW%b5F_6Yf@2q z&joc>YUxmcigdY9AnY?+P|=iFpYr6BvcjeW-FQ}|YYY)vo@8a!WL@OD13kuQ z>_z2=y=c@GSu#JwGK^VOE<>Zoh#C~TQunI-K#T`w-6@x(;I7Il$;E&TQlIPrnxk!U zI-^<4O%LCPR2Nh_TEItLqjCfn_T*mURu`QxAp*%ll=R1& zIJHnM>aVFMlXb4Q7(z_i;uw3XkhE${seV@rGjGxwctZ34b*_c+TB?PPH}?gFoVp<> zod$YU;24r6xUe};wRw14dz}IBBcMW#7cU%e_&D_*rSUg|IZG)|!BE$<-u9$|j^+A13x+lRIQa`OP z<@ft^Z5b)cv3oG;=qA7X7UQgtis4{1WhmRJ+x$mS1`OF?QVV%zj@04B*MiIZHPZT{ z4IL`OR2e)MSWj|z1dh)0OxMjk^i)NBxOhITHBk-`AAN!m5qEI<%(uJ(-Tra&{b4UH zkdB(RP1U|V#tkPD8aUz*?7d}A8f+L!m96pH9!K&KgC9W^o1RVuR`1jspfg5=J6aco z<{0ddL0p6c%Qbhrs6dQR2P0bI!L}3fa%)5SWBnO(g7RyR8o#5!goYeX(KdxsRpv+$-8|D;j8@`>&Kmgf$$Zj1PGB19Wx zuy1n?R)@}|?W=1d8YNCPRA`T=)fTg!MIO890~zna)Yq@NE{PCJ#<*baxamhW@kE^= z;L+_f^)r`c>?-}CnmP;>jc{FRPIPc1JT^WMPGeW1GGp|OnfSeg6m9rBMv&>^BoVe` z2UNIyh`E;^s#Ac;n`HCDC166v4I%L=e7W153DstMKkkU1*hFCdtWTtS<|&P2FuyDJMP&XxD+eJNinGm{y55^n*wh zDN@%0vY;>TGCR{Bn_v}RHhB_o7Ly8OZ#|Q_fL;y6U82<^+D@YH5EVSpq%_t%I7m<= zgC|V%1Ip-CYQ2u*NtGnY1P_Y_sVU)QrgHpRw_g@L-y65yRxbBtbg!k&QqDcq>Rrd;3P$4&Z0jzwvehx0P_KOFr0x62IuY$z$I=;M0S_7n2Jsrv3B9DiR`sMEmdln z&}c*;)iESQgSD&d-h9(gZG`UhsbPp%6tcN!yF2zPaFs9kbBXP6;Reg%!~!u3bZb~o z*xB4&;ptO}Jw2wU1xiEdFOVkxW!Gz=L5qtMRU(4G6pSVy>6&wdnLgp*r2N5h@ZI#s z$<^r+DqKKb`sW92M?2%C{@2BuOB1r9QJf>mYWfhP!ZD`MWP3$zyRWsxr3V5*ov58s z8H6jKG}mR|M0C=bdO|fSi*qEviiU2W1_;vZ$-{84FSt7B-LbZ*`i89|zd2`uHIy=? zP2I;TCBclXc%%voh|Fq=HM>N7^aPCpkJcuQK&S8b?H~iOzEM85K$k?m?c%}C)B~R@ z@^=W*hd-cnONnHS>GDnW+l`gdV%=dWF|c$f}0L0sRX_hNt2hfJsVjL znQ97G2%4Tia}*8k7_uhexu+)@xzfgZGjF6=W~pfBT8H-^Oy7f}TbsUjHfKOK?PN-n zoTNNKQRwzPRI033(eu_IXoL##>WQX0xqHQp!-b)AeVbI`b3=rij}R@O9@5#hvahXK z%c#d$6_DkK+dY@uyylqGd29cNv+s^;YTLF}M3jSoM^vQS0HrFug9j1mO7DpD5_)eD zQ7i}u2%&@Y-bo--Md^fI0|cdn7D7uPguu7)miOL0dhX%-#~%ig?7h}pvy3t3+$1); z*hSEwBHsQwk%krTR9fN*i(I=ZjU0;6f(v~pjdU#3Dc3GTKMR0XZHo+}>Hx}nqBx2W zR>3%saStTkMgDe~cNi(k+feQ0_~^^$weJI?%8?_uES=2Nv1qC912N0G0Uj!=e!iYh zXO|NzWNUawY=7H}WNfgiKDfd2k5YCb2X@9)tFo24yO^m{meWsiwPC?)Aq5Fpd%A-6 z?s`lto~ltxjK}`YxC>Do0835mWKc&?GqM3m#F*-kf@i$J9E;_o|nT+P#?($D=Rvv5ToCo9|{Qe zGy`xSer7yT>xL;mW{|v-tW4K=8t+GXR#%$i9zt_C$^R0#AR4yR< z{Xo-G_jPYr2QO1g8#Y+wu@G@JLXzXUE;HziryuJ{Zw1n$uhLt zmiq+Wopye8B+zPNWM1<-GT0qL$u%-m=CzCx>`ZAFzhX5qn(mbvP!^P`rE>S-RUQ$! z*>iY|?2NRBae=ICMope8())U93#dnJ`R2(DClZ2ahcyo z%c4EebB)XWfYduC08wSe#^xRnXv?p518SQr&(i4%pZzoCK`OGTi?Ma*_z!{PxcO6s z@6uE8N5*?H? zDot=7H`AUw?KF+wL(QkU(kYk?F~&;aH`?kq3=dW#n|4AMnHJZ|**ZO&{Kv`f$5CXC z*9wi^CGM=n=A=k;g*Sn9SW`D^CwD%zxnbP-JDT}TZwu3rKdyDJwVHZ4m;-PDmV+5D z@nyy(!8u3LX9D6&Z&t4_y&q^VXR^^NTn{m{*U5Vw@yW1qHfQ>e1^}>`raeUp#c)us zPimHF!M$uPoU%XSWpCeGE!Z7H(0vOVPHyB?OhH;#*)sKE({OGF_WJBx-lu4c0%{PLbt}28!J*|3W{3wJ7rn z!gznhs$S+?@tM%}YL1z~29`RoW4#5_l>)Tc=A1#rO7>R0?`(K}1PBlZBsFCqIRQn* z`twVkHY$N&<_LF5v}Y334+Fs8TV&!bC6e|{WgCV~)2kIoxB?yj7;oFEV^Q5wsz{zU z&lP0E@D8F%Tt%(VDo{Mz%o}Sc)Lx}9_`q$s*%X?z!x}c=-0xPYRWbh3eJKA_z58-4 zpfXW{&=k)XX%X8N2Cp^`p4r)ez8sxm!V$ZkO{dH^2Zzj)eaXa!sTp^?%nN#)7O89+ zwCd5N3tA_;{%RY`w`Vwlwsb+QnU-zrXBS$Kwh6-ytbT49{Q3rI$l!)LAJ1}i1|Hk= z#>O1Dw>>u_fYEnAFJehaYdeZi6Umi(|LG+D3EKD4$q= zK!PEP-Ioj9kS_z`=0eRvZ!xN;1c1`WnO31ptbm*4($Nw?kRB>+a_a)RSEw0gDgp_djq)o zT8D><00jQ8^kSBAO8iKFBf*_<+0(?$5mU6X^p>fHFezxr#7zw*m=!{qg%I6(=Y%v% zm6^TlOy7s%@!=n~(};I%q1e7iItMK$NSVsM8m0aS&^`PtNa*=PblLd6xIf^2pt9xg z4U{-)A1Y*zRCjZ8&qltEaw9~77pB<#K7pqrdBNkHY+cI?gI4R8%q}+&-KHLd&ws~~ z`X;Rd>r_cwY!;-;8oQ4~c;Y$HQ-|rWHy@{FG-ii5q=_CyW zaY%yUybwd`hpDv+Yk^iDC;eS*?54}00dULQcfv5u!=+*A25}lzt_GIRtHX7cLyrE{c3&CxPeDA)!9HWLS%eSGE!9RGX%m}A_z0hNE{6(g1A1D z4`+o@L@W{ILqzz+y=!1$^ZV=$-`AuJj4M#@68J;*yl?>C>+tPNTK6Vd_EcBIL@&!M zf3H!+gn#ey-gaQd;aF0oPqxR(*aNrZDLT(&r{0p4MPz2Cv6OxMP@X|cokI2D$mz#> zOJ3N+g#B94QM(X-l~jUaeV2@d1{v-6|Pts!cq~qv*i?^6t6p52IIMxMQ?{8H9IQPZy?TX3+?41IP2r9MK~ z-BILZFSdwa%$kJr9&(6(QAZyh5Ne|{Q(cA z^G&!bPilgl=~zg{#@L&M7>u0%#3Yal9*!@%^@bdLmEcHVpC5w}udN-*jUAs1(Qeya&p^^TM z=--UeF@+e3X=xfJIzWlKA#v;0%VA{Wffvx+fyuQ36!sf#FNeK)NLEJZUIz(Mbq^~t zU^1SHub|FyN#M2&JiKs;F8*y~mY4s0b4P)J6?Y~38mN@iV}u)ofIrm};vjWoQ|8*v zbWMHtZJJyUr{*Wb#2U#Bg`Ih9XxXkhN2V>4-WRtIOK*QI+3d42vCs;*Q!2-I`1ReG ztHN)nD}(ofywv!`9fNLF)nHC->Djj($d_K<;*`V#4QQ{Y;Zr&PXg}jWOp`-ChyWTg zYRGbvnrCvt8QYE6)Ljd2|7FXf!8MnS_k}%(WZJHtCRWVpIP0$&`uD5p^u2ooH_i)cNX39 zr(Vzy+=|^-Fs_eE?!`l=j2E8ac;QecHqQ97YOaKj*xW5q;kZa$6vp_w=aw^=W7H08` zwRiq>^YG#|d%sV8{shaVrNq=XImzX;wLhKVUqUj>dipy}V_vH#^$Gy>pF3qCTsukSG zxp>xNz&!MU+CuZgCD%~!^q8IXUfj@_&Orf}_`$dl_kFgTz(rmykfU}t!XHRWs`M~& zb^vV-nKYSfzkrASHSNoQcYQNYa()NUFKiccXW69$_&(kBtf+Absjudc_FnB?4zqCu z8vAbtm(|D_m7=SydRn5P<1BZkD)2jOuX>{Jd#V&J@GFLR*~zf-KoA?K(hAI4+7n)7 zbw*uJOx+iYVA<^G;&0E`VEeM9Ku4x?qs$d8h#J^`yjS7 zPBhoCAW^|p7r32tGO{D+>|{e-duju_fbYT|K#PrD@c5Otzvv_Sr@UGYe|D%O>PBxw zy-wo72qYtfeKXC|eI6i0+Z*N2RN6!pkpo@6na^6u?ANnp9LL%8^4b!gFITI#O^6tc zOmaw0vpP5-F+MO}-ud*HrUv(s_qVJH@ga$Br!U8lUX~9=> z%eiB~O4Hcbv7kk3BdQ|hR*QfC^S{2xKmGR7@9mPegQUz-Yb!k>*QP8d$God%6&73< z7iY`(NBv;xU2j+!g}n$R5`|Hy7Me9;C5id zND{gqf1j3VKf#voaPrtA61*sZ)*&YP_*sFAppZ$L?b;EvGr2$UU@NlJMf! z1TI244MfF_#5>lCnEYtOInJn`X!<^VIVO5r_Z0TWADde--35n+sde_aZXNM z_wE3Fpdj(btU|QM93pe|ku>>*uapyB4U8~m4`3=lobY)4cHXLkexO0*Z*g2Vo(bB% z+2B(}K4+BxM*46gy^%l%NwDKMi}6BuK4 z0&$DV7o8C)oNDjU8?}xbp!;ShrOgORK!1En6n)WA4p+ z*5LYBs+ffK@YCCERPZ)0buie7jorJA^^Kq$aU5v*lizRqICzt;nL$E{T}c084@i8m z3My@ymhP$v{#wxN$92r+{;T0+N{YK*Mc%&?9cZOp{$m!WGW$~7u|V7TK6o)iN)Qa__L}0rG-?;H?z?#0<*g642#`k4#%KCGKRljD>==}$w=>@ z0SsplxhHpR`40ekMDAPdjDh`NJ-kGs>v8vMoPVa33~qJUS=|GoHF}z*ZtHY4 zrwQcE2}BmXImH;&;AEU9E6^!7Vpl=huSG6+&K-BEkG8`}Chk0X^R9!*20756rMnTd zO=L$xXBblO+e{U`vwPUA1Td9g4Zi^aq0$NafybOKb|QH7@YDbiDbBCxVx+{n!K(;KE<|3q#X-KxQzFSjZql z^OC8p0DauD*bcJ(RF<{Sx1U9h@yTt}`Q{Ve=+RjqXKq1fXFP;a6X;0Vp{nA1BEky4Lkk!x7sbDfA$ zf0=dv`J~I?E|%R57J=zSEso(UYcbC5MrpS+px_KbJsxo54PLzGfAQV_$_Ib^-LTPB9^F1OtSnTIMb{b0&D(2v!lq9F?&%~xPBl`#wwqpI1X19U!3FeX<!MNh6tFRMA$8=`fs=UWSk|7U#Ir$DRSy2^G47l%m28d~FY_)2 ztG3cCPbMUzP8Ga6fk?OcY#QGavS*uu1m>aaA-%w?Ly%+6k38QVbP}wZE=^-p`_B(a zI)m%bH_fSlJP%!c)t*ZI&O-LUPvF6;!tK0IYp0vA8P`cxs3}8j&sxO^=D}An@u!5>Pze12x#x?d23}{N3W`0 zYpqqSZAi~eA-45;Yu$(=&5Xw=3&j;(Y=l~V(WfMwUXijLPss6>O|1X4m-o^KyXR$B zK9}vOmU8A3DlwOrm3U?Dq3t)wZ`M#G9xtMcMV*d7>Nn5cel{qhBqAGSR%ZOGrm6qq zP5#FU+hU%DQ&;&`H$j09shp5yFg~VZI-7ijjNB#lO0S^an`6G!;HKOOo6FJ+Jg0wU zX20+?>gP_9iD|q;ClvllPxy;{3Ff(Q^_gH;>bbb%7K5CjCr&1Fwr+LnT>r&N{?D5J zpP%F|Q=V^5P|%=I`4#o^t81l`e|7>vH|x7_>@+Wo{OnA324_h2aV`JNHMtY$Hycv! zvH#~D{`y1xp(10HT<1;E~J>oXtqjYKX2(|}$$ z>OW9+u>I%Inkw6ghi>*G@BcZpMgY9TI1X(nn@sLu6n#I!o7nz&-eGW)Im^G%Eo}Z5 zw)xZh7rO`Sh}6B*#m@Tf$ebQv(hF3nI5~0PqZ${QAgQOk&B4m}mcxB>aHH3IdEGh8>B1l@IV?c*#;6ii48X*G7`IhQ9G5P!1 z^UVxrI8)R-SLT$94aGHU>TabBTH}n*bVnq0jXHh+hA?IO=R&8QqKtm?vc@GAwtV07 zSh;$JhWUe?dt7_e4aIu)m9-?n1>RzsE}QUU8Oj5|qR%y*^EjhC74G&VjnA6Clj3Mp z)&SH>YrM>=j#=V^3G8C;#jm>VD$dk1jzd??CQl%a%p$BC)Zl$_ElOre@*&C3H{)I! zRb27LvZpj5$LUd8$-uxQS3NL|u%f{Q)@@KFAfln(bJ#nbBJ@5M@7**RbdmW<(WD*^T7xytxpbN? zkODMJRGC}dkoPh>?5|}N0`Q*{788zrOReI$M4OS{AR2~}DJu7nb6GblKlsLefLwg2 z>=v!s>w}4Bkq)^>+P<2&W8$tAPsjK=Mf9XPXFWqsm^AK{SsG;MgXN)>wAaKIp0$>k z)x(mMQHw7%Ie>D7R{3Ea?b=K(u_{Mp;s3kOV0_Vg{a+G z10Uj_*QhP7?~!aaEBvoURAM7Os{H1j-Tq={$YO7}h}w7AZDt}`?dyOBf(>@9xiqLA z6nBX4xJ!p8?oK`?w7w8%t#vV;TtFVIAhZ$$hIP1iE_I00oi{Nxg{mZ< znR-HVy}{WQg^NJbUsjvBJIa?(f~_!pzlA-lw|*t2Z^FmPrRD`V5pk?8k<(=dtvk#72n3aM~RejNWb5rf2 zir>Hest@KTVPn<&Bw4cSUQOzdWd}kPcCYbOPPDm$V(GY9`LJj&v*azt#KH7*UB`Dh z#q=WY7N$j)wnq8+nmRD}>Cg3!b;DMT@=2k>hg-;%gC`$#J3R`lMiQ^4>DumpdU4C9 z!5u#$8Huz6+kF%o*6WB(id?N-J23ZGF9e?Qq6R~R0qsr4B1vAC!>9K9oSN> zMqq$QLCyLtJ=43LLJa4f6+H4O%2jZ*EPq?V9_;+P6ScqH5V<*v3MM_)94j4oO=tcq z)NPrXt|VMhWhYD7)GLQN0?}O(WLCWI+d{5n0jeV$M(U$t5B|PuDHA0>>(aU;$tvw5 zSHP`|(VT&+*67BlDtRI2hlH8)F0lw--I=Eq&GdU-d{*;;aQ|hm0p$orXwh97#?Eub zGE)iG%`a$MbY2dvd?nO!)QI!P!EChFaIy+l>B*#nx`*aZDz=ICgx?!)2#AGIngZ);#bU)qdhd<25gY1>BN}JV=u5J zbSC#hr?k!2>9NA&qXTzRDehEr&HG^`1SOmJf_3jw)30qKcpqj^zOuiiTWGNtpfVsU zWDR-T>YiFexSR4Hs;gtAJu*Zh)ZJv15$YL5u%8nv`{m{QsAk! zY1^iGtH>%(yz=9q2!l9j4KB53T7~|N-dWSyJb`8X7>5$cVG-f7e3e#7HAsG>UeaA1 z9pQkwQZX|b(oEK_>bwTYv(eVgZ*NpiE7|9&l2Mn*0+yDi_B z{jqu=M>lvVr|XqN`s0>RK1kQWyp_#EJ=7y@jj|+wSD*58+!IS*g^-KJu4y7Lj=31+leDy?a@dF5SSnpRoYUs-!fv2drVfw>u3htKEA)ak18 z-o5n5)4-1e=9qvc9UQ*Kr~Hy#|B0jaH+pu$Cb`i$NIlNrxxdmf?W*9(y1Rr?4Cg@t z;|iCRot?p^n*a$tX26|3ELkZqIAYw_nQT~mx9qxgTE4wK+FITko+iQ%R?;Ddi zQ*JW~sF2oO5#!TC;jM$H258@j5?%auVnJT8ntTr<Fy`)3^!O3RunRQLvno{+rp62x&ywP?sABmMwj+$AXyM+Qu zwsm07Rss@A9VLHKy|g99+q3#A^l6F%LboNEm){@rk&eu>r2OZt7(dU6@rfr+BE*9F z@58$IzF|a=wCbXr{3u!Lme*Yzc{yEqgV(95Jh$J3p44E=rz@t&Klvpl+!OkFE@lKF zEb5)~HVOX1Pr)F{wnp<*aFSiGsRz;@CF$1}N+UQR?Y8fNOWdeJRpo#7-B`VQySppy zKFp}f*;64uzysfIofEN~D%eJ27&E!5N|YRUlEoKdT3hWa<_&P zibl2hho6=6NtR;Mv6}~PS`KD%2Ybhzgr*n2RM}n{42A#33(rQ?c%%l-KSv9=^TRf$e)K;(rMz|3@;7kC)rk4Dw%PM~$r5)?k|&>&)d} zb22()-4R9VPHo3`hN{PYhT$CR%uP+Mljfe8nYQ32lYAr1HY<3N*diL+y}A!(zAP z;i7$ymGbR#?}`$1!LLTwL{u^F={7dk-TbjU+JQ)CTsiHHoqT2b%WWU7smr zL>VI;#X1SdWoyTrGo~ zzA)lj>w-Sj`x5DmSn|r0<(173^kAwfb z0v4QacZ;kiuStU8#x9GEsqTmsU5JktLYxSnTI}^G4Fycz@XMN%{X_P5=GBKqT$J9;eRxu_|NMKvsu5hw%7waQ-jXeo{T?)N$X`EJAgD zX42Xv=Qu^+k6SgHjDd$Kvh> zm5P>0_8i*!4H9z! zJ6XR?9nqQ6214E;;x!&WnXat5rIBdn5!MTW)z@FIY%R$J>$6sS z)c21a+=IEGOU;a@Klfj;mkxf2Kvc(sS8M9&K&Q{&O+gi)+*M-+jzLf0>yI2X`1@uw z8DYjFQ_2||wX&1ekwGbR8I68O3J!ElC19va?+xedaoJ%Okz`8P|NEKKk%4%<{}GYi zbp|R#Cx5WydWNflfj6Zx5m+{qmHnG;M*(H^r=NX_3Jr^mjC&Bwjfl z$024A4`JV;0^Y8DX}B{(o!C;^t75NQ;eN|+RS4s~(=o!sAS)$ecPd%jfZ^UrgzdCw zi+&1_U+FwD;e)(WQ}1fA;U~@Bgimd@p^V?-*Me$EdBeT|b;bX;OpgeczH^v26|&%k z?rR#hs?iV`Rxa_i#!fz$vcqhfQBA&C@E0Rx`kG<<%izWyO0FQY0~f6v)1C=Z&wH$Cg0U~`0 z@yLs**u78fcbW{}M$DeHqK_)y-+#7aGwWp_y>E*wymY-fJVFCR;9-ARtEv6o0%DaW z5-de6Xl<2MoWQ9mOU)hR_N|8c8|kPVPE7rsIwe)md|-awYH68?h7tRL^5nG7x@s$j z>G*m|C>c<9lgwM24!069SaJ%S4}$y=tWv?xtoZd_2VqtuYi~Fb*0Z@LBkomWC?n&o z-FWw-Y9KvoAlmk&Hg#1fh1d?{^4R+P!{!}F(N7Ab!JObu7qwH+AEkPDZm6s9E#jx8 z2SBVO_l(5(FZM)Di`uU9O5Eb^gwXa1DH++M5qPB_r|IT+`PyTb-(EO8*DpqtOkyFi zmQzwbB8!cH64?T&lz zXXfT+Pjtq^w|8_!sT~e{W}~QCmUj(**M!j1f}-|V3T}()&<-lJB;8mzc&U(`r7$g>0^w;HiYwhi4xC;+ z;Iy=}8`t$1?yLw#wJgDOkosxPg$q{9$2js=A3ggmIhfI?zVyS!yeiRN}6?P1;< zxWV;ozFcXixZi0V8*(xfLn+U@ZE+n)2@|Ihr90lqJ_0#1OhzPIfrxNmWk9 z5QVPrHvI&Ey|@OIbSJXIIn}Zs=PvCq2*nv14A?b)x!zm4fX+ z*;VNPJJn+0Rs;Q`N;8MT6bApg7hohUJNx$!*9kJNBV4QLlt~0~{;fvTV8S>dFI7sj z>77vI&BRA@ek$M-*L4AOhCQtK_dts5>0jl@|m3>QQPgM59h)Wq(NU_Z_R z_R>s%J~d0zKz6ShfBBZ-=|%DQuvtX3X-{&{K7Rv%IQ9a9ds}6WkpY$I9EDP4fo?HI zkFMU13&qT}!(Dm&2`;umyia8-pTl&K^=>P!Lh=pOGO`KimAkzRYCtjjQRTBDV`-tY zJ|d0heR9z`x~C~o72gAvR#}X-garq#d>Yk#PTRr)`tE<<59Oj&-yvT491h~XKrngg zN<)+?MN<|nGeA)s8jTxUAU9K|<0L0!r@w2l3b=%N`+#jdstqktDP14ftJeq$>BmJU zUtEHUHsbADxo_X@aF$YEJdtw;9pMNM@GHm1^HU05j7A!! zXvwD}*};bb>|a-Q#Xe~}$INevy5SNbI-VxdL=RGDN8noDel=)&bl4oXx943t>j>rL zkZQNak#`b=*7xC|70UC~k*Qxmnp- zeg><13uza2ZVVClgK?t zwU|lg{`{-C4fyOPq~SkAx<72loHQf7;ROe_w!wkM{n?Sz7dWDgbq;JqE| z>mNB8H~{T0c5Nt8$y$MlO+UJkW)jd#L98m6n`fd0eQoRO@IRvnI(w(%#Uq?#s7PR` zEby3&^g%|sAz2VYvaoG_g;q_!O7~zk#$7NKvJrzp~R%d8vM1fr9jME3?e(H7esT2b ziiihFJon^K-_b7*H)u2KqnR2g}16mB8@`o{kB+jZU37f{Qqs*Q!G4@NoBmL37hCDXp0 z{;N-e6W?lzyRWA2k9v<93HJ0?F z$gR8l>mqk8`Fp0IVpfqDN`mCG6?Mpthg|Li7%$8Q>d;qhVS#>s)QhOD<6B-2{_Zk1 zs&8Nrb+{GD)?$i8#DGcPD^k`hEsBS9VnOJk?&CbjeD(H`XFo`uXFo0CvRmJvSb_p` zgf+6?M)XGQog#jh+FD}+Iy`-NZgO^KS$2Y4t_=xg)N&n}63bz}~QAI*6+=J3GDc zo(e39kEPq|U$X$#XWx0Nn|jf<0{{tatgL+?i@{wPS#6-FwyiTB!tUdP$r4xK2iEwa@ z5886CBciSUNN|D~Im!6&-NA6X3&FHi?$5{Wb_xOf5m7 z1Mx5hULZhcsCu5V-8t^kd>9_IvKqr=J$agLo%^4)png9h$?kL1k7AM-IuC#gB4T?$ zIY}p?wh@^v`DTGd=7{>`kRy~zYFOF(JrMfhhj>8^>2X^$Svb%)0-*eW0t>haN4vo- zKb}KGjI!iu(53IKF{IiPUhQl9TF4-vUIIW9Tp@y4DBgS|R|{e3vg z<2Vly^h$W<0FlO)_)TtCX&VS#x`E@@WD@>Z$1Td%G1FedlfaeezD4ELfm5KnUnF?x zatgpl#MZB>YK6N!^ze1X6vlFZFF9q+@rE`v~yR?oNiV)1?n(BY`5( z3!b1H*WcrxXWg_I@68J9c%GnGXg}@jWJVpnr${HRt;IKGMqOaN`Pczp>(%{?VkHsV8`qcvYjVnd@0tH*kl?DDHvCy!fSfD3dF#a14EEjn~|YmTpXkU zInw`FxR>D+rt#hO

z8II=N!{NrKM7|^g)B3g-B9Sa5CWtY(C#F>S9VOK<9olK_gbx*_kX?X}Q ztucHu!MPHd);+K&-RhW`Gi;cWgZcv^J!&);vdwmd)!PdL1=~)NKjBsr z6B&Q;jQ)9vg2ZdQ_j%0DYa#m4{}vkmR|pNB`cbi(A#AuT;oWZE`>P5zpBjO*P9@#$ zP&zP5o^Qf^d6&;O4iFyPkAwC;Db>|iQ!pkW;g0k7(K+v`hWm1LmnQQR1f3Hp*X@LR zn*qS~DYpql>Yy)O&nDJTTEKX4C5h6-epsz|B=6GomX{ZVpXBMN0J;MjoUg|l&*Dx3 z6m?^pFum<#ZhqJHY<2$*wPSyPSuQy5qkGY?ngylH1ES<>yuVm)+p!>E6(Bm!?g|Ub zC8Wi$D{*Mk+c9`jbEY0qsL?+t)Zc}H@`asT`lGJR(4$no+os0vZvbV7=2I@qpuqVE zRn_89o*sKNw76Zlui@jF@@@xv(a`Be{8)G4ut;gV&17dzEJrEIpX0`CjQOkK`Ke52 zXRLGvxV3nR0l71kD2};xmqe4X%WAZ1d+u1#^J4H~r~KN7+$pQxyFzeAv2MX@h=)Ho z(lsM)I|&`yYVeHP7=mPI^;kaj5Kg*(&~#YKL7Ucak@}8(`k)e ztRH)?^v>Egm2RE;h#C{uMfE`~XZ`ne(B~2;27gQp1jmBSqm89fdxGQyDsuNL}Vs-yIB6Q z*|TNK{y6tiQbF2BY~Md>-;7Tk7)PMu9pRD`oRTf3P{lF4>aLXJHO0D?!6*5cN>$oy ztg^%hF|R>Fs8Qd(=+DMBqfRP4<$S(uSGc?+Z1V=^-4amuU0&;zD*nUB+$)YnwMPE{ zwtHTFRj4J*)B-ZB_KlWd=-X^0rlKnq9IzG-UYP#$`)%IN7y&H>!G25}AiMEB{9<0Y zx@SNCFgG*wR&SbPnM$q$uIt0xP4OdMGx4_=xJT`v(3tOvc_F2ibtixWGvy7HnE?6+ zAf;(v;^;>Zf#P>D9?K$ai@J7W$^xa>ds+mOJ1oneDs7Frhf*Nw>V4=`w2375ni?QM zBV;xbxjyDb4viON+q+kkBSNXts=W5l6HC}{;6C1oH+Z7N;IMAoIO=t%0PW~1?Bi4$ zFAHKm9n{-g#*@UA49Ist?*-SNRyBp#HYv`GtsbYh!C&DCh*00KX)QS?4S<&O0IQl^ z-l!RLxWPcz=aGB=yo-wrWqFa}2Z&IIO4decjb9DJaPazgRn%k)oG|20}c?MFZ}` zjSg|GDA&iV2~W&od~=&~x?;ipxYWSW9SzpY8p>~}KQ2d4>`86d!|%fzS2jW!39H}z zMIPnpJmg3D+`2KVbztYc=PyRP2ZW>KsdS8NV^jH>z~(&`Gq(Fc{PA~|wJ#4w6`hMT zaWZXbGgJ^U-bzHRYrs<5-?TE?6%|$u>*JWtg{zNQtz;68(xcBkz2e7-N-(OMMO$Rw z5NA@UU`L$l1F#`Mi?}G_Vm7+?gW#l|@oeZ-cp6l;EIVXSlGfigGK!TA1?+lY!eG#KrH zA{fzBmZFol;KS^BDej0yprjTwaZGClWd%0msI^hsGsa~d2R;4AbvR}Mrwa7Nn(G)V z^&_OoodzgL$)Cb%|10GEiARJQ996_2g2!zHx+?c92K8SU**_QAWj#k_8$;QneGsLrXNpL|0?j9dpTx6 z#`BT24I8!S)y3b-1_1!{{oYvQ<{{YK#M&A?2f?z%6uqkwLv;83%y|FewXzt%N?JEo zwDmqbw1z7>$vVaAan#h7)!c%yR(E!Gz6HdNUtO(3kK4?a!qa?V*>zun_?F7DA5CS* z7iedv^q@5dsJ=BWT)5yE?_S7ut;Zq`2CPxL}x1{c@qi-HYI%5d zh1>C%b|H5<4K>#F603V_YjzoV9 zfck|kP15?0l!SX)eEg;DdJv#NexM>*cKnHa(vf5;bmBK7X>G|i0n@%2=ARVif3YLa znRm<5TFi9E0cx>$O#wpGb_d!8Zu#OsfBEz|3ZLfP|+zikLca?B*79U7GWzaa| zKRs=6m@Gba+zyX2z~4QIbbCbFvOmfV zPgqXAhw&U63>7-{Jk4*_jYJ&JWaYPYp*iOMUrh$S(uK>&Lmk4Qp>C>@yt$Xm2V#x% z0&2Tt+q@`Z4+cc|Iz*p=jb$6=rTZlzAs zP_9qy9|%b0J6Nk}s0eq(5-nRLfv5UK8#GEKUd~4aZ(d+vW`Ha}`M|)_yj8cd?h9@w z${0ycCsK<2>S41#x3*?Uj-#xTt=`#IJ39ITyDVpyY#0&~4P;*}A z>Lzfh+peH_L~|n(Zi^5{R;O~$DK0D9iLW{T)K`IN03;#o4=4~Ju~C<|nU8DQ&&~o; zkAvS#J#bxx45i5un%st-g5xj50qq?<#2{(kRYh?u!Ji`3YFzu9l(v>rNV2(PLQK>i zjwa%v-Q&~`m(VI1NYJTFY?_#3VNi*vY1WpJv}smt&GBSR&2?AL*LKeW|F(G#6~v-Q z0cPq#L!DR|DbT5eOggO}3Dxqj<5)t2JJsf3F{fKb`2#@r$^PkubQ3^3gg)SWzNfgn zQC}#}$zrlQ-jnG_PjV3&WYFpJzd|d$PDXF}NNJksaiaoA!^sIty$iiI2XUVrPxUS}Zo!zX z&3eVro^_jhVlHOApluuYm`pJ8A<<7QDS~N8h)3yo@Zr%#jy*Wic)jk?{wu%uv)tkt z06ormA2*ph>sWb|Q&HF)x209?*$K|3KV?imTgw0a+LI(8#ySa#4IVrB11JXSpP%QS zEyh1z`yYzIvHX9D${MXL8y2bn#R>rl(MxWLhY4TGc3T*`M<~UXbR7}oM3~$3YdY6o zJ^AQoiR4%|kiB`2zv@D$LNZ^`JOqlen{X2WSlKN)t8D5PjYzETel;QOXN&Yj_IgSXfMr1UWe-XGgpJl4 zzQTf>e!NxlNhfRoxQ)^eF3%5I_5rSKNPe|BN$6(bw;qKwjn225-ZONY507P=l4<}^ zBZ&#S?~@AT0tUw09mTJBD=I3U`rG4rBqNP(On%Mwp|Z7g2Q;nk)K!%X0F@i-Mrzc? zF^~j6N>&HhvB~yn0EV-iCKpRtU+{}O_(fmS+Vi{abiO1n>0QZ6NsD0s4}Jmij+PeLmOnzdN1co)hX&WEfmo>HQFZwC4bAdvRz%usX<)qz%z?#;!q-kDh8~) zoJ>=pUL9d22Ul!NFsaEOZYt91DETS{lUNq94^Y{eHZEGTu5^E{IezIb%8Y=%m$f*4 zt5hwBIzIrH14I?wS!R9FcES$Ww99c*Ys#ALYT%_op9yh(&Or*=Gofv&rx7~*xz}?|NAK7JodVRi{-F(f z?d|kw!U1L5^a?+WEd@`=G-)KQaCm`oa_)>>GS7o1aPvqBIw+?g;gE(y~;s5gI} zMV0f}R>~0QfS{(buMw!SvdQV~xjO*)k1Hp`SABr)07_Spw|tLN zqt@OKmQrS(%!K=?L&_18UwndR?^l`u9Z2%wOs8=k<2y&y;DNYGjG|!BleCsD1pQM(y7t1TgpLMa25b7TR~Sb^{8nroo`3P=*WtMt|53T_jmmjm*-!t&^}H5SWrKviySIT4*er# zfhTQ^*?Haz5NPp!b%E1aS+}MO$sJWrF$@+; z%u%UuVDEp`Yr0prLmSy;UU{w|h>0^f{~IERZgt3}#+fVGesDcQcXeoo#?g7!e{sLO zyResKEZL%1pa2k6A%PC5==eobZ%Utr#_?vSs$MzSW7O6&AWDE0H`;W*Q_449taBcR z-PodG`q65nlFvCdU!Tdc_oyeRQrk0p(%A_DnK}i)VLSGDnr&}whyW+c#5nY$-6)Ia z?#a&5(#a^w%5wxj{b=-Hu(Y|JX+Shi)#|TMGa;!h7GFb9fin*(Bb#dYH+(2izan zy9JTh_+=`}_mOr5t|BimNNlzx2VlC0?z*xn|u`G__lJ+wp4!QsnP)o(0J8$I|l;!frYTbPtz)jX|YF^ zqypLT^*29~edr*-$XQuWSkhTgfX?fM(RxpB-_Pk1T+ZLcW8Z+=sjnQcbYFFs?dt~T z+|f~Pb*o^F;yA|c0A$^B^-2z>kI-NvW`_sDuyxFd+2;fenx`NzsVD_OS>U$C%RXDT zQyAzWhbmLYG`lLt5?NgM}K*VGGV<0OMPa&gsss#F)v(ePtV(SQ}~aE zGYubWx}UXni$B+P^yzi>#mml@bOe1)*YHybm86-|rlFM+SF%bp-Wk7PM$C<< zJ&D&rU3;*@nSsA0v&wq5X8PM*t@@pngT&XKQ{!EtU=JzB1&(d+Zom{=7ydXm*uv#7 zWhc^LJ8tl;(TDhUXJx_dDi4NdggIw@0L`;|ZH$(H2V!RHRt_ua`^4(H_o9?WjxoZv zUnKES5Ibj=rSgJMz^AIhireI$zq7Ij z#<&4MZ?T=*&_${ zyoCYA@YL)H$oA1ZcHW_<2M;9CtGUVf2oD%6c&tJ{AWU7vsurXM9N)3MVmvlVJRUKA zGN&2lz4z#c3*SLj4CxDQGjBTZtVfHmGN|fwE#*U0u`s3RdrwY4KvJ$q zqmYD{mSLo&&Ue!>EH#?f;e)XsR%Edr$bmGY-C>0W#z~ZDbf2`HRc!_sP#M#yHYX2C zf=(dC#8Ja3$*0K@S{HyXIrwZVx3KVHWz)kQ_PPqeLmsSacl7sHUQ#rhbWLE0XpSMS z?l-w-J{gB=Q=x@Iku|h#I+x0S-aWN{X6-iIqSzl#FgE@3=RZxUdEK|3tu9Gp8(obp zxel)`)xX}=WSQW9#O`2K>M-;;gpK)n$o1=EOLPs%lnu9EqmEb*!Gwbmjl| zPyM$+^#AUkI(1%&o9!1Y?``&XSiW&8VhUeXtN|h)N>QPBnkKM68e@MAr*=9TTG33e zCG_;(^1B*80mD}JPTmScmKh~Q27~6xCP*sCDJi&8P@fDNT`~uigX-kp&NTCH&LK3o zERdIbJf!`81f8D4-p-uhg#w868sScAl~-rS*lc`XbrQXX_MJCOj1m>4*-De2ZGAlS zR^?n{7Z7O72AdoW0$}bkC+hf-sKd+m=tsO&s-iI1hRJDywYK*kt7|7-#RA(Z-yVL5 z1L0SV{NP3AYegGa@fLHyCr*9s>AD@Oqj>#1@K>)_ui^WIo~IV(U#gz)mTn~jw(e%z z`)SK2y8vUL>?u$Vunln9+nEtUNS#MLeR`1nXVGPmGsUe1857tQdE}2hPEKZ8b_o-@ zk`%&qIjxJqz-iK{N$OZ>FJ5=_>N~h{W^Odg$mH6d#Hit|hB&8N=@~U2!mQlnougT2 z@|15DH~Y5x)OZtGDsScFCKei9g#RqUgau*eS+a{Ot>p4M^#ZNaJf*>adC1p}EUhVr~($M9r&4SZER!vDVkwy}EzE+ubybVYuLaIfK%o zSm876qc5z06toR~A?DJNelBrVW+WzCXz4kCaDR61p6!%wY0 zT`VWCFyoHJpydRxdrx{C8in%~D);PWJeG~`j^kE4UPAxGQ{8h%{)am$rAZ~%aiMW% ziTzyMjhP`SaYx)YnqcnWF=~dyJi!QtwaJ<2O3FB~<~S+{UM(*MXGYdr6CRz;wI=Zv zAyGVhrxV}&s4={(sNk*HT<>T6nJ+hsqiebpbMPEBq~uInHPuKZzjE*-;a;Hvy<~9& zTK?(<=A2MTxq6$3lw9mGVfQ}Pz`)GnNMjv0HHoDEtUyQp2ro5P1i0%u8D}bgZdn#T zS*)7PzDfRgMXcmJx>VElP8tJ>Q`TGv%T%krwd)9*35BGJpfpT_a2ELE;*PqqGPa7N z{>z8-4fV_lOx9)MB^~J!ja?ODKt#yEom95WM$$$ozpOU^kx6Ob2v z#WMN(^@=5ZS6nMwoLmeMCJ;H*s{SH|zD4|`?<8b99@w|LrbQm{3-V|MG_G}o$yQJQ z9LsKb`6DCI9V7&>FuFy&#miVasik=&ewlp{X3Cy@?3_~}v6^l1!}m-MZ4!HL-dN7bp(ayGfDVV)_ErD1&dqUvgzEx8_XDBs>aJUp%H|9BL9)I@D*e z9_nf0#CWEpXd8Jd2mhnHyHMlFlIV1K)g$Ihgk+;PJ>nW6Ei01OMenQRn3Bue4Q7Vp z6JiAx9P%Zt&o1GsljcUNyKMF0IToqr?ISG$`V4qACL1`xuin?fQ63X}x!0wzJMnN| zR@~JTqvSNFCrNlwhs(AyTBT0hMuI+JP6y^q?W=Y3(&+UGCZBsnmxGH@jWDVbPHUrK z$fMqO2R1Y>*&>wYMv(HDG&ngz_N@0{v~z?aEks+&9(>3zCh7z}YxqdXW6>UK%{LO; zd1mqf4w2kiEOp!JnWQQ3!3%)w$l0TgxAoiN7#N&X64~Aj&5Id|KXd^ZGMT?(Lzop( zY7lcnDX>~cu)w>+jBz66=Rf4+*dnvHGV?Xjb_I&Axz%s)#JHq1>Agwg;ROmY2fAca zW_%M5VZDuT*sBAm;C?RN3ceYc@53hiD!)70Y3^>ZIjbqJ2gEA?(?04;XePv`qLza9 z_jw+Ex%MH3?6T0vHdZ9AJOC!>8lLMdf}W^cjl88(>kS_p0+Qk*EFD_bz8IaW=GZ(; zLtRqhb_DmUtKYN}0cg>fqis>u9C1w_b{*_v7We3`3h*XSVfCQYC<+vi0CHDTO zn-CuDZwJrucpa&Kr!i7GajgEA3v8{y;`?Le7MK?#5ing&@yMG0*?af(+ppiXe2_qKbzNG2CPnZEW+JKk z6G;d8RSt`*o|AG0Y9C_LWz9!oBC`5bM{J)lc)A)6739EGvb=o#im&EtB}Ad-4Zc?$ zzP~##H>9uSbFIoDUAO(U#N9%Ty1eWn4iS&-VujSVezpand}!MlE`(Cykm%5m&EOSu z$%rIBpH-)~F|OMiwK_4ktltZz0*Gw$+x>2(6Y1Cm9?!mFS|EY_2PJ`Wx_lf4^CaxBX;|{L18b|OIS|_0&=vOuxy%$`1Yr4EK{CG9zO~rAD#4%5I_mI8RSBFHGgRiZvVURF3?mk;S9@cR}`7Eyk>RxPbR(IGG?KydXlwgy@jhqf;r0e ztJH*_m5AEj4jq=vB;Wg0oRWmEUA3nHVA+_9UTCE>a3eZs^+^MkZBRTcH+t-UU zp#MQiQA)Fe&y+2seJ1D{u8>HI(+NzB)$7O;Iunk`Tbfj9X_aI4ka*DMP;tBDQ||?1 ztg}StFGxTZpa-XG8TcIU>8QXOJT}{~A~`h`&KRW}fYiU*xk(iVJ+#QN@H0;s8Qqs2 zK>%a^54kXm=1vRopW7Tq4!w5 z{Jcwx$>pZ5E9`*-PtQv@=WF!pAb3BF6kt7b)a~wC7+FjgQpJRUYp?1wbAa7e+7~VM z)uUV^+bL~^QDPnaiR66Aprs9X*HjY%BC8ijJH$nKSuBPBL^wb1r!WUk2TypakEF!) z`%^Q3FIe$DEBeiJr&!uhT?IS3n1ouD=s?#6{fn^ zRi&t^g*s)ewwNl5%jt#!rEkAN!c4T~75SkF&7VF;u1JoEXoRn85xU>MmK{1kOBLG} zY{3oSc;r_eRX?DV9VP?@z$`q_8%<;85ECNL;S+Cdo0w)*jrT15Ri_we`N>~7PJ=!3 zu)>xlqZ?R<9p!B!U~JPanQ}RUo80EyG;O4#<(IpU?gF5znV0Vc=eby928z=F!vkcS zm>A?#+H?{Z^55`;|M=Hs4^ZW0caE<4(}oUkmtm>XcD+v5QJOYb?x0p}mfMpHCpz#= z_5Vb#U;PJqU8RP`PjiwV+{WW4jOS{oqu=6XUPUPl0F(J(=X2a182~`x=<=D`9Y7!X zV#-sT{(>x=F#+Y~m#*SWOyBVLiZpz}n!TJe+M-Qv^;4T_u*Ta7iwHzfqp_$#m|y_9 zlBo3XaX`c{*BA>HF+KH@&A_FgGzM^IbjfZwv8!UVER5FUe`RdMCcn7^SmrmD`Y!mX zA(malR@kkaK%bt#p5wIqIItob%>c49v)KePDb?NeV)AdIk&yVfxVUsE>jEdRoINar zHkb>uCMCxMW~mGK&05loR^&zu_))70acvUJF)0W>u_$gQo3)m*xEu zT;k6lat5i9BdW=&f}R2HyLBM0bgq!gEy=bW?!Nq}+TVZmoj;V2`btalFs3x4y%0>F zKR&)0Hj3}8XL{B%xU2vSfomiu9OucMAtH!|@j{{lGvUqDSvlnc17vY+a{B`$Z%%>6 z_pm6)slOS0+@~B^r{IKb8BWBrXV>)amzm`Ic$Q4c@6AbK6VGGI!v7upmJSbUk?*I{g*LFQ@ekY zvIr01nE_lAP7%9&>{z5M(hJB7_?|fl_PDRSklbeVczpEu19-E2-tfTi??5f-mz1 zza9#OEx6Q@sfyi;od>T1Q<}!@+s{3GXPipiB6G*FpF@gQ);{;wdfo2(U+dJr{Kijh z5L)$W2Ft~kQk8|BvjJ-=K}Scz@VZKpQSUL0!^_%j?6@_3A{M{;aJ1@p5QQX&w5h!$Bz=m58=&VB>5^X?XYq$IE3pT0vCc4 zXw+g`>-_Z$Ebf`VNjj0L0hs1U3* zz$F0iby5=tY_$U0QL?pM)#GJ$5Wht5I?jIHu^W`##H~%{#>}S$?A`z3!nOg@2mZ?4 zt4Uw7#B2U7G$6aYM6c?`JB1;zBNks^>&9z=E$6@b&6{cblgY{_T7jGYU#_zMMK*(@ z##y;+jvA#zR)i_{<0xlO0Rpvn^^#v}Zy@gG?}j>+ z%cV_Cqh;;)KD?ew9=e-H!%Mr_vW!`Po;Hr(+{v;4Gb#IxQ>IGK;n%Exj$M7|O{!`a z8{%NXZ*OPrH3SS?yS4_Cfyfaz9gr@s*n3=0KG&jScuKUmK_N~;&v%>T=m}-sDYy(5 z(X}y}??w8%7FecS|0Rd9&SNMG-O@IDJRkla+mL^LViUqj`>-`4`w#pp(cHiJT5t*< z_>cd$D@HZ1@L2D=e^hQ|k$LAXZ7;PFcln`?R>as+`wtq3cxtoBz5ft6_M%nwVFyi% z1{}pP1|shHh+EhEXe&@BZz~0F96Tqc0vrhmt=Ke+sAnl6b5mK^l0r9aLaM%zvGJT@ zbK|oA>&>xyX1sZks|kC%OA-h~%84z1aSG%3pHej3V(>v`?jW)EXG*Rr8b?$u$E^=u zP}k9jBY)yWy!8`W;SjO4aUQgjwEc8Q&(QKhWGbO!ef?#BQ?LIEFIW<*ikw0$B3ig* z53~{L>d_jg31IGW)_6wF6A^Y9OM~J*zUOl&DV-{_ta5WUZW%~Dyh2MaT7Jsbh}rs0kvjFO@>YUNs zm&eptKky}xR0xepiCbtYo=#bxB@5OMO*yp8`&7*OIAy&#&HTK580AG3JMPB10$a%= z&ge%IPGkB4b`jMbd;Z=Qxpwo7{d+TNc{Z;1b%iaQ@RoQRyUy|hjC#687fleXe1$(M zUg>Cc$C;h5_5I|HB<(OtWSRvzk=H5glJ^EDzVK-e>(-_NhOrvYah7xXrl1?=Ku=O) zFkTfG^(sQAb1Lyp5~JY(nQD8YKE`s#SEpsN0mp7{R*SsRmvWtZ+LCM%*3QQ}N%-Q$ z;Ute*FT>|d!Zx;h6aLtX%(ilA=&|wI`8umCaJeH6O+Ms1wx*Zbv>aK6k{S^c6P>-S z(KKujk0vQiv!`f~3g2s0x2w?b8Y*Ri!wV3X1Z_X(o8EpxmxoA=h#( zQ=a9WKs#qnzy11&9Lg&oZq_P=GS+Z(eEHmM8&U?j(2%EGY!|U&RohFR-FUU_fsJP1 zhI8n`OzX2RP1AE8KNg8Nmh9G;{3_YcDnyqOcFQFwGjJa^wLlb+WK~5pl>!f{p<#Dx zV4ZBovi!JmPj#@A_;~$prFlKZwY?bkeIbjoA}8@&A|~^FF;|)*dHbP>%=qn2qx|A2+b5h-wXfJ7 z`H(-usZI@MCi{2FsfvpJd>Y-|d~e;od#a%FXh1!?RT}m6^i4EN;IW=ew;Sw5UwzKB z$P#Zy}q7?f-+tArYPt2e$1bUKp7<@arcE}n!Ul! z`CZ^_V!Hk#;BM)H z`cRoqXax+Jvv;OvveEwT%qmFRnhjy{qO9DH4PMj5Y9yhrCBmg$MhxPm^Vm5(VSc^C z`K-6w1R8jekW((O$+_YwMLFTaJ(9qmuKg&N-kX@zs?E`65&O*`oLof+3&vVcf6ZZ!`!_-(|cIb z{%**1&*jAEiot_-1K{Ku&9^QcSr+1BqRE*i7ZWU>v9y|4>_Q4Rc!Xf8imQ6ItJ3?E z{dR1p%aUbvAvF&DV#+VXAV~zh%Fyj}Z;D58rNvUY#m-CLPvR5vws>J$I+tJVlHHTi z&GoLA8z42XwbeXH-u?dLPFbExcE2wbwYh^fjpu3+A{b_X|MW_jLYcf#7 zqjy;vn^3J=yE2-tyJNp&6kDwq!Yz?5c4mKaxQ2!T>n#l4Jq@`ti8_$tbaMXV@T+ay z2`RX1nnHtBLAM6Hguoclh`xof`F4(@|4q*;5BA}OlG6)*b!m*AC1!PQ1{YS0L+%6Y zwy{_GeR(DH=#*2RdRc%~e~-G++u&+#*g0C-5Su@5YsJ4jzAxgy~{8%HMyO%Pk|#+-@T`v=_}S8ujqIcJ5*u`Lo?8M zqR`WKAJ(FGw=0i&A$>?qkNPFd9Nvj4ko{7=-Ridw8|@xBHs%Az!$|)So>(X*A#AWjul;T1;k6vvkrZbetyDQj`7v-O6ihUf(EQT+*6E| z6~<&fX5L~pI5aeh!AY2$_bhbly@*JS=R@8(GPg5x_#?ycMj*EVEQ@JK)al7}vm_^% zyQz?MIk-(4dK4o?d6>^|O5Qtj#5JYleNJKq{zq_AMOBq^?JyB1#Wv05)qIRCIbS&& zya7j%g4KgTJ=S(9z^pqnr}0CtJi3O0GYra<*U;=z$Hkzo*ZV~n=G?bTVJAuzmtIWOzP#@PIPZV1y3#mPC?H=fgGtH86 zckp?~?=&-<@?c>+*A#$z@iaDP$hVK7Z`e6$R~2=$GQ-ijNVcrT`_VAH?&2hD>)8&M zm~zy&;%6l@ErDAZ{rHAMO#xMPsIh!;sTTXuT#E=UXK|cL3Z`ba)EEbFyqjY?GA@{g zJp*k{M@jTuc}Z(hSUGG!c6O2|O~2&rYpdGav|f2`(wH9@1i4d-uCU7$nE~0OcM2`% zlU=M->Y|T5>Vbg^eW{0PMP?`@lJ%E<48ThqE`XB=>N$7%Q=Hg>76Vt!hi14G$-Qos zX$`+)EQVdNHRb8)FD@UKH0gWR2XClrXh;NSmCpJ0gS>tr z-dX>%;xrWFlcjX*pT7C(J!z*#5TH?$7lAu34sviignT8b|4vXAB{5Z&3nMwcXI?6e zBGeIyNQ!xB=+@iIH4&7v%jP(T0WbIw*6Z-=n=#9D4Jj~G;eLubv0juG!Ti49P3AAo z38!f&IDg63gN06W2SJaPFC3wvEdQF6?}EeWd{59cn}B~DEOZ83FZ}(&aaym(9Te;R zAEfsG^GA+70H3nyXEc#y~nF>Pau%&Zp1@M4$5X?oBU18O$W#d1{LMz zclv1A|EPG>@)hs^AJ$EIBD;CTcvNH>U?@UGP0c)1NCd^xZpQB7)i*qBKxG1I+AT!I zW$rK8+SzvD0z9=XiHz#?oj`e4@c<`Oh*f%(y`#kBOkP7cFOm*{ROt`3Ev>5>>apkIz@=PP^%IpdiiTB)`DLPbl@Jj$-- zrPUi^1k38g6jaA6{+#R=V2)Cai9_5UH;bnp6Xw2@k&;xKDGh2d5C3sVw1!mxhmw&>ZG3%AA`-uT2Dz{uQpN%C zU{u55!EmuYl9RN4B6qi*sd&A(KJ9+$-j9v@zPlCEt^*};DkDS9)Pc{`G;y+ybn{lv zS+T*P?CSi{4cPDotUb@l1)t53*%*K;uiqVa-CwF4CsPBW+?s^Qe18zWQ`Bdw$c%P8qHkkl~b0*hV<-sXtD@HgCz90iJ*j56+MX4r+L9E2qcZ@tp zZ@mg;kmAw7IyF5#z2z~_<7^lGP&S9El@U{>7wS`;1q6B$SejHs#bn(mqdjRTE$(~; zHCoII07XLs_>HxVSz8J%WNd7#LcFVM>P#oaVgnpAX2xkltzC=jdN>LB$4`_KwC=ux zmUwznpuMvTr?Hl0e!CNOOO<|F2Sg;2asvm_b@S|-09JrxL_&)*i^d@ABl~pnew?Et zWRcjH3YL>4qCB!>Om%7cyg!GvG47bED&T~fy^{eAb)-OE*5eMZd2js7^XwZ>{9J zlJ2?J-u51Y5xE}^KBgCJfcw>SS}s_Li{(M`Hu|j>tlTQ6NuvhiF5?pTx$O^`wTN>% z@g%tzCXAlEY_~Tj+aHUu${Q{riB0+;xAwdWEq15(a0ONJtEU)kMuN2eZ_j5FU{#FG z&LyRmsBqb(`L93fW^L@$?+29BrqU}Iy?ymY<=T{ebeIWv1EsB5IHdVVo*6c_nd2Ps}Lw`j|7_`JtSqnb2Cx zDjmPWYS{&iHw&~zyEm%D(S~}La9oPkH886w1LMdv!gKnE&E7UGDNdFp?R{pY_2Kls zsj>r(wc4n9h%KaJ-~fzzz!$(0CJm)&(U^nxSm4lLth>=;@%+@?`{bn&r28YQ6o0?9 zDuy?j0KiiP{&qryUhQp^OuH%jTrJB~EVVkN`3ohK)OL1kzM+12PC58_huqR3H6oh3 zYXKqc02}1h@*Ig-?aE|zZQ0t+adP3?tWw{do2i7D5qazPs?3ff>XF?~>Pt*y`V*}O zoS7Q)O{BmP6m!V5)lL9TATMKSKm|cny_f?!;L0Qaq63Dx$)b{>@>^-LDDizGdz1CK z?TP{GNz)L;l^<{F>!tCYGorQgYxUBYyCUR%B)nve*29QDFpXV0-XD8Pj_U5|XrAXI z&o7mV^L*v%QLBx!WO(FZH3Y?0vVHy4tSM-}Y1gYL~6KN4*|>o-D1`3)-hWMPok)QhLL|r<{E7sDUPv6ZNLR z&2ju6ZjKusPRWuzILhF^iw4LwS#MR|uX)un{pjBInh7gMT8v;V7kxvoL_*NGjIF1q z;qFX#cb{>@wNqc=5r1Ck{cU(Q%7#iLy?+t_UbCLQ-fj|V{FJ+7;MRA&8xgW+LWFzz z;{X(u(^z$Ta7fQK;ITSit|fpGG+0@aUe#%|6aV61A~wn1zUf7EN(EgSvjdf!AQ{Xz zA@|gxNxNJIeXsTJ;}_Q5)aUHJ{==&d6tE-f?z6;@z8{jBPG-XCOPigC+vflj<_{;w z_w};%ku3V$Sk-lZT8D7fpd_ z3zF+OO+?Ks6_@=TN}W>VpevvN8@w%U(%09wb#^x$=k)Fbz7m&aySXCyl@WFs1O=a=6z%50F|dtE3PyUsrB3IZW6bv_a!Jt@ zpb|j4v2PL4_iH#y6B4^bJG*+Kz-~GLAr&g8`EBtglS^RsM}<$b%y&Lv`0DXQD#Vku z(?6toW->gxsUqfv^sqU9;(f8+#-n0T{NRX~byIZ{IHow+#tw-4CvbfU{QXv)Da4M7 zPwNW&^OWTxX&M4{|F{A=6YGShbp>cC(0b+|y)S+B(K+W#eJ7|YAMp{xZuE>0-&LrI zCF852LaX}1dtKcxXA>FdQv&1#Rr##-YBxq>0JjL5eMrcKmaT8`=;JVx2u}N`U%fux zC8+ZQ(|BW}Z0g~^cef5dTFt*)`96VXMt>?GEh$KG$9^EFOG)*Bx=3@NfnQJj_k@ya zbZgHEZI07AM)5kHjGQ~0+t=7NH>Jd;1!AMCT)pd)CH7|)78ZJGxThWNNwCHfN|A=s zEN2q3GmGwdbbC~zFGWGc@QjkrZjToVi^QGK;fm>FfxGWaF`4+w&WDZPabpSG?KXrp zw`APf;26ejr8YZ#gKHO&$QG!i-%n7z?G-$1lV)AfC(<4(R6T;61ziWD-ZAlhI}wUY zSWjG>;Xu!`#QXR!&09)_E@ks4P`C+e&EDpGgebvRNQ*G?y|b?ZAz`Og^GP+9cO8rh zNjXCpOP#&4;ye?vl$iMV%Z78jnIy?0w>=iOxFnP$czHh&bKs6fy+}R{J3waXjKqIt zi?tOu76%N=E3+jp4^n4%^Z8hbs?M5k_#krT`+W9ZRLiOn+?(#P4i^S+KdFjnN7OyB z)P`=z{8GTwp4=qSLr9y$p#~RCztGn!((#fZYNQoY=*>x%Vkh7l`(F?g)OHS~EB|(F zh|z4761~#Nm(Vlha1G5Ufy}1A>}o(C)(&@NJQn%Fb*PwCVyC;cV>8U;l;E9k+fnED z3J#)XGO83#@%|t2i`oku=c@O^B@B_t8hX`*A6-jJ_9sW5>AfZGSmEV3dN5{B^^Du5=KIt43mv7bSmIkIcF6&MK4JBel^& z%PEhYAah9x0hGI?#X5mz&UKkByVD5=$}w;(K5d33c@0h_wq6ZAf1JiyN{k@1fa zV!YOT-)_b=ab7a}4ssg9v=&Cu-{KR%`MRq&5vQ4F5$T<*`6ZX)Soop_6rCV-D=&&d zOB}=U(2^q4{wEt&C)nQ4vupFPo!kNhU8ugqcNw$9^Beyvf~qX7Nw!Pb!9txsxrm zw>*&R3yAu3mBAjwYK%+G+-T-RhMHbgK#i5zR%1yuXWYVEx3&PdC*X-mcV!EcytKZ%Nqerw9**Kn)Voz^$xgB3`%0H#2~eq2~c_-b&t z!`8@|l_5nQd!4=KZ%)sngY@z&FL0*p)7mS?`c(kKZ)d>96lA_oxh_9He~SCAHy3zq zCeZz$znPL}UTv3>vof`;A&H7*#{Nhk`Kma&{<8q--z_X9x+8%0w+^Qw8n?7g&i@>< z>4>KC^u7+!jN?wATloILA4m-@%tCjWWsnq`KAScw6GI>As5JE=q_+k`+_yq_`zom+ z)UTL9a^!1kuWMR~a>3RzOpQ39nG6UGX|MGA7BEwtN)^lNPy?^H(}^o{LpPUz%Dhv} z?l?t<-?atsXwz&p3H5Ht(4?@IOyB%I}*RWWqK7UdFaio z+-ojQse`0lkwoy7PP(2boG~NGIxATRxJL9Q`|PYfL=yb&oj_h*II|Gg2?3P)nGShWrsPdXucu^qe#ru8pl%MP`Yl2o zcNRj2ViMyoi&f$W368C@3fP+=j-^w+Hd}>p(|cQuKN@76mWE%VgsqgQ823MQxm2%t9zuM{r`W4aPOlUTIX;9w1K++@+Ewe%mwrEo2ws zP=q!jDM`#qN*L$UO7B1TazyAJHPT>6v^@QAIC)XGd*z7p9x%1vXzSoYobpWo_ zpVIFb1Z|o(N6m~+;8vkcXBHn8jLED%EQ2=XsZJ0CY`$)Mdp<4FU`Y6}S=DJh#xCw7 z7GT%f@vnxtfAh80Ke>a3qMzflT-A5KI(R2X#(bkiV93 z)&G9o&7&t<7eqdI|MrXgw};RDnQq~lYubFR`d56vun)}A!Xj>ObG?E@F-rMymeZ^G z4Jyf2WdguU58(lK3 zo);MLyH8722aNO=@8NYsQsV(}O5Qb1@}`cWG4hI!%hVH>3Y$>1>mOO$jEIP1a9hBF zP-h%~D$foQBmp)A%X5eWG`^wl3pu}0p6+fz+_2FV&Ik{GvmHxSndW^t` z{zmoa;W=}2a=IqBpHNToT{)ccO>-$_=^*s+sw&>w9hp4M`tr9UvTMwPgobOAT%8WJ73IdsWj2J<61zV{o=3L z)Iuj&Cc#Z13Qj=U8mk9ixgy*-gDstXL0+ECL`X>-z=ltqcR~WS3gjL@ar3SR?BOw< zh*ShrxLYy6qpPGBV1C4LDr;3yDcVFLAoYjI+DP1)(L>~V_r;&h=}Xaw=<{l(i>hLR z0KpvF^?Z%AVwBN(*3=yRsvl7~q@GUds>fE^*81kz&Cll)+9_?Y3IU3X(w9ILNp4bg zo1|Spa;1?JV%9=uzP}O~TeDL1wb}~%Jo~wce}AC_DBXxG%3MoJ69mf~p>YxaMZ3-S z1l{pI+yl(2JFwlYTDHhO=*6(KkvYa|y=!+E8~m^t=j@B(K5UmU{mN!#Cz8rYbhOK>W;haFa@R zp`Fq24W$rsIoQqS`t?fSotc2hqfOkuE$c2wA@IT$Zm}C1;$)=$VcvFgF2d4gJ;R%! z$oPImq;(|wj`zBCta%G*Yy)p9etqdFxdeVZQpyxli@46jg|XgwP~ z&#^b(#esuyt!`fMTswZT^+kl*bnxptAZgx0h4IeFsR)fa{G+X7)vnr2chwk$QJKn9 z@9)&LsA#iq$z~|eBxcm)mzef(x<&KiCkT$#AM;Pehu6A4kA2k6k`I0ETL1d*yY7E@ zGs_Mf)bnmV7wi5~I@K8{A9;0;x||@y_kj#~a0J8V_H0`n`1uky6u)DsC3xz`)lt-8yWloJ~mz zPaQ?Zp@XEg;b;f|(_N+)0QqflEjXvamNX(8*@$tlUY=to^&+K&LBz|bN$ut+cPk|J zPszbk`5I+JiLvslA}C1Ph#%XP!u>NwF@08XKgG_ZUdt9S$?- z?tml#Pg~m&$nprmYtK@ekm>o#!+&5g!RvQ7niwlkA`V*2oQXBmt>kpvhk~;VmyJEqsDyd;pWO1YT89i#zXmuvmO|(*GW0!}Hl`LOdT%Z%9>yZ9+izPusm=+s%jdh-@Qxd0BpX;^s6e5J z5zP-6gdbgZp6|4FX|EVIOcW41!#lY4>A|t8 z7r(i(qLS&#U-+nN7uF#RZ}~N#Pc?MW7u1Jb=HW=^mlgXOKMoAul1pjyHyz?XnKLCz zU6uPb)JWgB(Bv)9&{U1IPg->jS%s~6Sw$D2;iI|~ zR3_^KmAI|MBT3*0D9K7uvBr`1DOWRG)5*{3*~0G=hxcjk$bKOyiD|5g1`p@+J<=CT zbNnz-RW%%C7IWB1F&Q>sT5B7N5g)NR$!Pdthr@k*V4B|>VdaCD%|t()CP;N+N}!wz zo5|Sgzm0dFyaN`{-5Za6TFOiA3~F(sVzH%D4j$3Y!6ok>zOgzees@jB%6x0?#_7HS ztz3N}HB;hvB(n1|RAh92^6D;KYLd57rHf@VnfvR?O7oA#ET>a)V$K>foRxF1tV#HW zd4nH)qjFE0|Dz9(mRCNUx*h{U7=F!LlQEh8tYQdl7R@L;fG7>#FeeoibxYRihsZHy z*mJu$=mz(4vOBXAZU;Q0>!Yl=>0d`H98lo;{#`aw#HtXOeaYHY9T=i#B{sMX8~xpv3y2i}UZ3ylq# z27Dl&GBB`iL2PA6jRd-ud{QFQF)Y`;T*O7}8UISAMHRk`hX!3vm7jd#r(q{whh<49 zVCXEuV_!hC%2$f>-vCtrDC&cGFI^8PwlK8VBY~D5(c110B*94y7Iguo`Inh?U@6I| z62ZxW3Nyz^8db?{D{l54dX9zNT2~!|2~$7nf4v$6l7ruTf@Ue&P3)bcY?9ExW{Stw zl%rjq)o(YIFZfUT1x|cS($|Zu>pOVUAsszqCx5?JKP1tE{UOE+({c=$?;_A%_iF9D zUeZGxYg#g*t!gy6&azb^Hblh&(2k}{n}<)zxIB7gu7nZvX729ToNaifrnxVTzL=S1 z9u0%y-RMT#<!iT)XBL>*gT%{F&!~#Y# zczP~A@T85&hh-u^&uqE~Z}?_GZv9MaJ1T~u?0=+zja4e$z6Zws?M(RHullq<;%p~NDS+R=Ga zcFp|#O?i>a6OZJRu}Mqb8URfU#NQ7@rQh#$_x;wRJFu3Um!oc3qQ>S}IGZK*tj)E( zz9UxVx_aVNkMN?PbVR3E)ltIX#S(2lcHMCjIV+T~M3(~h{yayND3R6-7q-1%bX z%x0a#C8U$u5+l2kqCg_A-)d#(^FYc<0`!BLh9~~@?g1x>yK4XlasD!IxA$lilHv?h zs%<@Q**tuK*tK%EdYxjG#$u7zTl5L2!~J1%cG7jF+Ea>;mE=rJB!B4Ep2V@Ud4{pC zT^ruK;9Kf(ZcBUVtYXt6Q$4NLuRY6+9!+dNBbMF|v5YE(SW0l4P~86F;rAafS9vWm zF5ED+Rh!?=3mwixTn|6u4&N2#c|0I_qgQ3=CURrr#9jp_i%wDy+JHhC!1-BG_x{gb zphseq>F@6?$L$i3kvZN!6G$R@EG|-PEwO!@HErmHB%a?yQ1|1hgIZosZ!e;hJ)vGAvcGpB%&#+WY6a?oY_X4T`_O1k z{*GLdeC~#9$Gf$VTj@Vz0+jB~dzBvA;fY$N14Lhq^6Va=S4Z-IOGSSI`8OeqPVO>Y z5Z1}-BbU}b6Akax`gO81`!mJ2k&qi^|BcT7H=5P&q;S`KIT-~PQGViQ)t`_E^ey=| z=X%ad4{f$Di;B_)6?|6#C3N%Nv#6|l03ra&Y?aY)!%!f1U7!Had=vK|`QMN1|Kqz+;?zRk zt0(#=jhDQAI(XwI>!q2=WF^S0hGZb}i9_DGC@7`^XzkfT4XFc?L}34#y^qzXC;jkk zrI=631gA8*{)4W-mE*d+%N_2i>7h4oPLmb)HlnM%8>4^(G%1ni#BZ+LKe$f{QpKgb zOOCZg*>{gipwt~62WS5i-cn*(E-hbGYT2a`J}06$6^*KdD$A0GSQ4I0=~N|wW>lyW ziU-D_nw#tmAKMyu3#<@i*}}^r(Q5sEK%2??Z8RWjfBTHz$2M3)bb_UonOUq2#p0-> z)#~ql$cym+x433m3W@;v>#(x#`gWW-l5#IzQ^`ctA$oZNDf?Nz#i2Q~=r-^f-k^7L z?X<2bi^VYK=tU(|@+w!9za8}|HM>A@NUx$l1Bgsst$lWlCGB1P zYYFhT;?c6a!Glk0I_8p2*jVkj0(}_w(Qu9RF&onGu{7<+i_+7iMC?YEEfvXX@EEG+ z-Sq^H5Gr^G!+}#3g$SK0y?O>wf*=ZZy<=GRi-91J-n!2bRq_($SHXE^Oox+}$qCcb zQTeD!1=7Qvob0R9&g@nfQCgpeyajA|L*3uiYl~++Of-pQt>l0i_XoX;){k+(Buj^` zhsTE#^Kc`7>4^D3eI1-y`@kC3ue%o!Q8gS6sp|bp$ACPJDWz{B1pLkaipP*M1G&7% zWmWfT0^AnCWA7Z1ZN?Rs;7b**pQm;SRCYYx9FDD?3jS;=(9?en*ZQPN5q{pHls-9k z%4PuB_UfBpy7E&~ z@s4Zv{!%kTLI#_u0tX^h%$%P24`SxDcAdnp9ZMw6q@_}qC1{v6lmB1(h0j|rKo}1CWyJ%BcW|aZ3K{vA8V=#ZbHnL{E&cc2ZEP@h19VO@X!QWz z(zl|*e^f|bNJ~K>u!nwNxA)*t9R#-xV}yd!69mxp&gJ-8cBk#Rj{v6c!A=P-7-_$g z8vJK|@wn}T*|q7EOw{tuuG9)}Z{RdS-lzfB^k3M`wfza*>s?h@ypwCvD+}fB#^rC4 z(v^3R$&&l~RQZCcxkcJ7KEav|m%e$tJ=P-9V7W!LoA2RuxTN*XyoX!Xfc%FdD@)a0 zLl^-H{r|D|o>5J%?bhhB7gSt=pwjH9fKlmPMMb3wN{66GFQNA$x)22c5$PpLuR&Vq z35tM}fOJAgP!K{7AwUQa`0iNNyZ5`lcN5R~bIus+A4Bsb&)u#vuQ{(<Nwg6DrOIR9(ovvY`R{`=iG13 zI-LWhjzu7}FM-CIPdZoz%QXiuub^@s z(6w=r8tz5hnvbP@lwTwHVSJ0%50Xbc10ivPmQU!_ifW-{mpmu5rixD2Txb}MFZrCs6=IUM%b82`upFR2 zRAI*ZR!jZe>?T~h8AS)Wjq8f@UIS^jlMcKs1OggfIv5{{ccgGm6%e-R?mEqcN&8!4r~U* z^VO%fq@$>Djlw`_-4++ySzU)jsV)1Kmp*js=LT1Dt4=!@yDN8H6&AL_wk&OL84UAt zRma5)`stVRs`fGr$qQk<2Eb)FD{x+isy|fBNAe@KN-&d<7^04&e)R(Q4qBc}`a?gn z!5$N(a-+1X>+{9J?`#hP25hAHW_wV1(dG$P!c4ON>xw*&+}nsEJ!*SroRDxoGvpKX;bjFZyBo&3*4PRQbI9E?o2K=AL=!}sw+h$@%g zn2oS#y|Tq$rCrCJYN(>dVLA-8w0qIo{rRXtxS#8L!)`AG^~5gr9yje$Lm22cU`E^;k6K$ zJ+#kSQ)NeAp10=oUn}a!T{E3u&SebvbqJx?W>dLs5a9f7>9E)=3LEem8bB&yQlsrQ z&xvdqt7JDvSQl~>$3E0ARrf7$*kxzKdGz|VAE45h=QT%7KzcBL{8vu1Qd)+e&f;BC zVMFAFOQS)D!#H4KJt{BMN+H84LK)9O>`3w%YzL}gpDR<=V!gUk^rPKfPi%E zoQhdrK4dWJd6xM5y63ZQDDs|iX1l}fZv&xcy}b*#sBc;fbL#Xn&C;iY%(2DsZ8XVA zJL^$#%x=zR<>*PNz4rI(UqGYR2+CF*uz1gQ^++Ws@T=I4ebfq8`LW~9<8E5=Smrh8EbwS0o zljEnd3x(Ac8I>;R_4ovkb#KiN0nWY22prx1dl(JQSw|4E-OW&*)A zJE)<|taWybL_wx2U~ zy)^5*kZ;7>9{+vwMho*{jJtHPg#+Tx`aJ5_%ZjUPAf{$p!_@rR#ZPvdt=Je6CE1j- zH}(489zi^SH!mA*xtwT|7#`Y|Q2?|I4W2d^UdKQ~=c%W(wsaV`W*Al@J>2TQxvxuy zMedkp7WMdO#IVUnAm@d!wkSq@lOaT?yM(bJ=i%m!WW2g?PUdd#y+@@P?3iO#U$B53 zV>Y?q9Bf-&qkb%xTNH7uhW~C|x+XX8Uc#b87Ip zkrQWL238)DAZaC>0br-+J+?no#ID*U#1pVV|IuaqtE43449e-smfRAdyo~n=Yjmjv zkw5f%jn;WZ6-){|AiQyV-l;x!_HK20ABrpBeP7Sk?1fucNq%2TR4tA(o+=e5Ez!#=@_Gy< zA_#PC?Z?vq!S2E^+IJ4c<}0klPhh59H%PoY!(T~7fzZl9zsRA2HFm?^9(JPx|EVAu9m z`wcyEFD$%PWHZq-BE`F&JPpE)n*i==VifP^W!(s-Zie{dUO=j4hVsEmPoYMfd>KfO zQ7b@tVs-NeeFUqYpy#>9m%NX#&YD*CoTyPJjB8Y_qXc89LbY3$z!h+p3sq**&G6ju zF;OJn3Vz|!TkBD7DG2Ei!;zBn`DNWt#wIxI>W6iWQcK2 z2*q_LV%+vO4DrVNQiGZ=1viz!b9$$PU8^^~tR>IG+fG%y3p5Y1gIjx(4=$AsdK+&! z0+Sz`uP&$?6k##BZ-nqB;}`Z8ov|ao9IB9BcfmFK5|tTUgZ+KnRyn`3b}VRU6s>Wq z5M*gE6iZF4;*4MAsJqz4$*++1v0hgt`s8@ap3zx^9YcpEJ z88-?R;b*gIF_%DRB+q5V4|XRw4lanHw7X!!AzGd&6r>hZSz~iKoewpm@a=ZQYZOODGq{vM8a9)Vm ziTv%c41pXVGHhbDe*Eb207I(krgQ+lrQ_(C+e5qF*-iFN0T8EMEpx6oM|)o3dG%$H z!On&@&uj9`3(SWMkBSm@d|G099)3z${2wx+BCOVH+6IO1e{Xwtc>pZwVXD%{KbRMV ze5aZm#WH|2Q?-4(Mxp_W$}SCIvvRz#Vk05B2YiG8?J=8o2`mFgpeye{Ny6@bk|y{+hb~yqo@6 z#{ax+|2d7n4*UPb{{6o_jUu6=6b3{;=KoaU$e#+oDTbHZ+A(Z$=*4kve?YzPSYO%% z>+bFrjZK8U)TY-uu3MH0Lh{{7b+wIfR2e&K?QG~*K6$BA*SC2ruJrfEwFn6|Eq$}L zB)K00uq?r|(N#ms;3$yv%#G=)4OE|KG{boRhO_Xu2ax0*?Q@|Mbl~x-jvNXH-S4vU z&PQto#;2v8fv$Nj*l~wkk_)^fEDl+xPm403w^f6%0vta)LH_(8w2~imXd_FGxpCji z+Vw>SR12gkR(5{zR#f8s9dU8;DIlI`f3>GwRn;|$eU$h#^B{KmRD=Tsl&n@jws3&1 z{($HExZ1D*ILTrqJ*CjO3%2x3``-y50J!@IxzPi%H(z(@JLk-Y(zIcKZgA$2@YwDIt^x9$okv`()ic9~qc?omm8z6;<8c48#D=L&r zM0Q^meq$BYTvvC>qrcl)NLx$0`tY6F*!)hTGP$_q@k zPQBi%!jqW%cMr*N?@0T8iq`1X-*JCm62VUKHvbU9^;xK*4Q^B79|vfog;s@fYdBb| z5@@HvsLTL%jhL^*ZMSo72bIcq&2_G&tJF6pKbATBsktC8Bi_N=do3n4wtzmps1O^M z6kdTJaqQV@jEMW=BhdB`?&Km6*e{)a5^l<^i58Kt{8hojhsWpI$7|;X9&YNcQ4?yz zB+RE=-*R)XDWSN27IeI$F@0c89M5`8C^eF$4W zZ9n(5m~%`L!ECYWyQQ&F1w>`A8i(|4NnCiW1naBPR4Z_WB3q*(V&G%gr(zk5&aCiN<>;(L4D{QzzJ+wxX!k} zuN}`1QAT2h^Hf}|9WvgpeZ43>1Nu&Tb-4l^+_df9-68j$o1#5VPmY2#MCfWmQT)9=LPtI8DX^dxOgQQ;u-kR%$2ZxXPAm=^MaS! z)RCuy!|`$Lc94#>tl#0cPhHz|$-dsAJ8WuDU~kDi*BnQDy+M(vnQ1z9x=Rxf!JlaE zNJFJxWxz@4iZ8>HfO5$7R?4{ImUDPqhIGij7cXEjE_dLGmvki?RA%9o^Kn9hlWy95 ze`xNV=t_PM*NeimOZI;05{AG;_O=T#{H=ibH3msmcYpF1Dh z{UHWN&%hVDsin=w7~d2^FIPsqnrwUpK~2F+&>&CD zdh~`Zrscd%-*ux<#SWGInbL3C4RuM@W(}o@)v_^{uV}w}6MCgIFih727rj^g#Z~kX z`z&0dbyN$ZKVt1E_32x9C!qn}0 z7DM0`f938LcRm*sAbagvH&gHpbM-nVo%&l>UOWPcKuYxG(y|`q_;E{1U^1@2@+C0zaJ%}S8vtloc9x5r!)j@%F zBvq7W#>QY9K1O5({V)Ff-$TTyn~chK@f5&=U318wnNu@hZLLmgRhDJ|6;WYf;ki@S zX#{JW!&m1j@3a#1-k2Pk{CKWTY*xo?l_ z9$u+>9_d^B`IR|N?e|b)XavN-r;}DaUD~NSb@F3q0T0&&G2sVE0Uu9ZE?Sx{nNHzu zJRr4syvE=B9?v2HTuIW?-=-DmL(VE8c#j0=#PM)q;A{|EJvNBUAUB*O&F#1By?(-{ z*D1Q6a_=%1S3T6!=@Nf`H=T~0>Ez;iT4CGNs-!#ZH0FOxYJ7S!OqjF(L7gzm3Cyub z4MT+Uz_a;%!WrWGFlKffW6D>$hBPKtqNMB`2FGOrAYq9WFSR*8B-hz4FRxIVEBZU; z`iB>^2TGkbZ{4{cTnzokrVajZzp!aj>kcP@@vOLsY#9UXM4mx+P|hZIV=j7)&L3*o z>}JWbplBGHAibETBxkRw{wr2hYz%c$B7m>+T-2ntP+NTDgyYau+E}aB9(rSN8}UkO{nO>Xp2E}X zqjkfP%c!(aX}_6VcUxS&_vAc#ld^w1hASx;CHTVgtD_hBGtFbAB%bDAODW9E0#`WD zpD$8V_T6P_oM0|${@Qk1{}t>5-t*|LRQqp!>*VH}0vGITb$M8bt*zR4|{>@gw5Dyak7EngD%o# zUth0N>Z;;B|CQ7c&HvL91DWl@5j}&3$YsHdGcL=pP?wf5; z7*$%0s&R|HPvERz7PEWd`?+|1=klYX7ky8UGjUFS4dLXbO{#u7m^@sJhx{+GbiMYF zsr}P^XxrLsZ?)R0wC5FOm&siMoX$WB7iFAEz)OzGil2ji39CeBtwzso-?~#KYO^hm zIB&B@Ec-^s9K1=*x7~eLeH>*MZftB7evNkjA#HHEyutCk%o&p(k3|R^t&uEG@;vcl zT6s17!G%k0%3>Yp6UDfy4w0|`r?RCQ><`z_dSP_VY4a8&qos7X7*VVA8+()V8^_X z*}OSn#6fKL_ZN$C_V=RB)IE2KPRXO+aCV_yGg0ud!4;qOC9~p&QqVKO*5cBE^{at! zo1pZmwFdZfFafpZeX0pwjXbqRg+NW;x1nOhZ1s3&ZH!P7+js03o_+4*F#nDAmYB%Q z{!~VQ@Rz?=6ZUsk^ixIXnBDR=$ChNLU&Ru`ZeB z^k6+a!eMd|KQ=QS(jy#QSYvLO>t&})n4#TkHhEvAtNVsBxkTX{tY5YbfrK-VD+=jZ(oV7XF zgf-A)`W4(_?q?kaBh4;{S}3d*3dH1kI1{e5A@{DRH4u2BBJ%k zsBRj^c!R3pp$V5()?@XkwY91l8Zk;y#l^+IsxoTF>CPgaURYg_IY%8f*!GUTCR;N{ zTbnjT(7B^ahmS&R337@?aAcoeSnAXkyADN`tSDkRNO)PFi(WT0b8UT3>#I2HePpF! zM2Mx(!2{jc-6?)}r!#gggy)gX9Ce|BC*zPl@?PYXa!dl@5UjdFvemTr?8&&tNwBJc z1Hc_cNSP=8`S%(9kALvqxcrE6e_lrE`;p__;-4sO^rBeB^IR#(wL#Gj9}kN(iqTW> zG31z-YKC-rG*^rYCU}TI?>kv`SJ~Waj+5}5@D>pj`89zNn7&5ujW+iUYL)sYip!ZEj8Thj9HWZ^RfZL znpp@$6H*Wzy^S}Fi0;zvPhL&zEgki5j3AzGSYM$GK3(=&Ivhr*orqK#HbZ8cHH5Uv zJd-T!E>hwgecyG}y~uw3!^onA5mKU~DZbQh5TkW4F+Tr{o{ob^No9wxPnR5vkHmrh zxGsMi(>8 zR&Yz)q8p))gT7^p!$LnsV!%_3#fWg|$OYR!;e_F8H@AJ=2jQtJXKS!s>e2e_gIq0p zI<Yu!jHZAig;Kx&Mo|0qQT0?{>}q)_#Wd;VPYZh)ZcZJR#JLNy4&2s^6W)Ij&ws zHJMiELbchRJbB{r&Y?Rftsu|Th1Yw)>zyd;-SfH<;=XIFkn1KK&#JmyWl?K0I!Fy7 zRlk7MMeY{=S@-@bZ->*^dmC}p^` zlL?bQYq52!1YbzyogYX4^{!MG(VRltPL@;Kj&iof^K^2N>fm9%`kAdrZdgz}HYn1v z1jB`$Nc3BsiZ5GJ&?ziKq_Zq%anr;2~;)kl=YL_}tOzJRYy7o`#}+kPS0a4s`v z0WF*IIP2twF$wSU%iSjqcfa{z>1tl@O#L!6rQ4Y@e1oXI4qV1JqM}-28&YB2my6tG zH#2pZ78Er;S`weA@D<~nID?;_Xw)K9arj_+hAl^0>##vFY-bI1UtWqF580~_CTdDN zQ5&fs?kCY$3X^v^hCcV#<<7ctrRj^1cx}Bu=Y3DvR?2}`Jrje}hrAY{hF|IUAgj1> zTvL;>A8Z-+iTWP{j5zwvsUf&P3b!ni7o}@>FEW73yq*hEdQH!ZQkjzR^v2C&Eyz$V zU7UGgURQ>>enxM-00a>{lZ293R8V)!B(w5cb~m$E8C2LCar~A0ToluG4de(X;_+0e zfSa9GFz>u=)F=yH0%)uNlz8awCtA#(7*>zeUGTv{V80OMl9m-7}ps=mfY z-f;nFkia^*tNX_%x;?-VW52jRXhN0DaJ1YaK| z-bI_{e)>2CmYh+V|M_9x#XMBG*WlMaq@{|8>8AGP>E2=3ATL*sSyeE5vhr~HyRk}n zTzK9XmS;y;BE5ZgoUHCkk-VsKlgXzRpFYG}c4Mq{mndp*QkXPWIG{uR!hSwKBt)+# z?}c`$?X&vjW$)|l#U;yiNRswp6zZ(Q5Vnz_y7qaZMk4hYPv~j5ME6%FvJP&53czv6@-^Szk^%8HUHjA3_ zkD2#XGCww5BizR#eM_|8X#;qwn>%=H~hDEh{*&Kc&)A&+%5g< zy*K#ME7~GPs!NP#a&;;! zjJV|-&M&X*NcWx{QR4WR4eledGwW1`a`|9mqWckb;(=;6wB6d?kteOJA57MF7YKT# zS5%xsxDLFK-JzF4X~n;FWgCac7E5t?0T3ZZwCL>i9i$PTX@b)kY1h3Ax)Y6eTk>BQ zC><{S+y-3DrCQO8Zi_EgVI!@{qV4KJ>6kNP)@Z z@)C^;H&m;JY9$>sTVs{Sva9S}Ad}5kl1uaD8D400sziv4_M=&4;^%hFQ-_v`4=_#U zLpiXpx;jA-J$+4{;L*0{?C*SDUcyoYT=u2Cz^Owy^Q+3vv!eIy+ZWYe;>zqMR^Pr! zKQnrh%OC{&jmriLW_CaeGF|;iPX0}MGniWk@u&M6m_2|wb40fsXx_)$S9RZ z3{JdgL&>qzb&nRP-ySDH{nqr50Um~5!*BKmID+J^&+A=@eQdyKB`&uy|CNN)ND;*- zb~}|*0sB}?v9D;faYnXOkZq+`HM`ohDr1|qhHvkrd1TQ=Ay5FE65?UKN%BiC*OeDn zZzrl|4HKrSxcFAbo4xgy*^Tb~GE^Ha(-$&`$OI{+H{UjcOxdQcAztA`?_KQ%Tt4ji zM0U1~K3Ty*KtJ4Zr0{xyN%QheeJH%z-6a#W-oUc^=>9R6VxvR`&NU`+8>L4~p6%?w zA^C6OO4FoLzrIB3%4+rq2-;LqCyS4dSfssnQ1n7{RTPJKo~ygxB3Wei&A*x5U1fxk zDtVSFVS9*YYvPb;eAbhc_T_kab1jtV$^>OIm510L2fahU!^9!49jJB9OJpZ-F8J4{qP0QWQCW|fO9_~f-evHgV zcaVJmn#*)G$UbB;WBOHLHqShY%P zD&HA5^t)4@{ry$y9TRV=eSB!9QZU_K?v;IsAP+axH7+@)U*bHqO17Y<3H1{(v4@4M zwd-f91BcdE#wa(3t1Da-1oc-`%lKfvs%}}S=SYXn z1-BmccSY(@3Dsis2d8141}?WMVnYSmVlClj&I2P$w499>Wy0oO`}_A^G!skQ(zl2Y zIROGWSEUlNU#ifd-i7P|E4i-*Nkm}Qk@eOf=n1u*g^u--V9EPtn z@U(|0F*_#rzc!md?l0}+UpTqfCnHCCZDklTC)65!K7lhIU3{^}RNtQ@Y41HI+UYt7 zZV&SK+0qup=W8VR0fww9 zoK) z>vs@N@^Zb!`%L9N7K&bIdbe@{q6#fL04K?EM@XV|h2zql2f|5T;+Qo%ym^oQFppjB z%nm>6?J>KAb$6gUIRFT}3}7|X6myS{#r>oFP@Y3>VsJCtxM)bLLP9WS%X9YKhK0s< z9Cibg%k&WGFRXiSA1Uo>^|jDWw0*MH36Ljnx|iIksIiHe!#xUaYA|Gpw`SG+hd2q} z<*NfzRS}@KQck1RS8i^kt#`#mBR^SPKNVeL5nEZPf#>ovW!6l=$CmfC&-xR?y3z(2 zcndA<(ahZt=~0%lD;qlQpMh_fvhloZCLh*0Y1LnIe}O#9emjGr2VlE>gH`Tun0cbCio(~{;BsosM(kp zaXhrKIV1E@H^y=I1zu^83QbL}do9@~(-#(~M4vAD{)d9|@;wf@4h>fFdOpqlkyzwd z@B`BO9tn7!oRr=zfv9xH`7YX-mV#p}73rVN^$Lp1UUv-E@h$5Y+S`#+3XBO;GJsEREiVuU>e3uL-t_o*9LUS{m77Tq^jEe=H-h1K9C8GHD7&O=U=a}BH%pW zJ8kA$f%MSr>e>TMyo5jFGH}&zmILeTf@j7VB( z3Z6`^%z*Vdr7cE34(qjZW`JoIVXZCR3oXTo^xVTL4qy6W+tcj_D}cTC*&$G&J5hE$ zOyU%iVWWS8vT^?*%73d{yl?+rx6t%_r>-rb)(}n%s3@i`{HMBw=)Ov9dA6ikl8kJw zT{7qT{Alx?_-#V#zykTsk4fjlLa;?tEXmM7T^1bak@^IDv?K8G0I>y49A zw9|rPVt4Pj&Q5FaPZPWzkA&mlSD7_7Ta#`BKX@NbSKQRf9(0%40O9-BH=O}6+YK89 zCLq(lzxUOnKQho;n006%54s$?ZL37{9y^KOc!u#C%pP-w(f#K)Hj01$wVnQ%tAFNd zBiG3|Q~#Vn=J(0}!?}{{x6es3Jiy3b%v53A(?}`iZRD3-Y?&S(2*d(!kdm4;q?ZVo zfY{}Zq2WP8ysQKHwjwC?N~KW@Uxt)Aw^vDc4;cV*h4Q$L)NdeBw>d%1jGyq=I}ZxY z1VDdbFgl>#PA8WOkB$t1(kcLKc{9<^WnOPiiSH3@Aa`u07G`*S$_IJ!S&NMoaBy z0CKi+2cH|;IShCubss#60Bj5ieVF5#9x2U|mQ^5!v@_tLjJ5>TFIU|3F{s2I9^{eb z0pZ(j%n$?25P3*tIc3Osdjdv!pM|fez7TbS>ERu{Pyyh-vGWlcm1l70WFtc-OK(q* z(g1cmvUb8@x_;U_c)=FCP{NhSs6VaJ(w{D8A`qeIY5615y6_Uw?g9r)kh^um=+anG zwFjgbT7PFr+yNF0 zmL#iDtC^ddThQaL;%u|GsnhKwSL(HXv1;`jI9J(j7s7Z@Y$q2BhG`%Xpv(;hfYq(J zAJjA7PMvsmJQ@X`8jf+qS*DW|NyiMTx8tPo{+RjuO!*lLvC*ysQW{uG%~?^_0zXx>2mwtWHOn&>)QE@xPT(3dPG zN-Do1w6M2e%dz{u26Q;rXip>A-IFN6U;1ENCmTh#gNoSX;aA3PWp zXEt8ihsZ?+Of6I7?%M@WH*gAvyYsl7K8`yfZG&Ew4(9eJPzB#)Lhv$m!mpUyo=Wk-3T#2khD$FkJ?7K=Q z(>Tn2XLe2jB7jMB$pbWF*%_968hb8F%FyR^B6ueY^!+DYa%=GY1pL>Y; z2CD3Gx{zewKjf*UX#RB^L-^rA@o4C(C&v<7b5V3 ze{F9&vQEKCCl4>@E!niaeJ+yr5z1%kgEP7$Iquouf!_1meVvUO)2?c=;gO-P53-)kdHFRUeZ% zyBVqOr-4eP{`eP@ow=T_w`>Lr<>Y7+mvgO+I9Q07pd`h+x9r8!<%*?VIGV#8f@E;Lk^w^%CF^(&7v-bNAlc}(8F2UbR;_6?WTCrYZ&N3) z-XaxHjjg|4ny|7Su3NwqcsD-I%+CpFl{O3?bq58Gw_@WbZ5vRwopCYJ)(nzcJ%Hk$ z;@y}4|6+)AKPtziK~!n^Dt~$nkQG<&|kZSE7OpK(geh&Uk$1 zO3MAto(UKlbt*e2dnn&)UtM0Sbb%u_xi36GXUC2mMe5_-tFged4(Dl$HbMQW zusSM#5$j9W@>Ray-mINZG-++rs!j+h^j(qRfx z_7{8iE#$y4ZB%Xx3@-Ja6uMFC&%EsAwY9q~D)5a>m;Yk`P+s{E{(Z;EeBFHZnl2;a z3ue$px+F?+R2COlzbP!51DB(>O*jM^o%d1l;Hdx&7#_vd3v=B!xdsSJt3I7g@mYdK z7sRD5MX*~4y%w16rIa5;@GxN{G*4X+ZG4^;m*@NA!~Q!s#-^va7HxLl>FK)5niD@C zZ1du7hYnU=0iQ9MZ4*q%IwKr+ghD-HZB2ES-zF1(WTNmTar(aIO?_0xLFt1VThW@k zjG(II-P-qGtYP2v?d_+F%c#NXi@ayO7FeZY)z-?e`^Eq>M-3&Ves$1~H8Se64jU6+ zoVrUH)1e(9Cd=~^5;ED9D%6+}^6Ig&OTctfgQsiNvcF9{rJ;mf=$qsO`*%cpx#AA# zpmw^@H?AASIoWYyruvnfVbcj4+km4rfCHV?foI(+ zJwfe5b3IIdd~)44m{QT-PIR5o@M?*dJ#nN+sDq@9C?0%9b22x5>Nj}9m;AUq<>>=b zZ4*0p&UhUP^JzKf7G0Eo%%_a(=Ci3w zD{9n_qk_&$42~!RMopopuF`_(4z!7dz{jBS5d!%3hxGNe6anT=%YF@L=PJc61D_gb zrOg-fmOQHcd&I zzrxY~Wid2`F4(@$(io#Ka_5zw)%)@~j*F@JF;cdTmPlLQ_H6`9x0NCn*X4za-fMF3#Zr`~PJb z{{6rFk8IUEF}t>-GCkd-`Zu^j2O)q+5VZam=={U1``@1_0(|D}l?S6g7LAEbvvud) zDq!;K{H{WNBSya)2cAjuxkzEQpaMp4&Ir_xhUDLJ{t8x@NW#vkfM+^TF|^;BH-N&E z;h$NU`2JrL6nPpvV+n2XX2L`LTwnhnfac^bsf}&>^(Ftl@c(nw{BN^?XNn*>pNz&mNJ^3QMVZ~opVm(u`pqt(wT{6pHn)WW_Q0-?OX8k^^@AO8E>{_`JiIsm;@ zU^+PVM~ueQ!m5)&B6VV5h*m6sd*{@K7b|_#A1kwvp?bg&1YnrJ?u}?a|wgaJIqOCX@8V>Q2a4%xJ2+ zbXJT4=EQ?rn_3mcHBT_jmh@KyNP=X*j7y1Vflo8V;h4Uy)8Jb+g%gH0lh2gEt+R&G z8wWps+o`1UV4lvd5(J`!f&lr!=2R1l{E-ntwO8$gnKJA}9+x&T$4Tqny*fbRCQpZJ zw)Z%kfb{|<-}*A8cW9~(sq;GT3s(NZ%RwlvO3khFfBqW!euCPj%N z_}baK*GS)75PA=!ai>76Nb{&;T0iJv08{@>FC1>U$>JEW^kTDNprP$l*Rz%eO-9AR=eSbu`@$lzBOZDd#1q?IH@*kVV z|LnocxDEn*t~Enk^r3&F|EimOkf`pX3we8J3vLa=cG_<9W}9jO`ISNx%d_TPwCMMg z%{z}x3b@*NF@OyMtxhV96}GN_hvYB3Y_9j-$=LV;^!eN}26wR#eV_G(L&1MZYLzI` z;D{tNYHh(+IH)McM+NX@yixvmYOmij%{kF= zaobhRamenc=}1eZ#o`C14rjMYv$#YT%@P5r$hQkzLX>N=`wF4LM;u{}tKf`g!QM^+ zqJWFSxHX@1P1g)MmdN5{>;*H8W{sfA%FovdQd?odu zGOKxx3n=q2kyHP)+3Mdd&1+Sha~oV(z^Z8f{re*=?N8%#g+4Xl8&?Apfg=5&bDvBR zvBN4J?(Jf%vv3!x9Hm5-k;f3;deDYsK9++ctpijSg1Q&P86vNNGMCD-ET(nmM=zLe z9)_?=nkvZCcuHLcTA+?b979#mTeuAIhdK74f|lEwif=z8pNlww8z4!6IXW_7MQIix ze7a;0&m;WE$iMo8c%zmoEbf0En%D(b3sBOFr2JRDnYY)syQoM{y+}>wt?Ykr7deg3H4QgQ{b`^|9Ay` zIq#5AZtRCwmMn|?aeR;_&!0&=&a=9Bzd3ZBiYw~vUMtJL&vu0PAlqPfZTbabfhOl% z^Ge=JJqyxJrt^u@N}z62cv5j32ty!i$aM>bK$ekj@5P>r0t#3f5)Yg1lbOFZIUd~J zjc0?P--*Q?nLM$Lue2l=Q~P>H82qr6czXmpx1oriDxs}jRN%qPRIG!=i?voL>WHQF z!jJrasNmmZ{|p6WOAO)APu;)zfQUQ{lr@@Cev~zQR-Z&Qy2i@8X;mR@O*md8MsW3- z3%s%;%o@xuED-fS%8$v&Dl$LJbF=nJCsmz`Gv3k5m9&R}@cB6-JMV>6zY5^X8M1SO z8CXpo>bR+&njxkyh6sgDuioN)Qw6jGQ_FKW@4A+^PuXN>%ay|(XPigp)5~z(JWyY! zljjX+D`jBDV{Bc$00;1g;<~Nl@}6e3p;BJPzUyYRiu)d8kEB< zFO)}L{93=NTwaC8^bPaXaSGjmpK|AKpTE%`m&>=Nhg|$@u~6rdk_Zf)LlsnyCR}o> zVTe*}VDc7WpA>qcjm72cBdIwCwy;iah4^*bPq|w=ySSJ}n?xhLLTc??5muqG{A#Q@ zIjWu)fkx)YU1@&`7sUV%8DQZr$~N!w@V@QGRW{bS%vHwTXkF6l8U~qNJKj zz_N1{wbq5~k3={b{<7oIrS=nT6o>Xcpag ze&0M`B+sLov1c~^>@QmeniPQ2Yi=VP=^cPJTZCEU1 zog8#l$jeGR8EYEofL92XQ`HTGCwT$QKPB-EbEjC6Q3x?9lJoVrVMg5EpBhojvw~Ey zelU}J7&rkghS7H&+_v$Ueg@lG=+ifk* zFVp$G0-VO!_>^Ib^*#Ft!GqJw=X@9YqZnOWmX?Zq?&#~Y!BBHo6J)&3vTCIhfp~^N z?_ebZH3@B?)5T4V6!ug_KL<@;j><%22LbI9d#M!ZW0Ps{14WJpJlxzZSJSj9=72i% zqSOe_O)Q)Q#pn1PvW!MGMWC7hT3a>Bx@IRm_MM_3%L*yb?d+#7T z{|2_Vmk*R2CDMwjofq^_{pLVhLAw}moe?32N%HG2IG$r@N+|c_G~J72x$?!m+5nJV zA8RA}IghLNW3grD-2L8sUorVXr#GUo2(9te)B4wovnN(oG%t$#FoBZo24{KOt{(#0 z*}qs)_nW>QsC0wO0RD*Pa2G-h+bjQ)6$=NgYAg@7i1-=;4b9!x?6<33R_qMWSjpTK zmS`M>sv-bOFLZxDF!o7V<4uV=q8Qpn)GBk`SWLWAIkKkfB!9XUa``88$gA_Fv=r7? z1S(CMnt&_a0V%wwam&PXeUA$qky9wtnvvXMVbwBdotua6q_fZ3SYggmVZ#SG*7+^Jl-(2;!CY}Po3(PpL!@q8#a@ElhG|E4~nF_eAZ>} z>RmNaVZx+GoW3Bc1XbyucfR)HguXcS5_u9Xc6;}w?t@4HzDX>3Vn?BED_^fh@k?T+ zW|dEdz{KR_jcRR!SmaCCp>{;JRsU<-X(JgV69MhNSaIraH(PSe0%=Ajf_Dv0VZW_R zWjxCqPz=6VGqcewCYr2o15;`uJGzwL1_6 z1jcpJnzbpY-h88^9g=+6MYnv*fIzCMp#;;MoPNez_t5ETub(n#ldo0Sa03SLa8Dqr zJ{|`>Bhnnwp+%ARAkzjQCD_9I_wIgJq$ohXZp>oH)bVeRMr8N(UUc|=RA=XRroJ2^ z$>Z7Li#~squckTP)xK8;%F|>bWU*l+r7}7h5E>G7!`ifzyM_BJ5FpCO?sZ>HW)m|+BAUPgdmDI)`qHEFl zKLyV=O0>0``Z-XW$wNJD2nZ^~R>*n?N+A3RT6K`|1)T+Fy;PZqQ8fdGyyPoRzD9|`4wg&+`!DaWY}mwbmlOzS<& zMasAF`z~zd?j(F7nNb@E_C5HxEi4D za_}F(9W3fU^{RjBRsYnh{;5}i60ypg^p4X8&nbC;z&^M-w7_U@#=uz0x?J-PKvTX+ zLSm!^qRz*02C5|ludVsNf_MJN{g|=wGD4fRz6jPvtKP3?CrklpKIl#cseKma`s`n+ zn{P4OMX8N*m!H1JU74Z9n!T@KLFt*UbXarr0*qXv!#6~>GW``tXC)X3Ll9b3&qFw` z#$)uVzVqt(6dvn#uQ&G0GOK9j+k-x=+y5Hhb4XgOK=Zjz6XLX_oJ&r9wdf7riTs?v z1W!rP)|++w{Ho{dnnHO7?t4BN_d0nk-)KNGy4n@_*U6K|8aJQ_cp~Xx&)r;r@f@E3%#nVhc(6({SIg{Mu8|a#KA)E8FgKO9^sRAoH)kByMNe&6V`D@uY>=wGo zKX%I&)*-7V@7O1YrR@fNGGBPMcF=|pJ!54q;&QU?$E7MOQUL$*yrFein4G*Gwo|9U zmhkRe+!3yz@|g_5BvAY!W8X%!<9hj|OKmb+#0e1$&Q&)u-ZBYwT)+Yr=QiDxHk?@R zzHw0Za{#kXIQN4}ro8{XX3p#S4SBD(6`^&;n@YH{w$eTTUT0qJ08op!SX#Z%%raR1 zEcSA(|(|R4D5{dvqq_a_4CJ z)57K-)Iw&?>;6Lkj%&SmRpp@$*eE*1#zyDhggfPQN2&a**n_|fWY1%4?asB5@TBbd zy>6F!98^Mz0)-3-*lsqLv-Tp?v-i_)}bp4F9TZ}&@x@))@$2D@x55Rsty_VH zLtm%F+hL%}2eQ`YwX!2`u4BgBq`R;#5+zNQ3Z`7DH5&;&#&5zC-JH<|@0IyB^DH5@ zC?72ku$>^1Ct6@qF6YG_`b_9k>_R&hPx(wYnZEg4Qk3bXzHH3C=~S2Edyj;Clu!iW zfGtlcZ(MrbuJxEXTo2w?Nk-x*zEKd% z=hn930zQd`_&Qobdi`yvlT=&UAYaD9q7TA_sJbr zO)6)Xl%UiR^-{{SjyWD3A@JnY`R;>D=*Q;MM-$87^|mlW?cCS~RI#KY z^+xsV%}GHZT#sv({RYjTd-Lu8!`^#GHJP=0!($s9M^Q%wk>=>QdOGT?Fq-mZ}>e zX=y&X&*@?6U}{we{us!1P@h;Xj%hp8#d3(K2zZ!@U(WjQ_8BCCUYi-2sD69R9SYgM zc!z)Q;j@m)wQ%S7O^b7V^oK6QlhLuP1L$zU1S_lf;HJB4-4xuuaxeZ^uF88l(8t3L zH;R6XyA_Z7{7%?cgs%yxVwI@M%P+O0Kv~I=1xBRFeQme}1NnOyXawXt_(a$MI!>@k zqwwqI8M_p+N=r2yV~6$RHMgxU5SzEDF+Zi8M5z2@0{XXqh${XeX7{~5O`pY3Tux3- ze^(8J%$XT-^vXt^%fu~C%F}E{(4;mql~aa`2JA#WXXSG(V~RD*er2}LJSNX5HaG*{ zrcBa*MRhs%H>&Oa6E6EUfz5WXBKzNd#edqswkN*JrL6m>AN;%8;gTx+6pbqC-}#jF zy8OQoa?w7iTM6us)VIo_6I+{i>}Z(O`5)%vfBCCqkEj|iCsL4x&1ZB#y@c__jOY!< zW{VhDOXRYiabVp#jKzw&PJyi) z6p6HV|060&ED)csui}7{6T=(rLLm5wOpkXhSR{Y;LVvRb{$3}hjk!GAFH$=6-&;7GC4(w>TGipuh5oB$u)J=bN_k2<2_;1vxdC^nVewsk8GK1z1Fs%vwd z=c^q@zdn9^Obg zSJgo=_h_seT<&J~5lOcJ3UsaGSZDw7T6Ghg%Fw)p~=kVDB zV{t-Yy*CIBnl#ZL8y_&4Z;2Y~?CzHSt9w1#9oJN?uA4soxodkjtAbJ|o7RVYh#&=+ zMpYsLznr&s@3+AmyCWZ{-pl86nMOxFj&5*a!9h^nK@YYFgw&ra{KCCkKTUN%H@NmF zTXDlRf1Ln|?1Jg;x2a4-(3{Q8?E^ZnoOXv`!^#e6tJ4;4r{Zl{$ku%W-9wU@vU;}H zCWoutPpKUiz}6O)Tpft1wiVzUBpYBLo*%g*w!L*$O}5hCPP){i<7jWI53L~8IY%j7 zORMzz5j?y*_30l$7so$$RFp9dmch;oT<$7=s&x~lMtCOObz1zQ>}1%zSi~@cw^eo4 zTUi}L``k+`nw>C<%7%Pi$|A*C<;ds5!cHNfkOXg(;KX=)66B8e688wfv^TJhn^pkt z_{*K}@0-sAh5Z|V2hk`fM_40jgS>q0DoZxE7pP=9kt4GU-QFub_}cVddBL@6^|h_Q z3@mupD0n6*4K?M@oOT&aEwf7TgHgLq$7@`peJ0LVV4jy4y3e%R9%@VKli9a9HB%X_ z!#oR`)1juP4}wk|rDSM?i7TUw23bk4@n;`+VZ*(Db=p}9B&eoku+orCdfU8pQOMKW zruo(->0&=t&4(0q*bc_E%K6!Vl0F@I5$e;|&0v57Z8bqBa(pXlDJ#7xS!OPI!*Q5Z z>m34VYXqg9JJPsJC+0K{G=3iv9K7_K;}&t{4c~LkX~ecZ|K0AN@CFN}YR=+C?bK4( z8Pa?Ot<0Zz)OO_i@8t}3V{|L4^ZE&Av-?@>QyX-RBgj)rhsO-CnM3tl$E=GwR`j4J z)6OFxE-ShXqYOW~UYAgdpQ?w5YNC%y`5C+f_mLaLxZ9il0P97m5RXT_zgoR<#RhO4 zePfCBCa$1fDqdf3DzL-p5}oQ+FZH$;6D8H|mN2{`AVW0nWEf%aOa!@@sMZ^Jh;ql+ zpe+=0&yic(cO|_C)*rZQI&zwCdc(0&R0z0X{D)5$$X7g4y`wPorE2btGJ`>9nsK{V zY}04vyLu`D+FoI8eySc1CK6Nb=qYr~hGfH~zsK5JXyhlChU044dRbRLR82AQOqRYE zsyZ=6*LaW=iebqN;A^yqMXD++gI#`n%R?jgh-zeh_IvC;Q6F?WICBG|G5tqOMSA{> zY$v8Q2uNdd)s@7|Rh+wi>KpX8T3tBD3~~R|SLE=MkF{xCWOxR{J2fJ(jrYpTeNebm zQ-4_CQ7gdgHG68^BdXypL!;V2)A5pNjt0t~^;@oDYXgNj{m`>lMIIhIuS5HRZoFpehI1LGG-XZtcWM;M8AZs!b=!4jPa6GAUQlFKHc_L zi~sz?t?CeCpPl0&EAI#H(R4G_{b@f@IlQaeKSINfiQEC{6svyIlAC?Q#dUr=*dMaZ=P<}f8 zdnzmaZDiq=C#!m3B4_P?02zeLz*Ukv_5LT56j9GGuwT#oJG?#CeCqM(1Zg+Xukp?btQ6odg{UUW%*co zi0?z$LPoT2ZWyI+FrYbpM3$CofDwYJcy8zSmDzL42auruku99y(% z{x8OsD?j>%dBEdaSSUOzw<=RT%lqFo_}`kpImxK_(YaKQM{=aTPoH+%+v$K4OAxik zI+j%@yU|AcMwxu>LKp}81XlDd3qWcw?f;(>%~Vd9VcX77?~OPkgxZ4fU#Dz{FyV~t zc^n0+b8Qw`-451)oCU!1+}79p4e$B><4&M9jP23*8#xjC=aSdMED) zA;YoTq5A5xBfN#ezN2ga41AquTFVod*8A~k2Ng(7K)`X>{a}J2<)v@yFCs3Km8dP7 zfly0QP>Rlo24ET?JAY=LoVne~$`&*eyzwU4rX{>!HVb$Y@*>#?3~#EBDbY4NQVg-v zDOfeCIPT@6;*}yV6zFGBFmX8C+7F~Odgu(*v61W?zKu)$CFJ6=9r#YaK#L|tcqGEF zJ$L6P0Pg*Pt%bUmITV*2nqo|WKntvm*G%i7P6~8KdHL%;Gj0Cp?uKeu+g;s2T9@0V z8%f;NMq}sZ^5ryYw&*!6$(rjf|DJG$GxwXYaEvUrr0Bx(^YRpls?PZNITu3_@1juM zw*3q^1yT{&LsxZ{`M$!G0e~@D zRo}pWj4uySyfo_<#aEY~F&0Kw@_wA>d`X~E%j8+Og*BpMjYL~l?F@sdO7>yg?U@Xt zeLZ@mq0=G};pb3$z=>R19+Xb8U1D>hgW9>E-AU3UeZ$bRJv&JDBdx{mlpCx>3Pc3i z&(}55u=oR5m97jHdT6u*3?qlS!Ha(%M^A36)6H*?_=g1miJMaKT-W~uiKm4;l%{3x zC;jtyGv3giSyy#1b>F`4O>MxK88EJ8(M%GYfBU$SIUEg!%qHCKy}FNEYcfsPsLaB= z;-8&{D)m*Ied0o(oTF1y-ecGT7mJ7-{*JjEkN>X4Sx5lElQPuAWgUP~Di z?vYWO?~`eaqTRb!tCWX&`+D~HIMxkjtXDPLbFy?-N{GwnlXd*rwH3xa{m$?>34L4Tn zdH2*5eqd&0^2fmP8IPSVsTw}ZIc&_G-$h1~lR4{mMW0FIkhHRbLx0}nH%qR_>$eae zZlgBl4=pq)E8Zy8_s_nN;@*~`j;(;dYRwDJT|Bs%Dc(wNsa?q=asmdN+gy(k?H^9 zIjS_GcUOi066UO0*l0 zK>76d1lcYYSH2z4zE`hmiZ1oT;RPSxIFo)S1{yAW+=e0~YS_e8u5tl1-^Im0js?=4=x?N3 zsol~R!5z<)J!ycr#a>HkKU8w>`;EPy+QVOMWRH|xY}~tEEWeK-Z+!gP5@HtwkIoGF zuy!{hHfVPG(#Ssik1yK&dkT*hyTjQp({-{9L$BJx8 z1E1Xa8K%fwt%*-4`8Gf$nA|rjbF2i1#()#JQtm38=4O(wN0M06a{ph!67@4@`eH3QU0x?(LYnpA!UW^rogtt#Q*bRi53 zPOV!AIEegq^VAmkned7aE|8Fm7SjV2#S{5k&ntvE5SKZFZMANbFE$AS?8Bx3g*xV` z3bi&Y3qpQrUHWjS1)w<=8-uKH$+v~%qr_V@Ck%FO0RoN_@L^~)tu`w6)m*hyvafqH z*N+QxEK^FBG=A;S8Zqb}Y5xKTZ=8CSza>Z+FVC3okqmyX;bZ+CLu)=@6D*944W~* zIGO);J_|c0Mny&z0WZeu(v0`vp zk!nde&Ctv|_^2pd}Rwv!Kd#$et%Ad)RWiwOpr5)ci%k%Z^b;f0Ilef3GD<#9P z^LVY15MEv=m!5CQ6o21vf_Nrh)Uy3uRIWW3p1S!+&*^T&8^OneAUqzC=P@5X4mw{x^ z=2gxPM++r0d5d9lpWi;6?HCt%#(4&$ES}|eSp*3%(3~6pu184i3fT#8+M^9F^eVtA zmfT2&gBw+6klMY4cKg8DO$Pvdb!@#c?D2GJqRG*97|xnZ8E3f`1Xu+lKqUAPAzPN8 zq#6t0n0}0sK$eZ6dvMWXgp6}boV5or%pxF8FTM8D%B0wdx32n?~m;*lCcATHC z3!8sF^xEX9j*ixQA&K_l4^~#*)!@rGUUQ*-;M__|RWFkDJVJg+rcklgGvMe0r=;`P zjAyEb%kin*&(hf02f~vY zu%y`cGgrh7#7t+0O0~ebeWSzKjO~UU53`kvl{WoB3!;V0lH>kh(VKai>?>vNc~`>y zXJjjl({YeZ`Mo)w9560)CN+a8s~O}rnICQ+0g9+h!lSehxf;|pc*wyl( z&@xQRd1XRvMsa$55^+-rJpRY;h!O1C{CgKB!^k0~N0~-R6<0eJ4h(sS_xpk=0ZecU zlbp8rjDy$|`^(-q-RO6-)_9~2*r-p~Kipd&V>p_X#d_@Q3t4FF$dT4psIc+0 zQL+p#WrGKc+b!zNztsSYn!3hMQhs0iqHdbhEzX|>Vw(eETD1K{P-D&z$E#e=nedkR zq*2;TErpzP^d;RljhS2Q=I4HC)P?{hRynL8=HGEHXWqVGd@KtG?;gCefUt-RTfBlX zLS;YsSyK#~^!Di~wpteq)ISOL{o{TB=$iCk2vod?zwj1s1(jm!es0Fr= zo^Z6!R9vsu_8qY{?|ta0E!vp889Q#FQ0<^}{v^iQVsbftP{YS+H-)}6tEaF!@A*&d zwj-nzU@?tX1oqI0EM%Y}SlysT)9y#-IJ2MRfRh zMR;FU$LHFen}Q%L&_lf{lx-hQzJ2^&k{&kW7qOp_h*j54OWBFAwIF*XyW={k`xJEU zYoPa$+y_{mYs0ju!Waf`y2%IND(LfB*zgJ5^QeMqUMSqYOC6djt&nt@^*$w9@kWVJ zZ;^w!AvKw3tzv&A6bYLRa_0d?joae>i8bhBt(fU_@}|gGPu7;Lu}U)VSE__#k2m}% z25KsG#Qva|UvE#ZM2Z&d&1i7|(K^Q-Qv~3r`XlKX8oJu;MR6lT84t+2x2Blnpp_{w z1@|xCSSME;kkh*U#X23khX^2KkjY8RQ^_evgxpX^ zOOp_-(#nZ$xth$GRg5;A84kO zc@NS;W}D=pMmwyF1ljLHLqY;tKiNui&5vJo+oIHND;qFew@bju-4KW^%`a&L=Nhl@ zfvz@InK4F)s03-rr*l@%!y?zBw(?$dMO(6q)#L$O)SSTjrvkx0A=31UQk)SPJ3m)3b-XW{w% zQuV)g9{r<`H6K21F5}c)&^g9QO-*$*VbDSWzD?hHU+~1Jx(+5dPME?)Iw8Nm3n3xg zQAS29vo)&*Y{RL9ySv?9$7Q9{nL%wVac_u?x#&YPct0fre-)cBHu2}Ui!aypR+~O2c$1?Or zHTs8R(#^)0?wE~tMdZmoKLB?d@HYXCsnq!~dZne+-o2qa*tIod@2CtRvD?|#=gbkl zKr3}AwQjI7e;dn5)DyE-&(vAZT72zybJ?{v7aNk+u^B-C7}R&GpNbrC$T zx{<9s#qf3;dF?(w3M(wW{-tgOutOIcr;`4MNB)2Ql}GA{jtfH{yX!K^PXk$LFmyDj zr0K_lC{59q#OHfLeGYq$h^15|w!Xy13OicBQMr*%0)Wc^kFaPZL!hbqJP1H^H2PN$ zf0E_a`(KQIqh8h2Q4JuirHM^POF^O>3Nfr^e=IM0SgsOi;4jE=!$?N^3TEwY##%?QRj47zjo_(9YZF>jz6`cF098O5Womz+Su~t#MeX7)JKu*h9PA7w($chb? zVRB{tR74&=8`>o<{mPFBg_=kzAF-+r}>GX)$M2S3LBwcU^V6H9cEf#Yct zKt`e-E}lLOI18`oj!`ZqzNE5J6}G{^mqcu`vb4{1mnz=|qskNJ^??@L9pw)ytVrJorDmFhG zF7tPf-vl$DcC)na{dl|`0ePe5c}HgOwZ?f`AlRq}(UWq6LAFa@&gXBJ)fwoE8>B28 zQtr$saix9NQ$qdR^F^kWX{_&5Hz)^=P%l2bsW{08>ritfC~Qfl0YcUu;7ZR{pNYVa zjbP_R+`jvd!@lm{+Le6y86_*l+eh?k$AsECFx37~Sof_PZ$H%3rd<{pKFge(RK}~s zhVg55|E6a#n6=zDtgEl*zl~_FcYN;Ihh|8;=LCZh_m>jvLW&8jP3j!%aQB0!iJ8-U zDnEwKr_GBDJQ<{bo;>$UJwUB4IaGBm_aCD^86gEwt~}idMXa6Q8JetsU@1f>I9TzD zb+o9vVz33AtP0p{*HWlE^NEcv%6}Lap=zbuo@< z=o-v_KNhR%=*O*yS$%k@I5N;g&02g4b5@$M7H&Z!4TH6(bvuXN*gE~qlQuU4m zOoYtyKWnANc_H8xF1wR$@n8w`0I#spMrYN~>#+VNb~Iy{+bMWo6$$u{4kN*&)g-L)3ey`?GH=c z7wxu|3rXL;?P3Ri41teZtsfWNKaw0KP9xQ~MYXf3I1+I<*xwd9 zQ88Z>G4CC*(;!77A_LF6!C5RO>h$u3o~ZinQ%w;O3@kfaDc>=0g#kN#+;i#9g>ioD zaztLnkjVO|b|HlC>shrnh5JHm!$n4ZQt(`BcUvf4t*dyAH6Rg$J*xN#Sz*IEhR1P2 zpaHzq_bn6C(-+VOD~V#JcW`y3fv4K_;U1SVMJ5h_xx#n-!KN=-9s3M3ut42WQRW{T zg5(BU+f6!mDWLOa|8jMZ8jjPevC|}i7{$>#la4b+bUk*xuoPIv;yL<RD zmxN`I+`mP{dL^t9Q76H_0mR1t*|1ySw z9u-wE0@Usb!G4IZK2dl}QGTwDKN%-2j1H)fu9WkcZNYKCv>Z~Bt0}s7B3oFhSDK1| z2TDjK>I$XSJZkZTDbRvg93M^cv@)A-rBD_jm*7D16$r=)!j{h%XeETq1Bq;)b6uI? zX67{XWqd%S^%g3{YAD}gad%a!14!Y&sM!Dd;xPbG=(@Rck2;F5(P*}Tw4_BvhiYu3 zh3zEpCRIi9&rkm?$NGw^&@BjuUsytl#6Uq9(|p>w?w zAOmwRJe8EU%iemn-B~zCZwmK+Bv>JtjS}mLt^IhxKs2x+?-IJ6vQ;yMV%kxgqh@5F zw(t#blmue&Jt>A?jcVvS1a%c=Kws~+c#S^*P^#KXVqvOplw)#A@c$eYpr5D?O)7av zYk%R+CE&vj0W}dC9ghW4F$6L+^3a&QMEj%J-3_*WmPDOJ-6Z-4wx*W$d(cn1ai09q z#0q9Fo+ixb&wt4^et{LJQpp6pqR0>`vSb9vd0X3B1O3B|g@eX5=fb?a2hx?Z@2v?J z2>?P!RHHh?1r-f{{`X;1f4zD$Yyfa(u`-pEZ9Z9D9x|Mdj)PzPCVT%!r}E$Y!7S%QM?&?Cf?FRfRXVyoxgKE|AC*wH#8I4^ zt2Yyuc#8^KllhI)QQfw^!)UPTpBTKGD6`h!ux}KeL^Z^tR^KBTVrxcX2C7u5wEe$lL}!%}jK0W@*FIqaDm@qJ2LE!MtV$q8}Qc)V+&`j@bQX0A+HFE$47 zjq+PADbtU|xyf3`%1>5-L37LSbgaE%qu0O!bJRQhUK_rx%Wj!Ic2nOPE6OQBe?Gz- z;}btDDc+nlk&?UT5sfLJ&W<^c4A)xUbrqW#>`UUuuPR1rU zo-srVXD4zC|){Ham2$yp^gZ{2Ee|5%^?L(=mxPvF)BG+)l=ep|)H$vgK04G3bB zw!nkzv@wtdvC4V&agT}JXeARR0Su3Y24gk^OaHypYu6Q5FW+4iIdTlS|Id=??1Q&_ z%6EEtTNhS69d9Q!4AK0kir&VYo|T%cAk5+;up0aJc|ey9PAP!UhTH;bK?=I4obZkV zGk5WIyzGQ9AjOy7%>cCG$~ERpT3=I9A!+xz^qzGXH(8w|u?}iCo#cbOex2B!l9{W= zrJcHS^IzW-|MgEAEEF%Xt{C-f4SNOn^*wLHfRMnjENjZ8gIE;M_MGJ9T@d4Qk^abH zhl8!{{&Wm}SOReIk7Um3rW95(hk`y=Yp#ry$vlHNv|Sv@!$ho&Yj@H=oJ@7j#M!i8 z{=QQ}^3<9Xqw42MOBHNI$Zr6wPy(K_E_CTV6@a>8Eff8*k&~j=*6^>j^==2E>KR?sU6Okg8R^Xf%>~} zm!dw+kEfW#-?_;u9S=$CBiCk?f7oxwmx)Uyutj^v1Fy#7=B=5dbCY2>CjT7B%9cZ| zOtkeEyCXm*659OnHPbD<(+}G6wuTM_-6a5#&(Yi3lvM^o7}z0>Lil%SxFs{xpq^SLL*yk?t)xyDo>c z=VTnj1hC=kAM<~-9y|(rv;?WCjj;H5%@1hw?KN{gcNkC}?Ka?MqV#sKpMavJrvmMR z3Py9OeoboVYoyHKR!<@bya;e1Et}4>`jA&PW(5OX4imXx2@cRwXPxT2uT`m6sWGJuis7m zP{=yj{Y;hVJG_H+F0 zbX*XJ@%(>8Ih4grM7EiIt%>A0cX8RjHlqU$ z$jfE?!n|6uc9<&2H}0n3g-*u=Pu0^PTKJ}64qT)L{sK6{ns`ouAN&p-43!2mfDC+j zD1=E6vQC!?ypMQ{qq8JvO?T&8D?I?_`6@EwF1K=2+^S`b-k{R5ZM&E zqTH&R_KMW>XqW?3dUt&iR9;t(V^Pt!JR|g6AyuUoS`7^z5b&#=#OwaP_bI5A(~FDC zO~URBk90TVWwMvebh%C{jdg)DtwIVhsQ2BM!7By&!+K_}lz+0D#r*hrkt`>W)JmWP zAZykZQOmBY_}ajkE$c9Gf|`#1ruY0$C3Vhl%csaVAN$mXSL=_VRUW!4PAq|x_4v}n zV4(ajX+dWJJ(o~W`HCI8Oa(H(7a%OC$N{f5{U*;Yq`&Fr#nfADZLg;q$QPh-aGk~E z0Fq%ghLXQcB#+hJbHJF6w9Tj76SD{nCrW0+JS0h^&q|)Jlk(l$vmxWHnRaG+!<*}nS78=JgB7zZt878mg z4Ujpy@=T}+IBvTVWT$CN7bevEi)$l*^Eq;n6W?Fq<*1b0C&SjO9h>X(90OjA?K5A5 zsW1fdu!UIr5I2C#C!>+l^?6sqkf>^Cr7_dhDU#meIp>+BR=3$1RQzhBQAO2ZVNJiT4y4md_TRA zanLiTz*L~agao?JS3d*x_H2+&16wfQWyxpApKdt!Jg#fVmZV6-L`Re` z3}o-mACFVegz<)Eu-M%Xg_Z)UgB%Js+v&Uf*HG64GQ3@#xAe9jY9u+<7)|6kpN)cL z%IGHbR@+~zXpg!X)&bI8ESrTnb)9MFlj(e+WcGG-hvMEFmc_waV<`p0tSSXatrO^z zw>V87%L!jj(TdLmPf_~xelL|j*1%LBsHy?obcq(sME1R5U+k$FSZZ7}3Yr@L8{3t_ zvXAy`%O1X0vB$*T)pkn~f2GYWP+?O8&9K9aR{e<1RzE(spNtGa?HuQ}l4njo4nVAq z@TRlrqu^kZrhp;`?q%-RiF2#+hMO!n@gZOXuhdmqwn-ST&CqZ4nTgB9W$fNxA9>Ei z*P-&oLO-RC`^xw8P6ao<@VP`B-SmcL<`4NCTBR+AjuhCei6=6K`LhCl@w>gEJ&Qb$ zvPHV3QwiDhD{U(}Q_cX;VZr!}-A4eDk#M@VDcxRsYi%RQ=Ll8a6hOQmh%Mx4%OYoWwsl05!P# zhi}7UeQ4q4@M*IF8Q9&A2c9WEN`IYHYPh_JOuMX^$vi9JEWxWxpB2(IuLSF7~n+jHQ5BK%=K5{|=t!7)22^#HpeTUO*=ARBeuQ471 z?&7ZUqvJ@>4QHoHU$(i`$A21mB~q9{hY4AV_MXIHh-D#k(_+D=)8DcH0%!;&CtYab zEoV94(Cl{2Q@^DNkb|>rQh=qMHz3`x)y?%D;Th%ItDMfXC*th*NEirYLAAU}#pVy$ zAXO9RDT&Fd8LV1Bo#Be|q-4I+&){3qGC!Y#h0{=>EW-8?M}C?iiWAQ3%e^PRPs5z> zbxr+J`Fvg1slLofrdu6q`&}c-Z&<>pIlmSqTtbL(iof5MYa{_$G{dLaYoYODj@EkX zeX;)`v9)@Pq2m_!rFriX`=6<;lsRsp5QL1yNMTbQUIV4)8EQr(fak=JzoZr9xbxZ;oWhEpfw z+#IvoZ|YT6tIZBGA}1>H?AXu$z?uAPlipD*qcMR`EwZ5!{JuQY$K@}ib7QT_7Cs$$Yc~Q9Ah3mBKWNC)@}0 zjt(?7G(($AX|tirG1gxH6c!7g@{-aSU>0tjRn`cMUED!ob3bsWutz72Zf1^wYHiC5 zdF`;kQns6$5sYAdun|c%IQLSRaIh|5);q#@{7Ki3w2r*#oE#4;B{L}%>?89RJUed3 z%Iad%^^rGv01nL?jsT+GEjLor&cn8g?=9FG+MXbzo6^@&ScyH#qKB})+9J*Szvjw> z)Q>^OOJ=HNp@^RgS$;oons+A;KFLt_+>NkbH#l1uLMf>^a2y=OZ#Y- z0stJu>NTQ`%}x{Fut8LA6C!L5NQNb|-#SNxg#bIn{wZ%CH~*uMy1`!i^O|8c$hBSnV1)mu%cHyrW?HV z=l)RFdFrQtyX%3HPslj(`u3a?-7 zzs_V1_c?Aqs5{)>e+k8Z9w5I6iw-Lp>)v2lN$||LUbwz<=aU%Ey{Let(0Q3mzEct$xCa}+9Exq$h1xV0e#DUYg&}9h`2*t zf|{kVvOxap#e6jK(xx{rxD@>%XrZ1adD3%f{7|nT1#OPvG(DP3k@;?<0v1p{y=u=y zJ=HF-jYn36IePnhzt*!k^N}?^fUU73Pp001GTNhB&Ou(W-{rs+llZYkX~n;DhlY&K z^#v)k70WPZ`V+O?(bg5d7j_Qor8V9O2;l0%$uyFM{o#Ed-Yzj0*O0Wf^dkL1Zgh52B`vJtIXrB&L$%@x$({0i z%!@u-q3^gBn|XRa8~JPXQj_q`;?XNZG5Bm}{Gvl@|8X_PTI4}lqqPo97`V5{2;k|N zte36r&e@i~0JDuhd?ESLNXW)#wJgEGS(-rXpdn>NpBLvWs5*?T*m*YAIwo8N6z#T> ziTmNl%4H30dsy2=p{$|;@*94?>L2-Pj32kH+*v+)<}7xCCq;i>AwZU~^5c5A#jmOk*ZAcyeQ3hDvSR9I3zYz;!;MvJ$25 z-|HyaPAScjf>#RE=8v?cnq5>jr9i> z+YEwDOO1+14}1R5vkml>aJkvLqChf#{?3Zr=FK;d+vQ3!|AxIYMirbJF~4LmyiJtv z_2=)*8SdEenI~s(4JvY0>E<1Ko!37aV+7;uLs$XOFzt>ENd4;NO8=4bvgtbAO>NfQ zvm1=NInmZQduRm(n*#Opu&cY_KNv9={b^*Ro0)JAuODXaHduzVv|Iha`}Fa+8Unam zR&Q9_d*g%oUnm%TQ3X6v%tyDxF>bt9SorW+0q0&wmtd{Cb}|5znMd2UP4+0Kb-mz} z5~>y&Yk74tpCesOO_uEC$&OncIVj%e2g1_4*vpmAhVtMMdqyKcearJ&c=;UvS@h$_ z_HF=oUG0SI)RWFAR%;K#YsfgF`}ZGqbqyxQiNjOf+>X2fXF?sVvlL9PLB-9j$7(f2ep-8vxcsmbCFizQGwYHIdkcg{trOFj!P&rRl)D|^{G z4eHmNtZ-(WjTns0J_v#fbpv{1ye0<}>@99q*M*fHn}J5YSiNW#5U|SVjmw8w8?BxV zM~)3Hf~{xWm#4bJq=ukA%jB`TS1;}J8=)zhdI3w0CH-R+o|rZ=Ihwe^*i*EXGp2z>W8|i zMGH?=tDy(G6kaOGJ71n5JekUz(Knyrii~-0jsebdy3LQqF>DTl#E<%%>BAT1qeo_r2o0^)s7(mM=1l$bHw(ZW%=}kJM`0L}ze4fWD z6YpDiv^T0PRNhhL-oC7A@+C@brdz4!MKf6)f2(zaRm8D4<@}^DD17 zh`ONHt#?xf54l)kHiSP}&7i@|pic0DVc6&E6a^n^Ws^=@jdy8u6^DyDuuBUK@d=X? zEu~)Ft38&NG%h;TyPoU$ve4-0XL($`ChUxL+tJ?!LdrXtF%`3AG(|6e@dt8-f!e}% z-qLt%nxsl)?o8%%{w#cP4_=RT(m9}PDNBE%qo`$-Bf$SKQgh5g0BDC9w;{=0{G#7s7hrQ%qfN?zfLZAj8a-JHMeZ95kY**$>5=pt0g zl^maXT<|F}a@Irz*MKvoQ0#DuSLdTr4b^O}f>iISY&3%!5{wb`~(+cf>{jn#`*^MHy61Ec@v=U34%pg5%a90YE zb5xg9O-=(=0HeYdlMqxD;{-)3*UazYqet5|+|EGA)6!RXV58@Sowt~3Z$odFYgD!{ zTB&=>G`814Ha^gxcY{c(u6*dA1AGTxJ!H$sP_R-uB*bgZ-8fS446Eb%7Hc*!S#Jq3 zc7dH1Z^q5fiuEWI-wcn*$RK+&Sy&z2Mk7H)Y<^QUomY_^*#^x67EA6(W>m z4T6uJ)**V2^IUEYMTQWH^vR|U2iGE8+@8X-$PpZ}U6S(aUoo9OXuf+D*PfYh>ajeM zr9V5`EVsCH#zOCQQK`oTQ<6cop6X@Pi>br=C_bsf$>9+ZOfSev=O?(h==Dm;rDNjl z0XLq}?K>lehF<(BDk8DSiE-A{G=WRrUI{Y{d52MdA9g1W4rgCMztT79?ej@B$Rih4 zWEDy)P-@`EWYBlkfIAWAxB8Vrm93 zAH(jm+BqhcD>s_UXt-m<7K*e?`*c;OEkf>OQ}}O%Z%>jsdke9b0}f793FaP5`N>KY zLZkhxLqe8<7hZO1h00Eb$6+0;lvM>oeNN{!a?NI(r;3)dHh9M%?!~c3+>(F4dh>+V zw{GR%wl*ge!F$KNi$C?-idv0zQODc&do$&E2g>Ox7*#{TO}pbER^GnX&vwJk!l(;P zlOQm6fZbseY?!o8@YW*E`Xo_U-dT5hD&esPs60bQ($B=D!nXWN6Q5`~i^CQ^uif8q zRe~kq^p2Dir{OxIrmo4ItuYM~-3&PsM@Wp`mAU!hKiVu)dXuvAii@A4TD%4R*pQ)3 zv-1skrIK9(GPfPeBhNXVN7yatzoVYX(dUHxxv7g$%a*S#Z1hy38l~2xLq6*&im5sGm#LVxeCe2|3H$lp zZ(~kHRG;l~JI{>NzybyzZ+Hm~A5QvoJkWJ8WFmC1y0%bn$sen;vdU%|IRLyb(P^Nh zxCUFwfz;K{jDd2*K&mim|IQ^J-U?g3MBvIxd*h8HhT@&#bz@+zLn!E=Rxe?S-!D$a z#-08SK8HjoRp3!eXnbmk;Dr6u#Kebyd-AfCINKxxqPO$F6Au|Zb)IB%rvg;aN&D12 zl(q)OyTF^){(pSCz5hPZ0L8gCd!HG`ns zM($6cGoGZ}$?nvApW*4Li|8S2cB3=q;=Kh%Z=pRKJ|o7Va$p`gO+V6e>c#M9M@9?} zI;}zlbHw}f82T=Wj?8qh8`5d*sTslYx~WEd-dr+yw#1m0o7g6q3(?5URWwa7x7IcS zEz)Wz^ZBR1EZB*eL(#Ns1v~4P0~~UAM0ocA3*BWLA)d4BBfEjf5_x*UjRdz#ZSS)JSa zG8=MfU}(sGnwF%Tk*OCDoQ;aC(s!&0(lX5)#5nbrY4dr;Y284taS0)z!}@R=0&1&a z%3y591tyK77C8ETaXO)Jk}9c_Enu@{5%fK^fq(qR^7)^asZ0AtrRPQ-WgApw*5N=0 z|FTe@oip*F>S)L{&DJgL84%@rztD3?u-wkdN`!gA3enNei%ZDNWl3URW(-?u{93PX zG?JO4t&X2rdv>lB%yA|trBvP%Rr3Z$G^nehmEW~O*mn{WZ{`L!;T z<84Q&G3fZvRz@fp27`$u;o#mXFaB%IkmdGInA(fYK{LWm6ui6hlE#ZvmzhwMn(Cxs zH8O1!VYn=-mk~VcpHInIm1AtU%=mxUd+)F&v+ZrzHtOgIG8ROdWfYYrB8Co%fP#Pp z3`G(Z0UC6Gka5d{G&9ik##K!nhfpa_USA~n>|2?R)hkc1GDe2;U^ncpez9Pp3t zy57QnxpL*nv-jG2-RoXu@AbTL!@_`=X|3_JA=3#g{adPs!X+8gP;x5%s&D+Y=j$H> zeXHNyjlfV7E(Od4hKzpLnQZE?1o$8(;5>+I$k$8?)Ps;BcoON*H5cESA;L|b*&OBo zjQe*k$d@y3$&o$XV56FD)u$4+*k=Hln!E?9D2MY(1>3}q<?`uNLSaCmKQ+nRQyeig>ia!P@xu3-$?UgHt1MB-W8{adZi@b_!q8PC?Z@#%iA z;cOy3HXvA1Y@Ekqld><~^6h{8&7rTE86t8T&&wyZ#QTUdpY`cj9bu;GZ1RaE(pBuk zgZ)Id8##Xw)z!MFD3l)jY%QWqXFO6yA<#~GAv+Xj`! zbZ8%HN>)4golgQjS1O{mdBQhXWQr|%zk~e#?~R_91%}XBIUsyw5O}z`PsPZ$zLM;v z8KxRG86#nuE-K@tP;OM~J?WsfyD9Pcy~O1969ayczM6waX`E$z@a5q-oHS=WLw}7m^O;PDU zXf|8(DXN&^xvMjTU9YC`s>{v~maADj#z(xfEhzmERYku@s?;ULtkkO2VygIunrr(R z+^fBpu#JBBRxQPNofgqDe?GAxv_!$q(y!-nm6Na6J7Zi3f-gAjk(xSw@pVg-Bq&GZ znOMkF|Nc2Ax?30e>}C~Bv#5xtj6+7d9xovXsD6ThlV6Md(hg~p62Nc!y}meFS9Ydp ze2yC}>YuSKSH4AvV1-$E&VQLz{UbQw!J2F1`vo#>>mq%J8o5Qj&6V@C=u^~TsNeOz z#ELj0<0~tsEwuz7B4H3{)MWNN=lb>g3v|EUqf0=1J{54xttN(XVJ(Bo=A7S;9h?G( z88fi-WW0F8C$wxWPS801iaRW;{q!&p$0#XAy)Z~B3G;L~&k{6bhf$e;>#tKizQ#71 z<3D90))Kj$3dFKnhko*DJ;uH6knu&IQn4FEewc-&EcwR3AklBd7q!-g0qpfP zb$j2*RMjz2P_C(*_nmz79o2Gq2&us(A8rr%T`Tnp5Y@>&&yQ`yC-!K2ja>_WHU6x! z(%-?r!1jrjx>28IrpAIT5aY=c!4@l+0a1s0dNM3t@j9H3V6puE^&rF8Z~jm4*0cR-&A|2=%bD zi?JlzmC%}Z<_mzULD=)u0KFyJ#gtzo#2d|EGIj|~k}X(m(eEq{;3zDi32kcX>+8-3 zKm;+syq*Lq_o#g~1*E9nE)wrN$NhjwKYk;wVlORF=wL8SuDhw#BCm0w(XZ}TWtAbh;)L%l zpS$8;zF8%le@(Ym1+@wws{ryZTJV1o;T)?>X#2-q0IMn8Wm2@On!c*(|Ho?js&=nx z_g~bCuJYSee*4!FShaeqR&Ukn{ig(0^s!s-RW@WEDXEMGO8< z3LLGvldJCJ|DNt-mC%`?TQO2f)c)A-y=F_y48?BUzMt>x%sui{G25>18KRtbn{$EV zL8?rG7ch7EQyW9>ro>Y;oXueYjz+DPS9Tt{e1G$kHQRn!`)NDy^vjPFxraxbc745d zb=R9MRg9D?U%z~j^g4(NQq2@cYyU6W7wOjkXJmHWlZ^Tc0sW_*Z{G*pq9F3W`u?vV z^4RX}1Ml|qdj7z`r{ zm0Cvk{^j2+6@XJwDy4(}R{Q=hzxn6?|1P3+Nn<~-cKWYw0@uNWf84g>E1G~++dgn| z(~k^k*VfIRSAX$PUm>IVKBGHpGBo~kS^xT*Ol$o+g*QE`f2d2jdeeWEA23by({>Q*w zzZm*u&}BW45fZts%59rit6Pw`QgaAw$Kxk})=JThq~?`+MZlXw9{`y;$-_QB@EZN= zw}3?2L8i~jo9yhLy|I`7g_V*bC%o-QpV8{)nibes78L|N8R0!VVskI!@n^C+r(+3vs&a zDr9}~3UF6?xZDoZ_E?t_t9XH=SE$QJ3+GVfsNoI=FCLmVOh(WPD=M`3CD6)Nrr<{Z zLIVhLfvOC&c%}C;-}irAPY@Uf$^0S?Z*Tc&;&(;K7E*tAN*;t@!*TiKP5P+ZO2>uK zG{-K*XGDKRwz1MC&hGoJf)0DH3G18Nba-sCF~uxv!U*Q70a7tr@bq`BKW!4Z+<3R| z(OdUi&g=r3o$TED`Dgcu>1oW{R%hk;Yu7d?9`w|(uJE2VTC9N!)U9JpbQ4v55Vq>@ ztKGbsg0`+MtNJ*W$Xv8uaK(7Jduu|Gf6W_&Xe(T5^W(8f#CGQOlljV}#=H78POG-j0O1~ zn6hs27zuUr+SMmX$?KC6$+_o%PK8ndYl$tS$!fXd6l(izX#Z^tV~|&C>|e9=fC_RM zx<9|vI@vNLmHGHsjTN>cfRU~om2B=zv^N34`^=}gvM1eoDi8C!a(Y@43Bn`xr6JhT z7X{_`Y^hqNdebm>NG2TCuuDBr+3egjT=B@Kq=1yNg^SmUBYs;BoucnmZ6&##1MYe@ z)zZ~?LO}_K+j@InK=>VF*B-qa^(sYj{^%_?tUvH*;!4ZivceYpwbR^Oy{WvZaPts9 zUFpR+%X0r=C)(GK&4K)ZVGWjjCqJD-TkVVm+dLSgwiztynbE(3S6Pk}$8_zx3se1z1-Y zNf4YcLpY;cd$dx?Hz>;?XsEH-_15lD7w%Ay3x{GbG0^8209Tgoc)z$S<`L&mlu=2YzBwEvV_6LM89ATdiWw$mZ>8gQqDy}GaT}w%2 z`xVhs*-5E59p{e6MH<45Z*JZ+P1#C@Q%u5q4&i5_I=)nf3%+Ag_)J$#1G%LZ+t5;^ zP1e+`2xl4|aaVWm`Dldd))6ma1Tt;*)f3dRPc|4DB=f@+G{zY(dWLu#O=*#50S zQUU!OZw?MZ5wPxm$1a3_O@NhFbc5(nfDKCNCQ+)IElsl0q&hRRX@d3T z4QJpe;jQrDV3VxcAW;jm;7u88e3CeaV#fRQ;LTEleC$mqyF z7wq%?-7oKIB2#pORdAd^%FUxJV8s>ApjqoK5C>ny#KhdJXZdK1@3GPbjeQ%QRMve? z3E@=x4z`d@n`5XEcSb6-^h}!rSRa0$;5v7R?|+jjM|@#VL+~f`2;g};g6nxP{|HWr z_xJ~R;uO;0A3B#adrpXn5|tNe535 zt*Bt6L-G96ix4~`w#5Acfyn8Kkv{NN(u#UupKZSBhF_uRiFjWHsIet1a!qGRp(*e4 zBisYjc1|PQ?Dw%Z-UT93 zhawj-{jg};`J@er2y=Xd69MkcA!6I*}OUkkj9}E>pLrfh#)lko! zm9Nn!P)9bo4J+GjaN@xn)GUm9@+rLABfN(i{msob+6<3A`Z-nai{8`WhK8e~ZS5E* zKi*C59KueAghi07dh)Zfj+!O+rUVQW9_7BROdfqWHa3`2@P zj81)S6XeunjeZ^piArztEj(lvf;Mj^IJAOE>&)i-* z?n-kmcPYSUbCFLMD+Tr_a~f8AWHdspPo2tMobG1UAc58bZCyi! znVp^Iv@q3I8B4Rz=iAF7vgE-bkGGE+bC;wwj!3+eI%isZGFEr2-CSu2? zd*DTs{QA>ro9%18XF=ZBn$6==OY_$+9r9w9@6xG$N3s&No4Y(G8xry=b3-7%Hn#}I z7~DCS(tQ<9=yurHLwS!le@Xg#J45f=ig}Fh%?xkny(t6u0Iybd>e{Gt1`%?EzW>aJ zOYO%$c>?_pb`O5K%_?`Z#ub%5I359un4#uft0~m5YcJm9C5xrXPWXpJlXP&W4AUCp zwu&`|r#-hAtMa%XYus|+9k*v1v-8G4CSHP4opns>87%NyU56HQFD{B77A;P?7YI3< zseSi>sW{Pn{-jGKsTIMe;i3^FP#W@bf|`SE_?-~#)*%V$T@NGH_!Oh!Y=NC&#GqoO zMucR&MUTa1j5g-Y-YLL_AGas&Uo`4T9qAp%>17JmB~`zO{5IB(<^;_K@EIcFrWqCj zMuYc7=A>)9S9g?DoE!e+e(cETH<;bz)1yaY%or(N3~2R8b{q#=sk|DP?!iZox*L}$vd#2TWlAIfz2xm3Sx&eVNFY7F+t1s6?w3G9NyK{DSn!e zd6-=C^7JQfVV0`6520S;7PrZJeAGF0Ruj!`l7h7;y04RA)*(C@~^&_9aQ; z9vG()4z-1G%}znxO|J+hzrI;?q_#*-bV$lwsT(sj^lO}Z5s(qE^X2h_<6Y26BF4B% z)wZsC3BAS}+q4~3%7iy<852sS)U+e7E9cyw!T^6Haa&l$_t#9^p)>7e%Y8Hf9=JUpsc#cq8Y!>H$Hf|4Lpi`LaZ*!Fdp2qmiQDEp`kDF`ir$DpIt&sD@Lk% zf0=`5>WX)`>6}o*3Q30dqP*plu_!I&ESZMaCCHQ=10cM>)?=wXR%2f`^_y<{DPBl7 z^Hj_w!t?cM$)@=k9o1KS?l4_lbxta#<)N~XY$?a(d%)~Dw=ZU?uiVS8;2nF=AtU(H zGqK&Yibl3RHSh^fGE1H8jGbSroI^paPkF_@CuQ@{HIXqgb8%O0CEMs4-EJe6ur+r< zcd_FBNA^n&vV`$0@1C?J)ww`{b+=cslNZl$Pwd$Fa&54G)U1&M)A5~B zanzdTJx#5(OAqkTMnv(CpEq|IKlBkK$+dg}sQle)HXJZx>OySZyohsjI|$U`)zHs5 zN8E8*XbR5DC@aKgHTHht%50(Jxe> zJZQ#zh;^aZ%M*5UAE-P0n`l47YwP-g;#u7tzB(AVHv^faA*A{b@r7QG(|S`)Zr9Or zvaEG=z&tmtC+m~;{@2*s_n^dah0vF#yLUD`H8w2iHi=!Ycu(8qCL^t-fm6`P^|Un$ ztw)JQGtFtvLF~-y;_c&KO`efcp%_@i8rQOsz@Rw<51eV4BTIuVt_KS5K4);3q6+v)sT5@_)k`}@u#(vD>o<^PO zVEPs<0l;sBkIp-lz(;>@IRgz!9WN%)~wSVeKtZ3}zmoS>MV(w7|J zGcmJZ$ZEA^o}k$`yH1Isyg8)&=6r>5+0xeyyCj>h~>&sU7o zDkRf?=OPuvAd-Tq_8SjD(#cS9Bu%nNL6k>Zsu{9%x{!F8er%m4)90shRW;Mi7+s(J zO{kj)5jRxfhLHIQyNNssO6T5ajS}q*^I+f(ZTZ>c^ZE`u7sq3)#!dKAJE(TSlitV73 z=ySf17WQCnd~z;(=a%=dYqqWC%xiZ}V_S&Jney4{9a*r!Cf^dQmLJt5PFhkaBM~iN ztv{eyEEE4sGj{ZqqaZ@peyc~i9oqYt;gOxRN-MoM)Y63p?{*^k)$njd+i-W6wxB z*i(j9-!>yWdWwqo2n*L6H+Z<9Mbr#`|2z)zI%Aj6qI*;acf*lJzoJtJtdks3vd#jd zNG!a;dUoK84X+EY9F>+S!tLHl+Ub8o9q>MFv2HgO>02$@{iWNd5%xoi2OiJoW{|~S zstXENKhpa}b0zr$-0DN!+hKs!f_LYcn@PYMlG3gMNR8RDue*s>nhzx!0*GLZRpLO)vIs@XxG zgje*6=U4M1KHm&-E0fMgmNXy@aDpb98qAIaEq$WN;LE>QO9%O-!v$9L`pB<*X^rji zw)FI$G4q1h6J{X0E94f*Ecs0;J>GGu*{;RXNh$QPv~LVI+!GHyG(Ph|5NMOK9rM|Q zqhVu5ez#9}Va2!)wTyW*8*bjK!J9Nm;fnOIiANrIl=nT0cCko?liAYx{IdH%!4#7H>E#>E z;dH?DzA_1ZtD#XP5!#qpC9$?lIV$B?xskrp>|G5P9vwLmzb&UFCNig`DC>D^o(|Ac zY#9171GbP4*dEBCl=lW^OBzUfZ#`rk4BZ)?lxV z3*wZLJl&m0)kCsxUb>M7iJj3&%)e1HH1!M_tC$qaz7-o^=6ZXBLv*sH-8JHEN}WCj z)WW~<>n@R4g{0@jx2Q8wRbG~=?~=Ew5HB*bD`VoAR3*sKYYoWtIFVDLuF;RS+`*tw zMgC-D4-3+d+$8ee;g4;(QJs&ha1bcelkUYOs`z(x z-}ft8Al}pxeC`CH34>5AY-DLTPdHGOCFeV1GW5so5QI+wu5@)moPI5m?O(a81j&3bIZS4F=##`Mf~2LLMa=rg`&6d%YBV+aS7 z4)kn=paSJj&$?5OeM6L3$Ka-!6VWv3a^+?7P1kpB zI81eH>+x12WxiLp&ME1?PMwkX+f-(-K+YvqbI07Tj1o-4p{7JedNC5J{85le$>uF@ zLig?n9PNs90U`@Q=NCFEZ-oRS1QUKWAIj_KiWo>rE(PU?&i`A^M4!K|ps|$w@aNtx z@BMwK!E3Jg?wG^;hEHw2edMKw$QqBZrVn`PMe~cWDu{N>k+=! z@(ZXt_MyKB!9)7oSNxkmUP0E@;}K~xZ%_c;GMUX-!D*<)9SZpSFuI&R-_7=`Ra_a6Epn0jr>U;h-mfJVYThVoItAR|f%yTWj z#1(Id>r)%LMH$i+AM2XGtV`nHAG?G_Jx>c^XJ8xeE604_=_pP+8?Dydu{J!V2UJd`qGuyUo_B^#Y zUF*+@F8|^B+wb}4UB24O|MX8Gy*dr#qc?u^m0Q*;_#e4Fw$%Ubn?(J{Gbe%GeO>$F z=quhCKuZ28Ac82%Z?ix0%*O9&{0*(i%R%Np?XTmP0TDdiqM7mo&wP6YoE^S*z5GW~ z4yAw#SZ@&<*tS$Rp3%Pt?419rl&9!ar?6lG zyE|r;gxI3 z#vP^Qt~J+X_1GAjGyDR~x;1~kW7!X(*}yX9>;}oKT$yJ&fpont`^uUh*vJUc)P3PX7=iKX9Pc6@)XOW?T;z~r7Oe|7wIPgd1;A5a!A2yu0F-8+#b1sJ{1A)C2W zy`cT>wiQVmS1^{U;~N0N_e)WM=36ZG5MLQBaP9V)X($S2ye=qz{I%8|>w6k;qv63k zv!pm9hYat~q4x_GuywVWkQe87$i3Z`6O%5DkvSK@pyYDii*deZ5L1F(-EEHtTuo4a zt!R%&=$C5h=oj*Y z2i<#$v3D=k3Ag(=nOl61!vqu5V>GoYjHd4jXPx2E-DNT_cjVd^9Py*RZ*Sh=d<>V? zGyJ4c)9WdAG21d}oMqVFAqj$~^;ZgK-@|jns5eB1 zs#L_(y_a&N2ChBtP=LPB)$;VckoWP`@|!+1%<7NP(^I4Uf!s~~bGZy@E&t7)6jcmR zK_WJ#={Yizxlvty!9+HD|IPY&D^v0G;S3TG$I-ZGhjY7Oiwn7tpyFo|zBd_~ToI?c zICWhFLfJe1WIu<5VV>0t9&9Q71zuYd&6$+^bzyCQwKm#6LD$dwyZa^<`8nup$nfGo z-qHn)rx?XcDmHg~uA!}zmcJ5YPuRMq;h6-PGSludA7|UjKrh5#jre6Z*oN$#ajefp zC(DpGHIeKygleNIW;@*7d=jj99y6u)47E5GJb@l@Bx-4DwQ?X`39$r^Nlq!k-#qE~ ze!tPLg~QlLV5qQ?Z8@>K8z`bD186v1BFjW4iEoa<#ig6OSXf-ZEP7Jz z&DI)uaBGDY`z6?sMVRcndRPBs@a6NiEt%RAHi^* zTvxFnEg!*U((6k$)87lTv}8jPI&!U(#)t`!$AyeH|6A~j3A!2Ur?E#X)E_rj2t??J_g*!;9eSZtaLw=*gvdDgy8 zfUrAACYQj`%xsERD)jyh4CVa+MVsStHH;-1E3t-EccKmJUP`*x?eo|-xZ|;l3q$}Z zW^{wzU14D*;mlW+SsH~-Z<}DT;-svYC8>&(*a*FlDc@P#3*>`@j9KjnYG2D0FMR7I-bvjQF}4DB(C@9i+5r zM~|&tORd!95PpsGoL@+s()`a$Iu$Nv-9UK)1+AECSElaXz(^%^ZHdccB{rU5letAghkVDczO8?jW^`YI@hhyrbzJ}&3MC?9I_}G1s z8ZpxX>l$c_X99(LvKbvyb2OjCE&0rQ$$?3zQ^@X-p|5$I!kviwn?ZRt2@K}*lo_WW zNwIOA*>tcF*s+P<}sz?}B)m&S)-+=n!$rBbpt;z1Sqnq0amF^BGCG?B#Ic*`GX_022Q7IET zeA|E((f|3jF2!n;tVGUy~Oqj{84#Wi=;iW9M?!90meyDRHf^{5Xp^_ zD@O0Lf+vM{+cO}{N-V)!Lb z9t@8#xo`ODNSG!MCRQAxxT){($Qp_rx-&T^tB;=F^NRQ8z_1p2={KYw{icf3eXMRb z(@<hPxls;8oYG>WJEqPV>Xts(`AoCiYUiv*Cnx0zbI@}QnLyVCi10j>O~Vp>-sYs8oJPl)lHT=;k@N!WiWK*?`ewH8py_j+gbkc%Ujg za7W6@c-SU&C3LS1Agwlp_V)<-m-bU2H*3scvjj^if*N?Jx6m05#8AhOR6D@%h9B}c zcuUnf!UWY{zpF3ZEItYKygHF@byaF=VXjcH4q2dlSLaf0Eoei{NO+* z2Asb`?>^j#%AoevG}OE_UZ7b(x}TX-gZXGQKWx5UQ0+3iq~S8trM{Gyq8IEW;z3c0 zu~H`ediwqQs6*{Mn3wxKE7&w6ZuW!nrw?H7_HPjyN%lSFBVVzzS~Ol_4kzeha9(pa zH9m!3I$8ve2v!CF9elNOs&R10TZ_1h6qPw*Q#@DragoKT ze1xn<>KZ#XGr1C!r}BLW!72&yQ&7IvbKt_xh6up0)}8p46QlISv=9g! zSCmH_YENApt`9+1yBkFIj0c5j_P>xy*FqMT1o!rM%0E1XK&JNT9b0R(t7-GbI%He% zW)UN6HQNpFVHv$H^xHt$m+Kw6sGE1`J?zkRIYYS7m{@k;c4JOl53(u0|8`}|)V)&n zV3H4Qggu1cXdAAh8-2$)m2_|LTEq3aqS5e=IWdX~cO$X;$38|$)H?&4|C3ht#kC8F zynL#F!N-U9+cd!yDIgGeDfp2lFLI0-b3_*?k*nhGqL!+ zA*<|HS4&2YyI6VnEBB(z7YM!)gOoqY5e3RQEz`QXClyL6yi2*S-OJA2uE0X&1!9S)ad3|YuR;5LKJ&aQ1-dzXV0Q3! z67}R~TnxE+d`Uj14uVIdYX1&i3D9c-?%1UMUXNISsHcz0$7Bd+(0mNed{$OCj5aAK zLb$7a?u^>*xMXtI&VQ@I(ishGHJPi7sO?q$kvFe_!4~00iF`Y)A z{Z!-Kerckx*wCu>M59dwA!XV+Mf+H{V#CL#z{q0(Xzc<*zX5DZ8;i%ao~}%ZWu*YD zG=K2mH_eOKRuDj>0hW1bUI0+wOI96RjC$c!uw#+2h&Y*tx$1LhHOr9p1m1!%C$jcREA1`ECWo zyHT^wlU-Nn%>R1G&~O0n&XS!{bR22zqd)E`Y*#uN=X51b_p? zu4ek|=4Gf%#99i1L_>=-s=i>1XgQ3O~cUl1s-eqxt6xvjrqDmySUo_776-qiU-(3o@ z`SPc+hL9&YPVulB>#EU1^NAEeVD-ef8^ zK;hIBc$3$orE`L|H$19i97wrx4+|9dm!<jI{!1GR5HNF&+-c^xxTaV3YbuaFJy2ozdq)_=L@WCo z{zdF9<$EvJOM}`3K$Y>xp*XwC8%z&2O00b)=q?Lx?|uWd`RM}zWyR>hR2N=4KUs}S2~?8h5;`rNPzlyvaq9v^g1I@WMu7O00X_IsCY?!PqOwUc?Pns$g| zV|?U9wl1FiNwo&9h&p&Bqey&z7Xv?INjelY67mkWkzb)#Rd%a>=%o8%QIV>0s?0QdI_k}_ z?g$NSjiX_pRGO2*Qvo1_jLEVyz1J0wA@OMVQI@mMcDkr6UHV;cK1r9qbUS>eiD{89 ze>YrAi5DmwEOO$$5&P*_>Xx3-9P$;*t4njAtJYTSVcsiXzN;&EqlO@6?phgyWAID40d z+V%-$2=A&`;N#hTioi=V*5JNF-viZrkFm133Q=8CJ5Vl>i3qxcRimq9kJf&eA)q*^ zMJ|1F^DV`4SAdFeLu<`-opPikMp>T`W)pUoTk5)TPdiJ8qV9HJFy2w%O z&71|)kQR3kM#>nA8S~Wvr|E<&CH8-@w(q~KhZzpyv3au8C-VvO`IJDFzOTYQ?r$kD zE(K->fL6`3%M!w@$nnDReNdu8(rihRrRd!qm$dB?u7n4Y}eGiy_%4$n_t=d3IO zH!^s22wY&N4)?#r^*%`i$q-dElv61&u?sSyFZYK{&ejVK9u+JWXP(+)8b_G@+Hv6i zuG3YSBS$`y!LA|r=tG25H)o{r+Pi=1QY!(;Z< z*4k6fR(()bHU}S!L7FFDDPd1Ln`U$%$h-m$%3n0rqkoCh&#m=<&cX#OOeIp_>dN~v zh=vJU!-P8g65+Uoi16zmgTYSjkf{`~D&Yb_iH9zWEeA)#kt5Y5LLQ@=2}iTYvP;T% zGEs$7v<^M?&H5U_wVYoK7tVHpgaQshn7Q#=A>8fY3L+tELPg)-*+>+fsUyti2uGlT ztso3{z6XS!mt#Ff5YLpjPz_Zb|P zkgWw`1xx3lIkE!gaX;T4=(EH%E+mb4T z_aQ5n^!C-|xwE=F>o%-v&%RadS+fdm8#b@n9{p9@vv%DoHCUwv`m59cAfKz$;QvBu z&}q7!D(bI1{Ue@F#`aD_(ja87daQ1z(vw&j$PZMjs@q*B>)a294@^N7We*gP>Q@qH z`kONr%HQ%9bli&opBt*@e5(plVwds!g_m$E^>=}T8fF`a(?4kk^gl1q3EN0K^fqnr zVWD4NDJgE{|IwLI)j|1~=4Z;1YYi0Hmc(~d{>eMbXR+`{W|ARZYH2Z$Ml5?+6-NF65sBt+Lu6(|T|r4hIg37quU4BM*E75t#HQA(E$z-v}StwX6sG z4}Y?%b*rUp3e0A}O3m}C+@xd}bZFuJgs<5Y(<-r@m+-aZodl;h8oeOdUr9Rj0|S&7 z>ulLt+6IopJsRHnHbtEX^;?n>iqlM$ujxy>nBsk7H2*P#KWFuqAKuG`D(c*Gzzi0m zDsU$~{TH0K(SD$1P$>A=`-qfka935(&#zFmmjxz5c~74eSO1v(9_7l}iJl)LXR;*E{=V=%yR6Iw`t@7Yx2u5NgFkm^r=EnznU{72i<6 z?Mk_*UxHk-=2*u0v!^ZFTp12*YEpaP^hTm!oemV4DgJzTaW4m|caW7G$y?Oa_yIBN zh0bqeqdry|x<1MRH{v074ZOva{AmpsJz-l?;U-Kw5iwJp-er?@C?&WUx$^l}F}I(y zxPB9zZIesFJR=bk+RZh5yr$J8)y3iE$5n#s+kUOfwUVWtFqh)&`GE>^-u83K*Kekj z*E-~qtaKAG?H?HDhp~Ir$q>&|v9>iM8TyZT-bO=5oo#*~>{N*eonLdfV}})`&JOc| zkvNTM_hxrr>qwHyZ?6iPI=$F1ug!D9>VlbtYB=3QlEi_POL%7KrZzKj$8&ND79CWjozqJ8!jcdHLCWDjf#b0 zK@{GexdS~~2kkZbH-Y@Ob!PATh7cCBx3c|$YT8cFz1;Kid!AwZI?A>s*~#24cAD6q zLqQ`MWx*aCep*RCp1V+|*Fk#z1DZ;*9B`236(rL5!Cq-4$A`nS4zbHdN_6f^B$Z{j z6G9Ls>#0+YGQ6|H@fz^6P30<1$eO8%*)bf4Qo%K5ELG#pf8cDC_LWfdQUs%yEx-j< zQg+0Jgv@1=)oR-=6YSdr3hh15QYQjy)gjsgO~1#3PId6gzp9$!Pv#95rc<+6)@JDv z9BnLi0ZicaF$7!;zl$IolMG|bO%i5V<*^Yzu(uzqBg}MRD!3JBDiHW@QRcS$) zOT6krKC_yEmPOU=RnO`2&hjcG2d6siJ-I{FxYC!!QY$NFr#01Wk<~07*hS*R$}+WW zX@v`RVTQ8YxmdzAvt&JH83{?R#t7y!Jn(bvXm4gPffG3;{sS}*{M$@HG{4h~4{t@& zRMFF85zwWf;II?|R73kKL3=5ClV^uyoNY>Jtse_PVmM*C`d!_U?CmCz_H#1wP+p`k z+bQx&jT&{ysKr=nr0NN|qOy63{eo$s7t>BSN}w!}`uTl{ zdJ7%0luBT?ff#sRMwp}0ifyz0z}|Im9P4SKD0(3`QIF${o*#RTS@_(Pff}lVQ&7BV z1JV9q5Om>_IE6<86KtCjg|rzSwwe*Vm9Pj_t1A=a`*vKbt0(4f^@;t>^j~}W18%T8@rHgE9QgxKP#y1)irW%h=;*YOiw1-f5A#h@+7Y` zLCJZT@+!Wz31V6}wr4UviZ5bHZ(7uvuguS?gbtV2IDN)G_@oPJxRMhG)j2!&`J`Ki zdJw$(+SlMhr`+?YP9$DJBk5-U^U9&PjF>p*JTHo~skQwR`Bw{gS&gNZr40P{rF!wnXw$s-Um#TNz`q-h;^yudtYi_qPDS?xNt=}P67o)cEGz9sVXY1(7g`SP zP-@D6FycaX!|-_t4eiXfV@~LN`;!Et_0BrJYO;s(bY@DOq@sgic$6P24pFmsu?$Vi zveNO%k~R_Ddn^a<+aBb=bOP??DF@22T)flbj*0DHMZ{y%z39a2VNmJqWH8!B@xL!+hQDIcl7%9&_ zHPok}y3I6Wze>=}>HLrhP-4F5Rcd7LWzscSaBi*Ns14mnS=dx&+LWjUbynchJ~tl9 zw+0ge0JUWZl-hZFS8j11>a(y-c@WH~m8lN*@;jNzgvo-rwEoU-8a=VWqmLg418h~R zmz^f_$x9c8kYeuDtM{wlNJKO{L_WZ#=h>3Ua`}Mpt={EC*H!A8Bqzu5=DkRhmE}=7 zoWAS|f*;9sWU7Fj&v$hmdk%h=(_cXiC=5Wy(vXoW=Q5V?p6+(4b5Q$b3;gaJx%{!< zcOgqn37|SP4LRq?VF*U2b+EWqrW#@aDag~z$V&3k^ZN<)_fdBOpLE&sswn4ep))?u!g!G|3GDLdM|K`n1`cw`rOt`YnWU-|es zSc=*9Yc!wH=HsvDTxn4o*3+kF;Oa01dj<|kBWr9+V%@||cuJACvTRAJU==Vi+t05( zusFk&50*U)yTZT%5*#g=gjM>ECaFY40@}hc$IS_Hn9jp(!N6W7Tk-5ys?hoa1?$?G zry#hPkli^QlM>)}C}4yD6c#|?V~z1$&u0UpAJ=gqdcBK+Y=nJ0K@z7YGilR9K4j4t zZ#vv2J`9XEB24-)Ewp2=s53L@(Nsf1IQRdn?CQgkO!M_jXJ$GxZCh7O>zI}EvDTJ~ zYlI&KI5Yb((>T_Qjv|OqGQlXK5<`KAj;FnLj-|3YPLhhavt^C)7167rh_)%5L}4>| zQNoXjf(aB;0s{OvukAH^o$ETgSHxfZ^*&#}`~Kbc?|I(mHKL$rM7BxIO{K>DN2W~1 z0A5MaC@wG{ayOpjowdsJhiUZ~h=A%u_O?uXyAKgMOaA@kb_4C9gP~9rwCy=6hSy<= zr_zPWwFt8a;%(tRKT;dBtG2tMpFEjr)i-uttE6J zTM4@ghIL+tamaFL@hG3I;$@ja9FIh>20RsPI6$ZAKjtRvYWYsXY!xEo-3VbWS8Z1S zZM0mzr*pybihljqF+X!uj&_Z^aq7oc0W@`Ofeq!JKRbq!xq_vlL5O7Cqu)==H#4Gb zeE)XS`9PB1GWXXoC)tmSWrQ55o5%Nk!Y#5IsZS?0=JY9p zn2T`9^{!P1S;HhqbaB>!cDwl-3T;rWHa!SAO8>IqGs7BLLKyO7`$uJ^5BKv`T8Ml0 z_6fK}F?HsiFC`;JA$r=~>P{!(d&c@F644#(t3?FL{kKjMG)l}MJ$Jgvvn?n?&zLhQ zyK?Rr?k>g^gMovIeFsz6gWaVcPdQQZ?o1gqtEXwG_)q`eqF7s%f=LqSHOH*MePUmv z%na{IKMD!QmKuh>3+4%SwN|>z7vA}%^iK!g@~UcQH%*wv@HOyh(sx&s*Bt)LUg_|O zG7R@gr}q+FLEf*&Do+o_%NQ9l_&!-QfIhN(>c%+)N$6?Y12e+oLOyg;`RQK*Gqt(c zx@;!}8opa+cC@BSw1lJRbD2&Ll+BfGCoClWkHC=gqq(BTD@pENv@WBERo4EB9?9x0 zY24yo3^z57?UR*iP)8+stH$-?<}Pe-RMh~#mKg67k5dt@zV6~k48fE>%Yg9Nea%;N z|2Yz+;9bv94MsKsIWuvYE-vkS6|e`g*ex{*7-P8_pnUHeE;EQgnfkKB3X##m(kMjp zkMhwUqcx>fV<6#sa4gdvC8IB%^@uiQU=UdS;@w+e6$M<0&ZxZ=4Wc-A$OW0ipi$Xy zbc$r4=1PTEK&#xZ&I;M*M&c_Sr|+KhFmG+8RxJJlr>&QDW6`z#LL}%Y=gyBVZ_>G1 zB#VGZ|3Bx;su3+X5WSDk{ZzMYc>KqK*07;QG!dwd_NveyHp1#K!f~wrv!!<;P+Xd&y{Lv={fl^J)8DI6X_G?A8Q7$8$!X zU;&{)lNnI~X$>4CNZUP1GFx_{Idsb)jrvo$YM|R7SJ~XUKs804NC3;8nklr4lTK_O zakb!3&e?+rDR&uiGbtj;`GeMW6TsQFxZg&gPZ_y^?)xZky<=muf>%MrGD zJ<-;9J-9>U@f(-q8wE@j>QQ>#4`%$XLS3U}4=2C^FE#AeBTYDQkXO@)kfPWfL;Uiu z+k(n?LswodySC@{&4naBB6LFOy7aC`^T5G223W(8a;r%MM=B)DcNR&Z{mYU7; zzvia2KpY>bWqQrb!g87{IaD9@WahQU=$?l6Tjtr4IqUs3<;q(g{X+wjY8{ROp@x#} zf+RJmrjx+z`+&$VfGT`%disskU9U4VV3=srCnJ32erdMv@EvlVar9>ms=P@Vc&Xm* z1crCkPs*SY!L;>)sY_~Zfy7Hy$VqGlx2_i>vnz?RC|OrMkAY30c3IMOjROZ|b_Ly) ze2(3D7C|(ntD1W6O;Zsi;D*+{aywPFfU@f zAa2XXMDl>1ZJXu~f%~NwmLV-igHeAQ>TJDP^6{}*!0Dtg)z1rX(<2S}~xWn#`+e{6R-eG!_)x~=V!w`Azvk3^#A@8WK4hI$-1Sr{v z>PvZ4wzse5)^}^Va){aVnKPLi0-yUr6WT zn$VG=b_k#a+aP;7@;kQ#qRy-GaC+GV$mT*a;HX4h<%!Y|1%IxVdw6|FgV#Z-XziE&c8An*p`h-5k-f(OjiTd6R1UP1UYENSVXCxeQ8 z4$+Xh;jvI9;EL=0KvI(kC!?sMK*i3SMCn7wjX52L+gd2mm!BZYYW(#GALj^dMqI-@ zgC*Tt{_bWYog3*x1~Z&;n?snH({t?#XzRRQ-jOhG^m%d3%tDw?r>@>g@nB<}zY85{ z`IXp|DRGuB4Yii5p!$~V1T9amW5w#%R2QU7`W$FwDFHyc*dULtYh2IK=g*Xt^f#O- z{rlAQ3kx`Lj!~%k+(3nKqUSLVpnPe>R!|H^r9^pw^Y(T)#gxv)_dM4wHKDljv9UQ& zpBf~42&v^C%~GQcGGVg+4L)=ic(%}w72|T5(|h{#wAU=xit+GT=ZTC%P;*p&62=+} zFj_=j<9SXsRFONE!cCJI7BfJ-R}{+o!)rRAfBgqD7V0m+iEY z*tX6A$RD~UWps=En__Go(7CX8^<{l&7ir|bv5H?fZ#V^{xF&c3H#giKuAv*@od{5A zH4{Wg40r;mA-S!kq|n!cJO=-w-p~5_h2GLZ%V=t>&$YNUPz)ehI(ySr72^^vU_bF zgh=^IkjPcA#93}`njyHXVOlMPs?VIBA;yEL@VG&V)8$Kf(AVG41pY?9eG3QWufM?P z91virbv(Iku`E?C!|Jrk?FOlZl5pkOU+nowj=^F->1Al8^t8xG9lQdH1O8&E4Sjm3 zOD0hflNBauV@e0TM7Pq4=q-;iEWNRTBK9n#mlsZ(%UVP#sIlSVJzx9BItI#J^&ar2 zr~cpS;lJF3tAv#lk?s`|WgeBwq`einH8>^oaaz%$6R@>`P+^$qCi5Fl&lS|h-F~y{ zLG?tV9MmexB=t(_;NONOo(5ufV5(=x+sB*eS%zhW>MQ!4yGu?net0pj<1{oBInr&a z^=#B~9-A*1G^s$lFWUDI81c6@{zs<(o&yI1PqNWXN9QcrCDtiuubV2B&O+so)zfV& zE3buzyyeRBtV^w_Qk2Se>L~8E=7?0PmfA`NhgHG{r8z&dZbKiJLSJ(05_r85)uodW?a(ADAq5$e&lo!r32Fy^-&0VhU8Je7KEhW}1 zeG5#y6Y359_Z|StjdEUO=^cgox7r-K!%c&H6d(|TWgUlfVr9Tp;zbT$jJP@dNvlM1lpLnNr zbc)^_ANulcTbBa~BXAg+?3k2CbY7=q%>zQyh8n9Jv~$!QQk!GDDb83F7AH0cLia@8-B-b@Nlk~cBJ4rdULLkZ zhKux-p} zvfnV{bb)~h^{`}+;gdAyKl`4hpZ30YY)$HXi>^~oG?D;%`3^gm?2!NmNgWB4xIuD-VAb6on((Z zq1HJ;0aC#aT0}hP)JVhnlq+wYq!~sndp#3otYqqVTX`XD{yC(N{a07VHna5#X)|yj zQB*83!u6ok$Qv;(&EmQZg>#1i;FZC;pX0D)dQ|NkwvpIwU(opJs_QThl!F>lq6mxP z3>P)_r`|pbb7fawhD``qv*K3299^r8yMCbWGs6nWjWt=5Ie``9l6iFQ40_j}A^WVkPs+FIUEVJ&)aoLcYb$8(Y%0dr)E@21g1mIz}5?OTCdzhVFP_Pcf20 zU$R6bY1b1ASoro7SeNbKSF9>KdpMjo5q!b>{u#R$8CDvr4OKq$ zpMOcnIb$UW=i@myxmEC!;Kh54!p*7yILceCbybXgAa8h9@cy$c6?q+xCMcU-xu+Tk z4-3W^fKL~22Bw4&=Y8*wXS`n3O>0D`X|I3l2Bc{BAbe^WXQ+@c46MPP*kWO?yE}rI zX}u@vo{t}~2k=_q-HgKb%BSh4qE-QYuZ7Qe_NC-MiLY^9 zKd?Pf#&j7^5WoKYp#9O$4L^iSS0c|v{Bs_j^`|myo#8wBVlwjP#Oix0Sk2cm9j7{S zk;#=UplU+J+RXlSJw*k`%45EHzwOnypnzZSWtpC+RfK9Cpk>|k(g8GjhLzuMkJ|Eb z$V&ez_Ly%q-DXQf(Z73T(0Z)8HUj%z$iVphiz7`JKa8LrDSRV(@0D#kGD22i$2R|6 zmHbup&B*N=LMM)p`xSEL2^kL03WHFSL&(pmq`dWL^+n(7Q<(I(eRVHDzr(*f@>}Wq I$NuYo0mX>o!~g&Q literal 0 HcmV?d00001 diff --git a/bin/codetyper b/bin/codetyper new file mode 120000 index 0000000..4e757b0 --- /dev/null +++ b/bin/codetyper @@ -0,0 +1 @@ +../lib/node_modules/codetyper-cli/dist/index.js \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..47cfde0 --- /dev/null +++ b/bun.lock @@ -0,0 +1,1228 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "codetyper-cli", + "dependencies": { + "boxen": "^7.1.1", + "chalk": "^5.3.0", + "chokidar": "^3.5.3", + "cli-highlight": "^2.1.11", + "commander": "^12.0.0", + "fast-glob": "^3.3.2", + "got": "^14.0.0", + "ink": "^6.6.0", + "ink-spinner": "^5.0.0", + "inquirer": "^9.2.12", + "ora": "^8.0.1", + "react": "^19.2.3", + "react-devtools-core": "^7.0.1", + "uuid": "^13.0.0", + "zod": "^4.3.5", + "zod-to-json-schema": "^3.25.1", + "zustand": "^5.0.10", + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", + "@types/inquirer": "^9.0.7", + "@types/node": "^20.10.0", + "@types/react": "^19.2.8", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.15.0", + "eslint": "^8.57.1", + "eslint-config-standard": "^17.1.0", + "eslint-config-standard-with-typescript": "^43.0.1", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-promise": "^6.6.0", + "globals": "^17.0.0", + "prettier": "^3.1.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.0", + "vitest": "^1.0.4", + }, + }, + }, + "overrides": { + "string-width": "^5.1.2", + "strip-ansi": "^6.0.1", + }, + "packages": { + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.3.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, ""], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/android-arm/-/android-arm-0.21.5.tgz", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/android-x64/-/android-x64-0.21.5.tgz", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", { "os": "darwin", "cpu": "arm64" }, ""], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, ""], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", {}, ""], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, ""], + + "@eslint/js": ["@eslint/js@9.39.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@eslint/js/-/js-9.39.2.tgz", {}, ""], + + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, ""], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", {}, ""], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", {}, ""], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@inquirer/external-editor/-/external-editor-1.0.3.tgz", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" } }, ""], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@inquirer/figures/-/figures-1.0.15.tgz", {}, ""], + + "@jest/schemas": ["@jest/schemas@29.6.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@jest/schemas/-/schemas-29.6.3.tgz", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, ""], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, ""], + + "@keyv/serialize": ["@keyv/serialize@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@keyv/serialize/-/serialize-1.1.1.tgz", {}, ""], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, ""], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", {}, ""], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, ""], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", { "os": "darwin", "cpu": "arm64" }, ""], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@rtsao/scc/-/scc-1.1.0.tgz", {}, ""], + + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", {}, ""], + + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@sinclair/typebox/-/typebox-0.27.8.tgz", {}, ""], + + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@sindresorhus/is/-/is-7.2.0.tgz", {}, ""], + + "@types/estree": ["@types/estree@1.0.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@types/estree/-/estree-1.0.8.tgz", {}, ""], + + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", {}, ""], + + "@types/inquirer": ["@types/inquirer@9.0.9", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@types/inquirer/-/inquirer-9.0.9.tgz", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, ""], + + "@types/json-schema": ["@types/json-schema@7.0.15", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@types/json-schema/-/json-schema-7.0.15.tgz", {}, ""], + + "@types/json5": ["@types/json5@0.0.29", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@types/json5/-/json5-0.0.29.tgz", {}, ""], + + "@types/node": ["@types/node@20.19.30", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@types/node/-/node-20.19.30.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, ""], + + "@types/react": ["@types/react@19.2.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@types/react/-/react-19.2.8.tgz", { "dependencies": { "csstype": "^3.2.2" } }, ""], + + "@types/semver": ["@types/semver@7.7.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@types/semver/-/semver-7.7.1.tgz", {}, ""], + + "@types/through": ["@types/through@0.0.33", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@types/through/-/through-0.0.33.tgz", { "dependencies": { "@types/node": "*" } }, ""], + + "@types/uuid": ["@types/uuid@10.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@types/uuid/-/uuid-10.0.0.tgz", {}, ""], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@6.21.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.5.1", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/type-utils": "6.21.0", "@typescript-eslint/utils": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", "natural-compare": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", "eslint": "^7.0.0 || ^8.0.0" } }, ""], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@6.21.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/parser/-/parser-6.21.0.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, ""], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.0", "@typescript-eslint/types": "^8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, ""], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.21.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" } }, ""], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, ""], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@6.21.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", { "dependencies": { "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, ""], + + "@typescript-eslint/types": ["@typescript-eslint/types@6.21.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/types/-/types-6.21.0.tgz", {}, ""], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@6.21.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, ""], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@6.21.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/utils/-/utils-6.21.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, ""], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, ""], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", {}, ""], + + "@vitest/expect": ["@vitest/expect@1.6.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@vitest/expect/-/expect-1.6.1.tgz", { "dependencies": { "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "chai": "^4.3.10" } }, ""], + + "@vitest/runner": ["@vitest/runner@1.6.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@vitest/runner/-/runner-1.6.1.tgz", { "dependencies": { "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" } }, ""], + + "@vitest/snapshot": ["@vitest/snapshot@1.6.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@vitest/snapshot/-/snapshot-1.6.1.tgz", { "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", "pretty-format": "^29.7.0" } }, ""], + + "@vitest/spy": ["@vitest/spy@1.6.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@vitest/spy/-/spy-1.6.1.tgz", { "dependencies": { "tinyspy": "^2.2.0" } }, ""], + + "@vitest/utils": ["@vitest/utils@1.6.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@vitest/utils/-/utils-1.6.1.tgz", { "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", "loupe": "^2.3.7", "pretty-format": "^29.7.0" } }, ""], + + "acorn": ["acorn@8.15.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/acorn/-/acorn-8.15.0.tgz", { "bin": "bin/acorn" }, ""], + + "acorn-jsx": ["acorn-jsx@5.3.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, ""], + + "acorn-walk": ["acorn-walk@8.3.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/acorn-walk/-/acorn-walk-8.3.4.tgz", { "dependencies": { "acorn": "^8.11.0" } }, ""], + + "ajv": ["ajv@6.12.6", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ajv/-/ajv-6.12.6.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, ""], + + "ansi-align": ["ansi-align@3.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-align/-/ansi-align-3.0.1.tgz", { "dependencies": { "string-width": "^4.1.0" } }, ""], + + "ansi-escapes": ["ansi-escapes@7.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-escapes/-/ansi-escapes-7.2.0.tgz", { "dependencies": { "environment": "^1.0.0" } }, ""], + + "ansi-regex": ["ansi-regex@5.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, ""], + + "ansi-styles": ["ansi-styles@6.2.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, ""], + + "any-promise": ["any-promise@1.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/any-promise/-/any-promise-1.3.0.tgz", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/anymatch/-/anymatch-3.1.3.tgz", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, ""], + + "argparse": ["argparse@2.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/argparse/-/argparse-2.0.1.tgz", {}, ""], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, ""], + + "array-includes": ["array-includes@3.1.9", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/array-includes/-/array-includes-3.1.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, ""], + + "array-union": ["array-union@2.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/array-union/-/array-union-2.1.0.tgz", {}, ""], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, ""], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, ""], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, ""], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, ""], + + "assertion-error": ["assertion-error@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/assertion-error/-/assertion-error-1.1.0.tgz", {}, ""], + + "async-function": ["async-function@1.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/async-function/-/async-function-1.0.0.tgz", {}, ""], + + "auto-bind": ["auto-bind@5.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/auto-bind/-/auto-bind-5.0.1.tgz", {}, ""], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, ""], + + "balanced-match": ["balanced-match@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/balanced-match/-/balanced-match-1.0.2.tgz", {}, ""], + + "base64-js": ["base64-js@1.5.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/base64-js/-/base64-js-1.5.1.tgz", {}, ""], + + "binary-extensions": ["binary-extensions@2.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/binary-extensions/-/binary-extensions-2.3.0.tgz", {}, ""], + + "bl": ["bl@4.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/bl/-/bl-4.1.0.tgz", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, ""], + + "boxen": ["boxen@7.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/boxen/-/boxen-7.1.1.tgz", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^7.0.1", "chalk": "^5.2.0", "cli-boxes": "^3.0.0", "string-width": "^5.1.2", "type-fest": "^2.13.0", "widest-line": "^4.0.1", "wrap-ansi": "^8.1.0" } }, ""], + + "brace-expansion": ["brace-expansion@1.1.12", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/brace-expansion/-/brace-expansion-1.1.12.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""], + + "braces": ["braces@3.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/braces/-/braces-3.0.3.tgz", { "dependencies": { "fill-range": "^7.1.1" } }, ""], + + "buffer": ["buffer@5.7.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/buffer/-/buffer-5.7.1.tgz", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, ""], + + "builtin-modules": ["builtin-modules@3.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/builtin-modules/-/builtin-modules-3.3.0.tgz", {}, ""], + + "builtins": ["builtins@5.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/builtins/-/builtins-5.1.0.tgz", { "dependencies": { "semver": "^7.0.0" } }, ""], + + "byte-counter": ["byte-counter@0.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/byte-counter/-/byte-counter-0.1.0.tgz", {}, ""], + + "cac": ["cac@6.7.14", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cac/-/cac-6.7.14.tgz", {}, ""], + + "cacheable-lookup": ["cacheable-lookup@7.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", {}, ""], + + "cacheable-request": ["cacheable-request@13.0.18", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cacheable-request/-/cacheable-request-13.0.18.tgz", { "dependencies": { "@types/http-cache-semantics": "^4.0.4", "get-stream": "^9.0.1", "http-cache-semantics": "^4.2.0", "keyv": "^5.5.5", "mimic-response": "^4.0.0", "normalize-url": "^8.1.1", "responselike": "^4.0.2" } }, ""], + + "call-bind": ["call-bind@1.0.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/call-bind/-/call-bind-1.0.8.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, ""], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, ""], + + "call-bound": ["call-bound@1.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/call-bound/-/call-bound-1.0.4.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, ""], + + "callsites": ["callsites@3.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/callsites/-/callsites-3.1.0.tgz", {}, ""], + + "camelcase": ["camelcase@7.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/camelcase/-/camelcase-7.0.1.tgz", {}, ""], + + "chai": ["chai@4.5.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/chai/-/chai-4.5.0.tgz", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, ""], + + "chalk": ["chalk@5.6.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/chalk/-/chalk-5.6.2.tgz", {}, ""], + + "chardet": ["chardet@2.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/chardet/-/chardet-2.1.1.tgz", {}, ""], + + "check-error": ["check-error@1.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/check-error/-/check-error-1.0.3.tgz", { "dependencies": { "get-func-name": "^2.0.2" } }, ""], + + "chokidar": ["chokidar@3.6.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/chokidar/-/chokidar-3.6.0.tgz", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, ""], + + "cli-boxes": ["cli-boxes@3.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cli-boxes/-/cli-boxes-3.0.0.tgz", {}, ""], + + "cli-cursor": ["cli-cursor@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cli-cursor/-/cli-cursor-4.0.0.tgz", { "dependencies": { "restore-cursor": "^4.0.0" } }, ""], + + "cli-highlight": ["cli-highlight@2.1.11", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cli-highlight/-/cli-highlight-2.1.11.tgz", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], + + "cli-spinners": ["cli-spinners@2.9.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cli-spinners/-/cli-spinners-2.9.2.tgz", {}, ""], + + "cli-truncate": ["cli-truncate@5.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cli-truncate/-/cli-truncate-5.1.1.tgz", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, ""], + + "cli-width": ["cli-width@4.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cli-width/-/cli-width-4.1.0.tgz", {}, ""], + + "cliui": ["cliui@7.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cliui/-/cliui-7.0.4.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + + "clone": ["clone@1.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/clone/-/clone-1.0.4.tgz", {}, ""], + + "code-excerpt": ["code-excerpt@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/code-excerpt/-/code-excerpt-4.0.0.tgz", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, ""], + + "color-convert": ["color-convert@2.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, ""], + + "color-name": ["color-name@1.1.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/color-name/-/color-name-1.1.4.tgz", {}, ""], + + "commander": ["commander@12.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/commander/-/commander-12.1.0.tgz", {}, ""], + + "concat-map": ["concat-map@0.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/concat-map/-/concat-map-0.0.1.tgz", {}, ""], + + "confbox": ["confbox@0.1.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/confbox/-/confbox-0.1.8.tgz", {}, ""], + + "convert-to-spaces": ["convert-to-spaces@2.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", {}, ""], + + "cross-spawn": ["cross-spawn@7.0.6", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, ""], + + "csstype": ["csstype@3.2.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/csstype/-/csstype-3.2.3.tgz", {}, ""], + + "data-view-buffer": ["data-view-buffer@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/data-view-buffer/-/data-view-buffer-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, ""], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, ""], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, ""], + + "debug": ["debug@4.4.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, ""], + + "decompress-response": ["decompress-response@10.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/decompress-response/-/decompress-response-10.0.0.tgz", { "dependencies": { "mimic-response": "^4.0.0" } }, ""], + + "deep-eql": ["deep-eql@4.1.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/deep-eql/-/deep-eql-4.1.4.tgz", { "dependencies": { "type-detect": "^4.0.0" } }, ""], + + "deep-is": ["deep-is@0.1.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/deep-is/-/deep-is-0.1.4.tgz", {}, ""], + + "defaults": ["defaults@1.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/defaults/-/defaults-1.0.4.tgz", { "dependencies": { "clone": "^1.0.2" } }, ""], + + "define-data-property": ["define-data-property@1.1.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/define-data-property/-/define-data-property-1.1.4.tgz", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, ""], + + "define-properties": ["define-properties@1.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/define-properties/-/define-properties-1.2.1.tgz", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, ""], + + "diff-sequences": ["diff-sequences@29.6.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/diff-sequences/-/diff-sequences-29.6.3.tgz", {}, ""], + + "dir-glob": ["dir-glob@3.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/dir-glob/-/dir-glob-3.0.1.tgz", { "dependencies": { "path-type": "^4.0.0" } }, ""], + + "doctrine": ["doctrine@3.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/doctrine/-/doctrine-3.0.0.tgz", { "dependencies": { "esutils": "^2.0.2" } }, ""], + + "dunder-proto": ["dunder-proto@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, ""], + + "eastasianwidth": ["eastasianwidth@0.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eastasianwidth/-/eastasianwidth-0.2.0.tgz", {}, ""], + + "emoji-regex": ["emoji-regex@9.2.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/emoji-regex/-/emoji-regex-9.2.2.tgz", {}, ""], + + "environment": ["environment@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/environment/-/environment-1.1.0.tgz", {}, ""], + + "es-abstract": ["es-abstract@1.24.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/es-abstract/-/es-abstract-1.24.1.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, ""], + + "es-define-property": ["es-define-property@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/es-define-property/-/es-define-property-1.0.1.tgz", {}, ""], + + "es-errors": ["es-errors@1.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/es-errors/-/es-errors-1.3.0.tgz", {}, ""], + + "es-object-atoms": ["es-object-atoms@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/es-object-atoms/-/es-object-atoms-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, ""], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, ""], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", { "dependencies": { "hasown": "^2.0.2" } }, ""], + + "es-to-primitive": ["es-to-primitive@1.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/es-to-primitive/-/es-to-primitive-1.3.0.tgz", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, ""], + + "es-toolkit": ["es-toolkit@1.44.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/es-toolkit/-/es-toolkit-1.44.0.tgz", {}, ""], + + "esbuild": ["esbuild@0.21.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/esbuild/-/esbuild-0.21.5.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, ""], + + "escalade": ["escalade@3.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, ""], + + "eslint": ["eslint@8.57.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint/-/eslint-8.57.1.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": "bin/eslint.js" }, ""], + + "eslint-compat-utils": ["eslint-compat-utils@0.5.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, ""], + + "eslint-config-standard": ["eslint-config-standard@17.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", { "peerDependencies": { "eslint": "^8.0.1", "eslint-plugin-import": "^2.25.2", "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", "eslint-plugin-promise": "^6.0.0" } }, ""], + + "eslint-config-standard-with-typescript": ["eslint-config-standard-with-typescript@43.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-43.0.1.tgz", { "dependencies": { "@typescript-eslint/parser": "^6.4.0", "eslint-config-standard": "17.1.0" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^6.4.0", "eslint": "^8.0.1", "eslint-plugin-import": "^2.25.2", "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", "eslint-plugin-promise": "^6.0.0", "typescript": "*" } }, ""], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, ""], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", { "dependencies": { "debug": "^3.2.7" } }, ""], + + "eslint-plugin-es-x": ["eslint-plugin-es-x@7.8.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.1.2", "@eslint-community/regexpp": "^4.11.0", "eslint-compat-utils": "^0.5.1" }, "peerDependencies": { "eslint": ">=8" } }, ""], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, ""], + + "eslint-plugin-n": ["eslint-plugin-n@16.6.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "builtins": "^5.0.1", "eslint-plugin-es-x": "^7.5.0", "get-tsconfig": "^4.7.0", "globals": "^13.24.0", "ignore": "^5.2.4", "is-builtin-module": "^3.2.1", "is-core-module": "^2.12.1", "minimatch": "^3.1.2", "resolve": "^1.22.2", "semver": "^7.5.3" }, "peerDependencies": { "eslint": ">=7.0.0" } }, ""], + + "eslint-plugin-promise": ["eslint-plugin-promise@6.6.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", { "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, ""], + + "eslint-scope": ["eslint-scope@7.2.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-scope/-/eslint-scope-7.2.2.tgz", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, ""], + + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, ""], + + "espree": ["espree@10.4.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/espree/-/espree-10.4.0.tgz", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, ""], + + "esquery": ["esquery@1.7.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/esquery/-/esquery-1.7.0.tgz", { "dependencies": { "estraverse": "^5.1.0" } }, ""], + + "esrecurse": ["esrecurse@4.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/esrecurse/-/esrecurse-4.3.0.tgz", { "dependencies": { "estraverse": "^5.2.0" } }, ""], + + "estraverse": ["estraverse@5.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/estraverse/-/estraverse-5.3.0.tgz", {}, ""], + + "estree-walker": ["estree-walker@3.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/estree-walker/-/estree-walker-3.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, ""], + + "esutils": ["esutils@2.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/esutils/-/esutils-2.0.3.tgz", {}, ""], + + "execa": ["execa@8.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/execa/-/execa-8.0.1.tgz", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, ""], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, ""], + + "fast-glob": ["fast-glob@3.3.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/fast-glob/-/fast-glob-3.3.3.tgz", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, ""], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", {}, ""], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", {}, ""], + + "fastq": ["fastq@1.20.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/fastq/-/fastq-1.20.1.tgz", { "dependencies": { "reusify": "^1.0.4" } }, ""], + + "fdir": ["fdir@6.5.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" } }, ""], + + "file-entry-cache": ["file-entry-cache@6.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/file-entry-cache/-/file-entry-cache-6.0.1.tgz", { "dependencies": { "flat-cache": "^3.0.4" } }, ""], + + "fill-range": ["fill-range@7.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/fill-range/-/fill-range-7.1.1.tgz", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""], + + "find-up": ["find-up@5.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/find-up/-/find-up-5.0.0.tgz", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, ""], + + "flat-cache": ["flat-cache@3.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/flat-cache/-/flat-cache-3.2.0.tgz", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, ""], + + "flatted": ["flatted@3.3.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/flatted/-/flatted-3.3.3.tgz", {}, ""], + + "for-each": ["for-each@0.3.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/for-each/-/for-each-0.3.5.tgz", { "dependencies": { "is-callable": "^1.2.7" } }, ""], + + "form-data-encoder": ["form-data-encoder@4.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/form-data-encoder/-/form-data-encoder-4.1.0.tgz", {}, ""], + + "fs.realpath": ["fs.realpath@1.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/fs.realpath/-/fs.realpath-1.0.0.tgz", {}, ""], + + "fsevents": ["fsevents@2.3.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, ""], + + "function-bind": ["function-bind@1.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/function-bind/-/function-bind-1.1.2.tgz", {}, ""], + + "function.prototype.name": ["function.prototype.name@1.1.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/function.prototype.name/-/function.prototype.name-1.1.8.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, ""], + + "functions-have-names": ["functions-have-names@1.2.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/functions-have-names/-/functions-have-names-1.2.3.tgz", {}, ""], + + "generator-function": ["generator-function@2.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/generator-function/-/generator-function-2.0.1.tgz", {}, ""], + + "get-caller-file": ["get-caller-file@2.0.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", {}, ""], + + "get-func-name": ["get-func-name@2.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/get-func-name/-/get-func-name-2.0.2.tgz", {}, ""], + + "get-intrinsic": ["get-intrinsic@1.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, ""], + + "get-proto": ["get-proto@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, ""], + + "get-stream": ["get-stream@8.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/get-stream/-/get-stream-8.0.1.tgz", {}, ""], + + "get-symbol-description": ["get-symbol-description@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/get-symbol-description/-/get-symbol-description-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, ""], + + "get-tsconfig": ["get-tsconfig@4.13.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/get-tsconfig/-/get-tsconfig-4.13.0.tgz", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, ""], + + "glob": ["glob@7.2.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/glob/-/glob-7.2.3.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, ""], + + "glob-parent": ["glob-parent@6.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, ""], + + "globals": ["globals@17.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/globals/-/globals-17.0.0.tgz", {}, ""], + + "globalthis": ["globalthis@1.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/globalthis/-/globalthis-1.0.4.tgz", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, ""], + + "globby": ["globby@11.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/globby/-/globby-11.1.0.tgz", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, ""], + + "gopd": ["gopd@1.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/gopd/-/gopd-1.2.0.tgz", {}, ""], + + "got": ["got@14.6.6", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/got/-/got-14.6.6.tgz", { "dependencies": { "@sindresorhus/is": "^7.0.1", "byte-counter": "^0.1.0", "cacheable-lookup": "^7.0.0", "cacheable-request": "^13.0.12", "decompress-response": "^10.0.0", "form-data-encoder": "^4.0.2", "http2-wrapper": "^2.2.1", "keyv": "^5.5.3", "lowercase-keys": "^3.0.0", "p-cancelable": "^4.0.1", "responselike": "^4.0.2", "type-fest": "^4.26.1" } }, ""], + + "graphemer": ["graphemer@1.4.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/graphemer/-/graphemer-1.4.0.tgz", {}, ""], + + "has-bigints": ["has-bigints@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/has-bigints/-/has-bigints-1.1.0.tgz", {}, ""], + + "has-flag": ["has-flag@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/has-flag/-/has-flag-4.0.0.tgz", {}, ""], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", { "dependencies": { "es-define-property": "^1.0.0" } }, ""], + + "has-proto": ["has-proto@1.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/has-proto/-/has-proto-1.2.0.tgz", { "dependencies": { "dunder-proto": "^1.0.0" } }, ""], + + "has-symbols": ["has-symbols@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/has-symbols/-/has-symbols-1.1.0.tgz", {}, ""], + + "has-tostringtag": ["has-tostringtag@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, ""], + + "hasown": ["hasown@2.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, ""], + + "highlight.js": ["highlight.js@10.7.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/highlight.js/-/highlight.js-10.7.3.tgz", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", {}, ""], + + "http2-wrapper": ["http2-wrapper@2.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/http2-wrapper/-/http2-wrapper-2.2.1.tgz", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, ""], + + "human-signals": ["human-signals@5.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/human-signals/-/human-signals-5.0.0.tgz", {}, ""], + + "iconv-lite": ["iconv-lite@0.7.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, ""], + + "ieee754": ["ieee754@1.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ieee754/-/ieee754-1.2.1.tgz", {}, ""], + + "ignore": ["ignore@5.3.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ignore/-/ignore-5.3.2.tgz", {}, ""], + + "import-fresh": ["import-fresh@3.3.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/import-fresh/-/import-fresh-3.3.1.tgz", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, ""], + + "imurmurhash": ["imurmurhash@0.1.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/imurmurhash/-/imurmurhash-0.1.4.tgz", {}, ""], + + "indent-string": ["indent-string@5.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/indent-string/-/indent-string-5.0.0.tgz", {}, ""], + + "inflight": ["inflight@1.0.6", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/inflight/-/inflight-1.0.6.tgz", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, ""], + + "inherits": ["inherits@2.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/inherits/-/inherits-2.0.4.tgz", {}, ""], + + "ink": ["ink@6.6.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ink/-/ink-6.6.0.tgz", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.2.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^8.1.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" } }, ""], + + "ink-spinner": ["ink-spinner@5.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ink-spinner/-/ink-spinner-5.0.0.tgz", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, ""], + + "inquirer": ["inquirer@9.3.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/inquirer/-/inquirer-9.3.8.tgz", { "dependencies": { "@inquirer/external-editor": "^1.0.2", "@inquirer/figures": "^1.0.3", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "1.0.0", "ora": "^5.4.1", "run-async": "^3.0.0", "rxjs": "^7.8.1", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, ""], + + "internal-slot": ["internal-slot@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/internal-slot/-/internal-slot-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, ""], + + "is-array-buffer": ["is-array-buffer@3.0.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-array-buffer/-/is-array-buffer-3.0.5.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, ""], + + "is-async-function": ["is-async-function@2.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-async-function/-/is-async-function-2.1.1.tgz", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, ""], + + "is-bigint": ["is-bigint@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-bigint/-/is-bigint-1.1.0.tgz", { "dependencies": { "has-bigints": "^1.0.2" } }, ""], + + "is-binary-path": ["is-binary-path@2.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-binary-path/-/is-binary-path-2.1.0.tgz", { "dependencies": { "binary-extensions": "^2.0.0" } }, ""], + + "is-boolean-object": ["is-boolean-object@1.2.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-boolean-object/-/is-boolean-object-1.2.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, ""], + + "is-builtin-module": ["is-builtin-module@3.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-builtin-module/-/is-builtin-module-3.2.1.tgz", { "dependencies": { "builtin-modules": "^3.3.0" } }, ""], + + "is-callable": ["is-callable@1.2.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-callable/-/is-callable-1.2.7.tgz", {}, ""], + + "is-core-module": ["is-core-module@2.16.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-core-module/-/is-core-module-2.16.1.tgz", { "dependencies": { "hasown": "^2.0.2" } }, ""], + + "is-data-view": ["is-data-view@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-data-view/-/is-data-view-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, ""], + + "is-date-object": ["is-date-object@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-date-object/-/is-date-object-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, ""], + + "is-extglob": ["is-extglob@2.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-extglob/-/is-extglob-2.1.1.tgz", {}, ""], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, ""], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, ""], + + "is-generator-function": ["is-generator-function@1.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-generator-function/-/is-generator-function-1.1.2.tgz", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, ""], + + "is-glob": ["is-glob@4.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-glob/-/is-glob-4.0.3.tgz", { "dependencies": { "is-extglob": "^2.1.1" } }, ""], + + "is-in-ci": ["is-in-ci@2.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-in-ci/-/is-in-ci-2.0.0.tgz", { "bin": "cli.js" }, ""], + + "is-interactive": ["is-interactive@2.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-interactive/-/is-interactive-2.0.0.tgz", {}, ""], + + "is-map": ["is-map@2.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-map/-/is-map-2.0.3.tgz", {}, ""], + + "is-negative-zero": ["is-negative-zero@2.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-negative-zero/-/is-negative-zero-2.0.3.tgz", {}, ""], + + "is-number": ["is-number@7.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-number/-/is-number-7.0.0.tgz", {}, ""], + + "is-number-object": ["is-number-object@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-number-object/-/is-number-object-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, ""], + + "is-path-inside": ["is-path-inside@3.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-path-inside/-/is-path-inside-3.0.3.tgz", {}, ""], + + "is-regex": ["is-regex@1.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-regex/-/is-regex-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, ""], + + "is-set": ["is-set@2.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-set/-/is-set-2.0.3.tgz", {}, ""], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, ""], + + "is-stream": ["is-stream@3.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-stream/-/is-stream-3.0.0.tgz", {}, ""], + + "is-string": ["is-string@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-string/-/is-string-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, ""], + + "is-symbol": ["is-symbol@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-symbol/-/is-symbol-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, ""], + + "is-typed-array": ["is-typed-array@1.1.15", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-typed-array/-/is-typed-array-1.1.15.tgz", { "dependencies": { "which-typed-array": "^1.1.16" } }, ""], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", {}, ""], + + "is-weakmap": ["is-weakmap@2.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-weakmap/-/is-weakmap-2.0.2.tgz", {}, ""], + + "is-weakref": ["is-weakref@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-weakref/-/is-weakref-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, ""], + + "is-weakset": ["is-weakset@2.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-weakset/-/is-weakset-2.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, ""], + + "isarray": ["isarray@2.0.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/isarray/-/isarray-2.0.5.tgz", {}, ""], + + "isexe": ["isexe@2.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/isexe/-/isexe-2.0.0.tgz", {}, ""], + + "js-tokens": ["js-tokens@9.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/js-tokens/-/js-tokens-9.0.1.tgz", {}, ""], + + "js-yaml": ["js-yaml@4.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, ""], + + "json-buffer": ["json-buffer@3.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/json-buffer/-/json-buffer-3.0.1.tgz", {}, ""], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, ""], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, ""], + + "json5": ["json5@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/json5/-/json5-1.0.2.tgz", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, ""], + + "keyv": ["keyv@5.5.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/keyv/-/keyv-5.5.5.tgz", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, ""], + + "levn": ["levn@0.4.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/levn/-/levn-0.4.1.tgz", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, ""], + + "local-pkg": ["local-pkg@0.5.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/local-pkg/-/local-pkg-0.5.1.tgz", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, ""], + + "locate-path": ["locate-path@6.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/locate-path/-/locate-path-6.0.0.tgz", { "dependencies": { "p-locate": "^5.0.0" } }, ""], + + "lodash.merge": ["lodash.merge@4.6.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, ""], + + "log-symbols": ["log-symbols@6.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/log-symbols/-/log-symbols-6.0.0.tgz", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, ""], + + "loupe": ["loupe@2.3.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/loupe/-/loupe-2.3.7.tgz", { "dependencies": { "get-func-name": "^2.0.1" } }, ""], + + "lowercase-keys": ["lowercase-keys@3.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/lowercase-keys/-/lowercase-keys-3.0.0.tgz", {}, ""], + + "magic-string": ["magic-string@0.30.21", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, ""], + + "math-intrinsics": ["math-intrinsics@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, ""], + + "merge-stream": ["merge-stream@2.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/merge-stream/-/merge-stream-2.0.0.tgz", {}, ""], + + "merge2": ["merge2@1.4.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/merge2/-/merge2-1.4.1.tgz", {}, ""], + + "micromatch": ["micromatch@4.0.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/micromatch/-/micromatch-4.0.8.tgz", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, ""], + + "mimic-fn": ["mimic-fn@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/mimic-fn/-/mimic-fn-4.0.0.tgz", {}, ""], + + "mimic-function": ["mimic-function@5.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/mimic-function/-/mimic-function-5.0.1.tgz", {}, ""], + + "mimic-response": ["mimic-response@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/mimic-response/-/mimic-response-4.0.0.tgz", {}, ""], + + "minimatch": ["minimatch@3.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/minimatch/-/minimatch-3.1.2.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""], + + "minimist": ["minimist@1.2.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/minimist/-/minimist-1.2.8.tgz", {}, ""], + + "mlly": ["mlly@1.8.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/mlly/-/mlly-1.8.0.tgz", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, ""], + + "ms": ["ms@2.1.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ms/-/ms-2.1.3.tgz", {}, ""], + + "mute-stream": ["mute-stream@1.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/mute-stream/-/mute-stream-1.0.0.tgz", {}, ""], + + "mz": ["mz@2.7.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/mz/-/mz-2.7.0.tgz", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/nanoid/-/nanoid-3.3.11.tgz", { "bin": "bin/nanoid.cjs" }, ""], + + "natural-compare": ["natural-compare@1.4.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/natural-compare/-/natural-compare-1.4.0.tgz", {}, ""], + + "normalize-path": ["normalize-path@3.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/normalize-path/-/normalize-path-3.0.0.tgz", {}, ""], + + "normalize-url": ["normalize-url@8.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/normalize-url/-/normalize-url-8.1.1.tgz", {}, ""], + + "npm-run-path": ["npm-run-path@5.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/npm-run-path/-/npm-run-path-5.3.0.tgz", { "dependencies": { "path-key": "^4.0.0" } }, ""], + + "object-assign": ["object-assign@4.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/object-inspect/-/object-inspect-1.13.4.tgz", {}, ""], + + "object-keys": ["object-keys@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/object-keys/-/object-keys-1.1.1.tgz", {}, ""], + + "object.assign": ["object.assign@4.1.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/object.assign/-/object.assign-4.1.7.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, ""], + + "object.fromentries": ["object.fromentries@2.0.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/object.fromentries/-/object.fromentries-2.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, ""], + + "object.groupby": ["object.groupby@1.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/object.groupby/-/object.groupby-1.0.3.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, ""], + + "object.values": ["object.values@1.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/object.values/-/object.values-1.2.1.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, ""], + + "once": ["once@1.4.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/once/-/once-1.4.0.tgz", { "dependencies": { "wrappy": "1" } }, ""], + + "onetime": ["onetime@6.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/onetime/-/onetime-6.0.0.tgz", { "dependencies": { "mimic-fn": "^4.0.0" } }, ""], + + "optionator": ["optionator@0.9.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/optionator/-/optionator-0.9.4.tgz", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, ""], + + "ora": ["ora@8.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ora/-/ora-8.2.0.tgz", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, ""], + + "own-keys": ["own-keys@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/own-keys/-/own-keys-1.0.1.tgz", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, ""], + + "p-cancelable": ["p-cancelable@4.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/p-cancelable/-/p-cancelable-4.0.1.tgz", {}, ""], + + "p-limit": ["p-limit@5.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/p-limit/-/p-limit-5.0.0.tgz", { "dependencies": { "yocto-queue": "^1.0.0" } }, ""], + + "p-locate": ["p-locate@5.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/p-locate/-/p-locate-5.0.0.tgz", { "dependencies": { "p-limit": "^3.0.2" } }, ""], + + "parent-module": ["parent-module@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/parent-module/-/parent-module-1.0.1.tgz", { "dependencies": { "callsites": "^3.0.0" } }, ""], + + "parse5": ["parse5@5.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/parse5/-/parse5-5.1.1.tgz", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="], + + "patch-console": ["patch-console@2.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/patch-console/-/patch-console-2.0.0.tgz", {}, ""], + + "path-exists": ["path-exists@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/path-exists/-/path-exists-4.0.0.tgz", {}, ""], + + "path-is-absolute": ["path-is-absolute@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/path-is-absolute/-/path-is-absolute-1.0.1.tgz", {}, ""], + + "path-key": ["path-key@3.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/path-key/-/path-key-3.1.1.tgz", {}, ""], + + "path-parse": ["path-parse@1.0.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/path-parse/-/path-parse-1.0.7.tgz", {}, ""], + + "path-type": ["path-type@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/path-type/-/path-type-4.0.0.tgz", {}, ""], + + "pathe": ["pathe@1.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/pathe/-/pathe-1.1.2.tgz", {}, ""], + + "pathval": ["pathval@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/pathval/-/pathval-1.1.1.tgz", {}, ""], + + "picocolors": ["picocolors@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/picocolors/-/picocolors-1.1.1.tgz", {}, ""], + + "picomatch": ["picomatch@2.3.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/picomatch/-/picomatch-2.3.1.tgz", {}, ""], + + "pkg-types": ["pkg-types@1.3.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/pkg-types/-/pkg-types-1.3.1.tgz", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, ""], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, ""], + + "postcss": ["postcss@8.5.6", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/postcss/-/postcss-8.5.6.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, ""], + + "prelude-ls": ["prelude-ls@1.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, ""], + + "prettier": ["prettier@3.8.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/prettier/-/prettier-3.8.0.tgz", { "bin": "bin/prettier.cjs" }, ""], + + "pretty-format": ["pretty-format@29.7.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/pretty-format/-/pretty-format-29.7.0.tgz", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, ""], + + "punycode": ["punycode@2.3.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/punycode/-/punycode-2.3.1.tgz", {}, ""], + + "queue-microtask": ["queue-microtask@1.2.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/queue-microtask/-/queue-microtask-1.2.3.tgz", {}, ""], + + "quick-lru": ["quick-lru@5.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/quick-lru/-/quick-lru-5.1.1.tgz", {}, ""], + + "react": ["react@19.2.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/react/-/react-19.2.3.tgz", {}, ""], + + "react-devtools-core": ["react-devtools-core@7.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/react-devtools-core/-/react-devtools-core-7.0.1.tgz", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="], + + "react-is": ["react-is@18.3.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/react-is/-/react-is-18.3.1.tgz", {}, ""], + + "react-reconciler": ["react-reconciler@0.33.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/react-reconciler/-/react-reconciler-0.33.0.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, ""], + + "readable-stream": ["readable-stream@3.6.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/readable-stream/-/readable-stream-3.6.2.tgz", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, ""], + + "readdirp": ["readdirp@3.6.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/readdirp/-/readdirp-3.6.0.tgz", { "dependencies": { "picomatch": "^2.2.1" } }, ""], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, ""], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, ""], + + "require-directory": ["require-directory@2.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/require-directory/-/require-directory-2.1.1.tgz", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resolve": ["resolve@1.22.11", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/resolve/-/resolve-1.22.11.tgz", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, ""], + + "resolve-alpn": ["resolve-alpn@1.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/resolve-alpn/-/resolve-alpn-1.2.1.tgz", {}, ""], + + "resolve-from": ["resolve-from@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/resolve-from/-/resolve-from-4.0.0.tgz", {}, ""], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, ""], + + "responselike": ["responselike@4.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/responselike/-/responselike-4.0.2.tgz", { "dependencies": { "lowercase-keys": "^3.0.0" } }, ""], + + "restore-cursor": ["restore-cursor@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/restore-cursor/-/restore-cursor-4.0.0.tgz", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, ""], + + "reusify": ["reusify@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/reusify/-/reusify-1.1.0.tgz", {}, ""], + + "rimraf": ["rimraf@3.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/rimraf/-/rimraf-3.0.2.tgz", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, ""], + + "rollup": ["rollup@4.55.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/rollup/-/rollup-4.55.1.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, ""], + + "run-async": ["run-async@3.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/run-async/-/run-async-3.0.0.tgz", {}, ""], + + "run-parallel": ["run-parallel@1.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/run-parallel/-/run-parallel-1.2.0.tgz", { "dependencies": { "queue-microtask": "^1.2.2" } }, ""], + + "rxjs": ["rxjs@7.8.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/rxjs/-/rxjs-7.8.2.tgz", { "dependencies": { "tslib": "^2.1.0" } }, ""], + + "safe-array-concat": ["safe-array-concat@1.1.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/safe-array-concat/-/safe-array-concat-1.1.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, ""], + + "safe-buffer": ["safe-buffer@5.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/safe-buffer/-/safe-buffer-5.2.1.tgz", {}, ""], + + "safe-push-apply": ["safe-push-apply@1.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/safe-push-apply/-/safe-push-apply-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, ""], + + "safe-regex-test": ["safe-regex-test@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/safe-regex-test/-/safe-regex-test-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, ""], + + "safer-buffer": ["safer-buffer@2.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, ""], + + "scheduler": ["scheduler@0.27.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/scheduler/-/scheduler-0.27.0.tgz", {}, ""], + + "semver": ["semver@7.7.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/semver/-/semver-7.7.3.tgz", { "bin": "bin/semver.js" }, ""], + + "set-function-length": ["set-function-length@1.2.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/set-function-length/-/set-function-length-1.2.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, ""], + + "set-function-name": ["set-function-name@2.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/set-function-name/-/set-function-name-2.0.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, ""], + + "set-proto": ["set-proto@1.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/set-proto/-/set-proto-1.0.0.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, ""], + + "shebang-command": ["shebang-command@2.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""], + + "shebang-regex": ["shebang-regex@3.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, ""], + + "shell-quote": ["shell-quote@1.8.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/shell-quote/-/shell-quote-1.8.3.tgz", {}, ""], + + "side-channel": ["side-channel@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/side-channel/-/side-channel-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, ""], + + "side-channel-list": ["side-channel-list@1.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/side-channel-list/-/side-channel-list-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, ""], + + "side-channel-map": ["side-channel-map@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/side-channel-map/-/side-channel-map-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, ""], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, ""], + + "siginfo": ["siginfo@2.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/siginfo/-/siginfo-2.0.0.tgz", {}, ""], + + "signal-exit": ["signal-exit@3.0.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/signal-exit/-/signal-exit-3.0.7.tgz", {}, ""], + + "slash": ["slash@3.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/slash/-/slash-3.0.0.tgz", {}, ""], + + "slice-ansi": ["slice-ansi@7.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/slice-ansi/-/slice-ansi-7.1.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, ""], + + "source-map-js": ["source-map-js@1.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/source-map-js/-/source-map-js-1.2.1.tgz", {}, ""], + + "stack-utils": ["stack-utils@2.0.6", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/stack-utils/-/stack-utils-2.0.6.tgz", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, ""], + + "stackback": ["stackback@0.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/stackback/-/stackback-0.0.2.tgz", {}, ""], + + "std-env": ["std-env@3.10.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/std-env/-/std-env-3.10.0.tgz", {}, ""], + + "stdin-discarder": ["stdin-discarder@0.2.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/stdin-discarder/-/stdin-discarder-0.2.2.tgz", {}, ""], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, ""], + + "string-width": ["string-width@5.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/string-width/-/string-width-5.1.2.tgz", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, ""], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, ""], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, ""], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, ""], + + "string_decoder": ["string_decoder@1.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/string_decoder/-/string_decoder-1.3.0.tgz", { "dependencies": { "safe-buffer": "~5.2.0" } }, ""], + + "strip-ansi": ["strip-ansi@6.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""], + + "strip-bom": ["strip-bom@3.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/strip-bom/-/strip-bom-3.0.0.tgz", {}, ""], + + "strip-final-newline": ["strip-final-newline@3.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/strip-final-newline/-/strip-final-newline-3.0.0.tgz", {}, ""], + + "strip-json-comments": ["strip-json-comments@3.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/strip-json-comments/-/strip-json-comments-3.1.1.tgz", {}, ""], + + "strip-literal": ["strip-literal@2.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/strip-literal/-/strip-literal-2.1.1.tgz", { "dependencies": { "js-tokens": "^9.0.1" } }, ""], + + "supports-color": ["supports-color@7.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, ""], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, ""], + + "text-table": ["text-table@0.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/text-table/-/text-table-0.2.0.tgz", {}, ""], + + "thenify": ["thenify@3.3.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/thenify/-/thenify-3.3.1.tgz", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/thenify-all/-/thenify-all-1.6.0.tgz", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinybench": ["tinybench@2.9.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/tinybench/-/tinybench-2.9.0.tgz", {}, ""], + + "tinyglobby": ["tinyglobby@0.2.15", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/tinyglobby/-/tinyglobby-0.2.15.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, ""], + + "tinypool": ["tinypool@0.8.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/tinypool/-/tinypool-0.8.4.tgz", {}, ""], + + "tinyspy": ["tinyspy@2.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/tinyspy/-/tinyspy-2.2.1.tgz", {}, ""], + + "to-regex-range": ["to-regex-range@5.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, ""], + + "ts-api-utils": ["ts-api-utils@1.4.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ts-api-utils/-/ts-api-utils-1.4.3.tgz", { "peerDependencies": { "typescript": ">=4.2.0" } }, ""], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, ""], + + "tslib": ["tslib@2.8.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/tslib/-/tslib-2.8.1.tgz", {}, ""], + + "type-check": ["type-check@0.4.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, ""], + + "type-detect": ["type-detect@4.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/type-detect/-/type-detect-4.1.0.tgz", {}, ""], + + "type-fest": ["type-fest@2.19.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/type-fest/-/type-fest-2.19.0.tgz", {}, ""], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, ""], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, ""], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, ""], + + "typed-array-length": ["typed-array-length@1.0.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/typed-array-length/-/typed-array-length-1.0.7.tgz", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, ""], + + "typescript": ["typescript@5.9.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""], + + "typescript-eslint": ["typescript-eslint@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/typescript-eslint/-/typescript-eslint-8.53.0.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, ""], + + "ufo": ["ufo@1.6.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ufo/-/ufo-1.6.3.tgz", {}, ""], + + "unbox-primitive": ["unbox-primitive@1.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/unbox-primitive/-/unbox-primitive-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, ""], + + "undici-types": ["undici-types@6.21.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/undici-types/-/undici-types-6.21.0.tgz", {}, ""], + + "uri-js": ["uri-js@4.4.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/uri-js/-/uri-js-4.4.1.tgz", { "dependencies": { "punycode": "^2.1.0" } }, ""], + + "util-deprecate": ["util-deprecate@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, ""], + + "uuid": ["uuid@13.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/uuid/-/uuid-13.0.0.tgz", { "bin": "dist-node/bin/uuid" }, ""], + + "vite": ["vite@5.4.21", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/vite/-/vite-5.4.21.tgz", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, ""], + + "vite-node": ["vite-node@1.6.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/vite-node/-/vite-node-1.6.1.tgz", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", "pathe": "^1.1.1", "picocolors": "^1.0.0", "vite": "^5.0.0" }, "bin": "vite-node.mjs" }, ""], + + "vitest": ["vitest@1.6.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/vitest/-/vitest-1.6.1.tgz", { "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", "@vitest/snapshot": "1.6.1", "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", "local-pkg": "^0.5.0", "magic-string": "^0.30.5", "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "1.6.1", "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": "vitest.mjs" }, ""], + + "wcwidth": ["wcwidth@1.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/wcwidth/-/wcwidth-1.0.1.tgz", { "dependencies": { "defaults": "^1.0.3" } }, ""], + + "which": ["which@2.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, ""], + + "which-builtin-type": ["which-builtin-type@1.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/which-builtin-type/-/which-builtin-type-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, ""], + + "which-collection": ["which-collection@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/which-collection/-/which-collection-1.0.2.tgz", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, ""], + + "which-typed-array": ["which-typed-array@1.1.20", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/which-typed-array/-/which-typed-array-1.1.20.tgz", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, ""], + + "why-is-node-running": ["why-is-node-running@2.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/why-is-node-running/-/why-is-node-running-2.3.0.tgz", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": "cli.js" }, ""], + + "widest-line": ["widest-line@4.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/widest-line/-/widest-line-4.0.1.tgz", { "dependencies": { "string-width": "^5.0.1" } }, ""], + + "word-wrap": ["word-wrap@1.2.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/word-wrap/-/word-wrap-1.2.5.tgz", {}, ""], + + "wrap-ansi": ["wrap-ansi@8.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/wrap-ansi/-/wrap-ansi-8.1.0.tgz", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, ""], + + "wrappy": ["wrappy@1.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/wrappy/-/wrappy-1.0.2.tgz", {}, ""], + + "ws": ["ws@8.19.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ws/-/ws-8.19.0.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, ""], + + "y18n": ["y18n@5.0.8", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@16.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/yargs/-/yargs-16.2.0.tgz", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + + "yargs-parser": ["yargs-parser@20.2.9", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/yargs-parser/-/yargs-parser-20.2.9.tgz", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + + "yocto-queue": ["yocto-queue@1.2.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/yocto-queue/-/yocto-queue-1.2.2.tgz", {}, ""], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", {}, ""], + + "yoga-layout": ["yoga-layout@3.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/yoga-layout/-/yoga-layout-3.2.1.tgz", {}, ""], + + "zod": ["zod@4.3.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/zod/-/zod-4.3.5.tgz", {}, ""], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", { "peerDependencies": { "zod": "^3.25 || ^4" } }, ""], + + "zustand": ["zustand@5.0.10", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/zustand/-/zustand-5.0.10.tgz", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["immer", "use-sync-external-store"] }, ""], + + "@alcalzone/ansi-tokenize/ansi-styles": ["ansi-styles@6.2.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, ""], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/globals/-/globals-14.0.0.tgz", {}, ""], + + "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/minimatch/-/minimatch-3.1.2.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""], + + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/types/-/types-8.53.0.tgz", {}, ""], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/minimatch/-/minimatch-9.0.3.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, ""], + + "cacheable-request/get-stream": ["get-stream@9.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/get-stream/-/get-stream-9.0.1.tgz", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, ""], + + "cacheable-request/keyv": ["keyv@5.5.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/keyv/-/keyv-5.5.5.tgz", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, ""], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, ""], + + "cli-highlight/chalk": ["chalk@4.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/wrap-ansi/-/wrap-ansi-7.0.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "eslint/@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, ""], + + "eslint/@eslint/js": ["@eslint/js@8.57.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@eslint/js/-/js-8.57.1.tgz", {}, ""], + + "eslint/chalk": ["chalk@4.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""], + + "eslint/espree": ["espree@9.6.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/espree/-/espree-9.6.1.tgz", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, ""], + + "eslint/globals": ["globals@13.24.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/globals/-/globals-13.24.0.tgz", { "dependencies": { "type-fest": "^0.20.2" } }, ""], + + "eslint/minimatch": ["minimatch@3.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/minimatch/-/minimatch-3.1.2.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, ""], + + "eslint-module-utils/debug": ["debug@3.2.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, ""], + + "eslint-plugin-import/debug": ["debug@3.2.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, ""], + + "eslint-plugin-import/doctrine": ["doctrine@2.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/doctrine/-/doctrine-2.1.0.tgz", { "dependencies": { "esutils": "^2.0.2" } }, ""], + + "eslint-plugin-import/minimatch": ["minimatch@3.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/minimatch/-/minimatch-3.1.2.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""], + + "eslint-plugin-import/semver": ["semver@6.3.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/semver/-/semver-6.3.1.tgz", { "bin": "bin/semver.js" }, ""], + + "eslint-plugin-n/globals": ["globals@13.24.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/globals/-/globals-13.24.0.tgz", { "dependencies": { "type-fest": "^0.20.2" } }, ""], + + "eslint-plugin-n/minimatch": ["minimatch@3.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/minimatch/-/minimatch-3.1.2.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""], + + "espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", {}, ""], + + "execa/signal-exit": ["signal-exit@4.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/signal-exit/-/signal-exit-4.1.0.tgz", {}, ""], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, ""], + + "fdir/picomatch": ["picomatch@4.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/picomatch/-/picomatch-4.0.3.tgz", {}, ""], + + "flat-cache/keyv": ["keyv@4.5.4", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, ""], + + "glob/minimatch": ["minimatch@3.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/minimatch/-/minimatch-3.1.2.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""], + + "got/type-fest": ["type-fest@4.41.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/type-fest/-/type-fest-4.41.0.tgz", {}, ""], + + "ink/type-fest": ["type-fest@4.41.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/type-fest/-/type-fest-4.41.0.tgz", {}, ""], + + "ink/widest-line": ["widest-line@5.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/widest-line/-/widest-line-5.0.0.tgz", { "dependencies": { "string-width": "^7.0.0" } }, ""], + + "ink/wrap-ansi": ["wrap-ansi@9.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, ""], + + "inquirer/ansi-escapes": ["ansi-escapes@4.3.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-escapes/-/ansi-escapes-4.3.2.tgz", { "dependencies": { "type-fest": "^0.21.3" } }, ""], + + "inquirer/ora": ["ora@5.4.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ora/-/ora-5.4.1.tgz", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, ""], + + "inquirer/wrap-ansi": ["wrap-ansi@6.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/wrap-ansi/-/wrap-ansi-6.2.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, ""], + + "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", {}, ""], + + "micromatch/picomatch": ["picomatch@2.3.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/picomatch/-/picomatch-2.3.1.tgz", {}, ""], + + "mlly/pathe": ["pathe@2.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/pathe/-/pathe-2.0.3.tgz", {}, ""], + + "npm-run-path/path-key": ["path-key@4.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/path-key/-/path-key-4.0.0.tgz", {}, ""], + + "ora/cli-cursor": ["cli-cursor@5.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cli-cursor/-/cli-cursor-5.0.0.tgz", { "dependencies": { "restore-cursor": "^5.0.0" } }, ""], + + "p-locate/p-limit": ["p-limit@3.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/p-limit/-/p-limit-3.1.0.tgz", { "dependencies": { "yocto-queue": "^0.1.0" } }, ""], + + "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/parse5/-/parse5-6.0.1.tgz", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], + + "pkg-types/pathe": ["pathe@2.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/pathe/-/pathe-2.0.3.tgz", {}, ""], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-styles/-/ansi-styles-5.2.0.tgz", {}, ""], + + "react-devtools-core/ws": ["ws@7.5.10", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ws/-/ws-7.5.10.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, ""], + + "readdirp/picomatch": ["picomatch@2.3.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/picomatch/-/picomatch-2.3.1.tgz", {}, ""], + + "restore-cursor/onetime": ["onetime@5.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/onetime/-/onetime-5.1.2.tgz", { "dependencies": { "mimic-fn": "^2.1.0" } }, ""], + + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, ""], + + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, ""], + + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", {}, ""], + + "tinyglobby/picomatch": ["picomatch@4.0.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/picomatch/-/picomatch-4.0.3.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, ""], + + "typescript-eslint/@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/parser/-/parser-8.53.0.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, ""], + + "typescript-eslint/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.53.0", "@typescript-eslint/tsconfig-utils": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, ""], + + "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/utils/-/utils-8.53.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, ""], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, ""], + + "@humanwhocodes/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/brace-expansion/-/brace-expansion-1.1.12.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/brace-expansion/-/brace-expansion-2.0.2.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, ""], + + "cacheable-request/get-stream/is-stream": ["is-stream@4.0.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-stream/-/is-stream-4.0.1.tgz", {}, ""], + + "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.12", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/brace-expansion/-/brace-expansion-1.1.12.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""], + + "eslint-plugin-n/globals/type-fest": ["type-fest@0.20.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/type-fest/-/type-fest-0.20.2.tgz", {}, ""], + + "eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@1.1.12", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/brace-expansion/-/brace-expansion-1.1.12.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""], + + "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, ""], + + "eslint/globals/type-fest": ["type-fest@0.20.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/type-fest/-/type-fest-0.20.2.tgz", {}, ""], + + "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/brace-expansion/-/brace-expansion-1.1.12.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""], + + "glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/brace-expansion/-/brace-expansion-1.1.12.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""], + + "inquirer/ansi-escapes/type-fest": ["type-fest@0.21.3", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/type-fest/-/type-fest-0.21.3.tgz", {}, ""], + + "inquirer/ora/chalk": ["chalk@4.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""], + + "inquirer/ora/cli-cursor": ["cli-cursor@3.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/cli-cursor/-/cli-cursor-3.1.0.tgz", { "dependencies": { "restore-cursor": "^3.1.0" } }, ""], + + "inquirer/ora/is-interactive": ["is-interactive@1.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-interactive/-/is-interactive-1.0.0.tgz", {}, ""], + + "inquirer/ora/is-unicode-supported": ["is-unicode-supported@0.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", {}, ""], + + "inquirer/ora/log-symbols": ["log-symbols@4.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/log-symbols/-/log-symbols-4.1.0.tgz", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, ""], + + "inquirer/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, ""], + + "ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/restore-cursor/-/restore-cursor-5.1.0.tgz", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, ""], + + "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, ""], + + "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/mimic-fn/-/mimic-fn-2.1.0.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, ""], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, ""], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, ""], + + "typescript-eslint/@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ignore/-/ignore-7.0.5.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/eslint-plugin/ts-api-utils": ["ts-api-utils@2.4.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ts-api-utils/-/ts-api-utils-2.4.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, ""], + + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, ""], + + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/types/-/types-8.53.0.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, ""], + + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/types/-/types-8.53.0.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, ""], + + "typescript-eslint/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/minimatch/-/minimatch-9.0.5.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, ""], + + "typescript-eslint/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.4.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ts-api-utils/-/ts-api-utils-2.4.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, ""], + + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, ""], + + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/types/-/types-8.53.0.tgz", {}, ""], + + "inquirer/ora/chalk/ansi-styles": ["ansi-styles@4.3.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, ""], + + "inquirer/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/restore-cursor/-/restore-cursor-3.1.0.tgz", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, ""], + + "ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/onetime/-/onetime-7.0.0.tgz", { "dependencies": { "mimic-function": "^5.0.0" } }, ""], + + "ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/signal-exit/-/signal-exit-4.1.0.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/types/-/types-8.53.0.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/types/-/types-8.53.0.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/types/-/types-8.53.0.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/brace-expansion/-/brace-expansion-2.0.2.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, ""], + + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, ""], + + "inquirer/ora/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/onetime/-/onetime-5.1.2.tgz", { "dependencies": { "mimic-fn": "^2.1.0" } }, ""], + + "inquirer/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/signal-exit/-/signal-exit-3.0.7.tgz", {}, ""], + + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", {}, ""], + + "inquirer/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "https://artifact.it.att.com/artifactory/api/npm/apm0039030-npm-cwdigital-group/mimic-fn/-/mimic-fn-2.1.0.tgz", {}, ""], + } +} diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..abfb453 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,6 @@ +# Bun configuration for @opentui/solid JSX transformation +preload = ["@opentui/solid/preload"] + +[run] +# Enable browser export conditions for @opentui/solid +conditions = ["browser"] diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..4d81f19 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,132 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- **Home Screen**: New welcome screen with centered gradient logo + - Displays version, provider, and model info + - Transitions to session view on first message + - Clean, centered layout + +- **MCP Integration**: Model Context Protocol server support + - Connect to external MCP servers + - Use tools from connected servers + - `/mcp` command for server management + - Status display in UI + +- **Reasoning System**: Advanced agent orchestration + - Memory selection for context optimization + - Quality evaluation of responses + - Termination detection for agent loops + - Context compression for long conversations + - Retry policies with exponential backoff + +- **Todo Panel**: Task tracking during sessions + - Toggle visibility with `Ctrl+T` + - `todo-read` and `todo-write` tools + - Zustand-based state management + +- **Theme System**: Customizable color themes + - `/theme` command to switch themes + - Dark, Light, Tokyo Night, Dracula themes + - Persistent theme preference + +- **Agent Selection**: Switch between agent modes + - `/agent` command for selection + - Coder, Architect, Reviewer agents + +- **Learning System**: Knowledge persistence + - Vector store for embeddings + - Semantic search capabilities + - Project learnings storage + +- **Streaming Responses**: Real-time message display + - Faster feedback from LLM + - Progress indicators + +- **Enhanced Navigation**: + - `PageUp/PageDown` for fast scrolling + - `Shift+Up/Down` for line-by-line scroll + - `Ctrl+Home/End` to jump to top/bottom + +- **Optimized Permissions**: Performance improvements + - Pattern caching + - Indexed pattern matching + - Faster permission checks + +- **Auto-Compaction**: Context management + - Automatic conversation compression + - Maintains context within limits + +### Changed + +- Improved session header with token count and context percentage +- Enhanced status bar with MCP connection info +- Better command menu with more commands + +## [0.1.0] - 2025-01-16 + +### Added + +- **Interactive TUI**: Full terminal UI using Ink (React for CLIs) + - Message-based input (Enter to send, Alt+Enter for newlines) + - Log panel showing conversation history + - Status bar with session info + - ASCII banner header + +- **Permission System**: Granular control over tool execution + - Interactive permission modal with keyboard navigation + - Scoped permissions: once, session, project, global + - Pattern-based matching: `Bash(command:args)`, `Read(*)`, `Write(path)`, `Edit(*.ext)` + - Persistent storage in `~/.codetyper/settings.json` and `.codetyper/settings.json` + +- **Agent System**: Autonomous task execution + - Multi-turn conversation with tool calls + - Automatic retry with exponential backoff for rate limits + - Configurable max iterations + +- **Tools**: + - `bash` - Execute shell commands + - `read` - Read file contents + - `write` - Create or overwrite files + - `edit` - Find and replace in files + +- **Provider Support**: + - GitHub Copilot (default) - OAuth device flow authentication, access to GPT-4o, GPT-5, Claude, Gemini via Copilot API + - Ollama - Local server (no auth), run any local model + +- **Session Management**: + - Persistent session storage + - Continue previous sessions with `--continue` + - Resume specific sessions with `--resume ` + +- **CLI Commands**: + - `codetyper` - Start interactive TUI + - `codetyper ` - Start with initial prompt + - `codetyper login ` - Authenticate with provider + - `codetyper status` - Show provider status + - `codetyper config` - Manage configuration + +### Changed + +- Migrated from readline-based input to Ink TUI +- Removed classic mode in favor of TUI-only interface +- Tool output now captured and displayed in log panel (not streamed to stdout) + +### Fixed + +- Permission modal not showing in TUI mode +- Input area blocking during command execution +- Rate limit handling for Copilot provider (429 errors) + +--- + +## Version History + +- **0.1.0** - Initial release with TUI, agent system, and multi-provider support diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..a016fe1 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,202 @@ +# Contributing to CodeTyper CLI + +Thank you for your interest in contributing to CodeTyper CLI! This document provides guidelines and instructions for contributing. + +## Code of Conduct + +Please be respectful and constructive in all interactions. We welcome contributors of all experience levels. + +## Getting Started + +### Prerequisites + +- Node.js >= 18.0.0 +- npm or yarn +- Git + +### Setup + +1. Fork the repository +2. Clone your fork: + ```bash + git clone https://github.com/YOUR_USERNAME/codetyper-cli.git + cd codetyper-cli + ``` +3. Install dependencies: + ```bash + npm install + ``` +4. Build the project: + ```bash + npm run build + ``` +5. Link for local testing: + ```bash + npm link + ``` + +### Development Workflow + +```bash +# Start TypeScript watch mode +npm run dev + +# Run tests +npm test + +# Lint code +npm run lint + +# Format code +npm run format +``` + +## How to Contribute + +### Reporting Bugs + +1. Check existing issues to avoid duplicates +2. Create a new issue with: + - Clear, descriptive title + - Steps to reproduce + - Expected vs actual behavior + - Environment details (OS, Node version, provider) + - Relevant logs or screenshots + +### Suggesting Features + +1. Check existing issues for similar requests +2. Create a feature request with: + - Clear description of the feature + - Use cases and motivation + - Proposed implementation (optional) + +### Submitting Pull Requests + +1. Create a branch from `main`: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. Make your changes following our coding standards + +3. Write/update tests if applicable + +4. Ensure all tests pass: + ```bash + npm test + ``` + +5. Commit with clear messages: + ```bash + git commit -m "feat: add new feature description" + ``` + +6. Push and create a Pull Request + +### Commit Message Format + +We follow [Conventional Commits](https://www.conventionalcommits.org/): + +- `feat:` - New feature +- `fix:` - Bug fix +- `docs:` - Documentation changes +- `style:` - Code style changes (formatting, etc.) +- `refactor:` - Code refactoring +- `test:` - Adding or updating tests +- `chore:` - Maintenance tasks + +Examples: +``` +feat: add permission caching for faster lookups +fix: resolve race condition in agent loop +docs: update README with new CLI options +``` + +## Coding Standards + +### TypeScript + +- Use TypeScript strict mode +- Define explicit types (avoid `any` when possible) +- Use interfaces for object shapes +- Export types that are part of the public API + +### Code Style + +- Use 2 spaces for indentation +- Use single quotes for strings +- Add trailing commas in multi-line structures +- Keep lines under 100 characters when reasonable + +### File Organization + +``` +src/ +├── index.ts # Entry point only +├── commands/ # CLI command implementations +├── providers/ # LLM provider integrations +├── tools/ # Agent tools (bash, read, write, edit) +├── tui/ # Terminal UI components +│ └── components/ # Reusable UI components +└── types.ts # Shared type definitions +``` + +### Testing + +- Write tests for non-UI logic +- Place tests in `tests/` directory +- Name test files `*.test.ts` +- Use descriptive test names + +```typescript +describe('PermissionManager', () => { + it('should match wildcard patterns correctly', () => { + // ... + }); +}); +``` + +### Documentation + +- Add JSDoc comments for public APIs +- Update README for user-facing changes +- Update CHANGELOG for notable changes + +## Project Structure + +### Key Files + +| File | Purpose | +|------|---------| +| `src/index.ts` | CLI entry point, command registration | +| `src/agent.ts` | Agent loop, tool orchestration | +| `src/permissions.ts` | Permission system | +| `src/commands/chat-tui.tsx` | Main TUI command | +| `src/tui/App.tsx` | Root TUI component | +| `src/tui/store.ts` | Zustand state management | + +### Adding a New Provider + +1. Create `src/providers/yourprovider.ts` +2. Implement the `Provider` interface +3. Register in `src/providers/index.ts` +4. Add authentication in `src/commands/login.ts` +5. Update documentation + +### Adding a New Tool + +1. Create `src/tools/yourtool.ts` +2. Define parameters with Zod schema +3. Implement `execute` function +4. Register in `src/tools/index.ts` +5. Add permission handling if needed + +## Questions? + +- Open a GitHub issue for questions +- Tag with `question` label + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..e876c4d --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,16 @@ +import globals from "globals"; +import tseslint from "typescript-eslint"; +import pluginJs from "@eslint/js"; + +export default tseslint.config( + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + }, +); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bdd98cd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9224 @@ +{ + "name": "codetyper-cli", + "version": "0.1.74", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codetyper-cli", + "version": "0.1.74", + "license": "MIT", + "dependencies": { + "@opentui/core": "^0.1.74", + "@opentui/solid": "^0.1.74", + "@solid-primitives/event-bus": "^1.0.11", + "@solid-primitives/scheduled": "^1.4.3", + "boxen": "^8.0.1", + "chalk": "^5.3.0", + "chokidar": "^5.0.0", + "cli-highlight": "^2.1.11", + "commander": "^14.0.2", + "fast-glob": "^3.3.2", + "got": "^14.0.0", + "inquirer": "^13.2.1", + "mimic-function": "^5.0.1", + "opentui-spinner": "^0.0.6", + "ora": "^9.1.0", + "solid-js": "^1.9.10", + "uuid": "^13.0.0", + "zod": "^4.3.5", + "zod-to-json-schema": "^3.25.1", + "zustand": "^5.0.10" + }, + "bin": { + "codetyper": "dist/index.js" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", + "@types/inquirer": "^9.0.7", + "@types/node": "^25.0.10", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^8.53.1", + "@typescript-eslint/parser": "^8.53.1", + "eslint": "^9.39.2", + "eslint-config-standard": "^17.1.0", + "eslint-config-standard-with-typescript": "^43.0.1", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-n": "^17.23.2", + "eslint-plugin-promise": "^7.2.1", + "globals": "^17.0.0", + "prettier": "^3.1.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.0", + "vitest": "^4.0.17" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dimforge/rapier2d-simd-compat": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@dimforge/rapier2d-simd-compat/-/rapier2d-simd-compat-0.17.3.tgz", + "integrity": "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.3.tgz", + "integrity": "sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.0.4.tgz", + "integrity": "sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.1", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.4.tgz", + "integrity": "sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.1.tgz", + "integrity": "sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3", + "cli-width": "^4.1.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^9.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.4.tgz", + "integrity": "sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/external-editor": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.4.tgz", + "integrity": "sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.3.tgz", + "integrity": "sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.3.tgz", + "integrity": "sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.4.tgz", + "integrity": "sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.4.tgz", + "integrity": "sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.4.tgz", + "integrity": "sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.2.0.tgz", + "integrity": "sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.0.4", + "@inquirer/confirm": "^6.0.4", + "@inquirer/editor": "^5.0.4", + "@inquirer/expand": "^5.0.4", + "@inquirer/input": "^5.0.4", + "@inquirer/number": "^4.0.4", + "@inquirer/password": "^5.0.4", + "@inquirer/rawlist": "^5.2.0", + "@inquirer/search": "^4.1.0", + "@inquirer/select": "^5.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.0.tgz", + "integrity": "sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.0.tgz", + "integrity": "sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.0.4.tgz", + "integrity": "sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.1", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.3.tgz", + "integrity": "sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jimp/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.0.tgz", + "integrity": "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w==", + "license": "MIT", + "dependencies": { + "@jimp/file-ops": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "await-to-js": "^3.0.0", + "exif-parser": "^0.1.12", + "file-type": "^16.0.0", + "mime": "3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/diff": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.0.tgz", + "integrity": "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw==", + "license": "MIT", + "dependencies": { + "@jimp/plugin-resize": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "pixelmatch": "^5.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/file-ops": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.0.tgz", + "integrity": "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-bmp": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.0.tgz", + "integrity": "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "bmp-ts": "^1.0.9" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-gif": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.0.tgz", + "integrity": "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "gifwrap": "^0.10.1", + "omggif": "^1.0.10" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-jpeg": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.0.tgz", + "integrity": "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "jpeg-js": "^0.4.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-png": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.0.tgz", + "integrity": "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "pngjs": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-tiff": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.0.tgz", + "integrity": "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "utif2": "^4.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.0.tgz", + "integrity": "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blit/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.0.tgz", + "integrity": "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/utils": "1.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.0.tgz", + "integrity": "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-circle/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.0.tgz", + "integrity": "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "tinycolor2": "^1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-color/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.0.tgz", + "integrity": "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/plugin-blit": "1.6.0", + "@jimp/plugin-resize": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-contain/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.0.tgz", + "integrity": "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/plugin-crop": "1.6.0", + "@jimp/plugin-resize": "1.6.0", + "@jimp/types": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-cover/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.0.tgz", + "integrity": "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-crop/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.0.tgz", + "integrity": "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-displace/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.0.tgz", + "integrity": "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.0.tgz", + "integrity": "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.0.tgz", + "integrity": "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-flip/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-hash": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.0.tgz", + "integrity": "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/js-bmp": "1.6.0", + "@jimp/js-jpeg": "1.6.0", + "@jimp/js-png": "1.6.0", + "@jimp/js-tiff": "1.6.0", + "@jimp/plugin-color": "1.6.0", + "@jimp/plugin-resize": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "any-base": "^1.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.0.tgz", + "integrity": "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.0.tgz", + "integrity": "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/js-jpeg": "1.6.0", + "@jimp/js-png": "1.6.0", + "@jimp/plugin-blit": "1.6.0", + "@jimp/types": "1.6.0", + "parse-bmfont-ascii": "^1.0.6", + "parse-bmfont-binary": "^1.0.6", + "parse-bmfont-xml": "^1.1.6", + "simple-xml-to-json": "^1.2.2", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-print/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-quantize": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.0.tgz", + "integrity": "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg==", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-quantize/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.0.tgz", + "integrity": "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-resize/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.0.tgz", + "integrity": "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/plugin-crop": "1.6.0", + "@jimp/plugin-resize": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-rotate/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.0.tgz", + "integrity": "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/plugin-color": "1.6.0", + "@jimp/plugin-hash": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-threshold/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/types": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.0.tgz", + "integrity": "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg==", + "license": "MIT", + "dependencies": { + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/types/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/utils": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentui/core": { + "version": "0.1.75", + "resolved": "https://registry.npmjs.org/@opentui/core/-/core-0.1.75.tgz", + "integrity": "sha512-8ARRZxSG+BXkJmEVtM2DQ4se7DAF1ZCKD07d+AklgTr2mxCzmdxxPbOwRzboSQ6FM7qGuTVPVbV4O2W9DpUmoA==", + "license": "MIT", + "dependencies": { + "bun-ffi-structs": "0.1.2", + "diff": "8.0.2", + "jimp": "1.6.0", + "marked": "17.0.1", + "yoga-layout": "3.2.1" + }, + "optionalDependencies": { + "@dimforge/rapier2d-simd-compat": "^0.17.3", + "@opentui/core-darwin-arm64": "0.1.75", + "@opentui/core-darwin-x64": "0.1.75", + "@opentui/core-linux-arm64": "0.1.75", + "@opentui/core-linux-x64": "0.1.75", + "@opentui/core-win32-arm64": "0.1.75", + "@opentui/core-win32-x64": "0.1.75", + "bun-webgpu": "0.1.4", + "planck": "^1.4.2", + "three": "0.177.0" + }, + "peerDependencies": { + "web-tree-sitter": "0.25.10" + } + }, + "node_modules/@opentui/core-darwin-arm64": { + "version": "0.1.75", + "resolved": "https://registry.npmjs.org/@opentui/core-darwin-arm64/-/core-darwin-arm64-0.1.75.tgz", + "integrity": "sha512-gGaGZjkFpqcXJk6321JzhRl66pM2VxBlI470L8W4DQUW4S6iDT1R9L7awSzGB4Cn9toUl7DTV8BemaXZYXV4SA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@opentui/core-darwin-x64": { + "version": "0.1.75", + "resolved": "https://registry.npmjs.org/@opentui/core-darwin-x64/-/core-darwin-x64-0.1.75.tgz", + "integrity": "sha512-tPlvqQI0whZ76amHydpJs5kN+QeWAIcFbI8RAtlAo9baj2EbxTDC+JGwgb9Fnt0/YQx831humbtaNDhV2Jt1bw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@opentui/core-linux-arm64": { + "version": "0.1.75", + "resolved": "https://registry.npmjs.org/@opentui/core-linux-arm64/-/core-linux-arm64-0.1.75.tgz", + "integrity": "sha512-nVxIQ4Hqf84uBergDpWiVzU6pzpjy6tqBHRQpySxZ2flkJ/U6/aMEizVrQ1jcgIdxZtvqWDETZhzxhG0yDx+cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@opentui/core-linux-x64": { + "version": "0.1.75", + "resolved": "https://registry.npmjs.org/@opentui/core-linux-x64/-/core-linux-x64-0.1.75.tgz", + "integrity": "sha512-1CnApef4kxA+ORyLfbuCLgZfEjp4wr3HjFnt7FAfOb73kIZH82cb7JYixeqRyy9eOcKfKqxLmBYy3o8IDkc4Rg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@opentui/core-win32-arm64": { + "version": "0.1.75", + "resolved": "https://registry.npmjs.org/@opentui/core-win32-arm64/-/core-win32-arm64-0.1.75.tgz", + "integrity": "sha512-j0UB95nmkYGNzmOrs6GqaddO1S90R0YC6IhbKnbKBdjchFPNVLz9JpexAs6MBDXPZwdKAywMxtwG2h3aTJtxng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opentui/core-win32-x64": { + "version": "0.1.75", + "resolved": "https://registry.npmjs.org/@opentui/core-win32-x64/-/core-win32-x64-0.1.75.tgz", + "integrity": "sha512-ESpVZVGewe3JkB2TwrG3VRbkxT909iPdtvgNT7xTCIYH2VB4jqZomJfvERPTE0tvqAZJm19mHECzJFI8asSJgQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opentui/solid": { + "version": "0.1.75", + "resolved": "https://registry.npmjs.org/@opentui/solid/-/solid-0.1.75.tgz", + "integrity": "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg==", + "license": "MIT", + "dependencies": { + "@babel/core": "7.28.0", + "@babel/preset-typescript": "7.27.1", + "@opentui/core": "0.1.75", + "babel-plugin-module-resolver": "5.0.2", + "babel-preset-solid": "1.9.9", + "s-js": "^0.4.9" + }, + "peerDependencies": { + "solid-js": "1.9.9" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@solid-primitives/event-bus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@solid-primitives/event-bus/-/event-bus-1.1.2.tgz", + "integrity": "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA==", + "license": "MIT", + "dependencies": { + "@solid-primitives/utils": "^6.3.2" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/scheduled": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@solid-primitives/scheduled/-/scheduled-1.5.2.tgz", + "integrity": "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA==", + "license": "MIT", + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/utils": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.3.2.tgz", + "integrity": "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==", + "license": "MIT", + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" + }, + "node_modules/@types/inquirer": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.9.tgz", + "integrity": "sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", + "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "license": "MIT" + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions": { + "version": "0.40.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.3.tgz", + "integrity": "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "7.18.6", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.20.7", + "html-entities": "2.3.3", + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.20.12" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/babel-plugin-module-resolver": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.2.tgz", + "integrity": "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==", + "license": "MIT", + "dependencies": { + "find-babel-config": "^2.1.1", + "glob": "^9.3.3", + "pkg-up": "^3.1.0", + "reselect": "^4.1.7", + "resolve": "^1.22.8" + } + }, + "node_modules/babel-preset-solid": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.9.tgz", + "integrity": "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw==", + "license": "MIT", + "dependencies": { + "babel-plugin-jsx-dom-expressions": "^0.40.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "solid-js": "^1.9.8" + }, + "peerDependenciesMeta": { + "solid-js": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bmp-ts": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz", + "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", + "license": "MIT" + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bun-ffi-structs": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/bun-ffi-structs/-/bun-ffi-structs-0.1.2.tgz", + "integrity": "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w==", + "license": "MIT", + "peerDependencies": { + "typescript": "^5" + } + }, + "node_modules/bun-webgpu": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/bun-webgpu/-/bun-webgpu-0.1.4.tgz", + "integrity": "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@webgpu/types": "^0.1.60" + }, + "optionalDependencies": { + "bun-webgpu-darwin-arm64": "^0.1.4", + "bun-webgpu-darwin-x64": "^0.1.4", + "bun-webgpu-linux-x64": "^0.1.4", + "bun-webgpu-win32-x64": "^0.1.4" + } + }, + "node_modules/bun-webgpu-darwin-arm64": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/bun-webgpu-darwin-arm64/-/bun-webgpu-darwin-arm64-0.1.4.tgz", + "integrity": "sha512-eDgLN9teKTfmvrCqgwwmWNsNszxYs7IZdCqk0S1DCarvMhr4wcajoSBlA/nQA0/owwLduPTS8xxCnQp4/N/gDg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/bun-webgpu-darwin-x64": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/bun-webgpu-darwin-x64/-/bun-webgpu-darwin-x64-0.1.4.tgz", + "integrity": "sha512-X+PjwJUWenUmdQBP8EtdItMyieQ6Nlpn+BH518oaouDiSnWj5+b0Y7DNDZJq7Ezom4EaxmqL/uGYZK3aCQ7CXg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/bun-webgpu-linux-x64": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/bun-webgpu-linux-x64/-/bun-webgpu-linux-x64-0.1.4.tgz", + "integrity": "sha512-zMLs2YIGB+/jxrYFXaFhVKX/GBt05UTF45lc9srcHc9JXGjEj+12CIo1CHLTAWatXMTqt0Jsu6ukWEoWVT/ayA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/bun-webgpu-win32-x64": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/bun-webgpu-win32-x64/-/bun-webgpu-win32-x64-0.1.4.tgz", + "integrity": "sha512-Z5yAK28xrcm8Wb5k7TZ8FJKpOI/r+aVCRdlHYAqI2SDJFN3nD4mJs900X6kNVmG/xFzb5yOuKVYWGg+6ZXWbyA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "13.0.18", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.18.tgz", + "integrity": "sha512-rFWadDRKJs3s2eYdXlGggnBZKG7MTblkFBB0YllFds+UYnfogDp2wcR6JN97FhRkHTvq59n2vhNoHNZn29dh/Q==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.5.5", + "mimic-response": "^4.0.0", + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacheable-request/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "license": "MIT" + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", + "license": "MIT", + "dependencies": { + "mimic-response": "^4.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.279", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", + "integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-compat-utils/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-config-standard": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", + "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0" + } + }, + "node_modules/eslint-config-standard-with-typescript": { + "version": "43.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-43.0.1.tgz", + "integrity": "sha512-WfZ986+qzIzX6dcr4yGUyVb/l9N3Z8wPXCc5z/70fljs3UbWhhV+WxrfgsqMToRzuuyX9MqZ974pq2UPhDTOcA==", + "deprecated": "Please use eslint-config-love, instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/parser": "^6.4.0", + "eslint-config-standard": "17.1.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.4.0", + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0", + "typescript": "*" + } + }, + "node_modules/eslint-config-standard-with-typescript/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-standard-with-typescript/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-standard-with-typescript/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-standard-with-typescript/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-standard-with-typescript/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-standard-with-typescript/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-config-standard-with-typescript/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-config-standard-with-typescript/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-config-standard-with-typescript/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-n": { + "version": "17.23.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.23.2.tgz", + "integrity": "sha512-RhWBeb7YVPmNa2eggvJooiuehdL76/bbfj/OJewyoGT80qn5PXdz8zMOTO6YHOsI7byPt7+Ighh/i/4a5/v7hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.5.0", + "enhanced-resolve": "^5.17.1", + "eslint-plugin-es-x": "^7.8.0", + "get-tsconfig": "^4.8.1", + "globals": "^15.11.0", + "globrex": "^0.1.2", + "ignore": "^5.3.2", + "semver": "^7.6.3", + "ts-declaration-location": "^1.0.6" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": ">=8.23.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz", + "integrity": "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-babel-config": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-2.1.2.tgz", + "integrity": "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.3" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, + "node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.2.0.tgz", + "integrity": "sha512-tovnCz/fEq+Ripoq+p/gN1u7l6A7wwkoBT9pRCzTHzsD/LvADIzXZdjmRymh5Ztf0DYC3Rwg5cZRYjxzBmzbWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz", + "integrity": "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^7.0.1", + "byte-counter": "^0.1.0", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^13.0.12", + "decompress-response": "^10.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "keyv": "^5.5.3", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inquirer": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.2.1.tgz", + "integrity": "sha512-kjIN+joqgbSncQJ6GfN7gV9AbDQlMA+hJ96xcwkQUwP9KN/ZIusoJ2mAfdt0LPrZJQsEyk5i/YrgJQTxSgzlPw==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.1", + "@inquirer/prompts": "^8.2.0", + "@inquirer/type": "^4.0.3", + "mute-stream": "^3.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jimp": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.0.tgz", + "integrity": "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/diff": "1.6.0", + "@jimp/js-bmp": "1.6.0", + "@jimp/js-gif": "1.6.0", + "@jimp/js-jpeg": "1.6.0", + "@jimp/js-png": "1.6.0", + "@jimp/js-tiff": "1.6.0", + "@jimp/plugin-blit": "1.6.0", + "@jimp/plugin-blur": "1.6.0", + "@jimp/plugin-circle": "1.6.0", + "@jimp/plugin-color": "1.6.0", + "@jimp/plugin-contain": "1.6.0", + "@jimp/plugin-cover": "1.6.0", + "@jimp/plugin-crop": "1.6.0", + "@jimp/plugin-displace": "1.6.0", + "@jimp/plugin-dither": "1.6.0", + "@jimp/plugin-fisheye": "1.6.0", + "@jimp/plugin-flip": "1.6.0", + "@jimp/plugin-hash": "1.6.0", + "@jimp/plugin-mask": "1.6.0", + "@jimp/plugin-print": "1.6.0", + "@jimp/plugin-quantize": "1.6.0", + "@jimp/plugin-resize": "1.6.0", + "@jimp/plugin-rotate": "1.6.0", + "@jimp/plugin-threshold": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", + "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "license": "MIT" + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opentui-spinner": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/opentui-spinner/-/opentui-spinner-0.0.6.tgz", + "integrity": "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA==", + "license": "MIT", + "dependencies": { + "cli-spinners": "^3.3.0" + }, + "peerDependencies": { + "@opentui/core": "^0.1.49", + "@opentui/react": "^0.1.49", + "@opentui/solid": "^0.1.49", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "@opentui/react": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.1.0.tgz", + "integrity": "sha512-53uuLsXHOAJl5zLrUrzY9/kE+uIFEx7iaH4g2BIJQK4LZjY4LpCCYZVKDWIkL+F01wAaCg93duQ1whnK/AmY1A==", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.2.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pixelmatch": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", + "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "license": "ISC", + "dependencies": { + "pngjs": "^6.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/planck": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/planck/-/planck-1.4.2.tgz", + "integrity": "sha512-mNbhnV3g8X2rwGxzcesjmN8BDA6qfXgQxXVMkWau9MCRlQY0RLNEkyHlVp6yFy/X6qrzAXyNONCnZ1cGDLrNew==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0" + }, + "peerDependencies": { + "stage-js": "^1.0.0-alpha.12" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/responselike": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/s-js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/s-js/-/s-js-0.4.9.tgz", + "integrity": "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ==", + "license": "MIT" + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz", + "integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.0.tgz", + "integrity": "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-xml-to-json": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.3.tgz", + "integrity": "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA==", + "license": "MIT", + "engines": { + "node": ">=20.12.2" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/solid-js": { + "version": "1.9.11", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz", + "integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.5.0", + "seroval-plugins": "~1.5.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/three": { + "version": "0.177.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.177.0.tgz", + "integrity": "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==", + "license": "MIT", + "optional": true + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-declaration-location": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", + "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", + "dev": true, + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/ts-declaration-location" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": ">=4.0.0" + } + }, + "node_modules/ts-declaration-location/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.11" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, + "node_modules/zustand": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz", + "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2313d4c --- /dev/null +++ b/package.json @@ -0,0 +1,100 @@ +{ + "name": "codetyper-cli", + "version": "0.1.74", + "description": "CodeTyper AI Agent - Standalone CLI for autonomous code generation", + "main": "dist/index.js", + "bin": { + "codetyper": "./dist/index.js" + }, + "type": "module", + "scripts": { + "dev": "bun src/index.ts", + "dev:nobump": "bun scripts/build.ts && npm link", + "dev:watch": "bun scripts/dev-watch.ts", + "build": "bun scripts/build.ts", + "sync-version": "bun scripts/sync-version.ts", + "start": "bun src/index.ts", + "test": "bun test", + "lint": "bun eslint src/**/*.ts", + "format": "npx prettier --write \"src/**/*.ts\"", + "prettier": "npx prettier --write \"src/**/*.ts\" \"src/**/*.tsx\" \"scripts/**/*.ts\"", + "typecheck": "bun tsc --noEmit" + }, + "keywords": [ + "ai", + "coding", + "assistant", + "cli", + "agent", + "typescript" + ], + "author": { + "name": "Carlos Gutierrez", + "email": "carlos.gutierrez@carg.dev", + "url": "https://github.com/CarGDev" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/CarGDev/codetyper.nvim.git" + }, + "homepage": "https://github.com/CarGDev/codetyper.nvim#readme", + "bugs": { + "url": "https://github.com/CarGDev/codetyper.nvim/issues" + }, + "files": [ + "dist/**/*.js", + "dist/**/*.wasm", + "dist/**/*.scm", + "src/version.json" + ], + "dependencies": { + "@opentui/core": "^0.1.74", + "@opentui/solid": "^0.1.74", + "@solid-primitives/event-bus": "^1.0.11", + "@solid-primitives/scheduled": "^1.4.3", + "boxen": "^8.0.1", + "chalk": "^5.3.0", + "chokidar": "^5.0.0", + "cli-highlight": "^2.1.11", + "commander": "^14.0.2", + "fast-glob": "^3.3.2", + "got": "^14.0.0", + "inquirer": "^13.2.1", + "mimic-function": "^5.0.1", + "opentui-spinner": "^0.0.6", + "ora": "^9.1.0", + "solid-js": "^1.9.10", + "uuid": "^13.0.0", + "zustand": "^5.0.10", + "zod": "^4.3.5", + "zod-to-json-schema": "^3.25.1" + }, + "overrides": { + "string-width": "^5.1.2", + "strip-ansi": "^6.0.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", + "@types/inquirer": "^9.0.7", + "@types/node": "^25.0.10", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^8.53.1", + "@typescript-eslint/parser": "^8.53.1", + "eslint": "^9.39.2", + "eslint-config-standard": "^17.1.0", + "eslint-config-standard-with-typescript": "^43.0.1", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-n": "^17.23.2", + "eslint-plugin-promise": "^7.2.1", + "globals": "^17.0.0", + "prettier": "^3.1.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.0", + "vitest": "^4.0.17" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..ae7c11a --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,62 @@ +#!/usr/bin/env bun +/** + * Build script for codetyper-cli + * + * Uses the @opentui/solid plugin for JSX transformation during bundling. + */ + +import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { readFile, writeFile } from "fs/promises"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const ROOT_DIR = join(__dirname, ".."); + +process.chdir(ROOT_DIR); + +// Sync version before building +const syncVersion = async (): Promise => { + const packageJson = JSON.parse( + await readFile(join(ROOT_DIR, "package.json"), "utf-8"), + ); + const { version } = packageJson; + + await writeFile( + join(ROOT_DIR, "src/version.json"), + JSON.stringify({ version }, null, 2) + "\n", + ); + + console.log(`Synced version: ${version}`); +}; + +await syncVersion(); + +// Build the application +console.log("Building codetyper-cli..."); + +const result = await Bun.build({ + entrypoints: ["./src/index.ts"], + outdir: "./dist", + target: "node", + conditions: ["node"], + plugins: [solidPlugin], + sourcemap: "external", +}); + +if (!result.success) { + console.error("Build failed:"); + for (const log of result.logs) { + console.error(log); + } + process.exit(1); +} + +// Update shebang to use node +const distPath = join(ROOT_DIR, "dist/index.js"); +let distContent = await readFile(distPath, "utf-8"); +distContent = distContent.replace(/^#!.*\n/, "#!/usr/bin/env node\n"); +await writeFile(distPath, distContent); + +console.log("Build completed successfully!"); diff --git a/scripts/dev-watch.ts b/scripts/dev-watch.ts new file mode 100644 index 0000000..cf8db3e --- /dev/null +++ b/scripts/dev-watch.ts @@ -0,0 +1,138 @@ +#!/usr/bin/env bun +/** + * Development watch script for codetyper-cli + * + * Watches for file changes and restarts the TUI application properly, + * handling terminal cleanup between restarts. + */ + +import { spawn, type Subprocess } from "bun"; +import { watch } from "chokidar"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const ROOT_DIR = join(__dirname, ".."); + +const WATCH_PATHS = ["src/**/*.ts", "src/**/*.tsx"]; + +const IGNORE_PATTERNS = [ + "**/node_modules/**", + "**/dist/**", + "**/*.test.ts", + "**/*.spec.ts", +]; + +const DEBOUNCE_MS = 300; + +let currentProcess: Subprocess | null = null; +let restartTimeout: ReturnType | null = null; + +const clearTerminal = (): void => { + // Reset terminal and clear screen + process.stdout.write("\x1b[2J\x1b[H\x1b[3J"); +}; + +const killProcess = async (): Promise => { + if (currentProcess) { + try { + currentProcess.kill("SIGTERM"); + // Wait a bit for graceful shutdown + await new Promise((resolve) => setTimeout(resolve, 100)); + if (currentProcess.exitCode === null) { + currentProcess.kill("SIGKILL"); + } + } catch { + // Process might already be dead + } + currentProcess = null; + } +}; + +const startProcess = (): void => { + clearTerminal(); + console.log("\x1b[36m[dev-watch]\x1b[0m Starting codetyper..."); + console.log("\x1b[90m─────────────────────────────────────\x1b[0m\n"); + + currentProcess = spawn({ + cmd: ["bun", "src/index.ts"], + cwd: ROOT_DIR, + stdio: ["inherit", "inherit", "inherit"], + env: { + ...process.env, + NODE_ENV: "development", + }, + }); + + currentProcess.exited.then((code) => { + if (code !== 0 && code !== null) { + console.log( + `\n\x1b[33m[dev-watch]\x1b[0m Process exited with code ${code}`, + ); + } + }); +}; + +const scheduleRestart = (path: string): void => { + if (restartTimeout) { + clearTimeout(restartTimeout); + } + + restartTimeout = setTimeout(async () => { + console.log(`\n\x1b[33m[dev-watch]\x1b[0m Change detected: ${path}`); + console.log("\x1b[33m[dev-watch]\x1b[0m Restarting...\n"); + + await killProcess(); + startProcess(); + }, DEBOUNCE_MS); +}; + +const main = async (): Promise => { + console.log("\x1b[36m[dev-watch]\x1b[0m Watching for changes..."); + console.log(`\x1b[90mRoot: ${ROOT_DIR}\x1b[0m`); + console.log(`\x1b[90mPaths: ${WATCH_PATHS.join(", ")}\x1b[0m`); + + const watcher = watch(WATCH_PATHS, { + ignored: IGNORE_PATTERNS, + persistent: true, + ignoreInitial: true, + cwd: ROOT_DIR, + usePolling: false, + awaitWriteFinish: { + stabilityThreshold: 100, + pollInterval: 100, + }, + }); + + watcher.on("ready", () => { + console.log("\x1b[32m[dev-watch]\x1b[0m Watcher ready\n"); + }); + + watcher.on("error", (error) => { + console.error("\x1b[31m[dev-watch]\x1b[0m Watcher error:", error); + }); + + watcher.on("change", scheduleRestart); + watcher.on("add", scheduleRestart); + watcher.on("unlink", scheduleRestart); + + // Handle exit signals + const cleanup = async (): Promise => { + console.log("\n\x1b[36m[dev-watch]\x1b[0m Shutting down..."); + await watcher.close(); + await killProcess(); + process.exit(0); + }; + + process.on("SIGINT", cleanup); + process.on("SIGTERM", cleanup); + + // Start the initial process + startProcess(); +}; + +main().catch((err) => { + console.error("Error:", err); + process.exit(1); +}); diff --git a/scripts/sync-version.ts b/scripts/sync-version.ts new file mode 100644 index 0000000..551f4ee --- /dev/null +++ b/scripts/sync-version.ts @@ -0,0 +1,29 @@ +import { readFile, writeFile } from "fs/promises"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const ROOT_DIR = join(__dirname, ".."); +const PACKAGE_JSON_PATH = join(ROOT_DIR, "package.json"); +const VERSION_JSON_PATH = join(ROOT_DIR, "src/version.json"); + +const syncVersion = async (): Promise => { + const packageJson = JSON.parse(await readFile(PACKAGE_JSON_PATH, "utf-8")); + const { version } = packageJson; + + const versionJson = { version }; + + await writeFile( + VERSION_JSON_PATH, + JSON.stringify(versionJson, null, 2) + "\n", + ); + + console.log(`Synced version: ${version}`); +}; + +syncVersion().catch((error) => { + console.error("Failed to sync version:", error); + process.exit(1); +}); diff --git a/src/commands/chat-tui.ts b/src/commands/chat-tui.ts new file mode 100644 index 0000000..6a97fca --- /dev/null +++ b/src/commands/chat-tui.ts @@ -0,0 +1,36 @@ +/** + * TUI-based Chat Command for CodeTyper CLI (Presentation Layer) + * + * This file is the main entry point for the chat TUI. + * It assembles callbacks and re-exports the execute function. + * All business logic is delegated to chat-tui-service.ts + */ + +import type { ChatServiceCallbacks } from "@services/chat-tui-service.ts"; +import { onModeChange } from "@commands/components/callbacks/on-mode-change.ts"; +import { onLog } from "@commands/components/callbacks/on-log.ts"; +import { onToolCall } from "@commands/components/callbacks/on-tool-call.ts"; +import { onToolResult } from "@commands/components/callbacks/on-tool-result.ts"; +import { onPermissionRequest } from "@commands/components/callbacks/on-permission-request.ts"; +import { onLearningDetected } from "@commands/components/callbacks/on-learning-detected.ts"; +import executeCommand from "@commands/components/execute/index.ts"; + +export const createCallbacks = (): ChatServiceCallbacks => ({ + onModeChange, + onLog, + onToolCall, + onToolResult, + onPermissionRequest, + onLearningDetected, +}); + +export const execute = executeCommand; + +export { + onModeChange, + onLog, + onToolCall, + onToolResult, + onPermissionRequest, + onLearningDetected, +}; diff --git a/src/commands/chat.ts b/src/commands/chat.ts new file mode 100644 index 0000000..132d1af --- /dev/null +++ b/src/commands/chat.ts @@ -0,0 +1,8 @@ +/** + * Interactive chat mode for CodeTyper CLI + * + * This file re-exports the modular chat implementation. + */ + +export { execute, createInitialState } from "@commands/components/chat/index"; +export type { ChatState } from "@commands/components/chat/index"; diff --git a/src/commands/components/callbacks/on-learning-detected.ts b/src/commands/components/callbacks/on-learning-detected.ts new file mode 100644 index 0000000..6dd5446 --- /dev/null +++ b/src/commands/components/callbacks/on-learning-detected.ts @@ -0,0 +1,23 @@ +import { v4 as uuidv4 } from "uuid"; +import { appStore } from "@tui/index.ts"; +import type { LearningResponse } from "@tui/types.ts"; +import type { LearningCandidate } from "@services/learning-service.ts"; + +export const onLearningDetected = async ( + candidate: LearningCandidate, +): Promise => { + return new Promise((resolve) => { + appStore.setMode("learning_prompt"); + appStore.setLearningPrompt({ + id: uuidv4(), + content: candidate.content, + context: candidate.context, + category: candidate.category, + resolve: (response: LearningResponse) => { + appStore.setLearningPrompt(null); + appStore.setMode("idle"); + resolve(response); + }, + }); + }); +}; diff --git a/src/commands/components/callbacks/on-log.ts b/src/commands/components/callbacks/on-log.ts new file mode 100644 index 0000000..117fc37 --- /dev/null +++ b/src/commands/components/callbacks/on-log.ts @@ -0,0 +1,14 @@ +import { appStore } from "@tui/index.ts"; +import type { LogType } from "@/types/log"; + +export const onLog = ( + type: string, + content: string, + metadata?: Record, +): void => { + appStore.addLog({ + type: type as LogType, + content, + metadata, + }); +}; diff --git a/src/commands/components/callbacks/on-mode-change.ts b/src/commands/components/callbacks/on-mode-change.ts new file mode 100644 index 0000000..c95d2af --- /dev/null +++ b/src/commands/components/callbacks/on-mode-change.ts @@ -0,0 +1,5 @@ +import { appStore } from "@tui/index.ts"; + +export const onModeChange = (mode: string): void => { + appStore.setMode(mode as Parameters[0]); +}; diff --git a/src/commands/components/callbacks/on-permission-request.ts b/src/commands/components/callbacks/on-permission-request.ts new file mode 100644 index 0000000..de443d3 --- /dev/null +++ b/src/commands/components/callbacks/on-permission-request.ts @@ -0,0 +1,7 @@ +interface PermissionResponse { + allowed: boolean; +} + +export const onPermissionRequest = async (): Promise => { + return { allowed: false }; +}; diff --git a/src/commands/components/callbacks/on-tool-call.ts b/src/commands/components/callbacks/on-tool-call.ts new file mode 100644 index 0000000..a5a6514 --- /dev/null +++ b/src/commands/components/callbacks/on-tool-call.ts @@ -0,0 +1,25 @@ +import { appStore } from "@tui/index.ts"; +import { isQuietTool } from "@utils/tools.ts"; +import type { ToolCallParams } from "@interfaces/ToolCallParams.ts"; + +export const onToolCall = (call: ToolCallParams): void => { + appStore.setCurrentToolCall({ + id: call.id, + name: call.name, + description: call.description, + status: "running", + }); + + const isQuiet = isQuietTool(call.name, call.args); + + appStore.addLog({ + type: "tool", + content: call.description, + metadata: { + toolName: call.name, + toolStatus: "running", + toolDescription: call.description, + quiet: isQuiet, + }, + }); +}; diff --git a/src/commands/components/callbacks/on-tool-result.ts b/src/commands/components/callbacks/on-tool-result.ts new file mode 100644 index 0000000..7f53f7b --- /dev/null +++ b/src/commands/components/callbacks/on-tool-result.ts @@ -0,0 +1,47 @@ +import { appStore } from "@tui/index.ts"; +import { + truncateOutput, + detectDiffContent, +} from "@services/chat-tui-service.ts"; +import { getThinkingMessage } from "@constants/status-messages.ts"; + +export const onToolResult = ( + success: boolean, + title: string, + output?: string, + error?: string, +): void => { + appStore.updateToolCall({ + status: success ? "success" : "error", + result: success ? output : undefined, + error: error, + }); + + const state = appStore.getState(); + const logEntry = state.logs.find( + (log) => log.type === "tool" && log.metadata?.toolStatus === "running", + ); + + if (logEntry) { + const diffData = output ? detectDiffContent(output) : undefined; + const displayContent = diffData?.isDiff + ? output + : output + ? truncateOutput(output) + : ""; + + appStore.updateLog(logEntry.id, { + content: success + ? `${title}${displayContent ? "\n" + displayContent : ""}` + : `${title}: ${error}`, + metadata: { + ...logEntry.metadata, + toolStatus: success ? "success" : "error", + toolDescription: title, + diffData: diffData, + }, + }); + } + + appStore.setThinkingMessage(getThinkingMessage()); +}; diff --git a/src/commands/components/chat/agents/show-agents.ts b/src/commands/components/chat/agents/show-agents.ts new file mode 100644 index 0000000..8a599e6 --- /dev/null +++ b/src/commands/components/chat/agents/show-agents.ts @@ -0,0 +1,35 @@ +/** + * Show available agents command + */ + +import chalk from "chalk"; +import { agentLoader } from "@services/agent-loader"; +import type { ChatState } from "@commands/components/chat/state"; + +export const showAgents = async (state: ChatState): Promise => { + const agents = await agentLoader.getAvailableAgents(process.cwd()); + const currentAgent = state.currentAgent ?? "coder"; + + console.log("\n" + chalk.bold.underline("Available Agents") + "\n"); + + for (const agent of agents) { + const isCurrent = agent.id === currentAgent; + const marker = isCurrent ? chalk.cyan("→") : " "; + const nameStyle = isCurrent ? chalk.cyan.bold : chalk.white; + + console.log(`${marker} ${nameStyle(agent.name)}`); + + if (agent.description) { + console.log(` ${chalk.gray(agent.description)}`); + } + + if (agent.model) { + console.log(` ${chalk.gray(`Model: ${agent.model}`)}`); + } + + console.log(); + } + + console.log(chalk.gray("Use /agent to switch agents")); + console.log(); +}; diff --git a/src/commands/components/chat/agents/switch-agent.ts b/src/commands/components/chat/agents/switch-agent.ts new file mode 100644 index 0000000..ab43a0f --- /dev/null +++ b/src/commands/components/chat/agents/switch-agent.ts @@ -0,0 +1,60 @@ +/** + * Switch agent command + */ + +import chalk from "chalk"; +import { errorMessage, infoMessage, warningMessage } from "@utils/terminal"; +import { agentLoader } from "@services/agent-loader"; +import type { ChatState } from "@commands/components/chat/state"; + +export const switchAgent = async ( + agentName: string, + state: ChatState, +): Promise => { + if (!agentName.trim()) { + warningMessage("Usage: /agent "); + infoMessage("Use /agents to see available agents"); + return; + } + + const normalizedName = agentName.toLowerCase().trim(); + const agents = await agentLoader.getAvailableAgents(process.cwd()); + + // Find agent by id or partial name match + const agent = agents.find( + (a) => + a.id === normalizedName || + a.name.toLowerCase() === normalizedName || + a.id.includes(normalizedName) || + a.name.toLowerCase().includes(normalizedName), + ); + + if (!agent) { + errorMessage(`Agent not found: ${agentName}`); + infoMessage("Use /agents to see available agents"); + return; + } + + state.currentAgent = agent.id; + + // Update system prompt with agent prompt + if (agent.prompt) { + // Prepend agent prompt to system prompt + const basePrompt = state.systemPrompt; + state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`; + + // Update the system message in messages array + if (state.messages.length > 0 && state.messages[0].role === "system") { + state.messages[0].content = state.systemPrompt; + } + } + + console.log(); + console.log(chalk.green(`✓ Switched to agent: ${chalk.bold(agent.name)}`)); + + if (agent.description) { + console.log(chalk.gray(` ${agent.description}`)); + } + + console.log(); +}; diff --git a/src/commands/components/chat/cleanup.ts b/src/commands/components/chat/cleanup.ts new file mode 100644 index 0000000..d6640c8 --- /dev/null +++ b/src/commands/components/chat/cleanup.ts @@ -0,0 +1,12 @@ +import chalk from "chalk"; +import type { ChatState } from "./state.ts"; + +export const createCleanup = (state: ChatState) => (): void => { + state.isRunning = false; + if (state.inputEditor) { + state.inputEditor.stop(); + state.inputEditor = null; + } + console.log("\n" + chalk.cyan("Goodbye!")); + process.exit(0); +}; diff --git a/src/commands/components/chat/commands/commandsRegistry.ts b/src/commands/components/chat/commands/commandsRegistry.ts new file mode 100644 index 0000000..36c9b81 --- /dev/null +++ b/src/commands/components/chat/commands/commandsRegistry.ts @@ -0,0 +1,111 @@ +import { saveSession } from "@services/session"; +import { showHelp } from "@commands/components/chat/commands/show-help"; +import { clearConversation } from "@commands/components/chat/history/clear-conversation"; +import { showContextFiles } from "@commands/components/chat/context/show-context-files"; +import { removeFile } from "@commands/components/chat/context/remove-file"; +import { showContext } from "@commands/components/chat/history/show-context"; +import { compactHistory } from "@commands/components/chat/history/compact-history"; +import { showHistory } from "@commands/components/chat/history/show-history"; +import { showModels } from "@commands/components/chat/models/show-models"; +import { showProviders } from "@commands/components/chat/models/show-providers"; +import { switchProvider } from "@commands/components/chat/models/switch-provider"; +import { switchModel } from "@commands/components/chat/models/switch-model"; +import { showSessionInfo } from "@commands/components/chat/session/show-session-info"; +import { listSessions } from "@commands/components/chat/session/list-sessions"; +import { showUsage } from "@commands/components/chat/usage/show-usage"; +import { showAgents } from "@commands/components/chat/agents/show-agents"; +import { switchAgent } from "@commands/components/chat/agents/switch-agent"; +import { handleMCP } from "@commands/components/chat/mcp/handle-mcp"; +import { CommandContext } from "@interfaces/commandContext"; +import type { CommandHandler } from "@/types/commandHandler"; +import { successMessage } from "@utils/terminal"; + +const COMMAND_REGISTRY: Map = new Map< + string, + CommandHandler +>([ + ["help", () => showHelp()], + ["h", () => showHelp()], + ["clear", (ctx: CommandContext) => clearConversation(ctx.state)], + ["c", (ctx: CommandContext) => clearConversation(ctx.state)], + ["files", (ctx: CommandContext) => showContextFiles(ctx.state.contextFiles)], + ["f", (ctx: CommandContext) => showContextFiles(ctx.state.contextFiles)], + ["exit", (ctx: CommandContext) => ctx.cleanup()], + ["quit", (ctx: CommandContext) => ctx.cleanup()], + ["q", (ctx: CommandContext) => ctx.cleanup()], + [ + "save", + async () => { + await saveSession(); + successMessage("Session saved"); + }, + ], + [ + "s", + async () => { + await saveSession(); + successMessage("Session saved"); + }, + ], + [ + "models", + async (ctx: CommandContext) => + showModels(ctx.state.currentProvider, ctx.state.currentModel), + ], + [ + "m", + async (ctx: CommandContext) => + showModels(ctx.state.currentProvider, ctx.state.currentModel), + ], + ["providers", async () => showProviders()], + ["p", async () => showProviders()], + [ + "provider", + async (ctx: CommandContext) => + switchProvider(ctx.args.join(" "), ctx.state), + ], + [ + "model", + async (ctx: CommandContext) => switchModel(ctx.args.join(" "), ctx.state), + ], + ["context", (ctx: CommandContext) => showContext(ctx.state)], + ["compact", (ctx: CommandContext) => compactHistory(ctx.state)], + ["history", (ctx: CommandContext) => showHistory(ctx.state)], + [ + "remove", + (ctx: CommandContext) => + removeFile(ctx.args.join(" "), ctx.state.contextFiles), + ], + [ + "rm", + (ctx: CommandContext) => + removeFile(ctx.args.join(" "), ctx.state.contextFiles), + ], + ["session", async () => showSessionInfo()], + ["sessions", async () => listSessions()], + ["usage", async (ctx: CommandContext) => showUsage(ctx.state)], + ["u", async (ctx: CommandContext) => showUsage(ctx.state)], + [ + "agent", + async (ctx: CommandContext) => { + if (ctx.args.length === 0) { + await showAgents(ctx.state); + } else { + await switchAgent(ctx.args.join(" "), ctx.state); + } + }, + ], + [ + "a", + async (ctx: CommandContext) => { + if (ctx.args.length === 0) { + await showAgents(ctx.state); + } else { + await switchAgent(ctx.args.join(" "), ctx.state); + } + }, + ], + ["mcp", async (ctx: CommandContext) => handleMCP(ctx.args)], +]); + +export default COMMAND_REGISTRY; diff --git a/src/commands/components/chat/commands/handle-command.ts b/src/commands/components/chat/commands/handle-command.ts new file mode 100644 index 0000000..3d4f3fa --- /dev/null +++ b/src/commands/components/chat/commands/handle-command.ts @@ -0,0 +1,28 @@ +import { warningMessage, infoMessage } from "@utils/terminal"; +import type { ChatState } from "@commands/components/chat/state"; +import COMMAND_REGISTRY from "@commands/components/chat/commands/commandsRegistry"; + +const isValidCommand = (cmd: string): boolean => { + return COMMAND_REGISTRY.has(cmd); +}; + +export const handleCommand = async ( + command: string, + state: ChatState, + cleanup: () => void, +): Promise => { + const parts = command.slice(1).split(/\s+/); + const cmd = parts[0].toLowerCase(); + const args = parts.slice(1); + + if (!isValidCommand(cmd)) { + warningMessage(`Unknown command: /${cmd}`); + infoMessage("Type /help for available commands"); + return; + } + + const handler = COMMAND_REGISTRY.get(cmd); + if (handler) { + await handler({ state, args, cleanup }); + } +}; diff --git a/src/commands/components/chat/commands/show-help.ts b/src/commands/components/chat/commands/show-help.ts new file mode 100644 index 0000000..e206530 --- /dev/null +++ b/src/commands/components/chat/commands/show-help.ts @@ -0,0 +1,25 @@ +import chalk from "chalk"; +import { HELP_COMMANDS } from "@constants/help-commands.ts"; + +export const showHelp = (): void => { + console.log("\n" + chalk.bold.underline("Commands") + "\n"); + + for (const [cmd, desc] of HELP_COMMANDS) { + console.log(` ${chalk.yellow(cmd.padEnd(20))} ${desc}`); + } + + console.log("\n" + chalk.bold.underline("File References") + "\n"); + console.log(` ${chalk.yellow("@")} Add a file to context`); + console.log( + ` ${chalk.yellow('@"file with spaces"')} Add file with spaces in name`, + ); + console.log( + ` ${chalk.yellow("@src/*.ts")} Add files matching glob pattern`, + ); + + console.log("\n" + chalk.bold.underline("Examples") + "\n"); + console.log(" @src/app.ts explain this code"); + console.log(" @src/utils.ts @src/types.ts refactor these files"); + console.log(" /model gpt-4o"); + console.log(" /provider copilot\n"); +}; diff --git a/src/commands/components/chat/context/add-context-file.ts b/src/commands/components/chat/context/add-context-file.ts new file mode 100644 index 0000000..9e6c5db --- /dev/null +++ b/src/commands/components/chat/context/add-context-file.ts @@ -0,0 +1,35 @@ +import { resolve } from "path"; +import { existsSync } from "fs"; +import fg from "fast-glob"; +import { errorMessage, warningMessage } from "@utils/terminal"; +import { loadFile } from "@commands/components/chat/context/load-file"; +import { IGNORE_FOLDERS } from "@constants/paths"; + +export const addContextFile = async ( + pattern: string, + contextFiles: Map, +): Promise => { + try { + const paths = await fg(pattern, { + cwd: process.cwd(), + absolute: true, + ignore: IGNORE_FOLDERS, + }); + + if (paths.length === 0) { + const absolutePath = resolve(process.cwd(), pattern); + if (existsSync(absolutePath)) { + await loadFile(absolutePath, contextFiles); + } else { + warningMessage(`File not found: ${pattern}`); + } + return; + } + + for (const filePath of paths) { + await loadFile(filePath, contextFiles); + } + } catch (error) { + errorMessage(`Failed to add file: ${error}`); + } +}; diff --git a/src/commands/components/chat/context/load-file.ts b/src/commands/components/chat/context/load-file.ts new file mode 100644 index 0000000..6a7da12 --- /dev/null +++ b/src/commands/components/chat/context/load-file.ts @@ -0,0 +1,32 @@ +import { readFile, stat } from "fs/promises"; +import { basename } from "path"; +import { warningMessage, successMessage, errorMessage } from "@utils/terminal"; +import { addContextFile } from "@services/session"; + +export const loadFile = async ( + filePath: string, + contextFiles: Map, +): Promise => { + try { + const stats = await stat(filePath); + if (stats.isDirectory()) { + warningMessage(`Skipping directory: ${filePath}`); + return; + } + + if (stats.size > 100 * 1024) { + warningMessage(`File too large (>100KB): ${basename(filePath)}`); + return; + } + + const content = await readFile(filePath, "utf-8"); + contextFiles.set(filePath, content); + successMessage( + `Added: ${basename(filePath)} (${content.split("\n").length} lines)`, + ); + + await addContextFile(filePath); + } catch (error) { + errorMessage(`Failed to read file: ${error}`); + } +}; diff --git a/src/commands/components/chat/context/process-file-references.ts b/src/commands/components/chat/context/process-file-references.ts new file mode 100644 index 0000000..a0dfa97 --- /dev/null +++ b/src/commands/components/chat/context/process-file-references.ts @@ -0,0 +1,27 @@ +import { FILE_REFERENCE_PATTERN } from "@constants/patterns"; +import { addContextFile } from "@commands/components/chat/context/add-context-file"; + +export const processFileReferences = async ( + input: string, + contextFiles: Map, +): Promise => { + const pattern = new RegExp(FILE_REFERENCE_PATTERN.source, "g"); + let match; + const filesToAdd: string[] = []; + + while ((match = pattern.exec(input)) !== null) { + const filePath = match[1] || match[2] || match[3]; + filesToAdd.push(filePath); + } + + for (const filePath of filesToAdd) { + await addContextFile(filePath, contextFiles); + } + + const textOnly = input.replace(pattern, "").trim(); + if (!textOnly && filesToAdd.length > 0) { + return `Analyze the files I've added to the context.`; + } + + return input; +}; diff --git a/src/commands/components/chat/context/remove-file.ts b/src/commands/components/chat/context/remove-file.ts new file mode 100644 index 0000000..e79a502 --- /dev/null +++ b/src/commands/components/chat/context/remove-file.ts @@ -0,0 +1,22 @@ +import { basename } from "path"; +import { warningMessage, successMessage } from "@utils/terminal"; + +export const removeFile = ( + filename: string, + contextFiles: Map, +): void => { + if (!filename) { + warningMessage("Please specify a file to remove"); + return; + } + + for (const [path] of contextFiles) { + if (path.includes(filename) || basename(path) === filename) { + contextFiles.delete(path); + successMessage(`Removed: ${basename(path)}`); + return; + } + } + + warningMessage(`File not found in context: ${filename}`); +}; diff --git a/src/commands/components/chat/context/show-context-files.ts b/src/commands/components/chat/context/show-context-files.ts new file mode 100644 index 0000000..b558100 --- /dev/null +++ b/src/commands/components/chat/context/show-context-files.ts @@ -0,0 +1,32 @@ +import chalk from "chalk"; +import { basename } from "path"; +import { getCurrentSession } from "@services/session"; +import { infoMessage, filePath } from "@utils/terminal"; + +export const showContextFiles = (contextFiles: Map): void => { + const session = getCurrentSession(); + const files = session?.contextFiles || []; + + if (files.length === 0 && contextFiles.size === 0) { + infoMessage("No context files loaded"); + return; + } + + console.log("\n" + chalk.bold("Context Files:")); + + if (contextFiles.size > 0) { + console.log(chalk.gray(" Pending (will be included in next message):")); + for (const [path] of contextFiles) { + console.log(` - ${filePath(basename(path))}`); + } + } + + if (files.length > 0) { + console.log(chalk.gray(" In session:")); + files.forEach((file, index) => { + console.log(` ${index + 1}. ${filePath(file)}`); + }); + } + + console.log(); +}; diff --git a/src/commands/components/chat/history/clear-conversation.ts b/src/commands/components/chat/history/clear-conversation.ts new file mode 100644 index 0000000..c4005e7 --- /dev/null +++ b/src/commands/components/chat/history/clear-conversation.ts @@ -0,0 +1,10 @@ +import { clearMessages } from "@services/session"; +import { successMessage } from "@utils/terminal"; +import type { ChatState } from "@commands/components/chat/state"; + +export const clearConversation = (state: ChatState): void => { + state.messages = [{ role: "system", content: state.systemPrompt }]; + state.contextFiles.clear(); + clearMessages(); + successMessage("Conversation cleared"); +}; diff --git a/src/commands/components/chat/history/compact-history.ts b/src/commands/components/chat/history/compact-history.ts new file mode 100644 index 0000000..ebffb4f --- /dev/null +++ b/src/commands/components/chat/history/compact-history.ts @@ -0,0 +1,15 @@ +import { successMessage, infoMessage } from "@utils/terminal"; +import type { ChatState } from "@commands/components/chat/state"; + +export const compactHistory = (state: ChatState): void => { + if (state.messages.length <= 11) { + infoMessage("History is already compact"); + return; + } + + const systemPrompt = state.messages[0]; + const recentMessages = state.messages.slice(-10); + state.messages = [systemPrompt, ...recentMessages]; + + successMessage(`Compacted to ${state.messages.length - 1} messages`); +}; diff --git a/src/commands/components/chat/history/show-context.ts b/src/commands/components/chat/history/show-context.ts new file mode 100644 index 0000000..83b9f5d --- /dev/null +++ b/src/commands/components/chat/history/show-context.ts @@ -0,0 +1,18 @@ +import chalk from "chalk"; +import type { ChatState } from "@commands/components/chat/state"; + +export const showContext = (state: ChatState): void => { + const messageCount = state.messages.length - 1; + const totalChars = state.messages.reduce( + (acc, m) => acc + m.content.length, + 0, + ); + const estimatedTokens = Math.round(totalChars / 4); + + console.log("\n" + chalk.bold("Context Information:")); + console.log(` Messages: ${messageCount}`); + console.log(` Characters: ${totalChars.toLocaleString()}`); + console.log(` Estimated tokens: ~${estimatedTokens.toLocaleString()}`); + console.log(` Pending files: ${state.contextFiles.size}`); + console.log(); +}; diff --git a/src/commands/components/chat/history/show-history.ts b/src/commands/components/chat/history/show-history.ts new file mode 100644 index 0000000..f11ed7e --- /dev/null +++ b/src/commands/components/chat/history/show-history.ts @@ -0,0 +1,18 @@ +import chalk from "chalk"; +import type { ChatState } from "@commands/components/chat/state"; + +export const showHistory = (state: ChatState): void => { + console.log("\n" + chalk.bold("Conversation History:") + "\n"); + + for (let i = 1; i < state.messages.length; i++) { + const msg = state.messages[i]; + const role = + msg.role === "user" ? chalk.cyan("You") : chalk.green("Assistant"); + const preview = msg.content.slice(0, 100).replace(/\n/g, " "); + console.log( + ` ${i}. ${role}: ${preview}${msg.content.length > 100 ? "..." : ""}`, + ); + } + + console.log(); +}; diff --git a/src/commands/components/chat/index.ts b/src/commands/components/chat/index.ts new file mode 100644 index 0000000..a703ede --- /dev/null +++ b/src/commands/components/chat/index.ts @@ -0,0 +1,224 @@ +import chalk from "chalk"; +import { infoMessage, errorMessage, warningMessage } from "@utils/terminal"; +import { + createSession, + loadSession, + getMostRecentSession, + findSession, + setWorkingDirectory, +} from "@services/session"; +import { getConfig } from "@services/config"; +import type { Provider as ProviderName, ChatSession } from "@/types/index"; +import { getProvider, getProviderStatus } from "@providers/index.ts"; +import { + printWelcome, + formatTipLine, + Style, + Theme, + createInputEditor, +} from "@ui/index"; +import { + DEFAULT_SYSTEM_PROMPT, + buildSystemPromptWithRules, +} from "@prompts/index.ts"; +import type { ChatOptions } from "@interfaces/ChatOptions.ts"; + +import { createInitialState, type ChatState } from "./state.ts"; +import { restoreMessagesFromSession } from "./session/restore-messages.ts"; +import { addContextFile } from "./context/add-context-file.ts"; +import { handleCommand } from "./commands/handle-command.ts"; +import { handleInput } from "./messages/handle-input.ts"; +import { executePrintMode } from "./print-mode.ts"; +import { createCleanup } from "./cleanup.ts"; + +export const execute = async (options: ChatOptions): Promise => { + const config = await getConfig(); + const state = createInitialState( + (options.provider || config.get("provider")) as ProviderName, + ); + + state.verbose = options.verbose || false; + state.autoApprove = options.autoApprove || false; + state.currentModel = options.model || config.get("model") || "auto"; + + const status = await getProviderStatus(state.currentProvider); + if (!status.valid) { + errorMessage(`Provider ${state.currentProvider} is not configured.`); + infoMessage(`Run: codetyper login ${state.currentProvider}`); + process.exit(1); + } + + if (options.systemPrompt) { + state.systemPrompt = options.systemPrompt; + } else { + const { prompt: promptWithRules, rulesPaths } = + await buildSystemPromptWithRules(DEFAULT_SYSTEM_PROMPT, process.cwd()); + state.systemPrompt = promptWithRules; + + if (rulesPaths.length > 0 && state.verbose) { + infoMessage(`Loaded ${rulesPaths.length} rule file(s):`); + for (const rulePath of rulesPaths) { + infoMessage(` - ${rulePath}`); + } + } + + if (options.appendSystemPrompt) { + state.systemPrompt = + state.systemPrompt + "\n\n" + options.appendSystemPrompt; + } + } + + let session: ChatSession; + + if (options.continueSession) { + const recent = await getMostRecentSession(process.cwd()); + if (recent) { + session = recent; + await loadSession(session.id); + state.messages = restoreMessagesFromSession(session, state.systemPrompt); + if (state.verbose) { + infoMessage(`Continuing session: ${session.id}`); + } + } else { + warningMessage( + "No previous session found in this directory. Starting new session.", + ); + session = await createSession("coder"); + } + } else if (options.resumeSession) { + const found = await findSession(options.resumeSession); + if (found) { + session = found; + await loadSession(session.id); + state.messages = restoreMessagesFromSession(session, state.systemPrompt); + if (state.verbose) { + infoMessage(`Resumed session: ${session.id}`); + } + } else { + errorMessage(`Session not found: ${options.resumeSession}`); + process.exit(1); + } + } else { + session = await createSession("coder"); + await setWorkingDirectory(process.cwd()); + } + + if (state.messages.length === 0) { + state.messages = [{ role: "system", content: state.systemPrompt }]; + } + + if (options.files && options.files.length > 0) { + for (const file of options.files) { + await addContextFile(file, state.contextFiles); + } + } + + if (options.printMode && options.initialPrompt) { + await executePrintMode(options.initialPrompt, state); + return; + } + + const hasInitialPrompt = + options.initialPrompt && options.initialPrompt.trim().length > 0; + + const provider = getProvider(state.currentProvider); + const model = state.currentModel || "auto"; + + printWelcome("0.1.0", provider.displayName, model); + + console.log( + Theme.textMuted + + " Session: " + + Style.RESET + + chalk.gray(session.id.slice(0, 16) + "..."), + ); + console.log(""); + console.log( + Theme.textMuted + + " Commands: " + + Style.RESET + + chalk.cyan("@file") + + " " + + chalk.cyan("/help") + + " " + + chalk.cyan("/clear") + + " " + + chalk.cyan("/exit"), + ); + console.log( + Theme.textMuted + + " Input: " + + Style.RESET + + chalk.cyan("Enter") + + " to send, " + + chalk.cyan("Alt+Enter") + + " for newline", + ); + console.log(""); + console.log(" " + formatTipLine()); + console.log(""); + + state.inputEditor = createInputEditor({ + prompt: "\x1b[36m> \x1b[0m", + continuationPrompt: "\x1b[90m│ \x1b[0m", + }); + + state.isRunning = true; + + const cleanup = createCleanup(state); + + const commandHandler = async (command: string, st: ChatState) => { + await handleCommand(command, st, cleanup); + }; + + state.inputEditor.on("submit", async (input: string) => { + if (state.isProcessing) return; + + state.isProcessing = true; + state.inputEditor?.lock(); + + try { + await handleInput(input, state, commandHandler); + } catch (error) { + errorMessage(`Error: ${error}`); + } + + state.isProcessing = false; + if (state.isRunning && state.inputEditor) { + state.inputEditor.unlock(); + } + }); + + state.inputEditor.on("interrupt", () => { + if (state.isProcessing) { + console.log("\n" + chalk.yellow("Interrupted")); + state.isProcessing = false; + state.inputEditor?.unlock(); + } else { + cleanup(); + } + }); + + state.inputEditor.on("close", () => { + cleanup(); + }); + + state.inputEditor.start(); + + if (hasInitialPrompt) { + state.isProcessing = true; + state.inputEditor.lock(); + console.log(chalk.cyan("> ") + options.initialPrompt); + try { + await handleInput(options.initialPrompt!, state, commandHandler); + } catch (error) { + errorMessage(`Error: ${error}`); + } + state.isProcessing = false; + if (state.isRunning && state.inputEditor) { + state.inputEditor.unlock(); + } + } +}; + +export { createInitialState, type ChatState } from "./state.ts"; diff --git a/src/commands/components/chat/mcp/handle-mcp.ts b/src/commands/components/chat/mcp/handle-mcp.ts new file mode 100644 index 0000000..2eb5476 --- /dev/null +++ b/src/commands/components/chat/mcp/handle-mcp.ts @@ -0,0 +1,143 @@ +/** + * Handle MCP commands in chat + */ + +import chalk from "chalk"; +import { + initializeMCP, + connectServer, + disconnectServer, + connectAllServers, + disconnectAllServers, + getAllTools, +} from "@services/mcp/index"; +import { showMCPStatus } from "@commands/components/chat/mcp/show-mcp-status"; +import { appStore } from "@tui-solid/context/app"; + +/** + * Handle MCP subcommands + */ +export const handleMCP = async (args: string[]): Promise => { + const subcommand = args[0] || "status"; + + const handlers: Record Promise> = { + status: handleStatus, + connect: handleConnect, + disconnect: handleDisconnect, + tools: handleTools, + add: handleAdd, + }; + + const handler = handlers[subcommand]; + if (!handler) { + console.log(chalk.yellow(`Unknown MCP command: ${subcommand}`)); + console.log( + chalk.gray("Available: status, connect, disconnect, tools, add"), + ); + return; + } + + await handler(args.slice(1)); +}; + +/** + * Show MCP status + */ +const handleStatus = async (_args: string[]): Promise => { + await showMCPStatus(); +}; + +/** + * Connect to MCP servers + */ +const handleConnect = async (args: string[]): Promise => { + await initializeMCP(); + + const name = args[0]; + + if (name) { + try { + console.log(chalk.gray(`Connecting to ${name}...`)); + const instance = await connectServer(name); + console.log(chalk.green(`✓ Connected to ${name}`)); + console.log(chalk.gray(` Tools: ${instance.tools.length}`)); + } catch (err) { + console.log(chalk.red(`✗ Failed to connect: ${err}`)); + } + } else { + console.log(chalk.gray("Connecting to all servers...")); + const results = await connectAllServers(); + + for (const [serverName, instance] of results) { + if (instance.state === "connected") { + console.log( + chalk.green(`✓ ${serverName}: ${instance.tools.length} tools`), + ); + } else { + console.log( + chalk.red(`✗ ${serverName}: ${instance.error || "Failed"}`), + ); + } + } + } + console.log(); +}; + +/** + * Disconnect from MCP servers + */ +const handleDisconnect = async (args: string[]): Promise => { + const name = args[0]; + + if (name) { + await disconnectServer(name); + console.log(chalk.green(`✓ Disconnected from ${name}`)); + } else { + await disconnectAllServers(); + console.log(chalk.green("✓ Disconnected from all servers")); + } + console.log(); +}; + +/** + * List available MCP tools + */ +const handleTools = async (_args: string[]): Promise => { + await connectAllServers(); + const tools = getAllTools(); + + if (tools.length === 0) { + console.log(chalk.yellow("\nNo tools available.")); + console.log(chalk.gray("Connect to MCP servers first with /mcp connect")); + console.log(); + return; + } + + console.log(chalk.bold("\nMCP Tools\n")); + + // Group by server + const byServer = new Map(); + for (const item of tools) { + const existing = byServer.get(item.server) || []; + existing.push(item); + byServer.set(item.server, existing); + } + + for (const [server, serverTools] of byServer) { + console.log(chalk.cyan(`${server}:`)); + for (const { tool } of serverTools) { + console.log(` ${chalk.white(tool.name)}`); + if (tool.description) { + console.log(` ${chalk.gray(tool.description)}`); + } + } + console.log(); + } +}; + +/** + * Open the MCP add form + */ +const handleAdd = async (_args: string[]): Promise => { + appStore.setMode("mcp_add"); +}; diff --git a/src/commands/components/chat/mcp/index.ts b/src/commands/components/chat/mcp/index.ts new file mode 100644 index 0000000..3a6d284 --- /dev/null +++ b/src/commands/components/chat/mcp/index.ts @@ -0,0 +1,6 @@ +/** + * MCP chat commands + */ + +export { showMCPStatus } from "@commands/components/chat/mcp/show-mcp-status"; +export { handleMCP } from "@commands/components/chat/mcp/handle-mcp"; diff --git a/src/commands/components/chat/mcp/show-mcp-status.ts b/src/commands/components/chat/mcp/show-mcp-status.ts new file mode 100644 index 0000000..b9e94f1 --- /dev/null +++ b/src/commands/components/chat/mcp/show-mcp-status.ts @@ -0,0 +1,76 @@ +/** + * Show MCP server status in chat + */ + +import chalk from "chalk"; +import { + initializeMCP, + getServerInstances, + getAllTools, + isMCPAvailable, +} from "@services/mcp/index"; + +/** + * Display MCP server status + */ +export const showMCPStatus = async (): Promise => { + await initializeMCP(); + + const hasServers = await isMCPAvailable(); + if (!hasServers) { + console.log(chalk.yellow("\nNo MCP servers configured.")); + console.log(chalk.gray("Add a server with: codetyper mcp add ")); + console.log(); + return; + } + + const instances = getServerInstances(); + const tools = getAllTools(); + + console.log(chalk.bold("\nMCP Status\n")); + + // Server status + console.log(chalk.cyan("Servers:")); + for (const [name, instance] of instances) { + const stateColors: Record string> = { + connected: chalk.green, + connecting: chalk.yellow, + disconnected: chalk.gray, + error: chalk.red, + }; + + const colorFn = stateColors[instance.state] || chalk.white; + const status = colorFn(instance.state); + const toolCount = + instance.state === "connected" ? ` (${instance.tools.length} tools)` : ""; + + console.log(` ${chalk.white(name)}: ${status}${chalk.gray(toolCount)}`); + + if (instance.error) { + console.log(` ${chalk.red(instance.error)}`); + } + } + + // Tool summary + if (tools.length > 0) { + console.log(); + console.log(chalk.cyan(`Available Tools: ${chalk.white(tools.length)}`)); + + // Group by server + const byServer = new Map(); + for (const { server, tool } of tools) { + const existing = byServer.get(server) || []; + existing.push(tool.name); + byServer.set(server, existing); + } + + for (const [server, toolNames] of byServer) { + console.log(` ${chalk.gray(server)}: ${toolNames.join(", ")}`); + } + } + + console.log(); + console.log(chalk.gray("Use /mcp connect to connect servers")); + console.log(chalk.gray("Use /mcp tools for detailed tool info")); + console.log(); +}; diff --git a/src/commands/components/chat/messages/handle-input.ts b/src/commands/components/chat/messages/handle-input.ts new file mode 100644 index 0000000..28f154a --- /dev/null +++ b/src/commands/components/chat/messages/handle-input.ts @@ -0,0 +1,17 @@ +import { processFileReferences } from "@commands/components/chat/context/process-file-references"; +import { sendMessage } from "@commands/components/chat/messages/send-message"; +import type { ChatState } from "@commands/components/chat/state"; + +export const handleInput = async ( + input: string, + state: ChatState, + handleCommand: (command: string, state: ChatState) => Promise, +): Promise => { + if (input.startsWith("/")) { + await handleCommand(input, state); + return; + } + + const processedInput = await processFileReferences(input, state.contextFiles); + await sendMessage(processedInput, state); +}; diff --git a/src/commands/components/chat/messages/send-message.ts b/src/commands/components/chat/messages/send-message.ts new file mode 100644 index 0000000..25f979e --- /dev/null +++ b/src/commands/components/chat/messages/send-message.ts @@ -0,0 +1,228 @@ +import chalk from "chalk"; +import { basename, extname } from "path"; +import { addMessage } from "@services/session"; +import { initializePermissions } from "@services/permissions"; +import { createAgent } from "@services/agent"; +import { infoMessage, errorMessage, warningMessage } from "@utils/terminal"; +import { getThinkingMessage } from "@constants/status-messages"; +import { + detectDebuggingRequest, + buildDebuggingContext, + getDebuggingPrompt, +} from "@services/debugging-service"; +import { + detectCodeReviewRequest, + buildCodeReviewContext, + getCodeReviewPrompt, +} from "@services/code-review-service"; +import { + detectRefactoringRequest, + buildRefactoringContext, + getRefactoringPrompt, +} from "@services/refactoring-service"; +import { + detectMemoryCommand, + processMemoryCommand, + buildRelevantMemoryPrompt, +} from "@services/memory-service"; +import type { ChatState } from "@commands/components/chat/state"; + +export const sendMessage = async ( + content: string, + state: ChatState, +): Promise => { + let userMessage = content; + + if (state.contextFiles.size > 0) { + const contextParts: string[] = []; + for (const [path, fileContent] of state.contextFiles) { + const ext = extname(path).slice(1) || "txt"; + contextParts.push( + `File: ${basename(path)}\n\`\`\`${ext}\n${fileContent}\n\`\`\``, + ); + } + userMessage = contextParts.join("\n\n") + "\n\n" + content; + state.contextFiles.clear(); + } + + // Detect debugging requests and enhance message with context + const debugContext = detectDebuggingRequest(userMessage); + if (debugContext.isDebugging) { + const debugPrompt = getDebuggingPrompt(); + const contextInfo = buildDebuggingContext(debugContext); + + // Inject debugging system message before user message if not already present + const hasDebuggingPrompt = state.messages.some( + (msg) => msg.role === "system" && msg.content.includes("debugging mode"), + ); + + if (!hasDebuggingPrompt) { + state.messages.push({ role: "system", content: debugPrompt }); + } + + // Append debug context to user message if extracted + if (contextInfo) { + userMessage = userMessage + "\n\n" + contextInfo; + } + + if (state.verbose) { + infoMessage(`Debugging mode activated: ${debugContext.debugType}`); + } + } + + // Detect code review requests and enhance message with context + const reviewContext = detectCodeReviewRequest(userMessage); + if (reviewContext.isReview) { + const reviewPrompt = getCodeReviewPrompt(); + const contextInfo = buildCodeReviewContext(reviewContext); + + // Inject code review system message before user message if not already present + const hasReviewPrompt = state.messages.some( + (msg) => + msg.role === "system" && msg.content.includes("code review mode"), + ); + + if (!hasReviewPrompt) { + state.messages.push({ role: "system", content: reviewPrompt }); + } + + // Append review context to user message if extracted + if (contextInfo) { + userMessage = userMessage + "\n\n" + contextInfo; + } + + if (state.verbose) { + infoMessage(`Code review mode activated: ${reviewContext.reviewType}`); + } + } + + // Detect refactoring requests and enhance message with context + const refactorContext = detectRefactoringRequest(userMessage); + if (refactorContext.isRefactoring) { + const refactorPrompt = getRefactoringPrompt(); + const contextInfo = buildRefactoringContext(refactorContext); + + // Inject refactoring system message before user message if not already present + const hasRefactoringPrompt = state.messages.some( + (msg) => + msg.role === "system" && msg.content.includes("refactoring mode"), + ); + + if (!hasRefactoringPrompt) { + state.messages.push({ role: "system", content: refactorPrompt }); + } + + // Append refactoring context to user message if extracted + if (contextInfo) { + userMessage = userMessage + "\n\n" + contextInfo; + } + + if (state.verbose) { + infoMessage( + `Refactoring mode activated: ${refactorContext.refactoringType}`, + ); + } + } + + // Detect memory commands + const memoryContext = detectMemoryCommand(userMessage); + if (memoryContext.isMemoryCommand) { + const result = await processMemoryCommand(memoryContext); + console.log(chalk.cyan("\n[Memory System]")); + console.log( + result.success + ? chalk.green(result.message) + : chalk.yellow(result.message), + ); + + // For store/forget commands, still send to agent for confirmation response + if ( + memoryContext.commandType === "list" || + memoryContext.commandType === "query" + ) { + // Just display results, don't send to agent + return; + } + + if (state.verbose) { + infoMessage(`Memory command: ${memoryContext.commandType}`); + } + } + + // Auto-retrieve relevant memories for context + const relevantMemoryPrompt = await buildRelevantMemoryPrompt(userMessage); + if (relevantMemoryPrompt) { + userMessage = userMessage + "\n\n" + relevantMemoryPrompt; + if (state.verbose) { + infoMessage("Relevant memories retrieved"); + } + } + + state.messages.push({ role: "user", content: userMessage }); + await addMessage("user", content); + + await initializePermissions(); + + const agent = createAgent(process.cwd(), { + provider: state.currentProvider, + model: state.currentModel, + verbose: state.verbose, + autoApprove: state.autoApprove, + onToolCall: (call) => { + console.log(chalk.cyan(`\n[Tool: ${call.name}]`)); + if (state.verbose) { + console.log(chalk.gray(JSON.stringify(call.arguments, null, 2))); + } + }, + onToolResult: (_callId, result) => { + if (result.success) { + console.log(chalk.green(`✓ ${result.title}`)); + } else { + console.log(chalk.red(`✗ ${result.title}: ${result.error}`)); + } + }, + onText: (_text) => {}, + }); + + process.stdout.write(chalk.gray(getThinkingMessage() + "\n")); + + try { + const result = await agent.run(state.messages); + + if (result.finalResponse) { + state.messages.push({ + role: "assistant", + content: result.finalResponse, + }); + await addMessage("assistant", result.finalResponse); + + console.log(chalk.bold("\nAssistant:")); + console.log(result.finalResponse); + console.log(); + + if (result.toolCalls.length > 0) { + const successful = result.toolCalls.filter( + (tc) => tc.result.success, + ).length; + infoMessage( + chalk.gray( + `Tools: ${successful}/${result.toolCalls.length} successful, ${result.iterations} iteration(s)`, + ), + ); + } + } else if (result.toolCalls.length > 0) { + const successful = result.toolCalls.filter( + (tc) => tc.result.success, + ).length; + infoMessage( + chalk.gray( + `Completed: ${successful}/${result.toolCalls.length} tools successful`, + ), + ); + } else { + warningMessage("No response received"); + } + } catch (error) { + errorMessage(`Failed: ${error}`); + } +}; diff --git a/src/commands/components/chat/models/show-models.ts b/src/commands/components/chat/models/show-models.ts new file mode 100644 index 0000000..a9c3e82 --- /dev/null +++ b/src/commands/components/chat/models/show-models.ts @@ -0,0 +1,31 @@ +import chalk from "chalk"; +import type { Provider as ProviderName } from "@/types/index"; +import { getProvider } from "@providers/index.ts"; + +export const showModels = async ( + currentProvider: ProviderName, + currentModel: string | undefined, +): Promise => { + const provider = getProvider(currentProvider); + const models = await provider.getModels(); + const activeModel = currentModel || "auto"; + const isAutoSelected = activeModel === "auto"; + + console.log(`\n${chalk.bold(provider.displayName + " Models")}\n`); + + // Show "auto" option first + const autoMarker = isAutoSelected ? chalk.cyan("→") : " "; + console.log( + `${autoMarker} ${chalk.cyan("auto")} - Let API choose the best model`, + ); + + for (const model of models) { + const isCurrent = model.id === activeModel; + const marker = isCurrent ? chalk.cyan("→") : " "; + + console.log(`${marker} ${chalk.cyan(model.id)} - ${model.name}`); + } + + console.log("\n" + chalk.gray("Use /model to switch")); + console.log(); +}; diff --git a/src/commands/components/chat/models/show-providers.ts b/src/commands/components/chat/models/show-providers.ts new file mode 100644 index 0000000..e1c2390 --- /dev/null +++ b/src/commands/components/chat/models/show-providers.ts @@ -0,0 +1,7 @@ +import { getConfig } from "@services/config"; +import { displayProvidersStatus } from "@providers/index.ts"; + +export const showProviders = async (): Promise => { + const config = await getConfig(); + await displayProvidersStatus(config.get("provider")); +}; diff --git a/src/commands/components/chat/models/switch-model.ts b/src/commands/components/chat/models/switch-model.ts new file mode 100644 index 0000000..9c6f58d --- /dev/null +++ b/src/commands/components/chat/models/switch-model.ts @@ -0,0 +1,50 @@ +import { + infoMessage, + warningMessage, + successMessage, + errorMessage, +} from "@utils/terminal"; +import { getConfig } from "@services/config"; +import { getProvider } from "@providers/index.ts"; +import { showModels } from "./show-models.ts"; +import type { ChatState } from "../state.ts"; + +export const switchModel = async ( + modelName: string, + state: ChatState, +): Promise => { + if (!modelName) { + warningMessage("Please specify a model name"); + await showModels(state.currentProvider, state.currentModel); + return; + } + + // Handle "auto" as a special case + if (modelName.toLowerCase() === "auto") { + state.currentModel = "auto"; + successMessage("Switched to model: auto (API will choose)"); + + const config = await getConfig(); + config.set("model", "auto"); + await config.save(); + return; + } + + const provider = getProvider(state.currentProvider); + const models = await provider.getModels(); + const model = models.find((m) => m.id === modelName || m.name === modelName); + + if (!model) { + errorMessage(`Model not found: ${modelName}`); + infoMessage("Use /models to see available models, or use 'auto'"); + return; + } + + state.currentModel = model.id; + successMessage(`Switched to model: ${model.name}`); + + // Persist model selection to config + const config = await getConfig(); + config.set("model", model.id); + await config.save(); +}; diff --git a/src/commands/components/chat/models/switch-provider.ts b/src/commands/components/chat/models/switch-provider.ts new file mode 100644 index 0000000..7919b5f --- /dev/null +++ b/src/commands/components/chat/models/switch-provider.ts @@ -0,0 +1,51 @@ +import type { Provider as ProviderName } from "@/types/index"; +import { + errorMessage, + warningMessage, + infoMessage, + successMessage, +} from "@utils/terminal"; +import { getConfig } from "@services/config"; +import { + getProvider, + getProviderStatus, + getDefaultModel, +} from "@providers/index.ts"; +import type { ChatState } from "../state.ts"; + +export const switchProvider = async ( + providerName: string, + state: ChatState, +): Promise => { + if (!providerName) { + warningMessage("Please specify a provider: copilot, or ollama"); + return; + } + + const validProviders = ["copilot", "ollama"]; + if (!validProviders.includes(providerName)) { + errorMessage(`Invalid provider: ${providerName}`); + infoMessage("Valid providers: " + validProviders.join(", ")); + return; + } + + const status = await getProviderStatus(providerName as ProviderName); + if (!status.valid) { + errorMessage(`Provider ${providerName} is not configured`); + infoMessage(`Run: codetyper login ${providerName}`); + return; + } + + state.currentProvider = providerName as ProviderName; + state.currentModel = undefined; + + const config = await getConfig(); + config.set("provider", providerName as ProviderName); + await config.save(); + + const provider = getProvider(state.currentProvider); + const model = getDefaultModel(state.currentProvider); + + successMessage(`Switched to ${provider.displayName}`); + infoMessage(`Using model: ${model}`); +}; diff --git a/src/commands/components/chat/print-mode.ts b/src/commands/components/chat/print-mode.ts new file mode 100644 index 0000000..09ee316 --- /dev/null +++ b/src/commands/components/chat/print-mode.ts @@ -0,0 +1,71 @@ +import chalk from "chalk"; +import { basename, extname } from "path"; +import { initializePermissions } from "@services/permissions"; +import { createAgent } from "@services/agent"; +import type { ChatState } from "@commands/components/chat/state"; +import { processFileReferences } from "@commands/components/chat/context/process-file-references"; + +export const executePrintMode = async ( + prompt: string, + state: ChatState, +): Promise => { + const processedPrompt = await processFileReferences( + prompt, + state.contextFiles, + ); + + let userMessage = processedPrompt; + if (state.contextFiles.size > 0) { + const contextParts: string[] = []; + for (const [path, fileContent] of state.contextFiles) { + const ext = extname(path).slice(1) || "txt"; + contextParts.push( + `File: ${basename(path)}\n\`\`\`${ext}\n${fileContent}\n\`\`\``, + ); + } + userMessage = contextParts.join("\n\n") + "\n\n" + processedPrompt; + } + + state.messages.push({ role: "user", content: userMessage }); + + await initializePermissions(); + + const agent = createAgent(process.cwd(), { + provider: state.currentProvider, + model: state.currentModel, + verbose: state.verbose, + autoApprove: state.autoApprove, + onToolCall: (call) => { + console.error(chalk.cyan(`[Tool: ${call.name}]`)); + }, + onToolResult: (_callId, result) => { + if (result.success) { + console.error(chalk.green(`✓ ${result.title}`)); + } else { + console.error(chalk.red(`✗ ${result.title}: ${result.error}`)); + } + }, + }); + + try { + const result = await agent.run(state.messages); + + if (result.finalResponse) { + console.log(result.finalResponse); + } + + if (state.verbose && result.toolCalls.length > 0) { + const successful = result.toolCalls.filter( + (tc) => tc.result.success, + ).length; + console.error( + chalk.gray( + `[Tools: ${successful}/${result.toolCalls.length} successful, ${result.iterations} iteration(s)]`, + ), + ); + } + } catch (error) { + console.error(chalk.red(`Error: ${error}`)); + process.exit(1); + } +}; diff --git a/src/commands/components/chat/session/list-sessions.ts b/src/commands/components/chat/session/list-sessions.ts new file mode 100644 index 0000000..988de2f --- /dev/null +++ b/src/commands/components/chat/session/list-sessions.ts @@ -0,0 +1,38 @@ +import chalk from "chalk"; +import { getSessionSummaries } from "@services/session"; +import { infoMessage } from "@utils/terminal"; + +export const listSessions = async (): Promise => { + const summaries = await getSessionSummaries(); + + if (summaries.length === 0) { + infoMessage("No saved sessions"); + return; + } + + console.log("\n" + chalk.bold("Saved Sessions:") + "\n"); + + for (const session of summaries.slice(0, 10)) { + const date = new Date(session.updatedAt).toLocaleDateString(); + const time = new Date(session.updatedAt).toLocaleTimeString(); + const preview = session.lastMessage + ? session.lastMessage.slice(0, 50).replace(/\n/g, " ") + : "(no messages)"; + + console.log(` ${chalk.cyan(session.id.slice(0, 20))}...`); + console.log( + ` ${chalk.gray(`${date} ${time}`)} - ${session.messageCount} messages`, + ); + console.log( + ` ${chalk.gray(preview)}${preview.length >= 50 ? "..." : ""}`, + ); + console.log(); + } + + if (summaries.length > 10) { + infoMessage(`... and ${summaries.length - 10} more sessions`); + } + + console.log(chalk.gray("Resume with: codetyper -r ")); + console.log(); +}; diff --git a/src/commands/components/chat/session/restore-messages.ts b/src/commands/components/chat/session/restore-messages.ts new file mode 100644 index 0000000..77ee13a --- /dev/null +++ b/src/commands/components/chat/session/restore-messages.ts @@ -0,0 +1,20 @@ +import type { ChatSession } from "@/types/index"; +import type { Message } from "@providers/index.ts"; + +export const restoreMessagesFromSession = ( + session: ChatSession, + systemPrompt: string, +): Message[] => { + const messages: Message[] = [{ role: "system", content: systemPrompt }]; + + for (const msg of session.messages) { + if (msg.role !== "system") { + messages.push({ + role: msg.role as "user" | "assistant", + content: msg.content, + }); + } + } + + return messages; +}; diff --git a/src/commands/components/chat/session/show-session-info.ts b/src/commands/components/chat/session/show-session-info.ts new file mode 100644 index 0000000..4228bfa --- /dev/null +++ b/src/commands/components/chat/session/show-session-info.ts @@ -0,0 +1,20 @@ +import chalk from "chalk"; +import { getCurrentSession } from "@services/session"; +import { warningMessage } from "@utils/terminal"; + +export const showSessionInfo = async (): Promise => { + const session = getCurrentSession(); + if (!session) { + warningMessage("No active session"); + return; + } + + console.log("\n" + chalk.bold("Session Information:")); + console.log(` ID: ${chalk.cyan(session.id)}`); + console.log(` Agent: ${session.agent}`); + console.log(` Messages: ${session.messages.length}`); + console.log(` Context files: ${session.contextFiles.length}`); + console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`); + console.log(` Updated: ${new Date(session.updatedAt).toLocaleString()}`); + console.log(); +}; diff --git a/src/commands/components/chat/state.ts b/src/commands/components/chat/state.ts new file mode 100644 index 0000000..f9c926d --- /dev/null +++ b/src/commands/components/chat/state.ts @@ -0,0 +1,34 @@ +import type { Provider as ProviderName } from "@/types/index"; +import type { Message } from "@providers/index"; +import type { InputEditorInstance } from "@ui/index"; +import { DEFAULT_SYSTEM_PROMPT } from "@prompts/index"; + +export interface ChatState { + inputEditor: InputEditorInstance | null; + isRunning: boolean; + isProcessing: boolean; + currentProvider: ProviderName; + currentModel: string | undefined; + currentAgent: string | undefined; + messages: Message[]; + contextFiles: Map; + systemPrompt: string; + verbose: boolean; + autoApprove: boolean; +} + +export const createInitialState = ( + provider: ProviderName = "copilot", +): ChatState => ({ + inputEditor: null, + isRunning: false, + isProcessing: false, + currentProvider: provider, + currentModel: undefined, + currentAgent: "coder", + messages: [], + contextFiles: new Map(), + systemPrompt: DEFAULT_SYSTEM_PROMPT, + verbose: false, + autoApprove: false, +}); diff --git a/src/commands/components/chat/usage/show-usage.ts b/src/commands/components/chat/usage/show-usage.ts new file mode 100644 index 0000000..a7719fd --- /dev/null +++ b/src/commands/components/chat/usage/show-usage.ts @@ -0,0 +1,129 @@ +/** + * Show usage statistics command + */ + +import chalk from "chalk"; +import { usageStore } from "@stores/usage-store"; +import { getUserInfo } from "@providers/copilot/credentials"; +import { getCopilotUsage } from "@providers/copilot/usage"; +import { getProvider } from "@providers/index"; +import { renderUsageBar, renderUnlimitedBar } from "@utils/progress-bar"; +import type { ChatState } from "@commands/components/chat/state"; +import type { CopilotQuotaDetail } from "@/types/copilot-usage"; + +const formatNumber = (num: number): string => { + return num.toLocaleString(); +}; + +const formatDuration = (ms: number): string => { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h ${minutes % 60}m`; + } + if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } + return `${seconds}s`; +}; + +const printQuotaBar = ( + name: string, + quota: CopilotQuotaDetail | undefined, + resetInfo?: string, +): void => { + if (!quota) { + console.log(chalk.bold(name)); + console.log(chalk.gray("N/A")); + console.log(); + return; + } + + if (quota.unlimited) { + renderUnlimitedBar(name).forEach((line) => console.log(line)); + console.log(); + return; + } + + const used = quota.entitlement - quota.remaining; + renderUsageBar(name, used, quota.entitlement, resetInfo).forEach((line) => + console.log(line), + ); + console.log(); +}; + +export const showUsage = async (state: ChatState): Promise => { + const stats = usageStore.getStats(); + const provider = getProvider(state.currentProvider); + const sessionDuration = Date.now() - stats.sessionStartTime; + + console.log(); + + // User info and quota for Copilot + if (state.currentProvider === "copilot") { + const userInfo = await getUserInfo(); + const copilotUsage = await getCopilotUsage(); + + if (copilotUsage) { + const resetDate = copilotUsage.quota_reset_date; + + printQuotaBar( + "Premium Requests", + copilotUsage.quota_snapshots.premium_interactions, + `Resets ${resetDate}`, + ); + + printQuotaBar( + "Chat", + copilotUsage.quota_snapshots.chat, + `Resets ${resetDate}`, + ); + + printQuotaBar( + "Completions", + copilotUsage.quota_snapshots.completions, + `Resets ${resetDate}`, + ); + } + + console.log(chalk.bold("Account")); + console.log(`${chalk.gray("Provider:")} ${provider.displayName}`); + console.log(`${chalk.gray("Model:")} ${state.currentModel ?? "auto"}`); + if (userInfo) { + console.log(`${chalk.gray("User:")} ${userInfo.login}`); + } + if (copilotUsage) { + console.log(`${chalk.gray("Plan:")} ${copilotUsage.copilot_plan}`); + } + console.log(); + } else { + console.log(chalk.bold("Provider")); + console.log(`${chalk.gray("Name:")} ${provider.displayName}`); + console.log(`${chalk.gray("Model:")} ${state.currentModel ?? "auto"}`); + console.log(); + } + + // Session stats with bar + console.log(chalk.bold("Current Session")); + renderUsageBar( + "Tokens", + stats.totalTokens, + stats.totalTokens || 1, + `${formatNumber(stats.promptTokens)} prompt + ${formatNumber(stats.completionTokens)} completion`, + ) + .slice(1) + .forEach((line) => console.log(line)); + console.log(`${chalk.gray("Requests:")} ${formatNumber(stats.requestCount)}`); + console.log(`${chalk.gray("Duration:")} ${formatDuration(sessionDuration)}`); + if (stats.requestCount > 0) { + const avgTokensPerRequest = Math.round( + stats.totalTokens / stats.requestCount, + ); + console.log( + `${chalk.gray("Avg tokens/request:")} ${formatNumber(avgTokensPerRequest)}`, + ); + } + console.log(); +}; diff --git a/src/commands/components/dashboard/build-config.ts b/src/commands/components/dashboard/build-config.ts new file mode 100644 index 0000000..55a2627 --- /dev/null +++ b/src/commands/components/dashboard/build-config.ts @@ -0,0 +1,25 @@ +/** + * Dashboard Config Builder + */ + +import os from "os"; +import { getConfig } from "@services/config"; +import { DASHBOARD_TITLE } from "@constants/dashboard"; +import type { DashboardConfig } from "@/types/dashboard"; + +export const buildDashboardConfig = async ( + version: string, +): Promise => { + const configMgr = await getConfig(); + const username = os.userInfo().username; + const cwd = process.cwd(); + const provider = configMgr.get("provider") as string; + + return { + title: DASHBOARD_TITLE, + version, + user: username, + cwd, + provider, + }; +}; diff --git a/src/commands/components/dashboard/display.ts b/src/commands/components/dashboard/display.ts new file mode 100644 index 0000000..3c6ecab --- /dev/null +++ b/src/commands/components/dashboard/display.ts @@ -0,0 +1,33 @@ +/** + * Dashboard Display + * + * Renders and displays the main dashboard UI. + */ + +import { DASHBOARD_LAYOUT } from "@constants/dashboard"; +import { buildDashboardConfig } from "@commands/components/dashboard/build-config"; +import { renderHeader } from "@commands/components/dashboard/render-header"; +import { renderContent } from "@commands/components/dashboard/render-content"; +import { renderFooter } from "@commands/components/dashboard/render-footer"; + +const getTerminalWidth = (): number => { + return process.stdout.columns || DASHBOARD_LAYOUT.DEFAULT_WIDTH; +}; + +const renderDashboard = async (version: string): Promise => { + const config = await buildDashboardConfig(version); + const width = getTerminalWidth(); + + const header = renderHeader(config, width); + const content = renderContent(config, width, DASHBOARD_LAYOUT.CONTENT_HEIGHT); + const footer = renderFooter(width); + + return [header, content, footer].join("\n"); +}; + +export const displayDashboard = async (version: string): Promise => { + console.clear(); + const dashboard = await renderDashboard(version); + console.log(dashboard); + process.exit(0); +}; diff --git a/src/commands/components/dashboard/render-content.ts b/src/commands/components/dashboard/render-content.ts new file mode 100644 index 0000000..d5af2d9 --- /dev/null +++ b/src/commands/components/dashboard/render-content.ts @@ -0,0 +1,41 @@ +/** + * Dashboard Content Renderer + */ + +import { DASHBOARD_LAYOUT, DASHBOARD_BORDER } from "@constants/dashboard"; +import { renderLeftContent } from "@commands/components/dashboard/render-left-content"; +import { renderRightContent } from "@commands/components/dashboard/render-right-content"; +import type { DashboardConfig } from "@/types/dashboard"; + +const padContent = (content: string[], height: number): string[] => { + const padded = [...content]; + while (padded.length < height) { + padded.push(""); + } + return padded; +}; + +export const renderContent = ( + config: DashboardConfig, + width: number, + height: number, +): string => { + const dividerPos = Math.floor(width * DASHBOARD_LAYOUT.LEFT_COLUMN_RATIO); + const leftWidth = dividerPos - DASHBOARD_LAYOUT.PADDING; + const rightWidth = width - dividerPos - DASHBOARD_LAYOUT.PADDING; + + const leftContent = padContent(renderLeftContent(config), height); + const rightContent = padContent(renderRightContent(), height); + + const lines: string[] = []; + + for (let i = 0; i < height; i++) { + const left = (leftContent[i] || "").padEnd(leftWidth); + const right = (rightContent[i] || "").padEnd(rightWidth); + lines.push( + `${DASHBOARD_BORDER.VERTICAL} ${left} ${DASHBOARD_BORDER.VERTICAL} ${right} ${DASHBOARD_BORDER.VERTICAL}`, + ); + } + + return lines.join("\n"); +}; diff --git a/src/commands/components/dashboard/render-footer.ts b/src/commands/components/dashboard/render-footer.ts new file mode 100644 index 0000000..5112054 --- /dev/null +++ b/src/commands/components/dashboard/render-footer.ts @@ -0,0 +1,31 @@ +/** + * Dashboard Footer Renderer + */ + +import chalk from "chalk"; +import { + DASHBOARD_BORDER, + DASHBOARD_QUICK_COMMANDS, +} from "@constants/dashboard"; + +export const renderFooter = (width: number): string => { + const dashCount = Math.max(0, width - 2); + const dashes = DASHBOARD_BORDER.HORIZONTAL.repeat(dashCount); + const borderLine = `${DASHBOARD_BORDER.BOTTOM_LEFT}${dashes}${DASHBOARD_BORDER.BOTTOM_RIGHT}`; + + const commandLines = DASHBOARD_QUICK_COMMANDS.map( + ({ command, description }) => + ` ${chalk.cyan(command.padEnd(18))} ${description}`, + ); + + const lines = [ + borderLine, + "", + chalk.dim("Quick Commands:"), + ...commandLines, + "", + chalk.dim("Press Ctrl+C to exit • Type 'codetyper chat' to begin"), + ]; + + return lines.join("\n"); +}; diff --git a/src/commands/components/dashboard/render-header.ts b/src/commands/components/dashboard/render-header.ts new file mode 100644 index 0000000..4c23c8e --- /dev/null +++ b/src/commands/components/dashboard/render-header.ts @@ -0,0 +1,18 @@ +/** + * Dashboard Header Renderer + */ + +import chalk from "chalk"; +import { DASHBOARD_BORDER } from "@constants/dashboard"; +import type { DashboardConfig } from "@/types/dashboard"; + +export const renderHeader = ( + config: DashboardConfig, + width: number, +): string => { + const title = ` ${config.title} ${config.version} `; + const dashCount = Math.max(0, width - title.length - 2); + const dashes = DASHBOARD_BORDER.HORIZONTAL.repeat(dashCount); + + return `${DASHBOARD_BORDER.TOP_LEFT}${DASHBOARD_BORDER.HORIZONTAL}${DASHBOARD_BORDER.HORIZONTAL}${DASHBOARD_BORDER.HORIZONTAL} ${chalk.cyan.bold(title)}${dashes}${DASHBOARD_BORDER.TOP_RIGHT}`; +}; diff --git a/src/commands/components/dashboard/render-left-content.ts b/src/commands/components/dashboard/render-left-content.ts new file mode 100644 index 0000000..d1beaf9 --- /dev/null +++ b/src/commands/components/dashboard/render-left-content.ts @@ -0,0 +1,24 @@ +/** + * Dashboard Left Content Renderer + */ + +import chalk from "chalk"; +import { DASHBOARD_LOGO } from "@constants/dashboard"; +import type { DashboardConfig } from "@/types/dashboard"; + +export const renderLeftContent = (config: DashboardConfig): string[] => { + const lines: string[] = []; + + lines.push(""); + lines.push(chalk.green(`Welcome back ${config.user}!`)); + lines.push(""); + + const coloredLogo = DASHBOARD_LOGO.map((line) => chalk.cyan.bold(line)); + lines.push(...coloredLogo); + + lines.push(""); + lines.push(chalk.cyan.bold(`${config.provider.toUpperCase()}`)); + lines.push(chalk.dim(`${config.user}@codetyper`)); + + return lines; +}; diff --git a/src/commands/components/dashboard/render-right-content.ts b/src/commands/components/dashboard/render-right-content.ts new file mode 100644 index 0000000..0f3eba4 --- /dev/null +++ b/src/commands/components/dashboard/render-right-content.ts @@ -0,0 +1,21 @@ +/** + * Dashboard Right Content Renderer + */ + +import chalk from "chalk"; +import { DASHBOARD_COMMANDS } from "@constants/dashboard"; + +export const renderRightContent = (): string[] => { + const lines: string[] = []; + + lines.push(chalk.bold("Ready to code")); + lines.push(""); + + for (const { command, description } of DASHBOARD_COMMANDS) { + lines.push(chalk.cyan(command)); + lines.push(` ${description}`); + lines.push(""); + } + + return lines; +}; diff --git a/src/commands/components/execute/execute-solid.tsx b/src/commands/components/execute/execute-solid.tsx new file mode 100644 index 0000000..16a45ba --- /dev/null +++ b/src/commands/components/execute/execute-solid.tsx @@ -0,0 +1,55 @@ +import { tui } from "@tui-solid/index"; +import { getProviderInfo } from "@services/chat-tui-service"; +import type { ChatServiceState } from "@services/chat-tui-service"; +import type { AgentConfig } from "@/types/agent-config"; +import type { PermissionScope, LearningScope } from "@/types/tui"; + +export interface RenderAppSolidProps { + sessionId: string; + handleSubmit: (message: string) => Promise; + handleCommand: (command: string) => Promise; + handleModelSelect: (model: string) => Promise; + handleAgentSelect: (agentId: string, agent: AgentConfig) => Promise; + handleThemeSelect: (theme: string) => void; + handlePermissionResponse?: ( + allowed: boolean, + scope?: PermissionScope, + ) => void; + handleLearningResponse?: ( + save: boolean, + scope?: LearningScope, + editedContent?: string, + ) => void; + handleExit: () => void; + showBanner: boolean; + state: ChatServiceState; + plan?: { + id: string; + title: string; + items: Array<{ id: string; text: string; completed: boolean }>; + } | null; +} + +export const renderAppSolid = async ( + props: RenderAppSolidProps, +): Promise => { + const { displayName, model: defaultModel } = getProviderInfo( + props.state.provider, + ); + const currentModel = props.state.model ?? defaultModel; + + await tui({ + sessionId: props.sessionId, + provider: displayName, + model: currentModel, + onSubmit: props.handleSubmit, + onCommand: props.handleCommand, + onModelSelect: props.handleModelSelect, + onThemeSelect: props.handleThemeSelect, + onPermissionResponse: props.handlePermissionResponse ?? (() => {}), + onLearningResponse: props.handleLearningResponse ?? (() => {}), + plan: props.plan, + }); + + props.handleExit(); +}; diff --git a/src/commands/components/execute/execute.tsx b/src/commands/components/execute/execute.tsx new file mode 100644 index 0000000..3c30ac4 --- /dev/null +++ b/src/commands/components/execute/execute.tsx @@ -0,0 +1,104 @@ +import { tui, appStore } from "@tui/index"; +import { getProviderInfo } from "@services/chat-tui-service"; +import { addServer, connectServer } from "@services/mcp/index"; +import type { ChatServiceState } from "@services/chat-tui-service"; +import type { AgentConfig } from "@/types/agent-config"; +import type { PermissionScope, LearningScope } from "@/types/tui"; +import type { ProviderModel } from "@/types/providers"; +import type { MCPAddFormData } from "@/types/mcp"; + +interface AgentOption { + id: string; + name: string; + description?: string; +} + +export interface RenderAppProps { + sessionId?: string; + handleSubmit: (message: string) => Promise; + handleCommand: (command: string) => Promise; + handleModelSelect: (model: string) => Promise; + handleAgentSelect: (agentId: string, agent: AgentConfig) => Promise; + handleThemeSelect: (theme: string) => void; + handleProviderSelect?: (providerId: string) => Promise; + handleCascadeToggle?: (enabled: boolean) => Promise; + handleMCPAdd?: (data: MCPAddFormData) => Promise; + handlePermissionResponse?: ( + allowed: boolean, + scope?: PermissionScope, + ) => void; + handleLearningResponse?: ( + save: boolean, + scope?: LearningScope, + editedContent?: string, + ) => void; + handleExit: () => void; + showBanner: boolean; + state: ChatServiceState; + availableModels?: ProviderModel[]; + agents?: AgentOption[]; + initialPrompt?: string; + theme?: string; + cascadeEnabled?: boolean; + plan?: { + id: string; + title: string; + items: Array<{ id: string; text: string; completed: boolean }>; + } | null; +} + +const defaultHandleMCPAdd = async (data: MCPAddFormData): Promise => { + const serverArgs = data.args.trim() + ? data.args.trim().split(/\s+/) + : undefined; + + await addServer( + data.name, + { + command: data.command, + args: serverArgs, + enabled: true, + }, + data.isGlobal, + ); + + await connectServer(data.name); +}; + +export const renderApp = async (props: RenderAppProps): Promise => { + const { displayName, model: defaultModel } = getProviderInfo( + props.state.provider, + ); + const currentModel = props.state.model ?? defaultModel; + + await tui({ + sessionId: props.sessionId, + provider: displayName, + model: currentModel, + theme: props.theme, + cascadeEnabled: props.cascadeEnabled, + availableModels: props.availableModels, + agents: props.agents, + initialPrompt: props.initialPrompt, + onSubmit: props.handleSubmit, + onCommand: props.handleCommand, + onModelSelect: props.handleModelSelect, + onThemeSelect: props.handleThemeSelect, + onProviderSelect: props.handleProviderSelect, + onCascadeToggle: props.handleCascadeToggle, + onAgentSelect: async (agentId: string) => { + const agent = props.agents?.find((a) => a.id === agentId); + if (agent) { + await props.handleAgentSelect(agentId, agent as AgentConfig); + } + }, + onMCPAdd: props.handleMCPAdd ?? defaultHandleMCPAdd, + onPermissionResponse: props.handlePermissionResponse ?? (() => {}), + onLearningResponse: props.handleLearningResponse ?? (() => {}), + plan: props.plan, + }); + + props.handleExit(); +}; + +export { appStore }; diff --git a/src/commands/components/execute/index.ts b/src/commands/components/execute/index.ts new file mode 100644 index 0000000..574ec03 --- /dev/null +++ b/src/commands/components/execute/index.ts @@ -0,0 +1,195 @@ +import { renderApp, appStore } from "@commands/components/execute/execute"; +import type { RenderAppProps } from "@commands/components/execute/execute"; +import { + initializeChatService, + loadModels, + handleModelSelect as serviceHandleModelSelect, + executePrintMode, + setupPermissionHandler, + cleanupPermissionHandler, + executeCommand, + handleMessage, +} from "@services/chat-tui-service"; +import type { ChatServiceState } from "@services/chat-tui-service"; +import type { ChatTUIOptions } from "@interfaces/ChatTUIOptions"; +import type { AgentConfig } from "@/types/agent-config"; +import { getConfig } from "@services/config"; +import { getThinkingMessage } from "@constants/status-messages"; +import { + enterFullscreen, + registerExitHandlers, + exitFullscreen, + clearScreen, +} from "@utils/terminal"; +import { createCallbacks } from "@commands/chat-tui"; +import { agentLoader } from "@services/agent-loader"; + +interface ExecuteContext { + state: ChatServiceState | null; +} + +const createHandleExit = (): (() => void) => (): void => { + cleanupPermissionHandler(); + exitFullscreen(); + clearScreen(); + console.log("Goodbye!"); + process.exit(0); +}; + +const createHandleModelSelect = + (ctx: ExecuteContext) => + async (model: string): Promise => { + if (!ctx.state) return; + await serviceHandleModelSelect(ctx.state, model, createCallbacks()); + }; + +const createHandleAgentSelect = + (ctx: ExecuteContext) => + async (agentId: string, agent: AgentConfig): Promise => { + if (!ctx.state) return; + + (ctx.state as ChatServiceState & { currentAgent?: string }).currentAgent = + agentId; + + if (agent.prompt) { + const basePrompt = ctx.state.systemPrompt; + ctx.state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`; + + if ( + ctx.state.messages.length > 0 && + ctx.state.messages[0].role === "system" + ) { + ctx.state.messages[0].content = ctx.state.systemPrompt; + } + } + }; + +const createHandleThemeSelect = + () => + (themeName: string): void => { + getConfig().then((config) => { + config.set("theme", themeName); + config.save(); + }); + }; + +const createHandleProviderSelect = + (ctx: ExecuteContext) => + async (providerId: string): Promise => { + if (!ctx.state) return; + ctx.state.provider = providerId as "copilot" | "ollama"; + const config = await getConfig(); + config.set("provider", providerId as "copilot" | "ollama"); + await config.save(); + }; + +const createHandleCascadeToggle = + () => + async (enabled: boolean): Promise => { + const config = await getConfig(); + config.set("cascadeEnabled", enabled); + await config.save(); + }; + +const createHandleCommand = + (ctx: ExecuteContext, handleExit: () => void) => + async (command: string): Promise => { + if (!ctx.state) return; + + if (["exit", "quit", "q"].includes(command.toLowerCase())) { + handleExit(); + return; + } + + await executeCommand(ctx.state, command, createCallbacks()); + }; + +const createHandleSubmit = + (ctx: ExecuteContext, handleCommand: (command: string) => Promise) => + async (message: string): Promise => { + if (!ctx.state) return; + + if (message.startsWith("/")) { + const [command] = message.slice(1).split(/\s+/); + await handleCommand(command); + return; + } + + // Set initial thinking message (streaming will update this) + appStore.setThinkingMessage(getThinkingMessage()); + + try { + await handleMessage(ctx.state, message, createCallbacks()); + } finally { + // Clean up any remaining state after message handling + appStore.setThinkingMessage(null); + appStore.setCurrentToolCall(null); + appStore.setMode("idle"); + } + }; + +const execute = async (options: ChatTUIOptions): Promise => { + const ctx: ExecuteContext = { + state: null, + }; + + const { state, session } = await initializeChatService(options); + ctx.state = state; + + if (options.printMode && options.initialPrompt) { + await executePrintMode(state, options.initialPrompt); + return; + } + + setupPermissionHandler(); + + const models = await loadModels(state.provider); + const agents = await agentLoader.getAvailableAgents(process.cwd()); + const config = await getConfig(); + const savedTheme = config.get("theme"); + + // Register exit handlers to ensure terminal cleanup on abrupt termination + registerExitHandlers(); + enterFullscreen(); + + const handleExit = createHandleExit(); + const handleModelSelectFn = createHandleModelSelect(ctx); + const handleAgentSelectFn = createHandleAgentSelect(ctx); + const handleThemeSelectFn = createHandleThemeSelect(); + const handleProviderSelectFn = createHandleProviderSelect(ctx); + const handleCascadeToggleFn = createHandleCascadeToggle(); + const handleCommand = createHandleCommand(ctx, handleExit); + const handleSubmit = createHandleSubmit(ctx, handleCommand); + + // Only pass sessionId if resuming/continuing - otherwise show Home view first + const isResuming = options.continueSession || options.resumeSession; + + const savedCascadeEnabled = config.get("cascadeEnabled"); + + const renderProps: RenderAppProps = { + sessionId: isResuming ? session.id : undefined, + handleSubmit, + handleCommand, + handleModelSelect: handleModelSelectFn, + handleAgentSelect: handleAgentSelectFn, + handleThemeSelect: handleThemeSelectFn, + handleProviderSelect: handleProviderSelectFn, + handleCascadeToggle: handleCascadeToggleFn, + handleExit, + showBanner: true, + state, + availableModels: models, + agents: agents.map((a) => ({ + id: a.id, + name: a.name, + description: a.description, + })), + initialPrompt: options.initialPrompt, + theme: savedTheme, + cascadeEnabled: savedCascadeEnabled ?? true, + }; + + await renderApp(renderProps); +}; + +export default execute; diff --git a/src/commands/dashboard.ts b/src/commands/dashboard.ts new file mode 100644 index 0000000..a0ff8d1 --- /dev/null +++ b/src/commands/dashboard.ts @@ -0,0 +1,8 @@ +/** + * Dashboard Command + * + * Re-exports the modular dashboard implementation. + */ + +export { displayDashboard } from "@commands/components/dashboard/display"; +export type { DashboardConfig } from "@/types/dashboard"; diff --git a/src/commands/handlers.ts b/src/commands/handlers.ts new file mode 100644 index 0000000..b5e0f87 --- /dev/null +++ b/src/commands/handlers.ts @@ -0,0 +1,25 @@ +/** + * Command handlers - Route commands to appropriate implementations + */ + +import { errorMessage } from "@utils/terminal"; +import { COMMAND_REGISTRY, isValidCommand } from "@commands/handlers/registry"; +import type { CommandOptions } from "@/types/index"; + +export const handleCommand = async ( + command: string, + options: CommandOptions, +): Promise => { + try { + if (!isValidCommand(command)) { + errorMessage(`Unknown command: ${command}`); + process.exit(1); + } + + const handler = COMMAND_REGISTRY[command]; + await handler(options); + } catch (error) { + errorMessage(`Command failed: ${error}`); + throw error; + } +}; diff --git a/src/commands/handlers/chat.ts b/src/commands/handlers/chat.ts new file mode 100644 index 0000000..8b550c9 --- /dev/null +++ b/src/commands/handlers/chat.ts @@ -0,0 +1,10 @@ +/** + * Chat command handler + */ + +import { execute as executeChat } from "@commands/chat"; +import type { CommandOptions } from "@/types/index"; + +export const handleChat = async (options: CommandOptions): Promise => { + await executeChat(options); +}; diff --git a/src/commands/handlers/classify.ts b/src/commands/handlers/classify.ts new file mode 100644 index 0000000..64c0413 --- /dev/null +++ b/src/commands/handlers/classify.ts @@ -0,0 +1,123 @@ +/** + * Classify command handler + */ + +import chalk from "chalk"; +import { + succeedSpinner, + startSpinner, + errorMessage, + failSpinner, + headerMessage, +} from "@utils/terminal"; +import { + INTENT_KEYWORDS, + CLASSIFICATION_CONFIDENCE, +} from "@constants/handlers"; +import type { + CommandOptions, + IntentRequest, + IntentResponse, +} from "@/types/index"; + +const classifyIntent = async ( + request: IntentRequest, +): Promise => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const prompt = request.prompt.toLowerCase(); + let intent: IntentResponse["intent"] = "ask"; + let confidence: number = CLASSIFICATION_CONFIDENCE.DEFAULT; + + const intentMatchers: Record void> = { + fix: () => { + intent = "fix"; + confidence = CLASSIFICATION_CONFIDENCE.HIGH; + }, + test: () => { + intent = "test"; + confidence = CLASSIFICATION_CONFIDENCE.MEDIUM; + }, + refactor: () => { + intent = "refactor"; + confidence = CLASSIFICATION_CONFIDENCE.LOW; + }, + code: () => { + intent = "code"; + confidence = CLASSIFICATION_CONFIDENCE.DEFAULT; + }, + document: () => { + intent = "document"; + confidence = CLASSIFICATION_CONFIDENCE.HIGH; + }, + }; + + for (const [intentKey, keywords] of Object.entries(INTENT_KEYWORDS)) { + const hasMatch = keywords.some((keyword) => prompt.includes(keyword)); + if (hasMatch) { + intentMatchers[intentKey]?.(); + break; + } + } + + return { + intent, + confidence, + reasoning: `Based on keywords in the prompt, this appears to be a ${intent} request.`, + needsClarification: confidence < CLASSIFICATION_CONFIDENCE.THRESHOLD, + clarificationQuestions: + confidence < CLASSIFICATION_CONFIDENCE.THRESHOLD + ? [ + "Which specific files should I focus on?", + "What is the expected outcome?", + ] + : undefined, + }; +}; + +export const handleClassify = async ( + options: CommandOptions, +): Promise => { + const { prompt, context, files = [] } = options; + + if (!prompt) { + errorMessage("Prompt is required"); + return; + } + + headerMessage("Classifying Intent"); + console.log(chalk.bold("Prompt:") + ` ${prompt}`); + if (context) { + console.log(chalk.bold("Context:") + ` ${context}`); + } + if (files.length > 0) { + console.log(chalk.bold("Files:") + ` ${files.join(", ")}`); + } + console.log(); + + startSpinner("Analyzing prompt..."); + + try { + const result = await classifyIntent({ prompt, context, files }); + succeedSpinner("Analysis complete"); + + console.log(); + console.log(chalk.bold("Intent:") + ` ${chalk.cyan(result.intent)}`); + console.log( + chalk.bold("Confidence:") + + ` ${chalk.green((result.confidence * 100).toFixed(1) + "%")}`, + ); + console.log(chalk.bold("Reasoning:") + ` ${result.reasoning}`); + + if (result.needsClarification && result.clarificationQuestions) { + console.log(); + console.log(chalk.yellow.bold("Clarification needed:")); + result.clarificationQuestions.forEach((q, i) => { + console.log(` ${i + 1}. ${q}`); + }); + } + } catch (error) { + failSpinner("Classification failed"); + throw error; + } +}; diff --git a/src/commands/handlers/config.ts b/src/commands/handlers/config.ts new file mode 100644 index 0000000..27076b3 --- /dev/null +++ b/src/commands/handlers/config.ts @@ -0,0 +1,113 @@ +/** + * Config command handler + */ + +import { + errorMessage, + filePath, + successMessage, + hightLigthedJson, + headerMessage, + infoMessage, +} from "@utils/terminal"; +import { getConfig } from "@services/config"; +import { + VALID_CONFIG_KEYS, + VALID_PROVIDERS, + CONFIG_VALIDATION, +} from "@constants/handlers"; +import type { CommandOptions, Provider } from "@/types/index"; +import type { ConfigAction, ConfigKey } from "@/types/handlers"; + +type ConfigActionHandler = (key?: string, value?: string) => Promise; + +const showConfig = async (): Promise => { + const config = await getConfig(); + headerMessage("Configuration"); + const allConfig = config.getAll(); + hightLigthedJson(allConfig); +}; + +const showPath = async (): Promise => { + const config = await getConfig(); + const configPath = config.getConfigPath(); + console.log(filePath(configPath)); +}; + +const setConfigValue = async (key?: string, value?: string): Promise => { + if (!key || value === undefined) { + errorMessage("Key and value are required"); + return; + } + + if (!VALID_CONFIG_KEYS.includes(key as ConfigKey)) { + errorMessage(`Invalid config key: ${key}`); + infoMessage(`Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`); + return; + } + + const config = await getConfig(); + + const keySetters: Record boolean> = { + provider: () => { + if (!VALID_PROVIDERS.includes(value as Provider)) { + errorMessage(`Invalid provider: ${value}`); + infoMessage(`Valid providers: ${VALID_PROVIDERS.join(", ")}`); + return false; + } + config.set("provider", value as Provider); + return true; + }, + model: () => { + config.set("model", value); + return true; + }, + maxIterations: () => { + const num = parseInt(value, 10); + if (isNaN(num) || num < CONFIG_VALIDATION.MIN_ITERATIONS) { + errorMessage("maxIterations must be a positive number"); + return false; + } + config.set("maxIterations", num); + return true; + }, + timeout: () => { + const num = parseInt(value, 10); + if (isNaN(num) || num < CONFIG_VALIDATION.MIN_TIMEOUT_MS) { + errorMessage( + `timeout must be at least ${CONFIG_VALIDATION.MIN_TIMEOUT_MS}ms`, + ); + return false; + } + config.set("timeout", num); + return true; + }, + }; + + const setter = keySetters[key as ConfigKey]; + const success = setter(); + + if (success) { + await config.save(); + successMessage(`Set ${key} = ${value}`); + } +}; + +const CONFIG_ACTION_HANDLERS: Record = { + show: showConfig, + path: showPath, + set: setConfigValue, +}; + +export const handleConfig = async (options: CommandOptions): Promise => { + const { action, key, value } = options; + + const handler = CONFIG_ACTION_HANDLERS[action as ConfigAction]; + + if (!handler) { + errorMessage(`Unknown config action: ${action}`); + return; + } + + await handler(key, value); +}; diff --git a/src/commands/handlers/plan.ts b/src/commands/handlers/plan.ts new file mode 100644 index 0000000..b43cbfa --- /dev/null +++ b/src/commands/handlers/plan.ts @@ -0,0 +1,62 @@ +/** + * Plan command handler + */ + +import chalk from "chalk"; +import { + hightLigthedJson, + filePath, + errorMessage, + failSpinner, + headerMessage, + startSpinner, + succeedSpinner, + successMessage, +} from "@utils/terminal"; +import type { CommandOptions } from "@/types/index"; + +export const handlePlan = async (options: CommandOptions): Promise => { + const { intent, task, files = [], output } = options; + + if (!task) { + errorMessage("Task description is required"); + return; + } + + headerMessage("Generating Plan"); + console.log(chalk.bold("Intent:") + ` ${chalk.cyan(intent || "unknown")}`); + console.log(chalk.bold("Task:") + ` ${task}`); + if (files.length > 0) { + console.log(chalk.bold("Files:") + ` ${files.join(", ")}`); + } + console.log(); + + startSpinner("Generating execution plan..."); + + try { + await new Promise((resolve) => setTimeout(resolve, 1500)); + succeedSpinner("Plan generated"); + + const plan = { + intent, + task, + files, + steps: [ + { id: "step_1", type: "read", description: "Analyze existing code" }, + { id: "step_2", type: "edit", description: "Apply changes" }, + { id: "step_3", type: "execute", description: "Run tests" }, + ], + }; + + if (output) { + const fs = await import("fs/promises"); + await fs.writeFile(output, JSON.stringify(plan, null, 2)); + successMessage(`Plan saved to ${filePath(output)}`); + } else { + hightLigthedJson(plan); + } + } catch (error) { + failSpinner("Plan generation failed"); + throw error; + } +}; diff --git a/src/commands/handlers/registry.ts b/src/commands/handlers/registry.ts new file mode 100644 index 0000000..d719669 --- /dev/null +++ b/src/commands/handlers/registry.ts @@ -0,0 +1,28 @@ +/** + * Command handler registry - object-based routing + */ + +import { handleChat } from "@commands/handlers/chat"; +import { handleRun } from "@commands/handlers/run"; +import { handleClassify } from "@commands/handlers/classify"; +import { handlePlan } from "@commands/handlers/plan"; +import { handleValidate } from "@commands/handlers/validate"; +import { handleConfig } from "@commands/handlers/config"; +import { handleServe } from "@commands/handlers/serve"; +import type { CommandRegistry } from "@/types/handlers"; + +export const COMMAND_REGISTRY: CommandRegistry = { + chat: handleChat, + run: handleRun, + classify: handleClassify, + plan: handlePlan, + validate: handleValidate, + config: handleConfig, + serve: handleServe, +}; + +export const isValidCommand = ( + command: string, +): command is keyof CommandRegistry => { + return command in COMMAND_REGISTRY; +}; diff --git a/src/commands/handlers/run.ts b/src/commands/handlers/run.ts new file mode 100644 index 0000000..798dd4f --- /dev/null +++ b/src/commands/handlers/run.ts @@ -0,0 +1,10 @@ +/** + * Run command handler + */ + +import { execute } from "@commands/runner"; +import type { CommandOptions } from "@/types/index"; + +export const handleRun = async (options: CommandOptions): Promise => { + await execute(options); +}; diff --git a/src/commands/handlers/serve.ts b/src/commands/handlers/serve.ts new file mode 100644 index 0000000..f5007b3 --- /dev/null +++ b/src/commands/handlers/serve.ts @@ -0,0 +1,15 @@ +/** + * Serve command handler + */ + +import { boxMessage, warningMessage, infoMessage } from "@utils/terminal"; +import type { CommandOptions } from "@/types/index"; +import { SERVER_INFO } from "@constants/serve"; + +export const handleServe = async (_options: CommandOptions): Promise => { + boxMessage(SERVER_INFO, "Server Mode"); + warningMessage("Server mode not yet implemented"); + infoMessage( + "This will integrate with the existing agent/main.py JSON-RPC server", + ); +}; diff --git a/src/commands/handlers/validate.ts b/src/commands/handlers/validate.ts new file mode 100644 index 0000000..c4f873d --- /dev/null +++ b/src/commands/handlers/validate.ts @@ -0,0 +1,78 @@ +/** + * Validate command handler + */ + +import chalk from "chalk"; +import { + failSpinner, + warningMessage, + successMessage, + succeedSpinner, + startSpinner, + errorMessage, + headerMessage, + filePath, +} from "@utils/terminal"; +import { getConfig } from "@services/config"; +import type { CommandOptions } from "@/types/index"; + +export const handleValidate = async ( + options: CommandOptions, +): Promise => { + const { planFile } = options; + + if (!planFile) { + errorMessage("Plan file is required"); + return; + } + + headerMessage("Validating Plan"); + console.log(chalk.bold("Plan file:") + ` ${filePath(planFile)}`); + console.log(); + + startSpinner("Validating plan..."); + + try { + const fs = await import("fs/promises"); + const planData = await fs.readFile(planFile, "utf-8"); + const plan = JSON.parse(planData); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const config = await getConfig(); + + const warnings: string[] = []; + const errors: string[] = []; + + plan.files?.forEach((file: string) => { + if (config.isProtectedPath(file)) { + warnings.push(`Protected path: ${file}`); + } + }); + + succeedSpinner("Validation complete"); + + console.log(); + if (errors.length > 0) { + console.log(chalk.red.bold("Errors:")); + errors.forEach((err) => console.log(` - ${err}`)); + } + + if (warnings.length > 0) { + console.log(chalk.yellow.bold("Warnings:")); + warnings.forEach((warn) => console.log(` - ${warn}`)); + } + + if (errors.length === 0 && warnings.length === 0) { + successMessage("Plan is valid and safe to execute"); + } else if (errors.length > 0) { + errorMessage("Plan has errors and cannot be executed"); + process.exit(1); + } else { + warningMessage("Plan has warnings - proceed with caution"); + } + } catch (error) { + failSpinner("Validation failed"); + throw error; + } +}; diff --git a/src/commands/mcp.ts b/src/commands/mcp.ts new file mode 100644 index 0000000..994b735 --- /dev/null +++ b/src/commands/mcp.ts @@ -0,0 +1,319 @@ +/** + * MCP Command - Manage MCP servers + * + * Usage: + * codetyper mcp list - List configured servers + * codetyper mcp add - Add a new server + * codetyper mcp remove - Remove a server + * codetyper mcp connect [name] - Connect to server(s) + * codetyper mcp disconnect [name] - Disconnect from server(s) + * codetyper mcp status - Show connection status + * codetyper mcp tools - List available tools + */ + +import chalk from "chalk"; +import { errorMessage, infoMessage, successMessage } from "@utils/terminal"; +import { + initializeMCP, + getMCPConfig, + addServer, + removeServer, + connectServer, + disconnectServer, + connectAllServers, + disconnectAllServers, + getServerInstances, + getAllTools, +} from "@services/mcp/index"; + +/** + * MCP command handler + */ +export const mcpCommand = async (args: string[]): Promise => { + const subcommand = args[0] || "status"; + + const handlers: Record Promise> = { + list: handleList, + add: handleAdd, + remove: handleRemove, + connect: handleConnect, + disconnect: handleDisconnect, + status: handleStatus, + tools: handleTools, + help: handleHelp, + }; + + const handler = handlers[subcommand]; + if (!handler) { + errorMessage(`Unknown subcommand: ${subcommand}`); + await handleHelp([]); + return; + } + + await handler(args.slice(1)); +}; + +/** + * List configured servers + */ +const handleList = async (_args: string[]): Promise => { + await initializeMCP(); + const config = await getMCPConfig(); + + const servers = Object.entries(config.servers); + if (servers.length === 0) { + infoMessage("No MCP servers configured."); + infoMessage("Add a server with: codetyper mcp add "); + return; + } + + console.log(chalk.bold("\nConfigured MCP Servers:\n")); + + for (const [name, server] of servers) { + const enabled = + server.enabled !== false ? chalk.green("✓") : chalk.gray("○"); + console.log(` ${enabled} ${chalk.cyan(name)}`); + console.log( + ` Command: ${server.command} ${(server.args || []).join(" ")}`, + ); + if (server.transport && server.transport !== "stdio") { + console.log(` Transport: ${server.transport}`); + } + console.log(); + } +}; + +/** + * Add a new server + */ +const handleAdd = async (args: string[]): Promise => { + const name = args[0]; + if (!name) { + errorMessage("Server name required"); + infoMessage( + "Usage: codetyper mcp add --command [--args ]", + ); + return; + } + + // Parse options + let command = ""; + const serverArgs: string[] = []; + let isGlobal = false; + + for (let i = 1; i < args.length; i++) { + const arg = args[i]; + if (arg === "--command" || arg === "-c") { + command = args[++i] || ""; + } else if (arg === "--args" || arg === "-a") { + // Collect remaining args + while (args[i + 1] && !args[i + 1].startsWith("--")) { + serverArgs.push(args[++i]); + } + } else if (arg === "--global" || arg === "-g") { + isGlobal = true; + } + } + + if (!command) { + // Interactive mode - ask for command + infoMessage("Adding MCP server interactively..."); + infoMessage("Example: npx @modelcontextprotocol/server-sqlite"); + + // For now, require command flag + errorMessage("Command required. Use --command "); + return; + } + + try { + await addServer( + name, + { + command, + args: serverArgs.length > 0 ? serverArgs : undefined, + enabled: true, + }, + isGlobal, + ); + + successMessage(`Added MCP server: ${name}`); + infoMessage(`Connect with: codetyper mcp connect ${name}`); + } catch (err) { + errorMessage(`Failed to add server: ${err}`); + } +}; + +/** + * Remove a server + */ +const handleRemove = async (args: string[]): Promise => { + const name = args[0]; + if (!name) { + errorMessage("Server name required"); + return; + } + + const isGlobal = args.includes("--global") || args.includes("-g"); + + try { + await removeServer(name, isGlobal); + successMessage(`Removed MCP server: ${name}`); + } catch (err) { + errorMessage(`Failed to remove server: ${err}`); + } +}; + +/** + * Connect to server(s) + */ +const handleConnect = async (args: string[]): Promise => { + const name = args[0]; + + if (name) { + // Connect to specific server + try { + infoMessage(`Connecting to ${name}...`); + const instance = await connectServer(name); + successMessage(`Connected to ${name}`); + console.log(` Tools: ${instance.tools.length}`); + console.log(` Resources: ${instance.resources.length}`); + } catch (err) { + errorMessage(`Failed to connect: ${err}`); + } + } else { + // Connect to all servers + infoMessage("Connecting to all servers..."); + const results = await connectAllServers(); + + for (const [serverName, instance] of results) { + if (instance.state === "connected") { + successMessage( + `${serverName}: Connected (${instance.tools.length} tools)`, + ); + } else { + errorMessage(`${serverName}: ${instance.error || "Failed"}`); + } + } + } +}; + +/** + * Disconnect from server(s) + */ +const handleDisconnect = async (args: string[]): Promise => { + const name = args[0]; + + if (name) { + await disconnectServer(name); + successMessage(`Disconnected from ${name}`); + } else { + await disconnectAllServers(); + successMessage("Disconnected from all servers"); + } +}; + +/** + * Show connection status + */ +const handleStatus = async (_args: string[]): Promise => { + await initializeMCP(); + const instances = getServerInstances(); + + if (instances.size === 0) { + infoMessage("No MCP servers configured."); + return; + } + + console.log(chalk.bold("\nMCP Server Status:\n")); + + for (const [name, instance] of instances) { + const stateColors: Record string> = { + connected: chalk.green, + connecting: chalk.yellow, + disconnected: chalk.gray, + error: chalk.red, + }; + + const colorFn = stateColors[instance.state] || chalk.white; + const status = colorFn(instance.state.toUpperCase()); + + console.log(` ${chalk.cyan(name)}: ${status}`); + + if (instance.state === "connected") { + console.log(` Tools: ${instance.tools.length}`); + console.log(` Resources: ${instance.resources.length}`); + } + + if (instance.error) { + console.log(` Error: ${chalk.red(instance.error)}`); + } + + console.log(); + } +}; + +/** + * List available tools + */ +const handleTools = async (_args: string[]): Promise => { + await connectAllServers(); + const tools = getAllTools(); + + if (tools.length === 0) { + infoMessage("No tools available. Connect to MCP servers first."); + return; + } + + console.log(chalk.bold("\nAvailable MCP Tools:\n")); + + // Group by server + const byServer = new Map(); + for (const item of tools) { + const existing = byServer.get(item.server) || []; + existing.push(item); + byServer.set(item.server, existing); + } + + for (const [server, serverTools] of byServer) { + console.log(chalk.cyan(` ${server}:`)); + for (const { tool } of serverTools) { + console.log(` - ${chalk.white(tool.name)}`); + if (tool.description) { + console.log(` ${chalk.gray(tool.description)}`); + } + } + console.log(); + } +}; + +/** + * Show help + */ +const handleHelp = async (_args: string[]): Promise => { + console.log(` +${chalk.bold("MCP (Model Context Protocol) Management")} + +${chalk.cyan("Usage:")} + codetyper mcp [options] + +${chalk.cyan("Commands:")} + list List configured servers + add Add a new server + --command, -c Command to run + --args, -a Arguments for command + --global, -g Add to global config + remove Remove a server + --global, -g Remove from global config + connect [name] Connect to server(s) + disconnect [name] Disconnect from server(s) + status Show connection status + tools List available tools from connected servers + +${chalk.cyan("Examples:")} + codetyper mcp add sqlite -c npx -a @modelcontextprotocol/server-sqlite + codetyper mcp connect sqlite + codetyper mcp tools +`); +}; + +export default mcpCommand; diff --git a/src/commands/runner.ts b/src/commands/runner.ts new file mode 100644 index 0000000..d4b1cfe --- /dev/null +++ b/src/commands/runner.ts @@ -0,0 +1,10 @@ +/** + * Autonomous task runner - executes agent tasks + */ + +export { execute } from "@commands/runner/execute"; +export { createPlan } from "@commands/runner/create-plan"; +export { executePlan } from "@commands/runner/execute-plan"; +export { displayPlan, getStepIcon } from "@commands/runner/display-plan"; +export { displayHeader } from "@commands/runner/display-header"; +export { delay } from "@commands/runner/utils"; diff --git a/src/commands/runner/create-plan.ts b/src/commands/runner/create-plan.ts new file mode 100644 index 0000000..7c317bd --- /dev/null +++ b/src/commands/runner/create-plan.ts @@ -0,0 +1,45 @@ +/** + * Plan creation utilities + */ + +import { + RUNNER_DELAYS, + MOCK_STEPS, + DEFAULT_FILE, + ESTIMATED_TIME_PER_STEP, +} from "@constants/runner"; +import { delay } from "@commands/runner/utils"; +import type { AgentType, ExecutionPlan, PlanStep } from "@/types/index"; + +export const createPlan = async ( + task: string, + _agent: AgentType, + files: string[], +): Promise => { + await delay(RUNNER_DELAYS.PLANNING); + + const targetFile = files[0] ?? DEFAULT_FILE; + + const steps: PlanStep[] = [ + { + ...MOCK_STEPS.READ, + file: targetFile, + }, + { + ...MOCK_STEPS.EDIT, + file: targetFile, + dependencies: [...MOCK_STEPS.EDIT.dependencies], + }, + { + ...MOCK_STEPS.EXECUTE, + dependencies: [...MOCK_STEPS.EXECUTE.dependencies], + }, + ]; + + return { + steps, + intent: "code", + summary: task, + estimatedTime: steps.length * ESTIMATED_TIME_PER_STEP, + }; +}; diff --git a/src/commands/runner/display-header.ts b/src/commands/runner/display-header.ts new file mode 100644 index 0000000..19668f4 --- /dev/null +++ b/src/commands/runner/display-header.ts @@ -0,0 +1,27 @@ +/** + * Runner header display utilities + */ + +import chalk from "chalk"; +import { headerMessage, filePath } from "@utils/terminal"; +import type { RunnerOptions } from "@/types/runner"; + +export const displayHeader = (options: RunnerOptions): void => { + const { task, agent, files, dryRun } = options; + + headerMessage("Running Task"); + console.log(chalk.bold("Agent:") + ` ${chalk.cyan(agent)}`); + console.log(chalk.bold("Task:") + ` ${task}`); + + if (files.length > 0) { + console.log( + chalk.bold("Files:") + ` ${files.map((f) => filePath(f)).join(", ")}`, + ); + } + + console.log( + chalk.bold("Mode:") + + ` ${dryRun ? chalk.yellow("Dry Run") : chalk.green("Execute")}`, + ); + console.log(); +}; diff --git a/src/commands/runner/display-plan.ts b/src/commands/runner/display-plan.ts new file mode 100644 index 0000000..b5e9ae5 --- /dev/null +++ b/src/commands/runner/display-plan.ts @@ -0,0 +1,34 @@ +/** + * Plan display utilities + */ + +import chalk from "chalk"; +import { filePath } from "@utils/terminal"; +import { STEP_ICONS, DEFAULT_STEP_ICON } from "@constants/runner"; +import type { ExecutionPlan, PlanStep } from "@/types/index"; + +export const getStepIcon = (type: PlanStep["type"]): string => + STEP_ICONS[type] ?? DEFAULT_STEP_ICON; + +export const displayPlan = (plan: ExecutionPlan): void => { + console.log("\n" + chalk.bold.underline("Execution Plan:")); + console.log(chalk.gray(`${plan.summary}`)); + console.log(); + + plan.steps.forEach((step, index) => { + const icon = getStepIcon(step.type); + const deps = step.dependencies + ? chalk.gray(` (depends on: ${step.dependencies.join(", ")})`) + : ""; + console.log( + `${icon} ${chalk.bold(`Step ${index + 1}:`)} ${step.description}${deps}`, + ); + if (step.file) { + console.log(` ${filePath(step.file)}`); + } + if (step.tool) { + console.log(` ${chalk.gray(`Tool: ${step.tool}`)}`); + } + }); + console.log(); +}; diff --git a/src/commands/runner/execute-plan.ts b/src/commands/runner/execute-plan.ts new file mode 100644 index 0000000..a0b03bb --- /dev/null +++ b/src/commands/runner/execute-plan.ts @@ -0,0 +1,35 @@ +/** + * Plan execution utilities + */ + +import { failSpinner, succeedSpinner, startSpinner } from "@utils/terminal"; +import { RUNNER_DELAYS } from "@constants/runner"; +import { getStepIcon } from "@commands/runner/display-plan"; +import { delay } from "@commands/runner/utils"; +import type { ExecutionPlan, PlanStep } from "@/types/index"; +import type { StepContext } from "@/types/runner"; + +const executeStep = async (context: StepContext): Promise => { + const { step, current, total } = context; + const icon = getStepIcon(step.type); + const message = `${icon} Step ${current}/${total}: ${step.description}`; + + startSpinner(message); + + try { + await delay(RUNNER_DELAYS.STEP_EXECUTION); + succeedSpinner(message); + } catch (error) { + failSpinner(message); + throw error; + } +}; + +export const executePlan = async (plan: ExecutionPlan): Promise => { + const total = plan.steps.length; + + for (let i = 0; i < plan.steps.length; i++) { + const step: PlanStep = plan.steps[i]; + await executeStep({ step, current: i + 1, total }); + } +}; diff --git a/src/commands/runner/execute.ts b/src/commands/runner/execute.ts new file mode 100644 index 0000000..037ad38 --- /dev/null +++ b/src/commands/runner/execute.ts @@ -0,0 +1,118 @@ +/** + * Main runner execution function + */ + +import { + askConfirm, + failSpinner, + successMessage, + succeedSpinner, + headerMessage, + startSpinner, + infoMessage, + errorMessage, + warningMessage, +} from "@utils/terminal"; +import { RUNNER_DELAYS, RUNNER_MESSAGES } from "@constants/runner"; +import { displayHeader } from "@commands/runner/display-header"; +import { displayPlan } from "@commands/runner/display-plan"; +import { createPlan } from "@commands/runner/create-plan"; +import { executePlan } from "@commands/runner/execute-plan"; +import { delay } from "@commands/runner/utils"; +import type { CommandOptions, AgentType } from "@/types/index"; +import type { RunnerOptions } from "@/types/runner"; + +const parseOptions = (options: CommandOptions): RunnerOptions | null => { + const { + task, + agent = "coder", + files = [], + dryRun = false, + autoApprove = false, + } = options; + + if (!task) { + errorMessage(RUNNER_MESSAGES.TASK_REQUIRED); + return null; + } + + return { + task, + agent: agent as AgentType, + files, + dryRun, + autoApprove, + }; +}; + +const runDiscoveryPhase = async (): Promise => { + startSpinner(RUNNER_MESSAGES.DISCOVERY_START); + await delay(RUNNER_DELAYS.DISCOVERY); + succeedSpinner(RUNNER_MESSAGES.DISCOVERY_COMPLETE); +}; + +const runPlanningPhase = async ( + task: string, + agent: AgentType, + files: string[], +) => { + startSpinner(RUNNER_MESSAGES.PLANNING_START); + const plan = await createPlan(task, agent, files); + succeedSpinner(`Plan created with ${plan.steps.length} steps`); + return plan; +}; + +const confirmExecution = async (autoApprove: boolean): Promise => { + if (autoApprove) { + return true; + } + + const approved = await askConfirm(RUNNER_MESSAGES.CONFIRM_EXECUTE); + + if (!approved) { + warningMessage(RUNNER_MESSAGES.EXECUTION_CANCELLED); + return false; + } + + return true; +}; + +export const execute = async (options: CommandOptions): Promise => { + const runnerOptions = parseOptions(options); + + if (!runnerOptions) { + return; + } + + const { task, agent, files, dryRun, autoApprove } = runnerOptions; + + displayHeader(runnerOptions); + + try { + await runDiscoveryPhase(); + + const plan = await runPlanningPhase(task, agent, files); + + displayPlan(plan); + + if (dryRun) { + infoMessage(RUNNER_MESSAGES.DRY_RUN_INFO); + return; + } + + const shouldExecute = await confirmExecution(autoApprove); + + if (!shouldExecute) { + return; + } + + headerMessage("Executing Plan"); + await executePlan(plan); + + successMessage(`\n${RUNNER_MESSAGES.TASK_COMPLETE}`); + } catch (error) { + failSpinner(RUNNER_MESSAGES.TASK_FAILED); + errorMessage(`Error: ${error}`); + throw error; + } +}; diff --git a/src/commands/runner/utils.ts b/src/commands/runner/utils.ts new file mode 100644 index 0000000..83cc886 --- /dev/null +++ b/src/commands/runner/utils.ts @@ -0,0 +1,6 @@ +/** + * Runner utility functions + */ + +export const delay = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/constants/agent.ts b/src/constants/agent.ts new file mode 100644 index 0000000..defd13f --- /dev/null +++ b/src/constants/agent.ts @@ -0,0 +1,5 @@ +/** + * Agent constants + */ + +export const MAX_ITERATIONS = 50; diff --git a/src/constants/auto-scroll.ts b/src/constants/auto-scroll.ts new file mode 100644 index 0000000..f7137be --- /dev/null +++ b/src/constants/auto-scroll.ts @@ -0,0 +1,23 @@ +/** + * Auto-Scroll Constants + * + * Constants for auto-scroll behavior in the TUI + */ + +/** Distance from bottom (in lines) to consider "at bottom" */ +export const BOTTOM_THRESHOLD = 3; + +/** Settling time after operations complete (ms) */ +export const SETTLE_TIMEOUT_MS = 300; + +/** Timeout for marking auto-scroll events (ms) */ +export const AUTO_SCROLL_MARK_TIMEOUT_MS = 250; + +/** Default scroll lines per keyboard event */ +export const KEYBOARD_SCROLL_LINES = 3; + +/** Default scroll lines per page event */ +export const PAGE_SCROLL_LINES = 10; + +/** Mouse scroll lines per wheel event */ +export const MOUSE_SCROLL_LINES = 3; diff --git a/src/constants/banner.ts b/src/constants/banner.ts new file mode 100644 index 0000000..bafc10e --- /dev/null +++ b/src/constants/banner.ts @@ -0,0 +1,103 @@ +/** + * Banner constants for CodeTyper CLI + */ + +// ASCII art for "codetyper" using block characters +export const BANNER_LINES = [ + " __ __ ", + " _______ _____/ /__ / /___ ______ ___ _____ ", + " / ___/ / / / _ \\/ _ \\/ __/ / / / __ \\/ _ \\/ ___/ ", + "/ /__/ /_/ / __/ __/ /_/ /_/ / /_/ / __/ / ", + "\\___/\\____/\\___/\\___/\\__/\\__, / .___/\\___/_/ ", + " /____/_/ ", +] as const; + +// Alternative minimal banner +export const BANNER_MINIMAL = [ + "╭───────────────────────────────────────╮", + "│ ▄▀▀ ▄▀▄ █▀▄ ██▀ ▀█▀ ▀▄▀ █▀▄ ██▀ █▀▄ │", + "│ ▀▄▄ ▀▄▀ █▄▀ █▄▄ █ █ █▀ █▄▄ █▀▄ │", + "╰───────────────────────────────────────╯", +] as const; + +// Block-style banner (similar to opencode) +export const BANNER_BLOCKS = [ + "█▀▀ █▀█ █▀▄ █▀▀ ▀█▀ █▄█ █▀█ █▀▀ █▀█", + "█ █ █ █ █ █▀▀ █ █ █▀▀ █▀▀ █▀▄", + "▀▀▀ ▀▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀", +] as const; + +// Gradient colors for banner (cyan to blue) +export const GRADIENT_COLORS = [ + "\x1b[96m", // Bright cyan + "\x1b[36m", // Cyan + "\x1b[94m", // Bright blue + "\x1b[34m", // Blue + "\x1b[95m", // Bright magenta + "\x1b[35m", // Magenta +] as const; + +// Banner style to lines mapping +export const BANNER_STYLE_MAP: Record = { + default: BANNER_LINES, + minimal: BANNER_MINIMAL, + blocks: BANNER_BLOCKS, +} as const; + +// Large ASCII art banner +export const BANNER = ` + ,gggg, _,gggggg,_ ,gggggggggggg, ,ggggggg, ,ggggggggggggggg ,ggg, gg ,ggggggggggg, ,ggggggg, ,ggggggggggg, + ,88"""Y8b, ,d8P""d8P"Y8b, dP"""88""""""Y8b, ,dP"""""""Y8bdP""""""88"""""""dP""Y8a 88 dP"""88""""""Y8, ,dP"""""""Y8bdP"""88""""""Y8, + d8" \`Y8,d8' Y8 "8b,dPYb, 88 \`8b, d8' a Y8Yb,_ 88 Yb, \`88 88 Yb, 88 \`8b d8' a Y8Yb, 88 \`8b + d8' 8b d8d8' \`Ybaaad88P' \`" 88 \`8b 88 "Y8P' \`"" 88 \`" 88 88 \`" 88 ,8P 88 "Y8P' \`" 88 ,8P +,8I "Y88P'8P \`"""Y8 88 Y8 \`8baaaa 88 88 88 88aaaad8P" \`8baaaa 88aaaad8P" +I8' 8b d8 88 d8,d8P"""" 88 88 88 88""""" ,d8P"""" 88""""Yb, +d8 Y8, ,8P 88 ,8Pd8" 88 88 ,88 88 d8" 88 "8b +Y8, \`Y8, ,8P' 88 ,8P'Y8, gg, 88 Y8b,___,d888 88 Y8, 88 \`8i +\`Yba,,_____, \`Y8b,,__,,d8P' 88______,dP' \`Yba,,_____, "Yb,,8P "Y88888P"88, 88 \`Yba,,_____, 88 Yb, + \`"Y8888888 \`"Y8888P"' 888888888P" \`"Y8888888 "Y8P' ,ad8888 88 \`"Y8888888 88 Y8 + d8P" 88 + ,d8' 88 + d8' 88 + 88 88 + Y8,_ _,88 + "Y888P" +`; + +// Welcome message with help information +export const WELCOME_MESSAGE = ` +🤖 CodeTyper AI Agent - Autonomous Code Generation Assistant +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Default Provider: GitHub Copilot (gpt-4) + +Getting Started: + codetyper chat Start interactive chat + codetyper run "your task" Execute autonomous task + codetyper classify "prompt" Analyze intent + codetyper config show View configuration + +Commands: + chat Interactive REPL session + run Execute task autonomously + classify Classify user intent + plan Generate execution plan + validate Validate plan safety + config Manage configuration + serve Start JSON-RPC server + +Options: + --help, -h Show help + --version, -V Show version + +Chat Commands: + /help Show help + /models View available LLM providers + /provider Switch LLM provider + /files List context files + /clear Clear conversation + /exit Exit chat + +💡 Tip: Use 'codetyper chat' then '/models' to see all available providers +📖 Docs: Run 'codetyper --help ' for detailed information +`; diff --git a/src/constants/bash.ts b/src/constants/bash.ts new file mode 100644 index 0000000..6d9e2f5 --- /dev/null +++ b/src/constants/bash.ts @@ -0,0 +1,30 @@ +/** + * Bash tool constants + */ + +export const BASH_DEFAULTS = { + MAX_OUTPUT_LENGTH: 30000, + TIMEOUT: 120000, + KILL_DELAY: 1000, +} as const; + +export const BASH_SIGNALS = { + TERMINATE: "SIGTERM", + KILL: "SIGKILL", +} as const; + +export const BASH_MESSAGES = { + PERMISSION_DENIED: "Permission denied by user", + TIMED_OUT: (timeout: number) => `Command timed out after ${timeout}ms`, + ABORTED: "Command aborted", + EXIT_CODE: (code: number) => `Command exited with code ${code}`, + TRUNCATED: "\n\n... (truncated)", +} as const; + +export const BASH_DESCRIPTION = `Execute a shell command. Use this tool to run commands like git, npm, mkdir, etc. + +Guidelines: +- Always provide a clear description of what the command does +- Use absolute paths when possible +- Be careful with destructive commands (rm, etc.) +- Commands that modify the filesystem will require user approval`; diff --git a/src/constants/bashPatterns.ts b/src/constants/bashPatterns.ts new file mode 100644 index 0000000..7e59e2c --- /dev/null +++ b/src/constants/bashPatterns.ts @@ -0,0 +1,20 @@ +/** Quiet bash commands - read-only exploration operations */ +export const QUIET_BASH_PATTERNS = [ + /^ls\b/, + /^cat\b/, + /^head\b/, + /^tail\b/, + /^find\b/, + /^grep\b/, + /^rg\b/, + /^fd\b/, + /^tree\b/, + /^pwd\b/, + /^echo\b/, + /^which\b/, + /^file\b/, + /^stat\b/, + /^wc\b/, + /^du\b/, + /^df\b/, +]; diff --git a/src/constants/chat-service.ts b/src/constants/chat-service.ts new file mode 100644 index 0000000..2ef3047 --- /dev/null +++ b/src/constants/chat-service.ts @@ -0,0 +1,106 @@ +/** + * Chat service constants + */ + +export const CHAT_TRUNCATE_DEFAULTS = { + MAX_LINES: 10, + MAX_LENGTH: 500, +} as const; + +export const FILE_SIZE_LIMITS = { + MAX_CONTEXT_FILE_SIZE: 100000, +} as const; + +export const DIFF_PATTERNS = [ + /@@\s*-\d+/m, + /---\s+[ab]?\//m, + /\+\+\+\s+[ab]?\//m, +] as const; + +export const GLOB_IGNORE_PATTERNS = [ + "**/node_modules/**", + "**/.git/**", +] as const; + +export const CHAT_MESSAGES = { + CONVERSATION_CLEARED: "Conversation cleared", + SESSION_SAVED: "Session saved", + LEARNING_SAVED: (scope: string) => `Learning saved (${scope})`, + LEARNING_SKIPPED: "Learning skipped", + NO_LEARNINGS: + "No learnings saved yet. Use /remember to save learnings about your project.", + NO_CONVERSATION: + "No conversation to create learning from. Start a conversation first.", + NO_LEARNINGS_DETECTED: + 'No learnings detected from the last exchange. Try being more explicit about preferences (e.g., "always use TypeScript", "prefer functional style").', + UNKNOWN_COMMAND: (cmd: string) => `Unknown command: ${cmd}`, + FILE_NOT_FOUND: (pattern: string) => `File not found: ${pattern}`, + FILE_TOO_LARGE: (name: string, size: number) => + `File too large: ${name} (${Math.round(size / 1024)}KB)`, + FILE_IS_BINARY: (name: string) => `Cannot add binary file: ${name}`, + FILE_ADDED: (name: string) => `Added to context: ${name}`, + FILE_ADD_FAILED: (error: unknown) => `Failed to add file: ${error}`, + FILE_READ_FAILED: (error: unknown) => `Failed to read file: ${error}`, + ANALYZE_FILES: "Analyze the files I've added to the context.", + GITHUB_ISSUES_FOUND: (count: number, issues: string) => + `Found ${count} GitHub issue(s): ${issues}`, + COMPACTION_STARTING: "Summarizing conversation history...", + COMPACTION_CONTINUING: "Continuing with your request...", +} as const; + +export const AUTH_MESSAGES = { + ALREADY_LOGGED_IN: "Already logged in. Use /logout first to re-authenticate.", + AUTH_SUCCESS: "Successfully authenticated with GitHub Copilot!", + AUTH_FAILED: (error: string) => `Authentication failed: ${error}`, + AUTH_START_FAILED: (error: string) => + `Failed to start authentication: ${error}`, + LOGGED_OUT: + "Logged out from GitHub Copilot. Run /login to authenticate again.", + NOT_LOGGED_IN: "Not logged in. Run /login to authenticate.", + NO_LOGIN_REQUIRED: (provider: string) => + `Provider ${provider} doesn't require login.`, + NO_LOGOUT_SUPPORT: (provider: string) => + `Provider ${provider} doesn't support logout.`, + OLLAMA_NO_AUTH: "Ollama is a local provider - no authentication required.", + COPILOT_AUTH_INSTRUCTIONS: (uri: string, code: string) => + `To authenticate with GitHub Copilot:\n\n1. Open: ${uri}\n2. Enter code: ${code}\n\nWaiting for authentication...`, + LOGGED_IN_AS: (login: string, name?: string) => + `Logged in as: ${login}${name ? ` (${name})` : ""}`, +} as const; + +export const MODEL_MESSAGES = { + MODEL_AUTO: "Model set to auto - the provider will choose the best model.", + MODEL_CHANGED: (model: string) => `Model changed to: ${model}`, +} as const; + +// Re-export HELP_TEXT from prompts for backward compatibility +export { HELP_TEXT } from "@prompts/ui/help"; + +export const LEARNING_CONFIDENCE_THRESHOLD = 0.7; +export const MAX_LEARNINGS_DISPLAY = 20; + +export type CommandName = + | "help" + | "h" + | "clear" + | "c" + | "save" + | "s" + | "context" + | "usage" + | "u" + | "model" + | "models" + | "agent" + | "a" + | "theme" + | "mcp" + | "mode" + | "whoami" + | "login" + | "logout" + | "provider" + | "p" + | "status" + | "remember" + | "learnings"; diff --git a/src/constants/command-suggestion.ts b/src/constants/command-suggestion.ts new file mode 100644 index 0000000..ecf46a7 --- /dev/null +++ b/src/constants/command-suggestion.ts @@ -0,0 +1,85 @@ +/** + * Command suggestion constants + */ + +import type { SuggestionPriority } from "@/types/command-suggestion"; + +export const PROJECT_FILES = { + PACKAGE_JSON: "package.json", + YARN_LOCK: "yarn.lock", + PNPM_LOCK: "pnpm-lock.yaml", + BUN_LOCK: "bun.lockb", + CARGO_TOML: "Cargo.toml", + GO_MOD: "go.mod", + PYPROJECT: "pyproject.toml", + REQUIREMENTS: "requirements.txt", + MAKEFILE: "Makefile", + DOCKERFILE: "Dockerfile", +} as const; + +export const PRIORITY_ORDER: Record = { + high: 0, + medium: 1, + low: 2, +}; + +export const PRIORITY_ICONS: Record = { + high: "⚡", + medium: "→", + low: "·", +}; + +export const FILE_PATTERNS = { + PACKAGE_JSON: /package\.json$/, + TSCONFIG: /tsconfig.*\.json$/, + SOURCE_FILES: /\.(ts|tsx|js|jsx)$/, + CARGO_TOML: /Cargo\.toml$/, + GO_MOD: /go\.mod$/, + PYTHON_DEPS: /requirements.*\.txt$|pyproject\.toml$/, + DOCKER: /Dockerfile$|docker-compose.*\.ya?ml$/, + MAKEFILE: /Makefile$/, + MIGRATIONS: /migrations?\/.*\.(sql|ts|js)$/, + ENV_EXAMPLE: /\.env\.example$|\.env\.sample$/, + LINTER_CONFIG: /\.eslintrc|\.prettierrc|eslint\.config|prettier\.config/, + TEST_FILE: /\.test\.|\.spec\.|__tests__/, +} as const; + +export const CONTENT_PATTERNS = { + DEPENDENCIES: /\"dependencies\"/, + DEV_DEPENDENCIES: /\"devDependencies\"/, + PEER_DEPENDENCIES: /\"peerDependencies\"/, +} as const; + +export const SUGGESTION_MESSAGES = { + INSTALL_DEPS: "Install dependencies", + REBUILD_PROJECT: "Rebuild the project", + RUN_TESTS: "Run tests", + START_DEV: "Start development server", + BUILD_RUST: "Build the Rust project", + TIDY_GO: "Tidy Go modules", + INSTALL_PYTHON_EDITABLE: "Install Python package in editable mode", + INSTALL_PYTHON_DEPS: "Install Python dependencies", + DOCKER_COMPOSE_BUILD: "Rebuild and start Docker containers", + DOCKER_BUILD: "Rebuild Docker image", + RUN_MAKE: "Run make", + RUN_MIGRATE: "Run database migrations", + CREATE_ENV: "Create local .env file", + RUN_LINT: "Run linter to check for issues", +} as const; + +export const SUGGESTION_REASONS = { + PACKAGE_JSON_MODIFIED: "package.json was modified", + TSCONFIG_CHANGED: "TypeScript configuration changed", + TEST_FILE_MODIFIED: "Test file was modified", + SOURCE_FILE_MODIFIED: "Source file was modified", + CARGO_MODIFIED: "Cargo.toml was modified", + GO_MOD_MODIFIED: "go.mod was modified", + PYTHON_DEPS_CHANGED: "Python dependencies changed", + REQUIREMENTS_MODIFIED: "requirements.txt was modified", + DOCKER_COMPOSE_CHANGED: "Docker Compose configuration changed", + DOCKERFILE_MODIFIED: "Dockerfile was modified", + MAKEFILE_MODIFIED: "Makefile was modified", + MIGRATION_MODIFIED: "Migration file was added or modified", + ENV_TEMPLATE_MODIFIED: "Environment template was modified", + LINTER_CONFIG_CHANGED: "Linter configuration changed", +} as const; diff --git a/src/constants/components.ts b/src/constants/components.ts new file mode 100644 index 0000000..d9681cc --- /dev/null +++ b/src/constants/components.ts @@ -0,0 +1,114 @@ +/** + * UI component constants + */ + +// Box drawing characters +export const BoxChars = { + // Single line + single: { + topLeft: "┌", + topRight: "┐", + bottomLeft: "└", + bottomRight: "┘", + horizontal: "─", + vertical: "│", + leftT: "├", + rightT: "┤", + topT: "┬", + bottomT: "┴", + cross: "┼", + }, + // Double line + double: { + topLeft: "╔", + topRight: "╗", + bottomLeft: "╚", + bottomRight: "╝", + horizontal: "═", + vertical: "║", + leftT: "╠", + rightT: "╣", + topT: "╦", + bottomT: "╩", + cross: "╬", + }, + // Rounded + rounded: { + topLeft: "╭", + topRight: "╮", + bottomLeft: "╰", + bottomRight: "╯", + horizontal: "─", + vertical: "│", + leftT: "├", + rightT: "┤", + topT: "┬", + bottomT: "┴", + cross: "┼", + }, + // Bold + bold: { + topLeft: "┏", + topRight: "┓", + bottomLeft: "┗", + bottomRight: "┛", + horizontal: "━", + vertical: "┃", + leftT: "┣", + rightT: "┫", + topT: "┳", + bottomT: "┻", + cross: "╋", + }, +} as const; + +// Default box options +export const BOX_DEFAULTS = { + style: "rounded" as const, + padding: 1, + align: "left" as const, +} as const; + +// Tool icon mapping +export const TOOL_ICONS = { + bash: "bash", + read: "read", + write: "write", + edit: "edit", + default: "default", +} as const; + +// State color mapping +export const STATE_COLORS = { + pending: "DIM", + running: "primary", + success: "success", + error: "error", +} as const; + +// Role configuration for message display +export const ROLE_CONFIG = { + user: { label: "You", colorKey: "primary" }, + assistant: { label: "CodeTyper", colorKey: "success" }, + system: { label: "System", colorKey: "textMuted" }, + tool: { label: "Tool", colorKey: "warning" }, +} as const; + +// Status indicator configuration +export const STATUS_INDICATORS = { + success: { iconKey: "success", colorKey: "success" }, + error: { iconKey: "error", colorKey: "error" }, + warning: { iconKey: "warning", colorKey: "warning" }, + info: { iconKey: "info", colorKey: "info" }, + pending: { iconKey: "pending", colorKey: "textMuted" }, + running: { iconKey: "running", colorKey: "primary" }, +} as const; + +// Tool call icon configuration +export const TOOL_CALL_ICONS = { + bash: { iconKey: "bash", colorKey: "warning" }, + read: { iconKey: "read", colorKey: "info" }, + write: { iconKey: "write", colorKey: "success" }, + edit: { iconKey: "edit", colorKey: "primary" }, + default: { iconKey: "gear", colorKey: "textMuted" }, +} as const; diff --git a/src/constants/copilot.ts b/src/constants/copilot.ts new file mode 100644 index 0000000..525a8d5 --- /dev/null +++ b/src/constants/copilot.ts @@ -0,0 +1,213 @@ +import type { ProviderModel, ProviderName } from "@/types/providers"; + +// Provider identification +export const COPILOT_PROVIDER_NAME: ProviderName = "copilot"; +export const COPILOT_DISPLAY_NAME = "GitHub Copilot"; + +// GitHub Copilot API endpoints +export const COPILOT_AUTH_URL = + "https://api.github.com/copilot_internal/v2/token"; +export const COPILOT_MODELS_URL = "https://api.githubcopilot.com/models"; + +// GitHub OAuth endpoints for device flow +export const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98"; +export const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code"; +export const GITHUB_ACCESS_TOKEN_URL = + "https://github.com/login/oauth/access_token"; + +// Cache and retry configuration +export const COPILOT_MODELS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes +export const COPILOT_MAX_RETRIES = 3; +export const COPILOT_INITIAL_RETRY_DELAY = 1000; // 1 second + +// Default model +export const COPILOT_DEFAULT_MODEL = "gpt-5-mini"; + +// Unlimited fallback model (used when quota is exceeded) +export const COPILOT_UNLIMITED_MODEL = "gpt-4o"; + +// Copilot messages +export const COPILOT_MESSAGES = { + QUOTA_EXCEEDED_SWITCHING: (from: string, to: string) => + `Quota exceeded for ${from}. Switching to unlimited model: ${to}`, + MODEL_SWITCHED: (from: string, to: string) => + `Model switched: ${from} → ${to} (quota exceeded)`, + FORMAT_MULTIPLIER: (multiplier: number) => + multiplier === 0 ? "Unlimited" : `${multiplier}x`, +} as const; + +// Model cost multipliers from GitHub Copilot +// 0x = Unlimited (no premium request usage) +// Lower multiplier = cheaper, Higher multiplier = more expensive +export const MODEL_COST_MULTIPLIERS: Record = { + // Unlimited models (0x) + "gpt-4o": 0, + "gpt-4o-mini": 0, + "gpt-5-mini": 0, + "grok-code-fast-1": 0, + "raptor-mini": 0, + + // Low cost models (0.33x) + "claude-haiku-4.5": 0.33, + "gemini-3-flash-preview": 0.33, + "gpt-5.1-codex-mini-preview": 0.33, + + // Standard cost models (1.0x) + "claude-sonnet-4": 1.0, + "claude-sonnet-4.5": 1.0, + "gemini-2.5-pro": 1.0, + "gemini-3-pro-preview": 1.0, + "gpt-4.1": 1.0, + "gpt-5": 1.0, + "gpt-5-codex-preview": 1.0, + "gpt-5.1": 1.0, + "gpt-5.1-codex": 1.0, + "gpt-5.1-codex-max": 1.0, + "gpt-5.2": 1.0, + "gpt-5.2-codex": 1.0, + + // Premium models (3.0x) + "claude-opus-4.5": 3.0, +}; + +// Models that are unlimited (0x cost multiplier) +export const UNLIMITED_MODELS = new Set([ + "gpt-4o", + "gpt-4o-mini", + "gpt-5-mini", + "grok-code-fast-1", + "raptor-mini", +]); + +// Model context sizes (input tokens, output tokens) +export interface ModelContextSize { + input: number; + output: number; +} + +export const MODEL_CONTEXT_SIZES: Record = { + // Claude models + "claude-haiku-4.5": { input: 128000, output: 16000 }, + "claude-opus-4.5": { input: 128000, output: 16000 }, + "claude-sonnet-4": { input: 128000, output: 16000 }, + "claude-sonnet-4.5": { input: 128000, output: 16000 }, + + // Gemini models + "gemini-2.5-pro": { input: 109000, output: 64000 }, + "gemini-3-flash-preview": { input: 109000, output: 64000 }, + "gemini-3-pro-preview": { input: 109000, output: 64000 }, + + // GPT-4 models + "gpt-4.1": { input: 111000, output: 16000 }, + "gpt-4o": { input: 64000, output: 4000 }, + + // GPT-5 models + "gpt-5": { input: 128000, output: 128000 }, + "gpt-5-mini": { input: 128000, output: 64000 }, + "gpt-5-codex-preview": { input: 128000, output: 128000 }, + "gpt-5.1": { input: 128000, output: 64000 }, + "gpt-5.1-codex": { input: 128000, output: 128000 }, + "gpt-5.1-codex-max": { input: 128000, output: 128000 }, + "gpt-5.1-codex-mini-preview": { input: 128000, output: 128000 }, + "gpt-5.2": { input: 128000, output: 64000 }, + "gpt-5.2-codex": { input: 272000, output: 128000 }, + + // Other models + "grok-code-fast-1": { input: 109000, output: 64000 }, + "raptor-mini": { input: 200000, output: 64000 }, +}; + +// Default context size for unknown models +export const DEFAULT_CONTEXT_SIZE: ModelContextSize = { + input: 128000, + output: 16000, +}; + +// Get context size for a model +export const getModelContextSize = (modelId: string): ModelContextSize => + MODEL_CONTEXT_SIZES[modelId] ?? DEFAULT_CONTEXT_SIZE; + +// Fallback models when API is unavailable +export const COPILOT_FALLBACK_MODELS: ProviderModel[] = [ + { + id: "gpt-4o", + name: "GPT-4o", + maxTokens: 4000, + supportsTools: true, + supportsStreaming: true, + costMultiplier: 0, + isUnlimited: true, + }, + { + id: "gpt-5-mini", + name: "GPT-5 mini", + maxTokens: 64000, + supportsTools: true, + supportsStreaming: true, + costMultiplier: 0, + isUnlimited: true, + }, + { + id: "claude-sonnet-4", + name: "Claude Sonnet 4", + maxTokens: 16000, + supportsTools: true, + supportsStreaming: true, + costMultiplier: 1.0, + isUnlimited: false, + }, + { + id: "claude-sonnet-4.5", + name: "Claude Sonnet 4.5", + maxTokens: 16000, + supportsTools: true, + supportsStreaming: true, + costMultiplier: 1.0, + isUnlimited: false, + }, + { + id: "claude-opus-4.5", + name: "Claude Opus 4.5", + maxTokens: 16000, + supportsTools: true, + supportsStreaming: true, + costMultiplier: 3.0, + isUnlimited: false, + }, + { + id: "gpt-4.1", + name: "GPT-4.1", + maxTokens: 16000, + supportsTools: true, + supportsStreaming: true, + costMultiplier: 1.0, + isUnlimited: false, + }, + { + id: "gpt-5", + name: "GPT-5", + maxTokens: 128000, + supportsTools: true, + supportsStreaming: true, + costMultiplier: 1.0, + isUnlimited: false, + }, + { + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + maxTokens: 64000, + supportsTools: true, + supportsStreaming: true, + costMultiplier: 1.0, + isUnlimited: false, + }, + { + id: "grok-code-fast-1", + name: "Grok Code Fast 1", + maxTokens: 64000, + supportsTools: true, + supportsStreaming: true, + costMultiplier: 0, + isUnlimited: true, + }, +]; diff --git a/src/constants/dashboard.ts b/src/constants/dashboard.ts new file mode 100644 index 0000000..96f5884 --- /dev/null +++ b/src/constants/dashboard.ts @@ -0,0 +1,42 @@ +/** + * Dashboard Constants + */ + +export const DASHBOARD_TITLE = "CodeTyper"; + +export const DASHBOARD_LAYOUT = { + DEFAULT_WIDTH: 120, + CONTENT_HEIGHT: 15, + LEFT_COLUMN_RATIO: 0.35, + PADDING: 3, +} as const; + +export const DASHBOARD_LOGO = [ + " ██████╗███████╗", + " ██╔════╝██╔════╝", + " ██║ ███████╗", + " ██║ ╚════██║", + " ╚██████╗███████║", + " ╚═════╝╚══════╝", +] as const; + +export const DASHBOARD_COMMANDS = [ + { command: "codetyper chat", description: "Start interactive chat" }, + { command: "codetyper run ", description: "Execute autonomous task" }, + { command: "/help", description: "Show all commands in chat" }, +] as const; + +export const DASHBOARD_QUICK_COMMANDS = [ + { command: "codetyper chat", description: "Start interactive chat" }, + { command: "codetyper run", description: "Execute autonomous task" }, + { command: "codetyper --help", description: "Show all commands" }, +] as const; + +export const DASHBOARD_BORDER = { + TOP_LEFT: "╭", + TOP_RIGHT: "╮", + BOTTOM_LEFT: "╰", + BOTTOM_RIGHT: "╯", + HORIZONTAL: "─", + VERTICAL: "│", +} as const; diff --git a/src/constants/diff.ts b/src/constants/diff.ts new file mode 100644 index 0000000..fd4f5ee --- /dev/null +++ b/src/constants/diff.ts @@ -0,0 +1,13 @@ +/** + * Diff utility constants + */ + +// Default context lines for hunks +export const DIFF_CONTEXT_LINES = 3; + +// Line type prefixes +export const LINE_PREFIXES = { + add: "+", + remove: "-", + context: " ", +} as const; diff --git a/src/constants/edit.ts b/src/constants/edit.ts new file mode 100644 index 0000000..d15b33f --- /dev/null +++ b/src/constants/edit.ts @@ -0,0 +1,25 @@ +/** + * Edit tool constants + */ + +export const EDIT_MESSAGES = { + NOT_FOUND: + "Could not find the text to replace. Make sure old_string matches exactly.", + MULTIPLE_OCCURRENCES: (count: number) => + `old_string appears ${count} times. Use replace_all=true or provide more context to make it unique.`, + PERMISSION_DENIED: "Permission denied by user", +} as const; + +export const EDIT_TITLES = { + FAILED: (path: string) => `Edit failed: ${path}`, + CANCELLED: (path: string) => `Edit cancelled: ${path}`, + SUCCESS: (path: string) => `Edited: ${path}`, + EDITING: (name: string) => `Editing ${name}`, +} as const; + +export const EDIT_DESCRIPTION = `Edit a file by replacing specific text. The old_string must match exactly. + +Guidelines: +- old_string must be unique in the file (or use replace_all) +- Preserve indentation exactly as it appears in the file +- Requires user approval for edits`; diff --git a/src/constants/embeddings.ts b/src/constants/embeddings.ts new file mode 100644 index 0000000..4312aa4 --- /dev/null +++ b/src/constants/embeddings.ts @@ -0,0 +1,30 @@ +/** + * Embedding Constants + * + * Configuration for semantic learning retrieval + */ + +export const EMBEDDING_DEFAULTS = { + MODEL: "nomic-embed-text", + FALLBACK_MODEL: "all-minilm", + DIMENSIONS: 768, +} as const; + +export const EMBEDDING_ENDPOINTS = { + EMBED: "/api/embed", +} as const; + +export const EMBEDDING_TIMEOUTS = { + EMBED: 30000, +} as const; + +export const EMBEDDING_SEARCH = { + TOP_K: 10, + MIN_SIMILARITY: 0.3, + CACHE_TTL_MS: 300000, // 5 minutes +} as const; + +export const EMBEDDING_STORAGE = { + INDEX_FILE: "embeddings.json", + VERSION: 1, +} as const; diff --git a/src/constants/file-picker.ts b/src/constants/file-picker.ts new file mode 100644 index 0000000..934b057 --- /dev/null +++ b/src/constants/file-picker.ts @@ -0,0 +1,123 @@ +/** + * File Picker constants + */ + +export const IGNORED_PATTERNS = [ + // Version control + ".git", + ".svn", + ".hg", + // AI/Code assistants + ".claude", + ".coder", + ".codetyper", + ".cursor", + ".copilot", + ".aider", + // Build outputs / binaries + "node_modules", + "dist", + "build", + "bin", + "obj", + "target", + ".next", + ".nuxt", + ".output", + "out", + // Cache directories + ".cache", + ".turbo", + ".parcel-cache", + ".vite", + // Test/Coverage + "coverage", + ".nyc_output", + // Python + "__pycache__", + ".venv", + "venv", + ".env", + // OS files + ".DS_Store", + "thumbs.db", + // IDE/Editor + ".idea", + ".vscode", + // Misc + ".terraform", + ".serverless", +] as const; + +export const BINARY_EXTENSIONS = [ + // Executables + ".exe", + ".dll", + ".so", + ".dylib", + ".bin", + ".app", + // Images + ".png", + ".jpg", + ".jpeg", + ".gif", + ".bmp", + ".ico", + ".webp", + ".svg", + ".tiff", + // Audio/Video + ".mp3", + ".mp4", + ".wav", + ".avi", + ".mov", + ".mkv", + ".flac", + ".ogg", + // Archives + ".zip", + ".tar", + ".gz", + ".rar", + ".7z", + ".bz2", + // Documents + ".pdf", + ".doc", + ".docx", + ".xls", + ".xlsx", + ".ppt", + ".pptx", + // Fonts + ".ttf", + ".otf", + ".woff", + ".woff2", + ".eot", + // Database + ".db", + ".sqlite", + ".sqlite3", + // Other binary + ".pyc", + ".pyo", + ".class", + ".o", + ".a", + ".lib", + ".node", + ".wasm", +] as const; + +export type BinaryExtension = (typeof BINARY_EXTENSIONS)[number]; + +export type IgnoredPattern = (typeof IGNORED_PATTERNS)[number]; + +export const FILE_PICKER_DEFAULTS = { + MAX_DEPTH: 2, + MAX_RESULTS: 15, + INITIAL_DEPTH: 0, +} as const; diff --git a/src/constants/files.ts b/src/constants/files.ts new file mode 100644 index 0000000..15a382d --- /dev/null +++ b/src/constants/files.ts @@ -0,0 +1,2 @@ +// File-related constants +// MAX_FILE_SIZE, etc. diff --git a/src/constants/general.ts b/src/constants/general.ts new file mode 100644 index 0000000..3725af7 --- /dev/null +++ b/src/constants/general.ts @@ -0,0 +1 @@ +export const PROVIDER_NAME_COPILOT = "copilot"; diff --git a/src/constants/github-issue.ts b/src/constants/github-issue.ts new file mode 100644 index 0000000..b236ca0 --- /dev/null +++ b/src/constants/github-issue.ts @@ -0,0 +1,31 @@ +/** + * GitHub Issue constants + */ + +export const ISSUE_PATTERNS = [ + /\bissue\s*#?(\d+)\b/gi, + /\bfix\s+#(\d+)\b/gi, + /\bclose\s+#(\d+)\b/gi, + /\bresolve\s+#(\d+)\b/gi, + /(?/dev/null", + VIEW_ISSUE: (issueNumber: number) => + `gh issue view ${issueNumber} --json number,title,state,body,author,labels,url 2>/dev/null`, +} as const; + +export const GITHUB_REMOTE_IDENTIFIER = "github.com"; diff --git a/src/constants/glob.ts b/src/constants/glob.ts new file mode 100644 index 0000000..d982810 --- /dev/null +++ b/src/constants/glob.ts @@ -0,0 +1,58 @@ +/** + * Glob tool constants + */ + +export const GLOB_DEFAULTS = { + DOT: false, + ONLY_FILES: true, + ONLY_DIRECTORIES: false, +} as const; + +export const GLOB_IGNORE_PATTERNS = [ + // Version control + "**/.git/**", + "**/.svn/**", + "**/.hg/**", + // AI/Code assistants + "**/.claude/**", + "**/.coder/**", + "**/.codetyper/**", + "**/.cursor/**", + "**/.copilot/**", + "**/.aider/**", + // Build outputs / binaries + "**/node_modules/**", + "**/dist/**", + "**/build/**", + "**/bin/**", + "**/obj/**", + "**/target/**", + "**/.next/**", + "**/.nuxt/**", + "**/.output/**", + "**/out/**", + // Cache directories + "**/.cache/**", + "**/.turbo/**", + "**/.parcel-cache/**", + "**/.vite/**", + // Test/Coverage + "**/coverage/**", + "**/.nyc_output/**", + // Python + "**/__pycache__/**", + "**/.venv/**", + "**/venv/**", + "**/.env/**", + // IDE/Editor + "**/.idea/**", + "**/.vscode/**", + // Misc + "**/.terraform/**", + "**/.serverless/**", +] as const; + +export const GLOB_MESSAGES = { + FAILED: (error: unknown) => `Glob failed: ${error}`, + LIST_FAILED: (error: unknown) => `List failed: ${error}`, +} as const; diff --git a/src/constants/grep.ts b/src/constants/grep.ts new file mode 100644 index 0000000..1e69fa3 --- /dev/null +++ b/src/constants/grep.ts @@ -0,0 +1,28 @@ +/** + * Grep tool constants + */ + +export const GREP_DEFAULTS = { + MAX_RESULTS: 100, + DEFAULT_PATTERN: "**/*", + NO_MATCHES_EXIT_CODE: 1, +} as const; + +export const GREP_IGNORE_PATTERNS = [ + "**/node_modules/**", + "**/.git/**", + "**/dist/**", + "**/build/**", + "**/.next/**", +] as const; + +export const GREP_MESSAGES = { + NO_MATCHES: "No matches found", + SEARCH_FAILED: (error: unknown) => `Search failed: ${error}`, + RIPGREP_FAILED: (message: string) => `ripgrep failed: ${message}`, +} as const; + +export const GREP_COMMANDS = { + RIPGREP: (pattern: string, directory: string) => + `rg --line-number --no-heading "${pattern}" "${directory}"`, +} as const; diff --git a/src/constants/handlers.ts b/src/constants/handlers.ts new file mode 100644 index 0000000..31f4477 --- /dev/null +++ b/src/constants/handlers.ts @@ -0,0 +1,45 @@ +/** + * Constants for command handlers + */ + +import type { ConfigKey, ConfigAction } from "@/types/handlers"; +import type { Provider } from "@/types/index"; + +export const VALID_CONFIG_KEYS: readonly ConfigKey[] = [ + "provider", + "model", + "maxIterations", + "timeout", +] as const; + +export const VALID_PROVIDERS: readonly Provider[] = [ + "copilot", + "ollama", +] as const; + +export const VALID_CONFIG_ACTIONS: readonly ConfigAction[] = [ + "show", + "path", + "set", +] as const; + +export const CONFIG_VALIDATION = { + MIN_TIMEOUT_MS: 1000, + MIN_ITERATIONS: 1, +} as const; + +export const INTENT_KEYWORDS = { + fix: ["fix", "bug"], + test: ["test", "spec"], + refactor: ["refactor", "improve"], + code: ["add", "implement"], + document: ["document", "comment"], +} as const; + +export const CLASSIFICATION_CONFIDENCE = { + HIGH: 0.9, + MEDIUM: 0.85, + DEFAULT: 0.8, + LOW: 0.75, + THRESHOLD: 0.7, +} as const; diff --git a/src/constants/help-commands.ts b/src/constants/help-commands.ts new file mode 100644 index 0000000..bd57b19 --- /dev/null +++ b/src/constants/help-commands.ts @@ -0,0 +1,20 @@ +export const HELP_COMMANDS: [string, string][] = [ + ["/help, /h", "Show this help message"], + ["/clear, /c", "Clear conversation history"], + ["/files, /f", "List files in context"], + ["/remove , /rm", "Remove file from context"], + ["/context", "Show current context size"], + ["/compact", "Compact conversation history"], + ["/history", "Show conversation history"], + ["/models, /m", "Show available models"], + ["/model ", "Switch to a different model"], + ["/providers, /p", "Show all providers status"], + ["/provider ", "Switch to a different provider"], + ["/agent, /a", "Select agent"], + ["/usage, /u", "Show token usage statistics"], + ["/mcp [cmd]", "MCP server status/connect/disconnect/tools"], + ["/session", "Show current session info"], + ["/sessions", "List all saved sessions"], + ["/save, /s", "Save current session"], + ["/exit, /quit, /q", "Exit chat"], +]; diff --git a/src/constants/home-screen.ts b/src/constants/home-screen.ts new file mode 100644 index 0000000..bb3828a --- /dev/null +++ b/src/constants/home-screen.ts @@ -0,0 +1,35 @@ +/** + * Home Screen Constants + * Constants for the welcome/home screen TUI layout + */ + +/** Layout constants for home screen */ +export const HOME_LAYOUT = { + maxWidth: 75, + topPadding: 3, + bottomPadding: 2, + horizontalPadding: 2, + logoGap: 1, +} as const; + +/** Input placeholders shown in the prompt box */ +export const PLACEHOLDERS = [ + "Fix a TODO in the codebase", + "What is the tech stack of this project?", + "Fix broken tests", + "Explain how this function works", + "Refactor this code for readability", + "Add error handling to this function", +]; + +/** Keyboard hints displayed below the prompt box */ +export const KEYBOARD_HINTS = { + agents: { key: "tab", label: "agents" }, + commands: { key: "ctrl+p", label: "commands" }, +} as const; + +/** MCP status indicators */ +export const MCP_INDICATORS = { + connected: "⊙", + error: "⊙", +} as const; diff --git a/src/constants/home.ts b/src/constants/home.ts new file mode 100644 index 0000000..54ef3b8 --- /dev/null +++ b/src/constants/home.ts @@ -0,0 +1,4 @@ +export const HOME_VARS = { + title: "Welcome to CodeTyper - Your AI Coding Assistant", + subTitle: "Type a prompt below to start a new session", +}; diff --git a/src/constants/input-editor.ts b/src/constants/input-editor.ts new file mode 100644 index 0000000..eb5fa11 --- /dev/null +++ b/src/constants/input-editor.ts @@ -0,0 +1,30 @@ +/** + * Input editor constants + */ + +// Default prompts +export const INPUT_EDITOR_DEFAULTS = { + prompt: "\x1b[36m> \x1b[0m", + continuationPrompt: "\x1b[90m│ \x1b[0m", +} as const; + +// ANSI escape sequences +export const ANSI = { + hideCursor: "\x1b[?25l", + showCursor: "\x1b[?25h", + clearLine: "\x1b[2K", + moveUp: (n: number) => `\x1b[${n}A`, + moveDown: (n: number) => `\x1b[${n}B`, + moveRight: (n: number) => `\x1b[${n}C`, + carriageReturn: "\r", +} as const; + +// Special key sequences for Alt+Enter +export const ALT_ENTER_SEQUENCES = ["\x1b\r", "\x1b\n"] as const; + +// Pasted text styling +export const PASTE_STYLE = { + // Gray/dim style for pasted text placeholder + start: "\x1b[90m", + end: "\x1b[0m", +} as const; diff --git a/src/constants/learning.ts b/src/constants/learning.ts new file mode 100644 index 0000000..5ea873e --- /dev/null +++ b/src/constants/learning.ts @@ -0,0 +1,96 @@ +/** + * Learning Service constants + */ + +import type { LearningCategory } from "@/types/learning"; + +export const LEARNING_PATTERNS = [ + // User preferences + /always use (\w+)/i, + /prefer (\w+) over (\w+)/i, + /use (\.\w+) files?/i, + /don't use (\w+)/i, + /never use (\w+)/i, + /code in (\w+)/i, + /write in (\w+)/i, + + // Project structure + /put .+ in (.+) directory/i, + /files? should be in (.+)/i, + /follow (.+) pattern/i, + /use (.+) architecture/i, + + // Coding style + /use (.+) naming convention/i, + /follow (.+) style/i, + /indent with (\w+)/i, + /use (single|double) quotes/i, + + // Testing + /use (.+) for testing/i, + /tests? should (.+)/i, + + // Dependencies + /use (.+) library/i, + /prefer (.+) package/i, +] as const; + +export const LEARNING_KEYWORDS = [ + "always", + "never", + "prefer", + "convention", + "standard", + "pattern", + "rule", + "style", + "remember", + "important", + "must", + "should", +] as const; + +export const ACKNOWLEDGMENT_PATTERNS = [ + /i('ll| will) (use|follow|apply) (.+)/i, + /using (.+) as (you|per your) (requested|preference)/i, + /following (.+) (convention|pattern|style)/i, + /noted.+ (will|going to) (.+)/i, +] as const; + +export const ACKNOWLEDGMENT_PHRASES = [ + "i understand", + "got it", + "noted", +] as const; + +export const LEARNING_DEFAULTS = { + BASE_PATTERN_CONFIDENCE: 0.7, + BASE_KEYWORD_CONFIDENCE: 0.5, + KEYWORD_CONFIDENCE_INCREMENT: 0.1, + ACKNOWLEDGMENT_CONFIDENCE: 0.8, + CONFIDENCE_BOOST: 0.2, + MAX_CONFIDENCE: 1.0, + MIN_KEYWORDS_FOR_LEARNING: 2, + MAX_CONTENT_LENGTH: 80, + TRUNCATE_LENGTH: 77, + MAX_SLICE_LENGTH: 100, +} as const; + +export const LEARNING_CONTEXTS = { + USER_PREFERENCE: "User preference", + CONVENTION_IDENTIFIED: "Convention identified", + MULTIPLE_INDICATORS: "Multiple preference indicators", + CONVENTION_CONFIRMED: "Convention confirmed by assistant", + PREFERENCE_ACKNOWLEDGED: "Preference acknowledged by assistant", +} as const; + +export const CATEGORY_PATTERNS: Record = { + prefer: "preference", + use: "preference", + directory: "architecture", + architecture: "architecture", + style: "style", + naming: "style", + indent: "style", + test: "testing", +}; diff --git a/src/constants/login.ts b/src/constants/login.ts new file mode 100644 index 0000000..6acd9de --- /dev/null +++ b/src/constants/login.ts @@ -0,0 +1,28 @@ +/** + * Login flow constants and messages + */ + +export const LOGIN_MESSAGES = { + COPILOT_ALREADY_CONFIGURED: "✓ Copilot is already configured and working!", + COPILOT_STARTING_AUTH: "\nStarting GitHub device flow authentication...\n", + COPILOT_AUTH_INSTRUCTIONS: "To authenticate with GitHub Copilot:\n", + COPILOT_WAITING: "Waiting for authentication (press Ctrl+C to cancel)...\n", + COPILOT_SUCCESS: "\n✓ GitHub Copilot authenticated successfully!", + OLLAMA_SUCCESS: "\n✓ Connected to Ollama!", + OLLAMA_NO_MODELS: "\nNo models found. Pull a model with: ollama pull ", + AVAILABLE_MODELS: "\nAvailable models:", + VALIDATION_FAILED: "\n✗ Validation failed:", + AUTH_FAILED: "\n✗ Authentication failed:", + CONNECTION_FAILED: "\n✗ Failed to connect:", + UNKNOWN_PROVIDER: "Unknown provider:", +} as const; + +export const LOGIN_PROMPTS = { + RECONFIGURE: "Do you want to re-authenticate?", + OLLAMA_HOST: "Ollama host URL:", +} as const; + +export const AUTH_STEP_PREFIXES = { + OPEN_URL: " 1. Open:", + ENTER_CODE: " 2. Enter code:", +} as const; diff --git a/src/constants/mouse-handler.ts b/src/constants/mouse-handler.ts new file mode 100644 index 0000000..a0583cc --- /dev/null +++ b/src/constants/mouse-handler.ts @@ -0,0 +1,41 @@ +/** + * Mouse Handler Constants + * + * Constants for terminal mouse event handling + */ + +// Mouse event button codes for SGR encoding +export const MOUSE_WHEEL_CODES = { + UP: 64, + DOWN: 65, +} as const; + +// Default scroll lines per wheel event +export const MOUSE_SCROLL_LINES = 3; + +// SGR mouse sequence pattern: \x1b[ = { + [MOUSE_WHEEL_CODES.UP]: "up", + [MOUSE_WHEEL_CODES.DOWN]: "down", +} as const; diff --git a/src/constants/mouse-scroll.ts b/src/constants/mouse-scroll.ts new file mode 100644 index 0000000..fb20d19 --- /dev/null +++ b/src/constants/mouse-scroll.ts @@ -0,0 +1,38 @@ +/** + * Mouse Scroll Constants + * + * Terminal escape sequences for mouse mode handling + */ + +// Mouse mode enable/disable escape sequences +export const MOUSE_ESCAPE_SEQUENCES = { + ENABLE: "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h", + DISABLE: "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l", +} as const; + +// Mouse button codes +export const MOUSE_BUTTON_CODES = { + SGR_SCROLL_UP: 64, + SGR_SCROLL_DOWN: 65, + X10_SCROLL_UP: 96, // 32 + 64 + X10_SCROLL_DOWN: 97, // 32 + 65 +} as const; + +// SGR mouse mode regex pattern +export const SGR_MOUSE_PATTERN = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/; + +// X10/Normal mouse mode prefix +export const X10_MOUSE_PREFIX = "\x1b[M"; +export const X10_MIN_LENGTH = 6; +export const X10_BUTTON_OFFSET = 3; + +// Scroll direction type +export type ScrollDirection = "up" | "down"; + +// Mouse button to scroll direction mapping +export const MOUSE_BUTTON_TO_DIRECTION: Record = { + [MOUSE_BUTTON_CODES.SGR_SCROLL_UP]: "up", + [MOUSE_BUTTON_CODES.SGR_SCROLL_DOWN]: "down", + [MOUSE_BUTTON_CODES.X10_SCROLL_UP]: "up", + [MOUSE_BUTTON_CODES.X10_SCROLL_DOWN]: "down", +} as const; diff --git a/src/constants/ollama.ts b/src/constants/ollama.ts new file mode 100644 index 0000000..d835e72 --- /dev/null +++ b/src/constants/ollama.ts @@ -0,0 +1,32 @@ +/** + * Ollama provider constants + */ + +export const OLLAMA_PROVIDER_NAME = "ollama" as const; +export const OLLAMA_DISPLAY_NAME = "Ollama (Local)"; + +export const OLLAMA_DEFAULTS = { + BASE_URL: "http://localhost:11434", + MODEL: "deepseek-coder:6.7b", +} as const; + +export const OLLAMA_ENDPOINTS = { + TAGS: "/api/tags", + CHAT: "/api/chat", + PULL: "/api/pull", +} as const; + +export const OLLAMA_TIMEOUTS = { + VALIDATION: 5000, + CHAT: 120000, +} as const; + +export const OLLAMA_CHAT_OPTIONS = { + DEFAULT_TEMPERATURE: 0.3, + DEFAULT_MAX_TOKENS: 4096, +} as const; + +export const OLLAMA_ERRORS = { + NOT_RUNNING: (baseUrl: string) => + `Ollama not running at ${baseUrl}. Start it with: ollama serve`, +} as const; diff --git a/src/constants/paste.ts b/src/constants/paste.ts new file mode 100644 index 0000000..fc92d25 --- /dev/null +++ b/src/constants/paste.ts @@ -0,0 +1,12 @@ +/** + * Constants for paste virtual text feature + */ + +/** Minimum number of lines to trigger paste summary */ +export const PASTE_LINE_THRESHOLD = 3; + +/** Minimum character count to trigger paste summary */ +export const PASTE_CHAR_THRESHOLD = 150; + +/** Format for paste placeholder display */ +export const PASTE_PLACEHOLDER_FORMAT = "[Pasted ~{lineCount} lines]"; diff --git a/src/constants/paths.ts b/src/constants/paths.ts new file mode 100644 index 0000000..8dfc1e4 --- /dev/null +++ b/src/constants/paths.ts @@ -0,0 +1,96 @@ +/** + * XDG-compliant storage paths + * + * Follows the XDG Base Directory Specification: + * - XDG_CONFIG_HOME: User configuration (~/.config) + * - XDG_DATA_HOME: User data (~/.local/share) + * - XDG_CACHE_HOME: Cache data (~/.cache) + * - XDG_STATE_HOME: State data (~/.local/state) + * + * See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + */ + +import { homedir } from "os"; +import { join } from "path"; + +const APP_NAME = "codetyper"; + +/** + * XDG base directories with fallbacks + */ +export const XDG = { + config: process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), + data: process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), + cache: process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), + state: process.env.XDG_STATE_HOME || join(homedir(), ".local", "state"), +} as const; + +/** + * Application directories + */ +export const DIRS = { + /** Configuration directory (~/.config/codetyper) */ + config: join(XDG.config, APP_NAME), + + /** Data directory (~/.local/share/codetyper) */ + data: join(XDG.data, APP_NAME), + + /** Cache directory (~/.cache/codetyper) */ + cache: join(XDG.cache, APP_NAME), + + /** State directory (~/.local/state/codetyper) */ + state: join(XDG.state, APP_NAME), + + /** Sessions directory (~/.local/share/codetyper/sessions) */ + sessions: join(XDG.data, APP_NAME, "sessions"), +} as const; + +/** + * Application files + */ +export const FILES = { + /** Main configuration file */ + config: join(DIRS.config, "config.json"), + + /** Keybindings configuration */ + keybindings: join(DIRS.config, "keybindings.json"), + + /** Provider credentials (stored in data, not config) */ + credentials: join(DIRS.data, "credentials.json"), + + /** Command history */ + history: join(DIRS.data, "history.json"), + + /** Models cache */ + modelsCache: join(DIRS.cache, "models.json"), + + /** Frecency cache for file/command suggestions */ + frecency: join(DIRS.cache, "frecency.json"), + + /** Key-value state storage */ + kvStore: join(DIRS.state, "kv.json"), + + /** Global settings (permissions, etc.) */ + settings: join(DIRS.config, "settings.json"), +} as const; + +/** + * Local project config directory name + */ +export const LOCAL_CONFIG_DIR = ".codetyper"; + +export const IGNORE_FOLDERS = [ + "**/node_modules/**", + "**/.git/**", + "**/.codetyper/**", + "**/.vscode/**", + "**/.idea/**", + "**/__pycache__/**", + "**/.DS_Store/**", + "**/dist/**", + "**/build/**", + "**/out/**", + "**/.next/**", + "**/.nuxt/**", + "**/venv/**", +]; diff --git a/src/constants/patterns.ts b/src/constants/patterns.ts new file mode 100644 index 0000000..813d4b6 --- /dev/null +++ b/src/constants/patterns.ts @@ -0,0 +1 @@ +export const FILE_REFERENCE_PATTERN = /@(?:"([^"]+)"|'([^']+)'|(\S+))/g; diff --git a/src/constants/provider-quality.ts b/src/constants/provider-quality.ts new file mode 100644 index 0000000..f2219b4 --- /dev/null +++ b/src/constants/provider-quality.ts @@ -0,0 +1,127 @@ +import type { TaskType } from "@/types/provider-quality"; + +export const QUALITY_THRESHOLDS = { + HIGH: 0.85, + MEDIUM: 0.6, + LOW: 0.4, + INITIAL: 0.5, +} as const; + +export const SCORE_ADJUSTMENTS = { + APPROVAL: 0.05, + CORRECTION: -0.08, + USER_REJECTION: -0.15, + MINOR_ISSUE: -0.02, + MAJOR_ISSUE: -0.05, +} as const; + +export const TASK_TYPE_PATTERNS: Record = { + code_generation: [ + /create\s+(a\s+)?function/i, + /write\s+(a\s+)?code/i, + /implement/i, + /generate\s+(a\s+)?/i, + /add\s+(a\s+)?(new\s+)?feature/i, + /build\s+(a\s+)?/i, + ], + refactoring: [ + /refactor/i, + /clean\s*up/i, + /restructure/i, + /reorganize/i, + /simplify/i, + /improve\s+(the\s+)?code/i, + ], + bug_fix: [ + /fix/i, + /bug/i, + /error/i, + /issue/i, + /not\s+working/i, + /broken/i, + /problem/i, + /debug/i, + ], + documentation: [ + /document/i, + /comment/i, + /readme/i, + /explain\s+.*\s+code/i, + /add\s+.*\s+docs/i, + /jsdoc/i, + /tsdoc/i, + ], + testing: [ + /test/i, + /spec/i, + /unit\s+test/i, + /integration/i, + /coverage/i, + /mock/i, + ], + explanation: [ + /explain/i, + /what\s+(does|is)/i, + /how\s+(does|do)/i, + /why/i, + /understand/i, + /clarify/i, + ], + review: [ + /review/i, + /check/i, + /evaluate/i, + /assess/i, + /audit/i, + /pr\s+review/i, + ], + general: [], +}; + +export const NEGATIVE_FEEDBACK_PATTERNS = [ + /fix\s+this/i, + /that'?s?\s+(wrong|incorrect)/i, + /not\s+(good|right|correct|working)/i, + /doesn'?t?\s+work/i, + /incorrect/i, + /broken/i, + /bad\s+(code|response)/i, + /try\s+again/i, + /redo/i, + /wrong/i, +]; + +export const POSITIVE_FEEDBACK_PATTERNS = [ + /thanks/i, + /thank\s+you/i, + /perfect/i, + /great/i, + /works/i, + /good\s+(job|work)/i, + /excellent/i, + /awesome/i, + /exactly/i, +]; + +export const DEFAULT_QUALITY_SCORES: Record = { + code_generation: QUALITY_THRESHOLDS.INITIAL, + refactoring: QUALITY_THRESHOLDS.INITIAL, + bug_fix: QUALITY_THRESHOLDS.INITIAL, + documentation: QUALITY_THRESHOLDS.INITIAL, + testing: QUALITY_THRESHOLDS.INITIAL, + explanation: QUALITY_THRESHOLDS.INITIAL, + review: QUALITY_THRESHOLDS.INITIAL, + general: QUALITY_THRESHOLDS.INITIAL, +}; + +export const PROVIDER_IDS = { + OLLAMA: "ollama", + COPILOT: "copilot", +} as const; + +export const CASCADE_CONFIG = { + MIN_AUDIT_THRESHOLD: QUALITY_THRESHOLDS.HIGH, + MAX_SKIP_THRESHOLD: QUALITY_THRESHOLDS.LOW, + DECAY_RATE: 0.01, + DECAY_INTERVAL_MS: 7 * 24 * 60 * 60 * 1000, // 1 week +} as const; diff --git a/src/constants/providers.ts b/src/constants/providers.ts new file mode 100644 index 0000000..17807f3 --- /dev/null +++ b/src/constants/providers.ts @@ -0,0 +1,22 @@ +/** + * Provider constants + */ + +import type { ProviderInfoRegistry } from "@/types/providers"; + +export const PROVIDER_INFO: ProviderInfoRegistry = { + copilot: { + envVar: "GitHub OAuth via device flow", + description: "GitHub Copilot - authenticate via GitHub device flow", + }, + ollama: { + envVar: "OLLAMA_HOST (default: http://localhost:11434)", + description: "Local Ollama models - no API key needed", + }, +}; + +export const DEFAULT_OLLAMA_HOST = "http://localhost:11434"; + +export const CREDENTIALS_FILE_MODE = 0o600; + +export const MAX_MODELS_DISPLAY = 10; diff --git a/src/constants/read.ts b/src/constants/read.ts new file mode 100644 index 0000000..9a229a3 --- /dev/null +++ b/src/constants/read.ts @@ -0,0 +1,29 @@ +/** + * Read tool constants + */ + +export const READ_DEFAULTS = { + MAX_LINES: 2000, + MAX_LINE_LENGTH: 2000, + MAX_BYTES: 100000, + LINE_NUMBER_PAD: 6, +} as const; + +export const READ_MESSAGES = { + PERMISSION_DENIED: "Permission denied by user", +} as const; + +export const READ_TITLES = { + DENIED: (path: string) => `Read denied: ${path}`, + FAILED: (path: string) => `Read failed: ${path}`, + READING: (name: string) => `Reading ${name}`, + DIRECTORY: (path: string) => `Listed directory: ${path}`, +} as const; + +export const READ_DESCRIPTION = `Read the contents of a file. Returns the file content with line numbers. + +Guidelines: +- Use absolute paths +- By default reads up to 2000 lines +- Long lines are truncated at 2000 characters +- Use offset and limit for large files`; diff --git a/src/constants/reasoning.ts b/src/constants/reasoning.ts new file mode 100644 index 0000000..e2cb66a --- /dev/null +++ b/src/constants/reasoning.ts @@ -0,0 +1,250 @@ +/** + * Configuration constants for the Reasoning Control Layer + * All tunable parameters in one place + */ + +import type { + EntityType, + MemoryItemType, + ValidationCheckType, +} from "@/types/reasoning"; + +// ============================================================================= +// QUALITY EVALUATION THRESHOLDS +// ============================================================================= + +export const QUALITY_THRESHOLDS = { + ACCEPT: 0.7, + RETRY: 0.4, + ESCALATE: 0.2, +} as const; + +export const QUALITY_WEIGHTS = { + structural: 0.3, + relevance: 0.25, + completeness: 0.25, + coherence: 0.2, +} as const; + +export const STRUCTURAL_CHECK_WEIGHTS = { + parseSucceeds: 0.4, + hasExpectedFormat: 0.3, + withinLengthBounds: 0.15, + noMalformedBlocks: 0.15, +} as const; + +// ============================================================================= +// HALLUCINATION DETECTION PATTERNS +// ============================================================================= + +export const HALLUCINATION_PATTERNS: RegExp[] = [ + /I don't have access to .* but/i, + /I cannot .* however/i, + /assuming .* exists/i, + /\[placeholder\]/i, + /TODO:.*implement/i, + /file:\/\/\/[a-z]:/i, + /I'll need to .* first, but/i, + /I'm not able to verify/i, + /hypothetically/i, + /in theory/i, +]; + +export const CONTRADICTION_PATTERNS: RegExp[] = [ + /but actually|actually, no/i, + /wait,? (?:no|I was wrong)/i, + /on second thought/i, + /correction:/i, + /I misspoke/i, +]; + +export const INCOMPLETE_STATEMENT_PATTERNS: RegExp[] = [ + /\.{3,}$/, + /\s+$/, + /(?:and|or|but|if|when|while)\s*$/i, +]; + +// ============================================================================= +// RETRY POLICY LIMITS +// ============================================================================= + +export const RETRY_LIMITS = { + maxTotalAttempts: 12, + maxPerTier: 2, + maxTimeMs: 60000, +} as const; + +export const RETRY_TIER_ORDER = [ + "INITIAL", + "RETRY_SAME", + "RETRY_SIMPLIFIED", + "RETRY_DECOMPOSED", + "RETRY_ALTERNATIVE", + "EXHAUSTED", +] as const; + +// ============================================================================= +// CONTEXT COMPRESSION SETTINGS +// ============================================================================= + +export const COMPRESSION_THRESHOLDS = { + COMPRESS_AT: 0.8, + MINIMAL_AT: 0.95, +} as const; + +export const COMPRESSION_LIMITS = { + maxToolResultTokens: 1000, + truncateHeadTokens: 500, + truncateTailTokens: 500, + maxCodeBlockLines: 30, + keepCodeHeadLines: 10, + keepCodeTailLines: 5, + maxMessageAge: 10, + preserveRecentMessages: 3, +} as const; + +// ============================================================================= +// MEMORY SELECTION SETTINGS +// ============================================================================= + +export const MEMORY_WEIGHTS = { + keywordOverlap: 0.25, + entityOverlap: 0.25, + recency: 0.2, + causalLink: 0.15, + pathOverlap: 0.1, + typeBonus: 0.05, +} as const; + +export const RECENCY_HALF_LIFE_MINUTES = 30; + +export const RELEVANCE_THRESHOLD = 0.15; + +export const MEMORY_TYPE_BONUSES: Record = { + ERROR: 0.8, + DECISION: 0.6, + TOOL_RESULT: 0.4, + FILE_CONTENT: 0.3, + CONVERSATION: 0.2, +}; + +export const MANDATORY_MEMORY_AGE_THRESHOLD = 3; +export const ERROR_MEMORY_AGE_THRESHOLD = 10; + +// ============================================================================= +// TERMINATION DETECTION SETTINGS +// ============================================================================= + +export const CONFIDENCE_THRESHOLDS = { + CONFIRMED_COMPLETE: 0.85, + POTENTIALLY_COMPLETE: 0.5, +} as const; + +export const VALIDATION_TIMEOUT_MS = 60000; +export const VALIDATION_RETRY_COUNT = 1; + +export const COMPLETION_SIGNAL_PATTERNS: Array<{ + type: "MODEL_STATEMENT"; + patterns: RegExp[]; + confidence: number; +}> = [ + { + type: "MODEL_STATEMENT", + patterns: [ + /^(?:I've|I have) (?:completed|finished|done)/i, + /^(?:The|Your) (?:task|request|change) (?:is|has been) (?:complete|done)/i, + /^All (?:changes|modifications) (?:have been|are) (?:made|applied)/i, + /^(?:Done|Finished|Complete)[.!]?$/i, + /successfully (?:created|modified|updated|deleted)/i, + ], + confidence: 0.3, + }, +]; + +export const TOOL_SUCCESS_CONFIDENCE = 0.5; +export const OUTPUT_PRESENT_CONFIDENCE = 0.7; +export const NO_PENDING_ACTIONS_CONFIDENCE = 0.4; + +export const VALIDATION_CHECK_CONFIGS: Record< + ValidationCheckType, + { + required: boolean; + timeout: number; + } +> = { + FILE_EXISTS: { required: true, timeout: 5000 }, + SYNTAX_VALID: { required: true, timeout: 10000 }, + DIFF_NONEMPTY: { required: true, timeout: 5000 }, + TESTS_PASS: { required: false, timeout: 60000 }, + SCHEMA_VALID: { required: false, timeout: 5000 }, + NO_REGRESSIONS: { required: false, timeout: 30000 }, +}; + +// ============================================================================= +// ENTITY EXTRACTION PATTERNS +// ============================================================================= + +export const ENTITY_PATTERNS: Record = { + FILE: /(?:^|\s|["'`])([\w\-./]+\.[a-z]{1,4})(?:\s|$|:|[()\[\]"'`])/gm, + FUNCTION: /(?:function|def|fn|func|const|let|var)\s+(\w+)\s*[=(]/g, + VARIABLE: /(?:const|let|var|val)\s+(\w+)\s*[=:]/g, + CLASS: /(?:class|struct|interface|type|enum)\s+(\w+)/g, + URL: /https?:\/\/[^\s<>"']+/g, + ERROR_CODE: /(?:error|err|E|errno)\s*[:=]?\s*(\d{3,5})/gi, +}; + +// ============================================================================= +// TOKEN ESTIMATION +// ============================================================================= + +export const TOKENS_PER_CHAR_ESTIMATE = 0.25; +export const DEFAULT_TOKEN_BUDGET = 8000; + +// Default max context (used when model is unknown or "auto") +export const DEFAULT_MAX_CONTEXT_TOKENS = 128000; + +// Compaction trigger percentage (compact at 80% of context limit) +export const COMPACTION_TRIGGER_PERCENT = 0.8; + +// ============================================================================= +// PRESERVATION PRIORITIES +// ============================================================================= + +export const PRESERVATION_PRIORITIES = { + CONTEXT_FILE: 1.0, + IMAGE: 1.0, + RECENT_MESSAGE: 0.8, + ERROR: 0.7, + DECISION: 0.6, + TOOL_RESULT: 0.4, + OLD_MESSAGE: 0.2, +} as const; + +export type PreservationPriority = + (typeof PRESERVATION_PRIORITIES)[keyof typeof PRESERVATION_PRIORITIES]; + +// ============================================================================= +// TASK DECOMPOSITION PATTERNS +// ============================================================================= + +export const TASK_SEGMENT_PATTERNS: RegExp[] = [ + /first,?\s+(.+?)\.\s*then,?\s+(.+)/i, + /(.+?)\s+and\s+(?:also|then)\s+(.+)/i, + /step\s*\d+[:.]\s*(.+)/gi, + /\d+[.)]\s*(.+)/gm, + /[-•]\s*(.+)/gm, +]; + +// ============================================================================= +// EXECUTION PHASE TIMEOUTS +// ============================================================================= + +export const PHASE_TIMEOUTS: Record = { + CONTEXT_PREPARATION: 5000, + LLM_INTERACTION: 120000, + QUALITY_EVALUATION: 1000, + RETRY_DECISION: 500, + EXECUTION: 300000, + TERMINATION_CHECK: 1000, + VALIDATION: 60000, +}; diff --git a/src/constants/rules.ts b/src/constants/rules.ts new file mode 100644 index 0000000..ac20864 --- /dev/null +++ b/src/constants/rules.ts @@ -0,0 +1,78 @@ +/** + * Constants for project rules loading + */ + +/** Locations to search for general rules file */ +export const GENERAL_RULES_PATHS = [ + "rules.md", + "RULES.md", + ".rules.md", + "codetyper.rules.md", + ".codetyper/rules.md", + ".github/copilot-instructions.md", + ".github/INSTRUCTIONS.md", + ".github/instructions.md", +] as const; + +/** Directories to search for categorized rules */ +export const RULES_DIRECTORIES = [ + "rules", + ".rules", + ".codetyper/rules", + ".github", +] as const; + +/** Well-known MCP categories */ +export const MCP_CATEGORIES = [ + "figma", + "browser", + "github", + "gitlab", + "slack", + "notion", + "linear", + "jira", + "confluence", + "google-docs", + "google-sheets", + "airtable", + "postgres", + "mysql", + "mongodb", + "redis", + "docker", + "kubernetes", + "aws", + "gcp", + "azure", + "vercel", + "netlify", + "supabase", + "firebase", +] as const; + +/** Well-known tool categories */ +export const TOOL_CATEGORIES = [ + "bash", + "read", + "write", + "edit", + "search", + "git", +] as const; + +/** Rule types for categorization */ +export const RULE_TYPES = ["mcp", "tool", "custom"] as const; + +/** Section titles for formatted rules */ +export const RULES_SECTION_TITLES = { + mcp: "MCP Integration Rules", + tool: "Tool Usage Rules", + custom: "Additional Rules", +} as const; + +/** Prompt templates for rules */ +export const RULES_PROMPT_TEMPLATES = { + PROJECT_RULES_HEADER: + "## Project Rules\n\nThe following rules are specific to this project and must be followed:\n\n", +} as const; diff --git a/src/constants/runner.ts b/src/constants/runner.ts new file mode 100644 index 0000000..c61e6a6 --- /dev/null +++ b/src/constants/runner.ts @@ -0,0 +1,61 @@ +/** + * Runner constants for task execution + */ + +import type { StepIconMap } from "@/types/runner"; + +export const RUNNER_DELAYS = { + DISCOVERY: 1500, + PLANNING: 1000, + STEP_EXECUTION: 1000, +} as const; + +export const STEP_ICONS: StepIconMap = { + read: "📖", + edit: "✏️", + create: "📝", + delete: "🗑️", + execute: "⚙️", +} as const; + +export const DEFAULT_STEP_ICON = "📌"; + +export const DEFAULT_FILE = "src/index.ts"; + +export const RUNNER_MESSAGES = { + DISCOVERY_START: "Discovery phase: Analyzing codebase...", + DISCOVERY_COMPLETE: "Discovery complete", + PLANNING_START: "Planning phase: Creating execution plan...", + DRY_RUN_INFO: "Dry run mode - plan generated but not executed", + EXECUTION_CANCELLED: "Execution cancelled by user", + TASK_COMPLETE: "Task completed successfully!", + TASK_FAILED: "Task failed", + TASK_REQUIRED: "Task description is required", + CONFIRM_EXECUTE: "\nExecute this plan?", +} as const; + +export const MOCK_STEPS = { + READ: { + id: "step_1", + type: "read" as const, + description: "Read existing files to understand context", + tool: "view", + }, + EDIT: { + id: "step_2", + type: "edit" as const, + description: "Apply necessary changes", + dependencies: ["step_1"], + tool: "edit", + }, + EXECUTE: { + id: "step_3", + type: "execute" as const, + description: "Verify changes work correctly", + dependencies: ["step_2"], + tool: "bash", + args: { command: "npm test" }, + }, +} as const; + +export const ESTIMATED_TIME_PER_STEP = 5; diff --git a/src/constants/serve.ts b/src/constants/serve.ts new file mode 100644 index 0000000..a3d2259 --- /dev/null +++ b/src/constants/serve.ts @@ -0,0 +1,6 @@ +export const SERVER_INFO = `JSON-RPC server mode for Neovim integration. + +This will start the agent in server mode, listening for +commands over stdin/stdout using the JSON-RPC protocol. + +Press Ctrl+C to stop the server.`; diff --git a/src/constants/spinner.ts b/src/constants/spinner.ts new file mode 100644 index 0000000..85af7aa --- /dev/null +++ b/src/constants/spinner.ts @@ -0,0 +1,78 @@ +/** + * Spinner animation constants + */ + +// Spinner frame sets +export const Spinners = { + dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], + line: ["-", "\\", "|", "/"], + circle: ["◐", "◓", "◑", "◒"], + arrow: ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"], + bounce: ["⠁", "⠂", "⠄", "⠂"], + bars: [ + "▏", + "▎", + "▍", + "▌", + "▋", + "▊", + "▉", + "█", + "▉", + "▊", + "▋", + "▌", + "▍", + "▎", + "▏", + ], + pulse: ["█", "▓", "▒", "░", "▒", "▓"], + blocks: ["▖", "▘", "▝", "▗"], + clock: [ + "🕐", + "🕑", + "🕒", + "🕓", + "🕔", + "🕕", + "🕖", + "🕗", + "🕘", + "🕙", + "🕚", + "🕛", + ], + moon: ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"], + braille: ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"], + scanner: [ + "[ ]", + "[= ]", + "[== ]", + "[=== ]", + "[ ===]", + "[ ==]", + "[ =]", + "[ ]", + ], +} as const; + +// Default spinner configuration +export const SPINNER_DEFAULTS = { + type: "dots" as const, + interval: 80, + text: "Loading...", +} as const; + +// Scanner spinner defaults +export const SCANNER_DEFAULTS = { + width: 10, + interval: 60, + char: "█", +} as const; + +// Progress bar defaults +export const PROGRESS_BAR_DEFAULTS = { + width: 30, + filledChar: "█", + emptyChar: "░", +} as const; diff --git a/src/constants/status-messages.ts b/src/constants/status-messages.ts new file mode 100644 index 0000000..9464047 --- /dev/null +++ b/src/constants/status-messages.ts @@ -0,0 +1,238 @@ +/** + * Whimsical status messages for different operations + */ + +// General thinking messages +export const THINKING_MESSAGES = [ + "Pondering", + "Contemplating", + "Cogitating", + "Ruminating", + "Deliberating", + "Musing", + "Brainstorming", + "Noodling", + "Percolating", + "Synthesizing", + "Ideating", + "Mulling it over", + "Connecting dots", + "Brewing thoughts", + "Hatching ideas", +] as const; + +// Tool-specific messages +export const BASH_MESSAGES = [ + "Executing", + "Running", + "Invoking", + "Launching", + "Firing up", + "Spinning up", + "Kickstarting", +] as const; + +export const READ_MESSAGES = [ + "Reading", + "Scanning", + "Perusing", + "Examining", + "Inspecting", + "Analyzing", + "Studying", + "Digesting", +] as const; + +export const WRITE_MESSAGES = [ + "Writing", + "Crafting", + "Composing", + "Authoring", + "Scribing", + "Generating", + "Creating", + "Materializing", +] as const; + +export const EDIT_MESSAGES = [ + "Editing", + "Refining", + "Polishing", + "Tweaking", + "Adjusting", + "Massaging", + "Tuning", + "Perfecting", +] as const; + +export const GLOB_MESSAGES = [ + "Searching", + "Hunting", + "Scouring", + "Exploring", + "Traversing", + "Scanning", +] as const; + +export const GREP_MESSAGES = [ + "Grepping", + "Searching", + "Pattern matching", + "Filtering", + "Sifting through", +] as const; + +export const TOOL_MESSAGES: Record = { + bash: BASH_MESSAGES, + read: READ_MESSAGES, + write: WRITE_MESSAGES, + edit: EDIT_MESSAGES, + glob: GLOB_MESSAGES, + grep: GREP_MESSAGES, +}; + +// Fallback messages for unknown tools +export const GENERIC_TOOL_MESSAGES = [ + "Processing", + "Working on", + "Handling", + "Tackling", + "Attending to", + "Taking care of", +] as const; + +// Fun filler words that can be combined +export const FILLER_ACTIONS = [ + "Flibbertigibbeting", + "Discombobulating", + "Recalibrating", + "Defenestrating bugs", + "Wrangling bits", + "Herding electrons", + "Caffeinating", + "Quantum entangling", + "Reverse engineering gravity", + "Consulting the oracle", + "Aligning chakras", + "Summoning expertise", + "Channeling wisdom", + "Marshalling resources", + "Orchestrating magic", +] as const; + +// Messages for specific file types +export const FILE_TYPE_MESSAGES: Record = { + ts: ["TypeScripting", "Type-checking", "Transpiling thoughts"], + tsx: ["Rendering components", "JSX-ing", "Reactifying"], + js: ["JavaScripting", "Interpreting"], + json: ["Parsing JSON", "Structuring data"], + md: ["Marking down", "Documenting"], + css: ["Styling", "Beautifying"], + html: ["Marking up", "Structuring"], + py: ["Pythoning", "Slithering through code"], + rs: ["Rusting", "Borrowing safely"], + go: ["Going", "Goroutining"], + sql: ["Querying", "Selecting wisdom"], + yaml: ["YAMLing", "Indenting carefully"], + toml: ["TOMLing", "Configuring"], + sh: ["Shelling", "Bashing"], +}; + +/** + * Get a random message from an array + */ +function randomFrom(arr: readonly T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +/** + * Get a thinking message + */ +export function getThinkingMessage(): string { + // 20% chance of a fun filler message + if (Math.random() < 0.2) { + return `✻ ${randomFrom(FILLER_ACTIONS)}…`; + } + return `✻ ${randomFrom(THINKING_MESSAGES)}…`; +} + +/** + * Get a tool execution message based on tool name and optional file path + */ +export function getToolMessage(toolName: string, filePath?: string): string { + // Check for file-type specific messages + if (filePath) { + const ext = filePath.split(".").pop()?.toLowerCase(); + if (ext && FILE_TYPE_MESSAGES[ext]) { + return `✻ ${randomFrom(FILE_TYPE_MESSAGES[ext])}…`; + } + } + + // Get tool-specific messages + const messages = TOOL_MESSAGES[toolName] || GENERIC_TOOL_MESSAGES; + return `✻ ${randomFrom(messages)}…`; +} + +/** + * Get a contextual message based on what's happening + */ +export function getContextualMessage(context: { + mode: "thinking" | "tool_execution"; + toolName?: string; + filePath?: string; + description?: string; +}): string { + if (context.mode === "thinking") { + return getThinkingMessage(); + } + + if (context.toolName) { + return getToolMessage(context.toolName, context.filePath); + } + + return `✻ ${randomFrom(GENERIC_TOOL_MESSAGES)}…`; +} + +/** + * Status message rotator - cycles through messages at intervals + */ +export class StatusMessageRotator { + private intervalId: ReturnType | null = null; + private currentMode: "thinking" | "tool_execution" = "thinking"; + private toolContext: { name?: string; path?: string } = {}; + + start(onMessage: (message: string) => void, intervalMs = 3000): void { + this.stop(); + + // Emit initial message + onMessage(this.getMessage()); + + // Rotate messages + this.intervalId = setInterval(() => { + onMessage(this.getMessage()); + }, intervalMs); + } + + stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } + + setMode(mode: "thinking" | "tool_execution"): void { + this.currentMode = mode; + } + + setToolContext(name?: string, path?: string): void { + this.toolContext = { name, path }; + } + + private getMessage(): string { + return getContextualMessage({ + mode: this.currentMode, + toolName: this.toolContext.name, + filePath: this.toolContext.path, + }); + } +} diff --git a/src/constants/styles.ts b/src/constants/styles.ts new file mode 100644 index 0000000..86ec7f2 --- /dev/null +++ b/src/constants/styles.ts @@ -0,0 +1,95 @@ +/** + * ANSI color styles constants for terminal output + */ + +// ANSI escape codes +export const Style = { + // Reset + RESET: "\x1b[0m", + + // Text styles + BOLD: "\x1b[1m", + DIM: "\x1b[2m", + ITALIC: "\x1b[3m", + UNDERLINE: "\x1b[4m", + + // Colors + BLACK: "\x1b[30m", + RED: "\x1b[31m", + GREEN: "\x1b[32m", + YELLOW: "\x1b[33m", + BLUE: "\x1b[34m", + MAGENTA: "\x1b[35m", + CYAN: "\x1b[36m", + WHITE: "\x1b[37m", + GRAY: "\x1b[90m", + + // Bright colors + BRIGHT_RED: "\x1b[91m", + BRIGHT_GREEN: "\x1b[92m", + BRIGHT_YELLOW: "\x1b[93m", + BRIGHT_BLUE: "\x1b[94m", + BRIGHT_MAGENTA: "\x1b[95m", + BRIGHT_CYAN: "\x1b[96m", + BRIGHT_WHITE: "\x1b[97m", + + // Background colors + BG_BLACK: "\x1b[40m", + BG_RED: "\x1b[41m", + BG_GREEN: "\x1b[42m", + BG_YELLOW: "\x1b[43m", + BG_BLUE: "\x1b[44m", + BG_MAGENTA: "\x1b[45m", + BG_CYAN: "\x1b[46m", + BG_WHITE: "\x1b[47m", +} as const; + +// Semantic colors +export const Theme = { + primary: Style.BRIGHT_CYAN, + secondary: Style.BRIGHT_MAGENTA, + accent: Style.BRIGHT_BLUE, + success: Style.BRIGHT_GREEN, + warning: Style.BRIGHT_YELLOW, + error: Style.BRIGHT_RED, + info: Style.BRIGHT_BLUE, + text: Style.WHITE, + textMuted: Style.GRAY, + textDim: Style.DIM, +} as const; + +// Icons +export const Icons = { + // Status + success: "\u2714", // ✔ + error: "\u2718", // ✘ + warning: "\u26A0", // ⚠ + info: "\u2139", // ℹ + pending: "\u25CB", // ○ + running: "\u25CF", // ● + + // Actions + arrow: "\u2192", // → + arrowLeft: "\u2190", // ← + arrowUp: "\u2191", // ↑ + arrowDown: "\u2193", // ↓ + + // Tools + bash: "$", + read: "\u2192", // → + write: "\u2190", // ← + edit: "\u270E", // ✎ + search: "\u2731", // ✱ + + // Misc + bullet: "\u2022", // • + dot: "\u00B7", // · + star: "\u2605", // ★ + sparkle: "\u2728", // ✨ + lightning: "\u26A1", // ⚡ + gear: "\u2699", // ⚙ + key: "\u1F511", // 🔑 + lock: "\u1F512", // 🔒 + folder: "\u1F4C1", // 📁 + file: "\u1F4C4", // 📄 +} as const; diff --git a/src/constants/syntax-highlight.ts b/src/constants/syntax-highlight.ts new file mode 100644 index 0000000..5fdb9d7 --- /dev/null +++ b/src/constants/syntax-highlight.ts @@ -0,0 +1,170 @@ +/** + * Syntax highlighting constants + */ + +/** Map file extensions to highlight.js language identifiers */ +export const EXTENSION_TO_LANGUAGE: Record = { + // JavaScript/TypeScript + ".js": "javascript", + ".jsx": "javascript", + ".ts": "typescript", + ".tsx": "typescript", + ".mjs": "javascript", + ".cjs": "javascript", + + // Web + ".html": "html", + ".htm": "html", + ".css": "css", + ".scss": "scss", + ".sass": "scss", + ".less": "less", + ".vue": "vue", + ".svelte": "svelte", + + // Data formats + ".json": "json", + ".yaml": "yaml", + ".yml": "yaml", + ".toml": "toml", + ".xml": "xml", + ".csv": "plaintext", + + // Systems programming + ".c": "c", + ".h": "c", + ".cpp": "cpp", + ".cc": "cpp", + ".cxx": "cpp", + ".hpp": "cpp", + ".rs": "rust", + ".go": "go", + ".zig": "zig", + + // JVM languages + ".java": "java", + ".kt": "kotlin", + ".kts": "kotlin", + ".scala": "scala", + ".groovy": "groovy", + ".gradle": "groovy", + + // .NET languages + ".cs": "csharp", + ".fs": "fsharp", + ".vb": "vbnet", + + // Scripting + ".py": "python", + ".rb": "ruby", + ".php": "php", + ".pl": "perl", + ".lua": "lua", + ".r": "r", + ".R": "r", + + // Shell + ".sh": "bash", + ".bash": "bash", + ".zsh": "bash", + ".fish": "fish", + ".ps1": "powershell", + ".psm1": "powershell", + ".bat": "batch", + ".cmd": "batch", + + // Config files + ".conf": "nginx", + ".ini": "ini", + ".env": "bash", + ".dockerfile": "dockerfile", + ".gitignore": "plaintext", + ".editorconfig": "ini", + + // Documentation + ".md": "markdown", + ".markdown": "markdown", + ".rst": "plaintext", + ".txt": "plaintext", + + // Database + ".sql": "sql", + ".prisma": "prisma", + + // Mobile + ".swift": "swift", + ".m": "objectivec", + ".mm": "objectivec", + ".dart": "dart", + + // Other + ".ex": "elixir", + ".exs": "elixir", + ".erl": "erlang", + ".hrl": "erlang", + ".hs": "haskell", + ".clj": "clojure", + ".cljs": "clojure", + ".elm": "elm", + ".nim": "nim", + ".v": "v", + ".asm": "x86asm", + ".s": "x86asm", + ".wasm": "wasm", + ".graphql": "graphql", + ".gql": "graphql", + ".tf": "hcl", + ".hcl": "hcl", + ".nix": "nix", + ".proto": "protobuf", +} as const; + +/** Special filename mappings */ +export const FILENAME_TO_LANGUAGE: Record = { + Dockerfile: "dockerfile", + Makefile: "makefile", + "CMakeLists.txt": "cmake", + Gemfile: "ruby", + Rakefile: "ruby", + Vagrantfile: "ruby", + Brewfile: "ruby", + ".bashrc": "bash", + ".zshrc": "bash", + ".bash_profile": "bash", + ".profile": "bash", + "package.json": "json", + "tsconfig.json": "json", + "jsconfig.json": "json", + ".prettierrc": "json", + ".eslintrc": "json", + "nginx.conf": "nginx", + "docker-compose.yml": "yaml", + "docker-compose.yaml": "yaml", +} as const; + +/** Human-readable language display names */ +export const LANGUAGE_DISPLAY_NAMES: Record = { + javascript: "JavaScript", + typescript: "TypeScript", + python: "Python", + java: "Java", + cpp: "C++", + csharp: "C#", + go: "Go", + rust: "Rust", + ruby: "Ruby", + php: "PHP", + swift: "Swift", + kotlin: "Kotlin", + scala: "Scala", + html: "HTML", + css: "CSS", + scss: "SCSS", + json: "JSON", + yaml: "YAML", + markdown: "Markdown", + bash: "Bash", + sql: "SQL", + dockerfile: "Dockerfile", + graphql: "GraphQL", +} as const; diff --git a/src/constants/terminal.ts b/src/constants/terminal.ts new file mode 100644 index 0000000..dbe8c35 --- /dev/null +++ b/src/constants/terminal.ts @@ -0,0 +1,9 @@ +/** + * Mouse tracking disable sequence (all modes) + */ +export const DISABLE_MOUSE_TRACKING = + "\x1b[?1006l" + // SGR Mouse Mode + "\x1b[?1015l" + // urxvt Mouse Mode + "\x1b[?1003l" + // All mouse events (motion) + "\x1b[?1002l" + // Button event mouse tracking + "\x1b[?1000l"; // Normal tracking mode diff --git a/src/constants/themes.ts b/src/constants/themes.ts new file mode 100644 index 0000000..16ef0b9 --- /dev/null +++ b/src/constants/themes.ts @@ -0,0 +1,834 @@ +/** + * Built-in Theme Definitions + * + * Provides pre-configured color themes for the TUI + */ + +import type { Theme, ThemeColors } from "@/types/theme"; + +const DEFAULT_COLORS: ThemeColors = { + primary: "#00ffff", + secondary: "#0088ff", + accent: "#ff00ff", + + success: "#00ff00", + error: "#ff0000", + warning: "#ffff00", + info: "#00ffff", + + text: "#ffffff", + textDim: "#808080", + textMuted: "#666666", + + background: "#0a0a0a", + backgroundPanel: "#141414", + backgroundElement: "#1e1e1e", + + border: "#808080", + borderFocus: "#00ffff", + borderWarning: "#ffff00", + borderModal: "#ff00ff", + + bgHighlight: "#00ffff", + bgCursor: "#00ffff", + bgAdded: "#00ff88", + bgRemoved: "#ff4444", + + diffAdded: "#00ff00", + diffRemoved: "#ff0000", + diffContext: "#808080", + diffHeader: "#ffffff", + diffHunk: "#00ffff", + + roleUser: "#00ffff", + roleAssistant: "#00ff00", + roleSystem: "#ffff00", + roleTool: "#ffff00", + + modeIdle: "#00ff00", + modeEditing: "#00ffff", + modeThinking: "#ff00ff", + modeToolExecution: "#ffff00", + modePermission: "#ffff00", + + toolPending: "#808080", + toolRunning: "#ffff00", + toolSuccess: "#00ff00", + toolError: "#ff0000", + + headerGradient: ["#00ffff", "#00dddd", "#0088ff"], +}; + +const DRACULA_COLORS: ThemeColors = { + primary: "#bd93f9", + secondary: "#6272a4", + accent: "#ff79c6", + + success: "#50fa7b", + error: "#ff5555", + warning: "#f1fa8c", + info: "#8be9fd", + + text: "#f8f8f2", + textDim: "#6272a4", + textMuted: "#44475a", + + background: "#282a36", + backgroundPanel: "#21222c", + backgroundElement: "#343746", + + border: "#44475a", + borderFocus: "#bd93f9", + borderWarning: "#f1fa8c", + borderModal: "#ff79c6", + + bgHighlight: "#44475a", + bgCursor: "#bd93f9", + bgAdded: "#50fa7b", + bgRemoved: "#ff5555", + + diffAdded: "#50fa7b", + diffRemoved: "#ff5555", + diffContext: "#6272a4", + diffHeader: "#f8f8f2", + diffHunk: "#8be9fd", + + roleUser: "#8be9fd", + roleAssistant: "#50fa7b", + roleSystem: "#f1fa8c", + roleTool: "#ffb86c", + + modeIdle: "#50fa7b", + modeEditing: "#8be9fd", + modeThinking: "#bd93f9", + modeToolExecution: "#f1fa8c", + modePermission: "#ffb86c", + + toolPending: "#6272a4", + toolRunning: "#f1fa8c", + toolSuccess: "#50fa7b", + toolError: "#ff5555", + + headerGradient: ["#bd93f9", "#ff79c6", "#8be9fd"], +}; + +const NORD_COLORS: ThemeColors = { + primary: "#88c0d0", + secondary: "#5e81ac", + accent: "#b48ead", + + success: "#a3be8c", + error: "#bf616a", + warning: "#ebcb8b", + info: "#88c0d0", + + text: "#eceff4", + textDim: "#4c566a", + textMuted: "#3b4252", + + background: "#2e3440", + backgroundPanel: "#3b4252", + backgroundElement: "#434c5e", + + border: "#3b4252", + borderFocus: "#88c0d0", + borderWarning: "#ebcb8b", + borderModal: "#b48ead", + + bgHighlight: "#3b4252", + bgCursor: "#88c0d0", + bgAdded: "#a3be8c", + bgRemoved: "#bf616a", + + diffAdded: "#a3be8c", + diffRemoved: "#bf616a", + diffContext: "#4c566a", + diffHeader: "#eceff4", + diffHunk: "#81a1c1", + + roleUser: "#88c0d0", + roleAssistant: "#a3be8c", + roleSystem: "#ebcb8b", + roleTool: "#d08770", + + modeIdle: "#a3be8c", + modeEditing: "#88c0d0", + modeThinking: "#b48ead", + modeToolExecution: "#ebcb8b", + modePermission: "#d08770", + + toolPending: "#4c566a", + toolRunning: "#ebcb8b", + toolSuccess: "#a3be8c", + toolError: "#bf616a", + + headerGradient: ["#88c0d0", "#81a1c1", "#5e81ac"], +}; + +const TOKYO_NIGHT_COLORS: ThemeColors = { + primary: "#7aa2f7", + secondary: "#565f89", + accent: "#bb9af7", + + success: "#9ece6a", + error: "#f7768e", + warning: "#e0af68", + info: "#7dcfff", + + text: "#c0caf5", + textDim: "#565f89", + textMuted: "#414868", + + background: "#1a1b26", + backgroundPanel: "#16161e", + backgroundElement: "#24283b", + + border: "#414868", + borderFocus: "#7aa2f7", + borderWarning: "#e0af68", + borderModal: "#bb9af7", + + bgHighlight: "#414868", + bgCursor: "#7aa2f7", + bgAdded: "#9ece6a", + bgRemoved: "#f7768e", + + diffAdded: "#9ece6a", + diffRemoved: "#f7768e", + diffContext: "#565f89", + diffHeader: "#c0caf5", + diffHunk: "#7dcfff", + + roleUser: "#7dcfff", + roleAssistant: "#9ece6a", + roleSystem: "#e0af68", + roleTool: "#ff9e64", + + modeIdle: "#9ece6a", + modeEditing: "#7aa2f7", + modeThinking: "#bb9af7", + modeToolExecution: "#e0af68", + modePermission: "#ff9e64", + + toolPending: "#565f89", + toolRunning: "#e0af68", + toolSuccess: "#9ece6a", + toolError: "#f7768e", + + headerGradient: ["#7aa2f7", "#bb9af7", "#7dcfff"], +}; + +const GRUVBOX_COLORS: ThemeColors = { + primary: "#83a598", + secondary: "#458588", + accent: "#d3869b", + + success: "#b8bb26", + error: "#fb4934", + warning: "#fabd2f", + info: "#83a598", + + text: "#ebdbb2", + textDim: "#665c54", + textMuted: "#504945", + + background: "#282828", + backgroundPanel: "#1d2021", + backgroundElement: "#3c3836", + + border: "#504945", + borderFocus: "#83a598", + borderWarning: "#fabd2f", + borderModal: "#d3869b", + + bgHighlight: "#504945", + bgCursor: "#83a598", + bgAdded: "#b8bb26", + bgRemoved: "#fb4934", + + diffAdded: "#b8bb26", + diffRemoved: "#fb4934", + diffContext: "#665c54", + diffHeader: "#ebdbb2", + diffHunk: "#8ec07c", + + roleUser: "#83a598", + roleAssistant: "#b8bb26", + roleSystem: "#fabd2f", + roleTool: "#fe8019", + + modeIdle: "#b8bb26", + modeEditing: "#83a598", + modeThinking: "#d3869b", + modeToolExecution: "#fabd2f", + modePermission: "#fe8019", + + toolPending: "#665c54", + toolRunning: "#fabd2f", + toolSuccess: "#b8bb26", + toolError: "#fb4934", + + headerGradient: ["#b8bb26", "#83a598", "#fabd2f"], +}; + +const MONOKAI_COLORS: ThemeColors = { + primary: "#66d9ef", + secondary: "#ae81ff", + accent: "#f92672", + + success: "#a6e22e", + error: "#f92672", + warning: "#e6db74", + info: "#66d9ef", + + text: "#f8f8f2", + textDim: "#75715e", + textMuted: "#49483e", + + background: "#272822", + backgroundPanel: "#1e1f1c", + backgroundElement: "#3e3d32", + + border: "#49483e", + borderFocus: "#66d9ef", + borderWarning: "#e6db74", + borderModal: "#ae81ff", + + bgHighlight: "#49483e", + bgCursor: "#66d9ef", + bgAdded: "#a6e22e", + bgRemoved: "#f92672", + + diffAdded: "#a6e22e", + diffRemoved: "#f92672", + diffContext: "#75715e", + diffHeader: "#f8f8f2", + diffHunk: "#66d9ef", + + roleUser: "#66d9ef", + roleAssistant: "#a6e22e", + roleSystem: "#e6db74", + roleTool: "#fd971f", + + modeIdle: "#a6e22e", + modeEditing: "#66d9ef", + modeThinking: "#ae81ff", + modeToolExecution: "#e6db74", + modePermission: "#fd971f", + + toolPending: "#75715e", + toolRunning: "#e6db74", + toolSuccess: "#a6e22e", + toolError: "#f92672", + + headerGradient: ["#a6e22e", "#66d9ef", "#ae81ff"], +}; + +const CATPPUCCIN_COLORS: ThemeColors = { + primary: "#89b4fa", + secondary: "#74c7ec", + accent: "#cba6f7", + + success: "#a6e3a1", + error: "#f38ba8", + warning: "#f9e2af", + info: "#89dceb", + + text: "#cdd6f4", + textDim: "#6c7086", + textMuted: "#45475a", + + background: "#1e1e2e", + backgroundPanel: "#181825", + backgroundElement: "#313244", + + border: "#45475a", + borderFocus: "#89b4fa", + borderWarning: "#f9e2af", + borderModal: "#cba6f7", + + bgHighlight: "#45475a", + bgCursor: "#89b4fa", + bgAdded: "#a6e3a1", + bgRemoved: "#f38ba8", + + diffAdded: "#a6e3a1", + diffRemoved: "#f38ba8", + diffContext: "#6c7086", + diffHeader: "#cdd6f4", + diffHunk: "#89dceb", + + roleUser: "#89dceb", + roleAssistant: "#a6e3a1", + roleSystem: "#f9e2af", + roleTool: "#fab387", + + modeIdle: "#a6e3a1", + modeEditing: "#89b4fa", + modeThinking: "#cba6f7", + modeToolExecution: "#f9e2af", + modePermission: "#fab387", + + toolPending: "#6c7086", + toolRunning: "#f9e2af", + toolSuccess: "#a6e3a1", + toolError: "#f38ba8", + + headerGradient: ["#89b4fa", "#cba6f7", "#f5c2e7"], +}; + +const ONE_DARK_COLORS: ThemeColors = { + primary: "#61afef", + secondary: "#528bff", + accent: "#c678dd", + + success: "#98c379", + error: "#e06c75", + warning: "#e5c07b", + info: "#56b6c2", + + text: "#abb2bf", + textDim: "#5c6370", + textMuted: "#3e4451", + + background: "#282c34", + backgroundPanel: "#21252b", + backgroundElement: "#2c323c", + + border: "#3e4451", + borderFocus: "#61afef", + borderWarning: "#e5c07b", + borderModal: "#c678dd", + + bgHighlight: "#3e4451", + bgCursor: "#61afef", + bgAdded: "#98c379", + bgRemoved: "#e06c75", + + diffAdded: "#98c379", + diffRemoved: "#e06c75", + diffContext: "#5c6370", + diffHeader: "#abb2bf", + diffHunk: "#56b6c2", + + roleUser: "#56b6c2", + roleAssistant: "#98c379", + roleSystem: "#e5c07b", + roleTool: "#d19a66", + + modeIdle: "#98c379", + modeEditing: "#61afef", + modeThinking: "#c678dd", + modeToolExecution: "#e5c07b", + modePermission: "#d19a66", + + toolPending: "#5c6370", + toolRunning: "#e5c07b", + toolSuccess: "#98c379", + toolError: "#e06c75", + + headerGradient: ["#61afef", "#c678dd", "#56b6c2"], +}; + +const SOLARIZED_DARK_COLORS: ThemeColors = { + primary: "#268bd2", + secondary: "#2aa198", + accent: "#6c71c4", + + success: "#859900", + error: "#dc322f", + warning: "#b58900", + info: "#2aa198", + + text: "#839496", + textDim: "#586e75", + textMuted: "#073642", + + background: "#002b36", + backgroundPanel: "#001f27", + backgroundElement: "#073642", + + border: "#073642", + borderFocus: "#268bd2", + borderWarning: "#b58900", + borderModal: "#6c71c4", + + bgHighlight: "#073642", + bgCursor: "#268bd2", + bgAdded: "#859900", + bgRemoved: "#dc322f", + + diffAdded: "#859900", + diffRemoved: "#dc322f", + diffContext: "#586e75", + diffHeader: "#93a1a1", + diffHunk: "#2aa198", + + roleUser: "#2aa198", + roleAssistant: "#859900", + roleSystem: "#b58900", + roleTool: "#cb4b16", + + modeIdle: "#859900", + modeEditing: "#268bd2", + modeThinking: "#6c71c4", + modeToolExecution: "#b58900", + modePermission: "#cb4b16", + + toolPending: "#586e75", + toolRunning: "#b58900", + toolSuccess: "#859900", + toolError: "#dc322f", + + headerGradient: ["#268bd2", "#2aa198", "#859900"], +}; + +const GITHUB_DARK_COLORS: ThemeColors = { + primary: "#58a6ff", + secondary: "#388bfd", + accent: "#a371f7", + + success: "#3fb950", + error: "#f85149", + warning: "#d29922", + info: "#58a6ff", + + text: "#c9d1d9", + textDim: "#8b949e", + textMuted: "#484f58", + + background: "#0d1117", + backgroundPanel: "#010409", + backgroundElement: "#161b22", + + border: "#30363d", + borderFocus: "#58a6ff", + borderWarning: "#d29922", + borderModal: "#a371f7", + + bgHighlight: "#161b22", + bgCursor: "#58a6ff", + bgAdded: "#238636", + bgRemoved: "#da3633", + + diffAdded: "#3fb950", + diffRemoved: "#f85149", + diffContext: "#8b949e", + diffHeader: "#c9d1d9", + diffHunk: "#58a6ff", + + roleUser: "#58a6ff", + roleAssistant: "#3fb950", + roleSystem: "#d29922", + roleTool: "#f0883e", + + modeIdle: "#3fb950", + modeEditing: "#58a6ff", + modeThinking: "#a371f7", + modeToolExecution: "#d29922", + modePermission: "#f0883e", + + toolPending: "#8b949e", + toolRunning: "#d29922", + toolSuccess: "#3fb950", + toolError: "#f85149", + + headerGradient: ["#58a6ff", "#a371f7", "#3fb950"], +}; + +const ROSE_PINE_COLORS: ThemeColors = { + primary: "#c4a7e7", + secondary: "#9ccfd8", + accent: "#ebbcba", + + success: "#31748f", + error: "#eb6f92", + warning: "#f6c177", + info: "#9ccfd8", + + text: "#e0def4", + textDim: "#6e6a86", + textMuted: "#26233a", + + background: "#191724", + backgroundPanel: "#1f1d2e", + backgroundElement: "#26233a", + + border: "#26233a", + borderFocus: "#c4a7e7", + borderWarning: "#f6c177", + borderModal: "#ebbcba", + + bgHighlight: "#21202e", + bgCursor: "#c4a7e7", + bgAdded: "#31748f", + bgRemoved: "#eb6f92", + + diffAdded: "#31748f", + diffRemoved: "#eb6f92", + diffContext: "#6e6a86", + diffHeader: "#e0def4", + diffHunk: "#9ccfd8", + + roleUser: "#9ccfd8", + roleAssistant: "#31748f", + roleSystem: "#f6c177", + roleTool: "#ebbcba", + + modeIdle: "#31748f", + modeEditing: "#c4a7e7", + modeThinking: "#ebbcba", + modeToolExecution: "#f6c177", + modePermission: "#eb6f92", + + toolPending: "#6e6a86", + toolRunning: "#f6c177", + toolSuccess: "#31748f", + toolError: "#eb6f92", + + headerGradient: ["#c4a7e7", "#ebbcba", "#9ccfd8"], +}; + +const KANAGAWA_COLORS: ThemeColors = { + primary: "#7e9cd8", + secondary: "#7fb4ca", + accent: "#957fb8", + + success: "#98bb6c", + error: "#c34043", + warning: "#dca561", + info: "#7fb4ca", + + text: "#dcd7ba", + textDim: "#727169", + textMuted: "#363646", + + background: "#1f1f28", + backgroundPanel: "#181820", + backgroundElement: "#2a2a37", + + border: "#363646", + borderFocus: "#7e9cd8", + borderWarning: "#dca561", + borderModal: "#957fb8", + + bgHighlight: "#2a2a37", + bgCursor: "#7e9cd8", + bgAdded: "#76946a", + bgRemoved: "#c34043", + + diffAdded: "#98bb6c", + diffRemoved: "#c34043", + diffContext: "#727169", + diffHeader: "#dcd7ba", + diffHunk: "#7fb4ca", + + roleUser: "#7fb4ca", + roleAssistant: "#98bb6c", + roleSystem: "#dca561", + roleTool: "#ffa066", + + modeIdle: "#98bb6c", + modeEditing: "#7e9cd8", + modeThinking: "#957fb8", + modeToolExecution: "#dca561", + modePermission: "#ffa066", + + toolPending: "#727169", + toolRunning: "#dca561", + toolSuccess: "#98bb6c", + toolError: "#c34043", + + headerGradient: ["#7e9cd8", "#957fb8", "#7fb4ca"], +}; + +const AYU_DARK_COLORS: ThemeColors = { + primary: "#39bae6", + secondary: "#59c2ff", + accent: "#d2a6ff", + + success: "#7fd962", + error: "#f07178", + warning: "#ffb454", + info: "#59c2ff", + + text: "#bfbdb6", + textDim: "#636e78", + textMuted: "#232834", + + background: "#0a0e14", + backgroundPanel: "#0d1016", + backgroundElement: "#1a1f29", + + border: "#232834", + borderFocus: "#39bae6", + borderWarning: "#ffb454", + borderModal: "#d2a6ff", + + bgHighlight: "#1a1f29", + bgCursor: "#39bae6", + bgAdded: "#7fd962", + bgRemoved: "#f07178", + + diffAdded: "#7fd962", + diffRemoved: "#f07178", + diffContext: "#636e78", + diffHeader: "#bfbdb6", + diffHunk: "#59c2ff", + + roleUser: "#59c2ff", + roleAssistant: "#7fd962", + roleSystem: "#ffb454", + roleTool: "#ff8f40", + + modeIdle: "#7fd962", + modeEditing: "#39bae6", + modeThinking: "#d2a6ff", + modeToolExecution: "#ffb454", + modePermission: "#ff8f40", + + toolPending: "#636e78", + toolRunning: "#ffb454", + toolSuccess: "#7fd962", + toolError: "#f07178", + + headerGradient: ["#39bae6", "#d2a6ff", "#7fd962"], +}; + +const CARGDEV_CYBERPUNK_COLORS: ThemeColors = { + primary: "#8be9fd", + secondary: "#bd93f9", + accent: "#ff79c6", + + success: "#50fa7b", + error: "#ff5555", + warning: "#ffb86c", + info: "#8be9fd", + + text: "#e0e0e0", + textDim: "#666666", + textMuted: "#44475a", + + background: "#0d1926", + backgroundPanel: "#071018", + backgroundElement: "#112233", + + border: "#003b46", + borderFocus: "#8be9fd", + borderWarning: "#ffb86c", + borderModal: "#ff79c6", + + bgHighlight: "#112233", + bgCursor: "#ff79c6", + bgAdded: "#50fa7b", + bgRemoved: "#ff5555", + + diffAdded: "#50fa7b", + diffRemoved: "#ff5555", + diffContext: "#666666", + diffHeader: "#f8f8f2", + diffHunk: "#8be9fd", + + roleUser: "#8be9fd", + roleAssistant: "#50fa7b", + roleSystem: "#ffb86c", + roleTool: "#bd93f9", + + modeIdle: "#50fa7b", + modeEditing: "#8be9fd", + modeThinking: "#ff79c6", + modeToolExecution: "#ffb86c", + modePermission: "#bd93f9", + + toolPending: "#666666", + toolRunning: "#ffb86c", + toolSuccess: "#50fa7b", + toolError: "#ff5555", + + headerGradient: ["#ff79c6", "#bd93f9", "#8be9fd"], +}; + +export const THEMES: Record = { + default: { + name: "default", + displayName: "Default", + colors: DEFAULT_COLORS, + }, + dracula: { + name: "dracula", + displayName: "Dracula", + colors: DRACULA_COLORS, + }, + nord: { + name: "nord", + displayName: "Nord", + colors: NORD_COLORS, + }, + "tokyo-night": { + name: "tokyo-night", + displayName: "Tokyo Night", + colors: TOKYO_NIGHT_COLORS, + }, + gruvbox: { + name: "gruvbox", + displayName: "Gruvbox", + colors: GRUVBOX_COLORS, + }, + monokai: { + name: "monokai", + displayName: "Monokai", + colors: MONOKAI_COLORS, + }, + catppuccin: { + name: "catppuccin", + displayName: "Catppuccin Mocha", + colors: CATPPUCCIN_COLORS, + }, + "one-dark": { + name: "one-dark", + displayName: "One Dark", + colors: ONE_DARK_COLORS, + }, + "solarized-dark": { + name: "solarized-dark", + displayName: "Solarized Dark", + colors: SOLARIZED_DARK_COLORS, + }, + "github-dark": { + name: "github-dark", + displayName: "GitHub Dark", + colors: GITHUB_DARK_COLORS, + }, + "rose-pine": { + name: "rose-pine", + displayName: "Rosé Pine", + colors: ROSE_PINE_COLORS, + }, + kanagawa: { + name: "kanagawa", + displayName: "Kanagawa", + colors: KANAGAWA_COLORS, + }, + "ayu-dark": { + name: "ayu-dark", + displayName: "Ayu Dark", + colors: AYU_DARK_COLORS, + }, + "cargdev-cyberpunk": { + name: "cargdev-cyberpunk", + displayName: "Cargdev Cyberpunk", + colors: CARGDEV_CYBERPUNK_COLORS, + }, +}; + +export const THEME_NAMES = Object.keys(THEMES); + +export const DEFAULT_THEME = "default"; + +export const getTheme = (name: string): Theme => { + return THEMES[name] ?? THEMES[DEFAULT_THEME]; +}; + +export const getThemeNames = (): string[] => { + return THEME_NAMES; +}; diff --git a/src/constants/tips.ts b/src/constants/tips.ts new file mode 100644 index 0000000..4c71298 --- /dev/null +++ b/src/constants/tips.ts @@ -0,0 +1,50 @@ +/** + * Tips and shortcuts constants for CodeTyper CLI + */ + +// Tips with {highlight}text{/highlight} markers +export const TIPS = [ + "Type {highlight}@filename{/highlight} to add a file to context", + "Use {highlight}/help{/highlight} to see all available commands", + "Press {highlight}/clear{/highlight} to start a fresh conversation", + "Use {highlight}--yes{/highlight} or {highlight}-y{/highlight} to auto-approve all commands", + "Add {highlight}--verbose{/highlight} to see detailed tool execution", + "Use {highlight}/models{/highlight} to see available models", + "Type {highlight}/provider{/highlight} to switch LLM providers", + "Files are automatically added to context with {highlight}@src/*.ts{/highlight} globs", + "Use {highlight}/save{/highlight} to save your session", + "Resume sessions with {highlight}-r{/highlight} or {highlight}--resume{/highlight}", + "Use {highlight}-c{/highlight} to continue your last session", + "Print mode {highlight}-p{/highlight} outputs response and exits", + "Permission patterns like {highlight}Bash(git:*){/highlight} auto-approve commands", + "Use {highlight}codetyper permissions ls{/highlight} to see allowed patterns", + "Add {highlight}Bash(npm install:*){/highlight} to allow npm install globally", + "Project settings are in {highlight}.codetyper/settings.json{/highlight}", + "Global settings are in {highlight}~/.codetyper/settings.json{/highlight}", + "Use {highlight}/context{/highlight} to check conversation size", + "Use {highlight}/compact{/highlight} to reduce context size", + "Commands like {highlight}ls{/highlight} and {highlight}cat{/highlight} are auto-approved", + "Use {highlight}/exit{/highlight} or {highlight}/quit{/highlight} to end the session", + "The agent can create folders with {highlight}mkdir{/highlight}", + "The agent can install packages with {highlight}npm install{/highlight}", + 'Use {highlight}@"file with spaces"{/highlight} for files with spaces', + "The read tool shows file content with line numbers", + "The edit tool replaces exact text matches", + "The write tool creates files and directories", + "Use {highlight}/history{/highlight} to see past messages", + "Session rules persist until you exit", + "Project rules persist across sessions in this directory", + "Global rules apply everywhere", +] as const; + +// Keyboard shortcuts +export const SHORTCUTS = [ + { key: "Ctrl+C", description: "Cancel current operation / Exit" }, + { key: "/help", description: "Show help" }, + { key: "/clear", description: "Clear conversation" }, + { key: "/exit", description: "Exit chat" }, + { key: "@file", description: "Add file to context" }, +] as const; + +// Highlight regex pattern +export const TIP_HIGHLIGHT_REGEX = /\{highlight\}(.*?)\{\/highlight\}/g; diff --git a/src/constants/tools.ts b/src/constants/tools.ts new file mode 100644 index 0000000..08d3d25 --- /dev/null +++ b/src/constants/tools.ts @@ -0,0 +1,13 @@ +/** + * Tool system constants + */ + +export const SCHEMA_SKIP_KEYS = ["$schema"] as const; + +export const SCHEMA_SKIP_VALUES: Record = { + additionalProperties: false, +} as const; + +export type SchemaSkipKey = (typeof SCHEMA_SKIP_KEYS)[number]; + +export const TOOL_NAMES = ["read", "glob", "grep"]; diff --git a/src/constants/tui-components.ts b/src/constants/tui-components.ts new file mode 100644 index 0000000..22ba098 --- /dev/null +++ b/src/constants/tui-components.ts @@ -0,0 +1,256 @@ +/** + * TUI Component Constants + * + * Constants used by TUI components extracted for modularity + */ + +import type { SelectOption, SlashCommand } from "@/types/tui"; + +// ============================================================================ +// Header Constants +// ============================================================================ + +export const TUI_BANNER = [ + "█▀▀ █▀█ █▀▄ █▀▀ ▀█▀ █▄█ █▀█ █▀▀ █▀█", + "█ █ █ █ █ █▀▀ █ █ █▀▀ █▀▀ █▀▄", + "▀▀▀ ▀▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀", +] as const; + +export const HEADER_GRADIENT_COLORS = [ + "cyanBright", + "cyan", + "blueBright", +] as const; + +export type HeaderGradientColor = (typeof HEADER_GRADIENT_COLORS)[number]; + +// ============================================================================ +// Status Bar Mode Display Constants +// ============================================================================ + +export type ModeDisplayConfig = { + readonly text: string; + readonly color: "green" | "cyan" | "magenta" | "yellow"; +}; + +export const MODE_DISPLAY_CONFIG: Record = { + idle: { text: "Ready", color: "green" }, + editing: { text: "Editing", color: "cyan" }, + thinking: { text: "✻ Thinking…", color: "magenta" }, + tool_execution: { text: "✻ Running tool…", color: "yellow" }, + permission_prompt: { text: "Awaiting permission", color: "yellow" }, + command_menu: { text: "Command Menu", color: "cyan" }, + model_select: { text: "Select Model", color: "magenta" }, + agent_select: { text: "Select Agent", color: "magenta" }, + theme_select: { text: "Select Theme", color: "magenta" }, + mcp_select: { text: "MCP Servers", color: "magenta" }, + mode_select: { text: "Select Mode", color: "magenta" }, + provider_select: { text: "Select Provider", color: "magenta" }, + learning_prompt: { text: "Save Learning?", color: "cyan" }, +} as const; + +export const DEFAULT_MODE_DISPLAY: ModeDisplayConfig = { + text: "Ready", + color: "green", +} as const; + +// ============================================================================ +// Log Panel Constants +// ============================================================================ + +export const LOG_ENTRY_EXTRA_LINES = { + user: 2, + assistant: 2, + tool: 1, + toolWithDiff: 10, + default: 1, +} as const; + +export const TOOL_STATUS_ICONS = { + pending: "○", + running: "◐", + success: "✓", + error: "✗", +} as const; + +export const TOOL_STATUS_COLORS = { + pending: "gray", + running: "yellow", + success: "green", + error: "red", +} as const; + +export type ToolStatusColor = + (typeof TOOL_STATUS_COLORS)[keyof typeof TOOL_STATUS_COLORS]; + +export const THINKING_SPINNER_FRAMES = [ + "⠋", + "⠙", + "⠹", + "⠸", + "⠼", + "⠴", + "⠦", + "⠧", + "⠇", + "⠏", +] as const; + +export const THINKING_SPINNER_INTERVAL = 80; + +export const LOG_PANEL_RESERVED_HEIGHT = 15; +export const LOG_PANEL_MIN_HEIGHT = 5; +export const LOG_PANEL_DEFAULT_TERMINAL_HEIGHT = 24; + +// ============================================================================ +// Permission Modal Constants +// ============================================================================ + +export const PERMISSION_OPTIONS: SelectOption[] = [ + { + key: "y", + label: "Yes (once)", + value: "once", + description: "Allow this command only", + }, + { + key: "s", + label: "Yes (session)", + value: "session", + description: "Allow pattern for this session", + }, + { + key: "l", + label: "Yes (project)", + value: "local", + description: "Allow pattern for this project", + }, + { + key: "g", + label: "Yes (global)", + value: "global", + description: "Allow pattern everywhere", + }, + { + key: "n", + label: "No", + value: "deny", + description: "Deny this command", + }, +]; + +export const PERMISSION_TYPE_LABELS = { + bash: "Execute command", + read: "Read file", + write: "Write file", + edit: "Edit file", +} as const; + +// ============================================================================ +// Command Menu Constants +// ============================================================================ + +export const SLASH_COMMANDS: SlashCommand[] = [ + // General commands + { name: "help", description: "Show available commands", category: "general" }, + { + name: "clear", + description: "Clear conversation history", + category: "general", + }, + { name: "exit", description: "Exit the chat", category: "general" }, + + // Session commands + { name: "save", description: "Save current session", category: "session" }, + { + name: "context", + description: "Show context information", + category: "session", + }, + { + name: "usage", + description: "Show token usage statistics", + category: "session", + }, + { + name: "remember", + description: "Save a learning about the project", + category: "session", + }, + { + name: "learnings", + description: "Show saved learnings", + category: "session", + }, + + // Settings commands + { name: "model", description: "Select AI model", category: "settings" }, + { name: "agent", description: "Select agent", category: "settings" }, + { name: "mode", description: "Switch interaction mode", category: "settings" }, + { + name: "provider", + description: "Switch LLM provider", + category: "settings", + }, + { + name: "status", + description: "Show provider status", + category: "settings", + }, + { name: "theme", description: "Change color theme", category: "settings" }, + { name: "mcp", description: "Manage MCP servers", category: "settings" }, + + // Account commands + { + name: "whoami", + description: "Show logged in account", + category: "account", + }, + { + name: "login", + description: "Authenticate with provider", + category: "account", + }, + { + name: "logout", + description: "Sign out from provider", + category: "account", + }, +]; + +export const COMMAND_CATEGORIES = [ + "general", + "session", + "settings", + "account", +] as const; + +export type CommandCategory = (typeof COMMAND_CATEGORIES)[number]; + +// ============================================================================ +// Learning Modal Constants +// ============================================================================ + +export const LEARNING_OPTIONS: SelectOption[] = [ + { + key: "y", + label: "Yes (project)", + value: "local", + description: "Save for this project", + }, + { + key: "g", + label: "Yes (global)", + value: "global", + description: "Save for all projects", + }, + { + key: "n", + label: "No", + value: "skip", + description: "Skip this learning", + }, +]; + +export const LEARNING_CONTENT_MAX_LENGTH = 100; +export const LEARNING_TRUNCATION_SUFFIX = "..."; diff --git a/src/constants/ui.ts b/src/constants/ui.ts new file mode 100644 index 0000000..4639a58 --- /dev/null +++ b/src/constants/ui.ts @@ -0,0 +1,41 @@ +/** + * UI Constants + */ + +// Keyboard hints displayed in status bar +export const STATUS_HINTS = { + INTERRUPT: "ctrl+c to interrupt", + INTERRUPT_CONFIRM: "ctrl+c again to confirm", + TOGGLE_TODOS: "ctrl+t to hide todos", + TOGGLE_TODOS_SHOW: "ctrl+t to show todos", +} as const; + +// Time formatting +export const TIME_UNITS = { + SECOND: 1000, + MINUTE: 60 * 1000, + HOUR: 60 * 60 * 1000, +} as const; + +// Token display formatting +export const TOKEN_DISPLAY = { + K_THRESHOLD: 1000, + DECIMALS: 1, +} as const; + +// Status bar separator +export const STATUS_SEPARATOR = " · "; + +// Interrupt timeout (ms) - time before interrupt pending resets +export const INTERRUPT_TIMEOUT = 2000; + +// Terminal escape sequences for fullscreen mode +export const TERMINAL_SEQUENCES = { + ENTER_ALTERNATE_SCREEN: "\x1b[?1049h", + LEAVE_ALTERNATE_SCREEN: "\x1b[?1049l", + CLEAR_SCREEN: "\x1b[2J", + CLEAR_SCROLLBACK: "\x1b[3J", + CURSOR_HOME: "\x1b[H", + HIDE_CURSOR: "\x1b[?25l", + SHOW_CURSOR: "\x1b[?25h", +} as const; diff --git a/src/constants/view.ts b/src/constants/view.ts new file mode 100644 index 0000000..b5bf860 --- /dev/null +++ b/src/constants/view.ts @@ -0,0 +1,11 @@ +/** + * View tool constants + */ + +export const VIEW_MESSAGES = { + FAILED: (error: unknown) => `Failed to read file: ${error}`, +} as const; + +export const VIEW_DEFAULTS = { + START_LINE: 1, +} as const; diff --git a/src/constants/write.ts b/src/constants/write.ts new file mode 100644 index 0000000..b2861f7 --- /dev/null +++ b/src/constants/write.ts @@ -0,0 +1,24 @@ +/** + * Write tool constants + */ + +export const WRITE_MESSAGES = { + PERMISSION_DENIED: "Permission denied by user", +} as const; + +export const WRITE_TITLES = { + CANCELLED: (path: string) => `Write cancelled: ${path}`, + FAILED: (path: string) => `Write failed: ${path}`, + WRITING: (name: string) => `Writing ${name}`, + OVERWROTE: (path: string) => `Overwrote: ${path}`, + CREATED: (path: string) => `Created: ${path}`, + OVERWRITE_DESC: (path: string) => `Overwrite file: ${path}`, + CREATE_DESC: (path: string) => `Create file: ${path}`, +} as const; + +export const WRITE_DESCRIPTION = `Write content to a file. Creates the file if it doesn't exist, or overwrites if it does. + +Guidelines: +- Use absolute paths +- Parent directories will be created automatically +- Requires user approval for file writes`; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a947b57 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,662 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { Command } from "commander"; +import { handleCommand } from "@commands/handlers"; +import { readFile } from "fs/promises"; +import { execute } from "@commands/chat-tui"; +import { + initializeProviders, + loginProvider, + getProviderNames, + displayProvidersStatus, +} from "@providers/index"; +import { getConfig } from "@services/config"; +import { deleteSession, getSessionSummaries } from "@services/session"; +import { + initializePermissions, + listPatterns, + addGlobalPattern, + addLocalPattern, +} from "@services/permissions"; +import { + projectConfig, + initProject, + initGlobal, + getRules, + addRule, + getAgents, + getSkills, + getLearnings, + addLearning, + getSettings, + buildLearningsContext, +} from "@services/project-config"; +import { createPlan, displayPlan, approvePlan } from "@services/planner"; +import { ensureXdgDirectories } from "@utils/ensure-directories"; +import chalk from "chalk"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Read version from package.json +const packageJson = JSON.parse( + await readFile(join(__dirname, "../package.json"), "utf-8"), +); +const { version } = packageJson; + +// Ensure XDG directories exist +await ensureXdgDirectories(); + +// Auto-initialize config folders on startup +await projectConfig.autoInitialize(); + +const program = new Command(); + +program + .name("codetyper") + .description("CodeTyper AI Agent - Autonomous code generation assistant") + .version(version) + .argument("[prompt...]", "Initial prompt to start chat with") + .option("-p, --print", "Print response and exit (non-interactive mode)") + .option( + "-c, --continue", + "Continue most recent conversation in current directory", + ) + .option("-r, --resume ", "Resume a specific session by ID") + .option("-m, --model ", "Model to use") + .option("--provider ", "Provider to use (copilot, ollama)") + .option("-f, --file ", "Files to add to context") + .option("--system-prompt ", "Replace the system prompt") + .option("--append-system-prompt ", "Append to the system prompt") + .option("--verbose", "Enable verbose output") + .option("-y, --yes", "Auto-approve all tool executions") + .action(async (promptParts, options) => { + await initializeProviders(); + + const initialPrompt = promptParts.join(" ").trim(); + + // Check for piped input + let pipedInput = ""; + if (!process.stdin.isTTY) { + const chunks: Buffer[] = []; + for await (const chunk of process.stdin) { + chunks.push(chunk); + } + pipedInput = Buffer.concat(chunks).toString("utf-8").trim(); + } + + // Combine piped input with prompt + const fullPrompt = pipedInput + ? initialPrompt + ? `${pipedInput}\n\n${initialPrompt}` + : pipedInput + : initialPrompt; + + const chatOptions = { + provider: options.provider, + model: options.model, + files: options.file, + initialPrompt: fullPrompt || undefined, + continueSession: options.continue, + resumeSession: options.resume, + systemPrompt: options.systemPrompt, + appendSystemPrompt: options.appendSystemPrompt, + printMode: options.print, + verbose: options.verbose, + autoApprove: options.yes, + }; + + await execute(chatOptions); + }); + +// ========== LOGIN COMMAND ========== +program + .command("login [provider]") + .description("Configure API credentials for a provider") + .action(async (provider?: string) => { + await initializeProviders(); + + const validProviders = getProviderNames(); + + if (!provider) { + console.log("\n" + chalk.bold("Available providers:")); + for (const p of validProviders) { + console.log(` - ${p}`); + } + console.log("\n" + chalk.gray("Usage: codetyper login ")); + return; + } + + if (!validProviders.includes(provider as any)) { + console.error(chalk.red(`Invalid provider: ${provider}`)); + console.log("Valid providers: " + validProviders.join(", ")); + process.exit(1); + } + + await loginProvider(provider as any); + }); + +// ========== RUN COMMAND ========== +program + .command("run ") + .description("Execute autonomous task with the agent") + .option("-a, --agent ", "Agent type to use", "coder") + .option("-f, --file ", "Context files for the task") + .option("-d, --dry-run", "Generate plan only, don't execute", false) + .option("-i, --max-iterations ", "Maximum iterations", "20") + .option("--auto-approve", "Automatically approve all actions", false) + .action(async (task, options) => { + await initializeProviders(); + await handleCommand("run", { task, files: options.file, ...options }); + }); + +// ========== SESSION COMMAND ========== +const sessionCommand = program + .command("session") + .description("Manage chat sessions"); + +sessionCommand + .command("list") + .alias("ls") + .description("List all saved sessions") + .action(async () => { + const summaries = await getSessionSummaries(); + + if (summaries.length === 0) { + console.log(chalk.gray("No saved sessions")); + return; + } + + console.log("\n" + chalk.bold("Saved Sessions:") + "\n"); + + for (const session of summaries) { + const date = new Date(session.updatedAt).toLocaleDateString(); + const time = new Date(session.updatedAt).toLocaleTimeString(); + const preview = session.lastMessage + ? session.lastMessage.slice(0, 60).replace(/\n/g, " ") + : "(no messages)"; + + console.log(`${chalk.cyan(session.id)}`); + console.log( + ` ${chalk.gray(`${date} ${time}`)} | ${session.messageCount} messages`, + ); + console.log( + ` ${chalk.gray(preview)}${preview.length >= 60 ? "..." : ""}`, + ); + if (session.workingDirectory) { + console.log(` ${chalk.gray(`Dir: ${session.workingDirectory}`)}`); + } + console.log(); + } + }); + +sessionCommand + .command("delete ") + .alias("rm") + .description("Delete a session by ID") + .action(async (id: string) => { + try { + await deleteSession(id); + console.log(chalk.green(`Deleted session: ${id}`)); + } catch (error) { + console.error(chalk.red(`Failed to delete session: ${error}`)); + } + }); + +// ========== MODELS COMMAND ========== +program + .command("models [provider]") + .description("List available models for a provider") + .action(async (provider?: string) => { + await initializeProviders(); + const config = await getConfig(); + const targetProvider = (provider || config.get("provider")) as any; + + const { getProvider, getProviderStatus } = await import("@providers/index"); + const providerInstance = getProvider(targetProvider); + const status = await getProviderStatus(targetProvider); + + console.log(`\n${chalk.bold(providerInstance.displayName)} Models\n`); + + if (!status.valid) { + console.log( + chalk.yellow( + `Provider not configured. Run: codetyper login ${targetProvider}`, + ), + ); + return; + } + + const models = await providerInstance.getModels(); + const defaultModel = providerInstance.getDefaultModel(); + + for (const model of models) { + const isDefault = model.id === defaultModel; + const marker = isDefault ? chalk.green("*") : " "; + const tools = model.supportsTools ? chalk.gray("[tools]") : ""; + const streaming = model.supportsStreaming ? chalk.gray("[stream]") : ""; + + console.log( + `${marker} ${chalk.cyan(model.id)} - ${model.name} ${tools} ${streaming}`, + ); + } + + console.log(`\n${chalk.gray("* = default model")}`); + }); + +// ========== PROVIDERS COMMAND ========== +program + .command("providers") + .description("Show status of all LLM providers") + .action(async () => { + await initializeProviders(); + const config = await getConfig(); + await displayProvidersStatus(config.get("provider")); + }); + +// ========== CONFIG COMMAND ========== +const configCommand = program + .command("config") + .description("View or modify CLI configuration"); + +configCommand + .command("show") + .description("Show current configuration") + .action(async () => { + await handleCommand("config", { action: "show" }); + }); + +configCommand + .command("path") + .description("Show config file path") + .action(async () => { + await handleCommand("config", { action: "path" }); + }); + +configCommand + .command("set ") + .description("Set configuration value (provider, model)") + .action(async (key, value) => { + await handleCommand("config", { action: "set", key, value }); + }); + +// ========== CLASSIFY COMMAND ========== +program + .command("classify ") + .description("Analyze user prompt and classify the intent") + .option("-c, --context ", "Additional context for classification") + .option("-f, --file ", "Referenced files for context") + .action(async (prompt, options) => { + await initializeProviders(); + await handleCommand("classify", { + prompt, + files: options.file, + ...options, + }); + }); + +// ========== PLAN COMMAND ========== +const planCommand = program + .command("plan") + .description("Generate a detailed execution plan for a task"); + +["code", "fix", "refactor", "test", "document", "explain"].forEach((intent) => { + planCommand + .command(intent) + .description(`Plan for ${intent} intent`) + .requiredOption("-t, --task ", "Task description") + .option("-f, --file ", "Files to operate on") + .option("-o, --output ", "Save plan to file (JSON format)") + .action(async (options) => { + await initializeProviders(); + await handleCommand("plan", { intent, files: options.file, ...options }); + }); +}); + +// ========== INIT COMMAND ========== +program + .command("init") + .description("Initialize codetyper configuration in current directory") + .option("-g, --global", "Initialize global configuration") + .action(async (options) => { + if (options.global) { + await initGlobal(); + console.log( + chalk.green( + "✓ Initialized global configuration at ~/.config/codetyper/", + ), + ); + } else { + await initProject(); + console.log( + chalk.green("✓ Initialized project configuration at .codetyper/"), + ); + } + + console.log("\nCreated directories:"); + console.log(chalk.gray(" - rules/ (project-specific rules)")); + console.log(chalk.gray(" - agents/ (custom agent configurations)")); + console.log(chalk.gray(" - skills/ (custom skills/commands)")); + console.log(chalk.gray(" - learnings/ (saved learnings)")); + }); + +// ========== PERMISSIONS COMMAND ========== +const permCommand = program + .command("permissions") + .alias("perm") + .description("Manage command execution permissions"); + +permCommand + .command("list") + .alias("ls") + .description("List all permission patterns") + .action(async () => { + await initializePermissions(); + const patterns = listPatterns(); + + console.log("\n" + chalk.bold("Permission Patterns") + "\n"); + + const hasPatterns = + patterns.global.length > 0 || + patterns.local.length > 0 || + patterns.session.length > 0; + if (!hasPatterns) { + console.log(chalk.gray("No permission patterns configured")); + console.log( + chalk.gray("\nPatterns are auto-created when you approve commands."), + ); + console.log( + chalk.gray( + "Format: Bash(command:args), Read(*), Write(*.ts), Edit(src/*)", + ), + ); + return; + } + + if (patterns.global.length > 0) { + console.log( + chalk.magenta("Global Patterns (~/.config/codetyper/settings.json):"), + ); + for (const pattern of patterns.global) { + console.log(` ${chalk.green("allow")} ${pattern}`); + } + console.log(); + } + + if (patterns.local.length > 0) { + console.log(chalk.cyan("Project Patterns (.codetyper/settings.json):")); + for (const pattern of patterns.local) { + console.log(` ${chalk.green("allow")} ${pattern}`); + } + console.log(); + } + + if (patterns.session.length > 0) { + console.log(chalk.yellow("Session Patterns (temporary):")); + for (const pattern of patterns.session) { + console.log(` ${chalk.green("allow")} ${pattern}`); + } + } + }); + +permCommand + .command("allow ") + .option("-g, --global", "Add to global patterns") + .option("-l, --local", "Add to project patterns (default)") + .description("Allow a pattern (e.g., Bash(npm install:*), Read(*))") + .action(async (pattern: string, options) => { + await initializePermissions(); + if (options.global) { + await addGlobalPattern(pattern); + console.log(chalk.magenta(`✓ Added global pattern: ${pattern}`)); + } else { + await addLocalPattern(pattern); + console.log(chalk.cyan(`✓ Added project pattern: ${pattern}`)); + } + }); + +// ========== RULES COMMAND ========== +const rulesCommand = program + .command("rules") + .description("Manage project rules"); + +rulesCommand + .command("list") + .alias("ls") + .description("List all rules") + .action(async () => { + const rules = await getRules(); + + console.log("\n" + chalk.bold("Project Rules") + "\n"); + + if (rules.length === 0) { + console.log(chalk.gray("No rules configured")); + console.log(chalk.gray("Run: codetyper init")); + return; + } + + for (const rule of rules) { + console.log(chalk.cyan(`• ${rule.name}`)); + const preview = rule.content.split("\n").slice(0, 3).join("\n"); + console.log( + chalk.gray(preview.slice(0, 200) + (preview.length > 200 ? "..." : "")), + ); + console.log(); + } + }); + +rulesCommand + .command("add ") + .description("Add a new rule") + .option("-g, --global", "Add as global rule") + .option("-c, --content ", "Rule content") + .action(async (name: string, options) => { + if (!options.content) { + console.log( + chalk.yellow("Rule content is required. Use --content flag."), + ); + return; + } + + await addRule(name, options.content, options.global); + console.log(chalk.green(`✓ Added rule: ${name}`)); + }); + +// ========== AGENTS COMMAND ========== +const agentsCommand = program + .command("agents") + .description("Manage custom agents"); + +agentsCommand + .command("list") + .alias("ls") + .description("List all agents") + .action(async () => { + const agents = await getAgents(); + + console.log("\n" + chalk.bold("Custom Agents") + "\n"); + + if (agents.length === 0) { + console.log(chalk.gray("No custom agents configured")); + return; + } + + for (const agent of agents) { + console.log(chalk.cyan(`• ${agent.name}`)); + console.log(` ${chalk.gray(agent.description)}`); + if (agent.model) console.log(` ${chalk.gray(`Model: ${agent.model}`)}`); + console.log(); + } + }); + +// ========== SKILLS COMMAND ========== +const skillsCommand = program + .command("skills") + .description("Manage custom skills"); + +skillsCommand + .command("list") + .alias("ls") + .description("List all skills") + .action(async () => { + const skills = await getSkills(); + + console.log("\n" + chalk.bold("Custom Skills") + "\n"); + + if (skills.length === 0) { + console.log(chalk.gray("No custom skills configured")); + return; + } + + for (const skill of skills) { + console.log(chalk.cyan(`/${skill.command}`) + ` - ${skill.name}`); + console.log(` ${chalk.gray(skill.description)}`); + console.log(); + } + }); + +// ========== LEARNINGS COMMAND ========== +const learningsCommand = program + .command("learnings") + .alias("learn") + .description("Manage project learnings"); + +learningsCommand + .command("list") + .alias("ls") + .description("List all learnings") + .action(async () => { + const learnings = await getLearnings(); + + console.log("\n" + chalk.bold("Project Learnings") + "\n"); + + if (learnings.length === 0) { + console.log(chalk.gray("No learnings saved")); + return; + } + + for (const learning of learnings.slice(0, 20)) { + const date = new Date(learning.createdAt).toLocaleDateString(); + console.log(`${chalk.gray(date)} - ${learning.content.slice(0, 80)}`); + } + }); + +learningsCommand + .command("add ") + .description("Add a new learning") + .option("-g, --global", "Add as global learning") + .option("-c, --context ", "Context for the learning") + .action(async (content: string, options) => { + await addLearning(content, options.context, options.global); + console.log(chalk.green("✓ Learning saved")); + }); + +// ========== UPGRADE COMMAND ========== +program + .command("upgrade") + .description("Update codetyper to the latest version") + .option("-c, --check", "Check for updates without installing") + .option("-v, --version ", "Install a specific version") + .action(async (options) => { + const { performUpgrade } = await import("@services/upgrade"); + await performUpgrade({ + check: options.check, + version: options.version, + }); + }); + +// ========== MCP COMMAND ========== +program + .command("mcp [subcommand] [args...]") + .description("Manage MCP (Model Context Protocol) servers") + .action(async (subcommand, args) => { + const { mcpCommand } = await import("@commands/mcp"); + await mcpCommand([subcommand, ...args].filter(Boolean)); + }); + +// ========== TASK COMMAND ========== +program + .command("task ") + .alias("do") + .description("Execute a task with automatic planning") + .option("-f, --file ", "Context files for the task") + .option("-d, --dry-run", "Show plan without executing") + .option("--auto-approve", "Automatically approve all actions") + .action(async (description: string, options) => { + await initializeProviders(); + await initializePermissions(); + + const settings = await getSettings(); + + // Build context from files and learnings + let context = ""; + + if (options.file) { + const { readFile } = await import("fs/promises"); + for (const file of options.file) { + try { + const content = await readFile(file, "utf-8"); + context += `\n\n--- ${file} ---\n${content}`; + } catch { + console.log(chalk.yellow(`Warning: Could not read file ${file}`)); + } + } + } + + const learningsContext = await buildLearningsContext(); + if (learningsContext) { + context += "\n\n" + learningsContext; + } + + // Generate plan + console.log(chalk.cyan("\nGenerating execution plan...\n")); + + const plan = await createPlan( + description, + context, + settings.defaultProvider as any, + settings.defaultModel, + ); + + displayPlan(plan); + + if (options.dryRun) { + console.log(chalk.yellow("Dry run - not executing plan")); + return; + } + + // Ask for approval + console.log(""); + console.log(chalk.yellow("Execute this plan? [y/n]: ")); + + const answer = await new Promise((resolve) => { + process.stdin.setRawMode?.(true); + process.stdin.resume(); + process.stdin.once("data", (data) => { + process.stdin.setRawMode?.(false); + resolve(data.toString().trim().toLowerCase()); + }); + }); + + if (answer !== "y" && answer !== "yes") { + console.log(chalk.gray("Plan cancelled")); + return; + } + + approvePlan(); + console.log(chalk.green("\n✓ Plan approved - executing...\n")); + + // Execute steps - this is a simplified version + // In a full implementation, each step type would have specific handlers + await execute({ + provider: settings.defaultProvider as any, + model: settings.defaultModel, + files: options.file, + initialPrompt: `Execute this plan step by step:\n\n${plan.steps.map((s) => `${s.id}. ${s.description}`).join("\n")}`, + }); + }); + +// Parse arguments +program.parse(process.argv); diff --git a/src/interfaces/AgentOptions.ts b/src/interfaces/AgentOptions.ts new file mode 100644 index 0000000..3e80319 --- /dev/null +++ b/src/interfaces/AgentOptions.ts @@ -0,0 +1,22 @@ +/** + * Agent Options Interface + */ + +import type { ProviderName } from "@/types/providers"; +import type { ToolCall, ToolResult } from "@tools/index"; + +export interface AgentOptions { + provider: ProviderName; + model?: string; + maxIterations?: number; + onToolCall?: (call: ToolCall) => void; + onToolResult?: (callId: string, result: ToolResult) => void; + onText?: (text: string) => void; + onThinking?: (text: string) => void; + onError?: (error: string) => void; + onWarning?: (warning: string) => void; + verbose?: boolean; + autoApprove?: boolean; + /** Chat mode - only read-only tools, no file modifications */ + chatMode?: boolean; +} diff --git a/src/interfaces/AgentResult.ts b/src/interfaces/AgentResult.ts new file mode 100644 index 0000000..8ede9c3 --- /dev/null +++ b/src/interfaces/AgentResult.ts @@ -0,0 +1,12 @@ +/** + * Agent Result Interface + */ + +import type { ToolCall, ToolResult } from "@tools/index"; + +export interface AgentResult { + success: boolean; + finalResponse: string; + iterations: number; + toolCalls: { call: ToolCall; result: ToolResult }[]; +} diff --git a/src/interfaces/AppProps.ts b/src/interfaces/AppProps.ts new file mode 100644 index 0000000..9a99ba6 --- /dev/null +++ b/src/interfaces/AppProps.ts @@ -0,0 +1,34 @@ +/** + * App Props Interface + * + * Props for the main TUI Application component + */ + +import type { AgentConfig } from "@/types/agent-config"; + +export interface AppProps { + /** Unique session identifier */ + sessionId: string; + /** LLM provider name */ + provider: string; + /** Model name */ + model: string; + /** Current agent ID */ + agent?: string; + /** Application version */ + version: string; + /** Called when user submits a message */ + onSubmit: (message: string) => Promise; + /** Called when user exits the application */ + onExit: () => void; + /** Called when user executes a slash command */ + onCommand?: (command: string) => Promise; + /** Called when user selects a model */ + onModelSelect?: (model: string) => void | Promise; + /** Called when user selects an agent */ + onAgentSelect?: (agentId: string, agent: AgentConfig) => void | Promise; + /** Called when user selects a theme */ + onThemeSelect?: (theme: string) => void | Promise; + /** Whether to show the banner on startup */ + showBanner?: boolean; +} diff --git a/src/interfaces/AutoScrollOptions.ts b/src/interfaces/AutoScrollOptions.ts new file mode 100644 index 0000000..5bda15e --- /dev/null +++ b/src/interfaces/AutoScrollOptions.ts @@ -0,0 +1,67 @@ +/** + * Auto-Scroll Options Interface + * + * Configuration for auto-scroll behavior + */ + +export interface AutoScrollOptions { + /** Function that returns whether content is actively being generated */ + isWorking: () => boolean; + + /** Total content height in lines */ + totalLines: number; + + /** Visible viewport height in lines */ + visibleHeight: number; + + /** Callback when user interrupts auto-scroll by scrolling up */ + onUserInteracted?: () => void; + + /** Distance from bottom (in lines) to consider "at bottom" */ + bottomThreshold?: number; +} + +export interface AutoScrollState { + /** Current scroll offset in lines */ + scrollOffset: number; + + /** Whether auto-scroll to bottom is enabled */ + autoScroll: boolean; + + /** Whether the user has manually scrolled (pausing auto-scroll) */ + userScrolled: boolean; + + /** Whether we're in the settling period after operations complete */ + isSettling: boolean; +} + +export interface AutoScrollActions { + /** Scroll up by specified lines (user-initiated) */ + scrollUp: (lines?: number) => void; + + /** Scroll down by specified lines (user-initiated) */ + scrollDown: (lines?: number) => void; + + /** Scroll to the top (user-initiated) */ + scrollToTop: () => void; + + /** Scroll to the bottom and resume auto-scroll */ + scrollToBottom: () => void; + + /** Resume auto-scroll mode */ + resume: () => void; + + /** Pause auto-scroll (called when user scrolls up) */ + pause: () => void; + + /** Get the effective scroll offset (clamped to valid range) */ + getEffectiveOffset: () => number; + + /** Check if can scroll up */ + canScrollUp: () => boolean; + + /** Check if can scroll down */ + canScrollDown: () => boolean; +} + +export type AutoScrollReturn = AutoScrollState & AutoScrollActions; diff --git a/src/interfaces/BoxOptions.ts b/src/interfaces/BoxOptions.ts new file mode 100644 index 0000000..67814af --- /dev/null +++ b/src/interfaces/BoxOptions.ts @@ -0,0 +1,30 @@ +/** + * Box options interface + */ + +import type { BoxStyle, BoxAlign } from "@/types/components"; + +export interface BoxOptions { + title?: string; + style?: BoxStyle; + padding?: number; + color?: string; + width?: number; + align?: BoxAlign; +} + +export interface KeyValueOptions { + separator?: string; + labelColor?: string; + valueColor?: string; +} + +export interface ListOptions { + bullet?: string; + indent?: number; + color?: string; +} + +export interface MessageOptions { + showRole?: boolean; +} diff --git a/src/interfaces/ChatOptions.ts b/src/interfaces/ChatOptions.ts new file mode 100644 index 0000000..e5011ec --- /dev/null +++ b/src/interfaces/ChatOptions.ts @@ -0,0 +1,15 @@ +import type { ProviderName } from "@/types/providers"; + +export interface ChatOptions { + provider?: ProviderName; + model?: string; + files?: string[]; + initialPrompt?: string; + continueSession?: boolean; + resumeSession?: string; + systemPrompt?: string; + appendSystemPrompt?: string; + printMode?: boolean; + verbose?: boolean; + autoApprove?: boolean; +} diff --git a/src/interfaces/ChatTUIOptions.ts b/src/interfaces/ChatTUIOptions.ts new file mode 100644 index 0000000..43d8039 --- /dev/null +++ b/src/interfaces/ChatTUIOptions.ts @@ -0,0 +1,15 @@ +import type { ProviderName } from "@/types/providers"; + +export interface ChatTUIOptions { + provider?: ProviderName; + model?: string; + files?: string[]; + initialPrompt?: string; + continueSession?: boolean; + resumeSession?: string; + systemPrompt?: string; + appendSystemPrompt?: string; + printMode?: boolean; + verbose?: boolean; + autoApprove?: boolean; +} diff --git a/src/interfaces/ExecutionResult.ts b/src/interfaces/ExecutionResult.ts new file mode 100644 index 0000000..03f7446 --- /dev/null +++ b/src/interfaces/ExecutionResult.ts @@ -0,0 +1,11 @@ +/** + * Execution Result Interface + */ + +export interface ExecutionResult { + success: boolean; + stdout?: string; + stderr?: string; + error?: string; + exitCode?: number; +} diff --git a/src/interfaces/FileOperation.ts b/src/interfaces/FileOperation.ts new file mode 100644 index 0000000..584657c --- /dev/null +++ b/src/interfaces/FileOperation.ts @@ -0,0 +1,10 @@ +/** + * File Operation Interface + */ + +export interface FileOperation { + type: "read" | "write" | "edit" | "delete" | "create"; + path: string; + content?: string; + description?: string; +} diff --git a/src/interfaces/InputEditorOptions.ts b/src/interfaces/InputEditorOptions.ts new file mode 100644 index 0000000..342bd22 --- /dev/null +++ b/src/interfaces/InputEditorOptions.ts @@ -0,0 +1,8 @@ +/** + * Input editor options interface + */ + +export interface InputEditorOptions { + prompt?: string; + continuationPrompt?: string; +} diff --git a/src/interfaces/MouseHandlerCallbacks.ts b/src/interfaces/MouseHandlerCallbacks.ts new file mode 100644 index 0000000..55e3acf --- /dev/null +++ b/src/interfaces/MouseHandlerCallbacks.ts @@ -0,0 +1,12 @@ +/** + * Mouse Handler Callbacks Interface + * + * Callbacks for handling mouse scroll events + */ + +export interface MouseHandlerCallbacks { + /** Called when scroll wheel moves up */ + onScrollUp: (lines: number) => void; + /** Called when scroll wheel moves down */ + onScrollDown: (lines: number) => void; +} diff --git a/src/interfaces/MouseScrollOptions.ts b/src/interfaces/MouseScrollOptions.ts new file mode 100644 index 0000000..d544c8e --- /dev/null +++ b/src/interfaces/MouseScrollOptions.ts @@ -0,0 +1,14 @@ +/** + * Mouse Scroll Options Interface + * + * Configuration options for the useMouseScroll hook + */ + +export interface MouseScrollOptions { + /** Callback fired when scrolling up */ + onScrollUp: () => void; + /** Callback fired when scrolling down */ + onScrollDown: () => void; + /** Whether mouse scroll is enabled (default: true) */ + enabled?: boolean; +} diff --git a/src/interfaces/PastedContent.ts b/src/interfaces/PastedContent.ts new file mode 100644 index 0000000..2425b4f --- /dev/null +++ b/src/interfaces/PastedContent.ts @@ -0,0 +1,30 @@ +/** + * Interface for tracking pasted content with virtual text + */ + +export interface PastedContent { + /** Unique identifier for the pasted block */ + id: string; + /** The actual pasted content */ + content: string; + /** Number of lines in the pasted content */ + lineCount: number; + /** The placeholder text displayed in the input */ + placeholder: string; + /** Start position in the input buffer */ + startPos: number; + /** End position in the input buffer (exclusive) */ + endPos: number; +} + +export interface PasteState { + /** Map of pasted blocks by their ID */ + pastedBlocks: Map; + /** Counter for generating unique IDs */ + pasteCounter: number; +} + +export const createInitialPasteState = (): PasteState => ({ + pastedBlocks: new Map(), + pasteCounter: 0, +}); diff --git a/src/interfaces/SpinnerOptions.ts b/src/interfaces/SpinnerOptions.ts new file mode 100644 index 0000000..7577189 --- /dev/null +++ b/src/interfaces/SpinnerOptions.ts @@ -0,0 +1,26 @@ +/** + * Spinner options interface + */ + +import type { SpinnerType } from "@/types/spinner"; + +export interface SpinnerOptions { + type?: SpinnerType; + color?: string; + text?: string; + interval?: number; +} + +export interface ScannerOptions { + width?: number; + text?: string; + char?: string; +} + +export interface ProgressBarOptions { + width?: number; + chars?: { + filled: string; + empty: string; + }; +} diff --git a/src/interfaces/ToolCallParams.ts b/src/interfaces/ToolCallParams.ts new file mode 100644 index 0000000..6b1ea53 --- /dev/null +++ b/src/interfaces/ToolCallParams.ts @@ -0,0 +1,6 @@ +export interface ToolCallParams { + id: string; + name: string; + description: string; + args?: Record; +} diff --git a/src/interfaces/commandContext.ts b/src/interfaces/commandContext.ts new file mode 100644 index 0000000..f0ff726 --- /dev/null +++ b/src/interfaces/commandContext.ts @@ -0,0 +1,7 @@ +import type { ChatState } from "@commands/components/chat/state"; + +export interface CommandContext { + state: ChatState; + args: string[]; + cleanup: () => void; +} diff --git a/src/interfaces/memory.ts b/src/interfaces/memory.ts new file mode 100644 index 0000000..0083f43 --- /dev/null +++ b/src/interfaces/memory.ts @@ -0,0 +1,6 @@ +import type { MemoryItem } from "@/types/reasoning"; + +export interface MemoryStore { + items: MemoryItem[]; + maxItems: number; +} diff --git a/src/prompts/audit-prompt.ts b/src/prompts/audit-prompt.ts new file mode 100644 index 0000000..b3f8ce8 --- /dev/null +++ b/src/prompts/audit-prompt.ts @@ -0,0 +1,81 @@ +/** + * Audit Prompt + * + * Prompt template for Copilot to audit Ollama's responses + */ + +export const AUDIT_SYSTEM_PROMPT = `You are an expert code reviewer and AI response auditor. Your task is to evaluate responses from another AI assistant (Ollama) and identify any issues. + +Review the response for: +1. **Correctness**: Is the code/solution correct? Are there bugs or logical errors? +2. **Completeness**: Does the response fully address the user's request? +3. **Best Practices**: Does the code follow best practices and conventions? +4. **Security**: Are there any security vulnerabilities? +5. **Performance**: Are there obvious performance issues? + +You must respond in the following JSON format: +{ + "approved": boolean, + "severity": "none" | "minor" | "major" | "critical", + "issues": ["list of issues found"], + "suggestions": ["list of improvement suggestions"], + "correctedResponse": "If major/critical issues, provide the corrected response here" +} + +If the response is correct and complete, set approved to true and severity to "none". +Be concise but thorough. Focus on actionable feedback.`; + +export const createAuditPrompt = ( + userRequest: string, + ollamaResponse: string, +): string => { + return `## User's Original Request +${userRequest} + +## Ollama's Response +${ollamaResponse} + +## Your Task +Evaluate the response above and provide your assessment in the specified JSON format.`; +}; + +export const parseAuditResponse = ( + response: string, +): { + approved: boolean; + severity: "none" | "minor" | "major" | "critical"; + issues: string[]; + suggestions: string[]; + correctedResponse?: string; +} => { + try { + // Try to extract JSON from the response + const jsonMatch = response.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + return { + approved: true, + severity: "none", + issues: [], + suggestions: [], + }; + } + + const parsed = JSON.parse(jsonMatch[0]); + + return { + approved: Boolean(parsed.approved), + severity: parsed.severity || "none", + issues: Array.isArray(parsed.issues) ? parsed.issues : [], + suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions : [], + correctedResponse: parsed.correctedResponse, + }; + } catch { + // If parsing fails, assume approved + return { + approved: true, + severity: "none", + issues: [], + suggestions: [], + }; + } +}; diff --git a/src/prompts/index.ts b/src/prompts/index.ts new file mode 100644 index 0000000..fdde5d1 --- /dev/null +++ b/src/prompts/index.ts @@ -0,0 +1,122 @@ +/** + * Prompts Index + * + * Centralized exports for all system prompts. + */ + +// System prompts +export { DEFAULT_SYSTEM_PROMPT } from "@prompts/system/default"; +export { AGENTIC_SYSTEM_PROMPT, buildAgenticPrompt } from "@prompts/system/agent"; +export { PLAN_SYSTEM_PROMPT } from "@prompts/system/planner"; +export { + DEBUGGING_SYSTEM_PROMPT, + DEBUGGING_CONTEXT_TEMPLATE, +} from "@prompts/system/debugging"; +export { + CODE_REVIEW_SYSTEM_PROMPT, + CODE_REVIEW_CONTEXT_TEMPLATE, +} from "@prompts/system/code-review"; +export { + REFACTORING_SYSTEM_PROMPT, + REFACTORING_CONTEXT_TEMPLATE, +} from "@prompts/system/refactoring"; +export { + MEMORY_SYSTEM_PROMPT, + MEMORY_CONTEXT_TEMPLATE, + MEMORY_RETRIEVAL_PROMPT, +} from "@prompts/system/memory"; + +// Environment template +export { ENVIRONMENT_PROMPT_TEMPLATE } from "@prompts/system/environment"; + +// Environment service (logic moved to services) +export { + buildEnvironmentPrompt, + getEnvironmentContext, + type EnvironmentContext, +} from "@services/environment-service"; + +// Debugging service +export { + detectDebuggingRequest, + buildDebuggingContext, + getDebuggingPrompt, + enhancePromptForDebugging, + type DebugContext, + type DebugType, +} from "@services/debugging-service"; + +// Code review service +export { + detectCodeReviewRequest, + buildCodeReviewContext, + getCodeReviewPrompt, + enhancePromptForCodeReview, + type CodeReviewContext, + type ReviewType, + type ReviewFocusArea, +} from "@services/code-review-service"; + +// Refactoring service +export { + detectRefactoringRequest, + buildRefactoringContext, + getRefactoringPrompt, + enhancePromptForRefactoring, + type RefactoringContext, + type RefactoringType, + type RefactoringGoal, +} from "@services/refactoring-service"; + +// Memory service +export { + detectMemoryCommand, + storeMemory, + getMemories, + findMemories, + getRelevantMemories, + buildMemoryContext, + buildRelevantMemoryPrompt, + getMemoryPrompt, + processMemoryCommand, + type MemoryContext, + type MemoryCommandType, + type MemoryCategory, +} from "@services/memory-service"; + +// Tool instructions +export { + BASH_TOOL_INSTRUCTIONS, + READ_TOOL_INSTRUCTIONS, + WRITE_TOOL_INSTRUCTIONS, + EDIT_TOOL_INSTRUCTIONS, + GLOB_TOOL_INSTRUCTIONS, + GREP_TOOL_INSTRUCTIONS, + ALL_TOOL_INSTRUCTIONS, +} from "@prompts/system/tools"; + +// Git instructions +export { + GIT_COMMIT_INSTRUCTIONS, + GIT_PR_INSTRUCTIONS, +} from "@prompts/system/git"; + +// UI prompts +export { HELP_TEXT, COMMAND_DESCRIPTIONS } from "@prompts/ui/help"; + +// Re-export rules utilities for backwards compatibility +export { + loadProjectRules, + buildSystemPromptWithRules, + getRulesForCategory, +} from "@services/rules-service"; + +export { MCP_CATEGORIES, TOOL_CATEGORIES } from "@constants/rules"; + +export type { + ProjectRules, + RuleFile, + RuleCategory, + MCPCategory, + ToolCategory, +} from "@/types/rules"; diff --git a/src/prompts/system/agent.ts b/src/prompts/system/agent.ts new file mode 100644 index 0000000..9dc561e --- /dev/null +++ b/src/prompts/system/agent.ts @@ -0,0 +1,210 @@ +/** + * Agentic System Prompt - CodeTyper + * + * A comprehensive prompt for autonomous coding assistance based on + * Claude Code and opencode patterns. + */ + +export const AGENTIC_SYSTEM_PROMPT = `You are CodeTyper, an autonomous AI coding agent that helps users with software engineering tasks. You have access to tools that let you read files, write code, run commands, and search the codebase. + +IMPORTANT: You must NEVER generate or guess URLs unless you are confident they help the user with programming. + +# Core Principle: ACT, DON'T ASK + +You are an AUTONOMOUS agent. When given a task: +1. **START WORKING IMMEDIATELY** - Don't ask for confirmation +2. **GATHER CONTEXT** - Use glob, grep, and read tools to understand the codebase +3. **MAKE DECISIONS** - Choose the best approach based on what you find +4. **EXECUTE** - Make changes, run commands, complete the task +5. **VERIFY** - Test your work when possible + +## When to Use Tools Proactively + +Before answering questions or making changes, ALWAYS: +- **Use glob** to find relevant files when you need to understand project structure +- **Use grep** to search for patterns, function definitions, or implementations +- **Use read** to understand existing code before making changes +- **Use bash** for git operations, running tests, builds, and npm/bun commands + +## Examples of Agentic Behavior + + +user: fix the login bug +assistant: [Uses grep to find login-related code] +[Uses read to examine the login function] +[Identifies the issue] +[Uses edit to fix the bug] +[Uses bash to run tests] +Fixed the login validation in src/auth/login.ts:42. Tests pass. + + + +user: add a new API endpoint for user preferences +assistant: [Uses glob to find existing API endpoints] +[Uses read to understand the endpoint pattern] +[Uses write to create new endpoint file] +[Uses edit to update routes] +[Uses bash to run the server and test] +Created /api/preferences endpoint following the existing pattern. + + + +user: what does the auth middleware do? +assistant: [Uses grep to find auth middleware] +[Uses read to examine the implementation] +The auth middleware in src/middleware/auth.ts:15 validates JWT tokens and attaches the user object to the request. + + +# Tone and Style + +- Be concise. Keep responses under 4 lines unless the task requires more detail +- Don't add unnecessary preamble or postamble +- After working on a file, briefly confirm completion rather than explaining everything +- Output text to communicate; never use tools to communicate +- Only use emojis if explicitly requested +- Your output will be displayed on a command line interface using Github-flavored markdown + +## Verbosity Examples + + +user: what command lists files? +assistant: ls + + + +user: is 11 prime? +assistant: Yes + + + +user: what files are in src/? +assistant: [Uses bash to run ls src/] +foo.ts, bar.ts, index.ts + + +# Tool Usage Policy + +You have access to these tools - use them proactively: + +## Search Tools (Use First) +- **glob**: Find files by pattern. Use for exploring project structure. +- **grep**: Search file contents. Use for finding code patterns and implementations. + +## File Tools +- **read**: Read file contents. ALWAYS read before editing. +- **write**: Create new files. Prefer editing existing files. +- **edit**: Modify files with search-replace. Most common for code changes. + +## System Tools +- **bash**: Run shell commands. Use for git, npm/bun, tests, builds. +- **todowrite**: Track multi-step tasks. Use for complex work. +- **todoread**: Check task progress. + +## Tool Guidelines + +1. **Search before acting**: Use glob/grep to find relevant files before making changes +2. **Read before editing**: Always read a file before modifying it +3. **Prefer edit over write**: Edit existing files rather than creating new ones +4. **Use specialized tools**: Don't use bash for file operations (cat, head, tail) +5. **Run in parallel**: When operations are independent, run multiple tools at once +6. **Chain dependent operations**: Use && for commands that must run in sequence + +# Doing Tasks + +When performing software engineering tasks: + +1. **Understand the codebase**: Use glob and grep to find relevant files +2. **Read existing code**: Understand patterns and conventions before changes +3. **Make incremental changes**: One logical change at a time +4. **Follow conventions**: Match existing code style and patterns +5. **Verify changes**: Run tests/lint when possible + +## Task Tracking + +For complex multi-step tasks, use todowrite to track progress: +- Create tasks at the start of complex work +- Update status as you complete each step +- Mark tasks completed immediately when done + +Use todowrite proactively when: +- The task has 3+ distinct steps +- Working on a feature spanning multiple files +- Debugging complex issues +- Refactoring significant code + +# Following Conventions + +When making changes: +- NEVER assume a library is available - check package.json first +- Look at existing components/functions to understand patterns +- Match code style, naming conventions, and typing +- Follow security best practices - never expose secrets + +# Code References + +When referencing code, include file_path:line_number: + +user: Where are errors handled? +assistant: Errors are handled in src/services/error-handler.ts:42. + + +# Git Operations + +Only commit when requested. When creating commits: +- NEVER use destructive commands (push --force, reset --hard) unless explicitly asked +- NEVER skip hooks unless explicitly asked +- Use clear, concise commit messages focusing on "why" not "what" +- Avoid committing secrets (.env, credentials) + +# When to Ask + +ONLY ask when: +- Multiple fundamentally different approaches exist AND choice significantly affects result +- Critical information is genuinely missing (API keys, credentials) +- About to delete data or make irreversible changes + +If you must ask: do all non-blocked work first, ask ONE targeted question, include your recommended default. + +# Security + +- Assist with defensive security tasks only +- Refuse to create code for malicious purposes +- Allow security analysis and defensive tools`; + +/** + * Build the complete agentic system prompt with environment context + */ +export const buildAgenticPrompt = (context: { + workingDir: string; + isGitRepo: boolean; + platform: string; + today: string; + model?: string; + gitStatus?: string; + gitBranch?: string; + recentCommits?: string[]; +}): string => { + const envSection = ` +# Environment + + +Working directory: ${context.workingDir} +Is directory a git repo: ${context.isGitRepo ? "Yes" : "No"} +Platform: ${context.platform} +Today's date: ${context.today} +${context.model ? `Model: ${context.model}` : ""} +`; + + const gitSection = context.isGitRepo + ? ` +# Git Status + +Current branch: ${context.gitBranch || "unknown"} +${context.gitStatus ? `Status: ${context.gitStatus}` : ""} +${context.recentCommits?.length ? `Recent commits:\n${context.recentCommits.join("\n")}` : ""}` + : ""; + + return `${AGENTIC_SYSTEM_PROMPT} +${envSection} +${gitSection}`; +}; diff --git a/src/prompts/system/code-review.ts b/src/prompts/system/code-review.ts new file mode 100644 index 0000000..d913329 --- /dev/null +++ b/src/prompts/system/code-review.ts @@ -0,0 +1,115 @@ +/** + * Code Review Mode System Prompt + * + * Specialized prompt for code review tasks. + */ + +export const CODE_REVIEW_SYSTEM_PROMPT = `## Code Review Mode + +You are now in code review mode. Provide thorough, constructive feedback on code quality. + +### Review Principles + +1. **Be constructive**: Focus on improvement, not criticism +2. **Be specific**: Point to exact lines and explain why +3. **Prioritize**: Distinguish critical issues from suggestions +4. **Explain reasoning**: Help the author understand the "why" + +### Review Checklist + +#### Correctness +- Does the code do what it's supposed to do? +- Are there edge cases not handled? +- Are there potential runtime errors? +- Is error handling appropriate? + +#### Security +- Input validation present? +- SQL injection vulnerabilities? +- XSS vulnerabilities? +- Sensitive data exposure? +- Authentication/authorization issues? +- Insecure dependencies? + +#### Performance +- Unnecessary computations or loops? +- N+1 query problems? +- Memory leaks or excessive allocations? +- Missing caching opportunities? +- Blocking operations in async code? + +#### Maintainability +- Is the code readable and self-documenting? +- Are functions/methods reasonably sized? +- Is there code duplication? +- Are names clear and descriptive? +- Is the code testable? + +#### Best Practices +- Following language idioms? +- Consistent with codebase style? +- Appropriate use of design patterns? +- Proper separation of concerns? +- Dependencies well-managed? + +### Severity Levels + +Use these levels to categorize findings: + +- **🔴 Critical**: Must fix before merge (security, data loss, crashes) +- **🟠 Major**: Should fix (bugs, significant issues) +- **🟡 Minor**: Consider fixing (code quality, maintainability) +- **🔵 Suggestion**: Optional improvements (style, optimization) +- **💚 Positive**: Good practices worth highlighting + +### Review Format + +Structure your review as: + +\`\`\` +## Summary +Brief overview of the changes and overall assessment. + +## Critical Issues +[List any blocking issues] + +## Recommendations +[List suggested improvements with explanations] + +## Positive Aspects +[Highlight good practices observed] +\`\`\` + +### Code Examples + +When suggesting changes, show the improvement: + +\`\`\` +**Current:** +\`\`\`typescript +// problematic code +\`\`\` + +**Suggested:** +\`\`\`typescript +// improved code +\`\`\` + +**Reason:** Explanation of why this is better. +\`\`\` + +### Don't + +- Don't be overly critical or dismissive +- Don't nitpick style issues already handled by linters +- Don't suggest rewrites without clear justification +- Don't ignore the context or constraints of the change +- Don't focus only on negatives - acknowledge good work`; + +export const CODE_REVIEW_CONTEXT_TEMPLATE = ` +## Review Context + +**Review Type**: {{reviewType}} +**Files Changed**: {{filesChanged}} +**Focus Area**: {{focusArea}} +`; diff --git a/src/prompts/system/debugging.ts b/src/prompts/system/debugging.ts new file mode 100644 index 0000000..172475b --- /dev/null +++ b/src/prompts/system/debugging.ts @@ -0,0 +1,109 @@ +/** + * Debugging Mode System Prompt + * + * Specialized prompt for debugging tasks. + */ + +export const DEBUGGING_SYSTEM_PROMPT = `## Debugging Mode + +You are now in debugging mode. Follow these guidelines to effectively diagnose and fix issues. + +### Debugging Principles + +1. **Understand before fixing**: Never make code changes until you understand the root cause +2. **Address root causes**: Fix the underlying issue, not just the symptoms +3. **Preserve behavior**: Ensure fixes don't break existing functionality +4. **Verify the fix**: Test that the issue is actually resolved + +### Debugging Workflow + +1. **Gather Information** + - Read any error messages or stack traces provided + - Identify the file(s) and line(s) where the error occurs + - Understand what the code is supposed to do + +2. **Reproduce the Issue** + - Understand the steps that lead to the error + - Identify the input/state that triggers the problem + +3. **Isolate the Problem** + - Narrow down to the specific function or code block + - Check recent changes that might have introduced the bug + - Look for related issues in connected code + +4. **Analyze Root Cause** + - Check for common issues: + - Null/undefined values + - Type mismatches + - Off-by-one errors + - Race conditions + - Missing error handling + - Incorrect assumptions about data + +5. **Implement Fix** + - Make the minimal change needed to fix the issue + - Add appropriate error handling if missing + - Consider edge cases + +6. **Verify Fix** + - Suggest how to test the fix + - Check for regression risks + +### Debugging Tools + +Use these approaches to gather information: + +- **Read error logs**: Look for stack traces, error messages +- **Read relevant files**: Understand the code context +- **Search for patterns**: Find related code or similar issues +- **Check types/interfaces**: Verify data structures +- **Run commands**: Execute tests, type checks, linters + +### Adding Debug Output + +When needed, suggest adding: + +\`\`\`typescript +// Temporary debug logging +console.log('[DEBUG] functionName:', { variable1, variable2 }); +\`\`\` + +Remember to note which debug statements should be removed after fixing. + +### Common Bug Patterns + +**Async/Await Issues** +- Missing await +- Unhandled promise rejections +- Race conditions + +**Null/Undefined** +- Accessing properties on undefined +- Missing null checks +- Optional chaining needed + +**Type Issues** +- Type coercion problems +- Interface mismatches +- Missing type guards + +**Logic Errors** +- Incorrect conditionals +- Wrong comparison operators +- Off-by-one in loops + +### Don't + +- Don't guess at fixes without understanding the problem +- Don't make unrelated changes while debugging +- Don't modify tests to pass (unless the test is wrong) +- Don't remove error handling to hide errors`; + +export const DEBUGGING_CONTEXT_TEMPLATE = ` +## Debug Context + +**Error/Issue**: {{errorMessage}} +**Location**: {{location}} +**Expected Behavior**: {{expected}} +**Actual Behavior**: {{actual}} +`; diff --git a/src/prompts/system/default.ts b/src/prompts/system/default.ts new file mode 100644 index 0000000..21b21eb --- /dev/null +++ b/src/prompts/system/default.ts @@ -0,0 +1,158 @@ +/** + * Default System Prompt - CodeTyper Agent + * + * A comprehensive prompt for autonomous coding assistance. + */ + +export const DEFAULT_SYSTEM_PROMPT = `You are CodeTyper, an autonomous AI coding agent designed to help users with software engineering tasks. + +You are an interactive CLI tool that assists with coding tasks including solving bugs, adding features, refactoring code, explaining code, and more. Use the instructions below and the tools available to you to assist the user. + +IMPORTANT: You must NEVER generate or guess URLs unless you are confident they help the user with programming. You may use URLs provided by the user in their messages or local files. + +## Tone and Style + +- Be concise, direct, and to the point while providing complete information +- Match the level of detail in your response to the complexity of the user's query +- Keep responses short (generally less than 4 lines, excluding tool calls or generated code) unless the task is complex +- Minimize output tokens while maintaining helpfulness, quality, and accuracy +- Do NOT add unnecessary preamble or postamble (like explaining your code or summarizing your action) unless asked +- After working on a file, briefly confirm task completion rather than explaining what you did +- Only use emojis if the user explicitly requests it +- Your output will be displayed on a command line interface using Github-flavored markdown +- Output text to communicate with the user; never use tools like Bash or code comments to communicate + +### Verbosity Examples + + +user: what command should I run to list files? +assistant: ls + + + +user: is 11 a prime number? +assistant: Yes + + + +user: what files are in src/? +assistant: [runs ls and sees foo.ts, bar.ts, index.ts] +foo.ts, bar.ts, index.ts + + +## Core Principle: ACT, DON'T ASK + +- Execute tasks immediately without asking for confirmation +- Make reasonable assumptions when details are missing +- Only ask questions for truly ambiguous requirements +- When given a task, START WORKING IMMEDIATELY +- Use common conventions (TypeScript, modern frameworks, best practices) +- Chain multiple tool calls to complete tasks efficiently +- If something fails, try alternatives before giving up + +### When to Ask + +ONLY ask when: +- Multiple fundamentally different approaches exist AND the choice significantly affects the result +- Critical information is genuinely missing (API keys, credentials, account IDs) +- About to delete data or make irreversible changes +- About to change security/billing posture + +If you must ask: do all non-blocked work first, ask ONE targeted question, include your recommended default. + +## Professional Objectivity + +Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without unnecessary superlatives, praise, or emotional validation. Objective guidance and respectful correction are more valuable than false agreement. Investigate to find the truth rather than instinctively confirming the user's beliefs. + +If you cannot or will not help with something, don't explain why at length. Offer helpful alternatives if possible, and keep your response brief. + +## Tools Available + +- **bash**: Run shell commands (npm, git, mkdir, curl, etc.) +- **read**: Read file contents +- **write**: Create/overwrite files +- **edit**: Modify files by replacing text +- **glob**: Find files by name pattern +- **grep**: Search file contents +- **todowrite**: Track progress through multi-step tasks +- **todoread**: Read current task list and progress + +## Task Tracking + +For complex multi-step tasks, use todowrite to track progress: + +1. Create a task list at the start of complex work +2. Update task status as you complete each step +3. Mark tasks as "completed" or "failed" + +Example: +\`\`\`json +{ + "todos": [ + { "id": "1", "title": "Read the source file", "status": "completed" }, + { "id": "2", "title": "Identify the issue", "status": "in_progress" }, + { "id": "3", "title": "Apply the fix", "status": "pending" } + ] +} +\`\`\` + +Use todowrite proactively when: +- The task has 3+ distinct steps +- Working on a feature that spans multiple files +- Debugging complex issues +- Refactoring significant code sections + +### Tool Usage Policy + +- Use specialized tools instead of bash commands when possible +- For file operations, use dedicated tools: Read instead of cat/head/tail, Edit instead of sed/awk, Write instead of echo/cat heredoc +- Reserve Bash for actual terminal operations (git, npm, builds, tests) +- NEVER use bash echo to communicate with the user - output text directly +- When multiple independent operations are needed, run tool calls in parallel +- When operations depend on each other, run them sequentially + +## Doing Tasks + +When performing software engineering tasks: + +1. **Understand first**: Read relevant files before making changes +2. **Work incrementally**: Make one change at a time +3. **Verify changes**: Test your work when possible +4. **Keep it simple**: Don't over-engineer solutions + +### Don't + +- Don't ask "should I proceed?" - just proceed +- Don't list plans - execute them +- Don't ask for paths if working directory is obvious +- Don't ask about preferences for standard choices (TypeScript, ESLint, etc.) +- Don't add features, refactor code, or make "improvements" beyond what was asked +- Don't add docstrings, comments, or type annotations to code you didn't change +- Don't create files unless absolutely necessary - prefer editing existing files + +## Code References + +When referencing specific functions or code, include the pattern \`file_path:line_number\` to help users navigate: + + +user: Where are errors from the client handled? +assistant: Clients are marked as failed in the \`connectToServer\` function in src/services/process.ts:712. + + +## Git Operations + +Only create commits when requested by the user. When creating commits: + +- NEVER run destructive commands (push --force, reset --hard) unless explicitly requested +- NEVER skip hooks (--no-verify) unless explicitly requested +- NEVER force push to main/master - warn the user if they request it +- NEVER commit changes unless the user explicitly asks +- Avoid committing files that may contain secrets (.env, credentials.json) +- Use clear, concise commit messages that focus on the "why" rather than the "what" + +## Security + +- Assist with defensive security tasks only +- Refuse to create or improve code that may be used maliciously +- Do not assist with credential discovery or harvesting +- Allow security analysis, vulnerability explanations, and defensive tools`; diff --git a/src/prompts/system/environment.ts b/src/prompts/system/environment.ts new file mode 100644 index 0000000..c9017a6 --- /dev/null +++ b/src/prompts/system/environment.ts @@ -0,0 +1,17 @@ +/** + * Environment Prompt Template + * + * Template for dynamic environment context injection. + * Use placeholders: {{workingDirectory}}, {{isGitRepo}}, {{platform}}, {{osVersion}}, {{date}} + */ + +export const ENVIRONMENT_PROMPT_TEMPLATE = ` +## Environment + + +Working directory: {{workingDirectory}} +Is directory a git repo: {{isGitRepo}} +Platform: {{platform}} +OS Version: {{osVersion}} +Today's date: {{date}} +`; diff --git a/src/prompts/system/git.ts b/src/prompts/system/git.ts new file mode 100644 index 0000000..af067f8 --- /dev/null +++ b/src/prompts/system/git.ts @@ -0,0 +1,74 @@ +/** + * Git Operations Prompt + * + * Detailed instructions for git operations. + */ + +export const GIT_COMMIT_INSTRUCTIONS = `## Git Commit Guidelines + +Only create commits when requested by the user. If unclear, ask first. + +### Safety Protocol + +- NEVER update the git config +- NEVER run destructive commands (push --force, reset --hard) unless explicitly requested +- NEVER skip hooks (--no-verify) unless explicitly requested +- NEVER force push to main/master - warn the user first +- NEVER commit changes unless the user explicitly asks +- Avoid git commit --amend unless explicitly requested + +### Commit Process + +1. **Check status**: Run git status to see untracked and modified files +2. **Review changes**: Run git diff to see what will be committed +3. **Check history**: Run git log to match existing commit message style +4. **Stage files**: Add specific files (avoid "git add ." which may include sensitive files) +5. **Create commit**: Write a clear message focusing on "why" not "what" +6. **Verify**: Run git status to confirm success + +### Commit Message Format + +Use a HEREDOC to ensure proper formatting: + +\`\`\`bash +git commit -m "$(cat <<'EOF' +Brief description of what changed + +More details if needed. +EOF +)" +\`\`\` + +### Files to Never Commit + +- .env, .env.local, .env.* (environment files) +- credentials.json, secrets.*, *.pem, *.key (credentials) +- node_modules/, dist/, build/ (generated files) + +Warn the user if they specifically request committing these files.`; + +export const GIT_PR_INSTRUCTIONS = `## Pull Request Guidelines + +Use the gh command for all GitHub-related tasks. + +### Creating a Pull Request + +1. **Check branch status**: Ensure changes are committed and pushed +2. **Review changes**: Run git diff against the base branch +3. **Create PR**: Use gh pr create with a clear title and description + +### PR Description Format + +\`\`\`markdown +## Summary +- Brief description of changes + +## Changes Made +- List specific changes + +## Testing +- How the changes were tested + +## Notes +- Any additional context +\`\`\``; diff --git a/src/prompts/system/memory.ts b/src/prompts/system/memory.ts new file mode 100644 index 0000000..ca84464 --- /dev/null +++ b/src/prompts/system/memory.ts @@ -0,0 +1,78 @@ +/** + * Memory System Prompt + * + * Specialized prompt for memory and context management. + */ + +export const MEMORY_SYSTEM_PROMPT = `## Memory System + +You have access to a persistent memory system that stores important information across sessions. + +### Memory Types + +1. **Preferences**: User's coding preferences (language, frameworks, style) +2. **Conventions**: Project-specific conventions and patterns +3. **Architecture**: System architecture decisions and structures +4. **Workflow**: Development workflow preferences +5. **Context**: Important project context and background + +### Memory Commands + +Users can manage memory with natural language: +- "Remember that..." - Store new information +- "Always..." / "Never..." - Store preferences +- "Forget about..." - Remove stored information +- "What do you remember about...?" - Query memories + +### Auto-Detection + +Automatically detect and offer to remember: +- Explicit preferences stated by user +- Corrections to your responses +- Project conventions mentioned +- Architecture decisions made +- Important context shared + +### Memory Guidelines + +**DO remember:** +- User preferences for code style, naming, formatting +- Project-specific conventions and patterns +- Technology choices and constraints +- Important context about the codebase +- Workflow preferences (testing, deployment, etc.) + +**DO NOT remember:** +- Sensitive information (passwords, API keys, secrets) +- Temporary or one-off information +- Information already in project files +- Obvious or universal programming concepts + +### Using Memories + +When responding: +1. Check relevant memories for context +2. Apply stored preferences and conventions +3. Reference architecture decisions when relevant +4. Follow remembered workflow patterns + +### Memory Confirmation + +When storing new memories, confirm with the user: +- What information will be stored +- The category it will be saved under +- Whether it should be project-local or global`; + +export const MEMORY_CONTEXT_TEMPLATE = ` +## Active Memories + +{{memories}} +`; + +export const MEMORY_RETRIEVAL_PROMPT = ` +Based on the user's message, these memories may be relevant: + +{{relevantMemories}} + +Consider this context when responding. +`; diff --git a/src/prompts/system/planner.ts b/src/prompts/system/planner.ts new file mode 100644 index 0000000..237f8ec --- /dev/null +++ b/src/prompts/system/planner.ts @@ -0,0 +1,98 @@ +/** + * Plan Mode System Prompt + * + * Used when the agent is in planning mode to design implementation approaches. + */ + +export const PLAN_SYSTEM_PROMPT = `You are CodeTyper in planning mode. Your role is to design detailed, actionable implementation plans. + +## Plan Mode Rules + +IMPORTANT: In plan mode, you MUST NOT make any code edits or run non-readonly tools. You can only: +- Read files to understand the codebase +- Search for patterns and existing implementations +- Ask clarifying questions +- Write to the plan file + +## Planning Workflow + +### Phase 1: Understanding +Goal: Comprehensively understand the user's request. + +1. **Explore the codebase** to understand the current architecture +2. **Read relevant files** to understand existing patterns and conventions +3. **Ask clarifying questions** if requirements are ambiguous + +### Phase 2: Design +Goal: Design an implementation approach. + +1. **Identify the scope** of changes required +2. **List affected files** and components +3. **Consider alternatives** and trade-offs +4. **Choose the best approach** based on: + - Minimal changes needed + - Consistency with existing patterns + - Maintainability + - Performance implications + +### Phase 3: Write Plan +Goal: Document your implementation plan. + +Your plan should include: + +1. **Summary**: Brief description of what will be implemented +2. **Approach**: The chosen solution and why +3. **Files to Modify**: + - List each file that needs changes + - Describe what changes are needed +4. **New Files** (if any): + - List any new files to create + - Describe their purpose +5. **Steps**: Numbered, actionable implementation steps +6. **Testing**: How to verify the changes work +7. **Risks**: Any potential issues or edge cases + +### Plan Format + +\`\`\`markdown +# Implementation Plan: [Feature Name] + +## Summary +[1-2 sentence description] + +## Approach +[Explain the chosen approach and rationale] + +## Files to Modify +- \`path/to/file.ts\` - [describe changes] +- \`path/to/another.ts\` - [describe changes] + +## New Files +- \`path/to/new.ts\` - [describe purpose] + +## Implementation Steps +1. [First step] +2. [Second step] +3. [Third step] +... + +## Testing +- [ ] [How to test step 1] +- [ ] [How to test step 2] + +## Risks & Considerations +- [Potential issue 1] +- [Edge case to handle] +\`\`\` + +## Guidelines + +- **Be specific**: Each step should be clear enough to execute without additional context +- **Be incremental**: Break large changes into small, reviewable steps +- **Be conservative**: Prefer minimal changes over rewrites +- **Consider dependencies**: Note when steps depend on each other +- **Include verification**: Add testing steps throughout + +## When Planning is Complete + +After writing your plan, signal that you're ready for the user to review and approve it. Do not ask "Is this okay?" - simply indicate that planning is complete.`; diff --git a/src/prompts/system/refactoring.ts b/src/prompts/system/refactoring.ts new file mode 100644 index 0000000..8aaaf65 --- /dev/null +++ b/src/prompts/system/refactoring.ts @@ -0,0 +1,178 @@ +/** + * Refactoring Mode System Prompt + * + * Specialized prompt for code refactoring tasks. + */ + +export const REFACTORING_SYSTEM_PROMPT = `## Refactoring Mode + +You are now in refactoring mode. Transform code to improve its internal structure without changing external behavior. + +### Core Principle + +**Refactoring changes HOW code works, not WHAT it does.** + +Every refactoring must preserve: +- All existing functionality +- All public interfaces +- All test behaviors +- All edge case handling + +### Before Refactoring + +1. **Understand the code**: Read and comprehend the current implementation +2. **Identify the goal**: What specific improvement are you making? +3. **Verify tests exist**: Ensure behavior can be verified after changes +4. **Plan small steps**: Break large refactors into atomic changes + +### Refactoring Catalog + +#### Extract Function +When: Code block does one logical thing, used in multiple places, or needs a name +\`\`\`typescript +// Before +function process(data) { + // ... validation logic ... + // ... transformation logic ... + // ... save logic ... +} + +// After +function process(data) { + validate(data); + const transformed = transform(data); + save(transformed); +} +\`\`\` + +#### Inline Function +When: Function body is as clear as its name, or function is trivial wrapper +\`\`\`typescript +// Before +function isAdult(age) { return age >= 18; } +if (isAdult(user.age)) { ... } + +// After (if used once and obvious) +if (user.age >= 18) { ... } +\`\`\` + +#### Extract Variable +When: Expression is complex or its purpose is unclear +\`\`\`typescript +// Before +if (user.age >= 18 && user.hasVerifiedEmail && !user.isBanned) { ... } + +// After +const canAccessContent = user.age >= 18 && user.hasVerifiedEmail && !user.isBanned; +if (canAccessContent) { ... } +\`\`\` + +#### Replace Conditional with Polymorphism +When: Switch/if-else on type determines behavior +\`\`\`typescript +// Before +function getArea(shape) { + if (shape.type === 'circle') return Math.PI * shape.radius ** 2; + if (shape.type === 'rectangle') return shape.width * shape.height; +} + +// After +interface Shape { getArea(): number; } +class Circle implements Shape { getArea() { return Math.PI * this.radius ** 2; } } +class Rectangle implements Shape { getArea() { return this.width * this.height; } } +\`\`\` + +#### Replace Magic Values with Constants +When: Literal values have meaning not obvious from context +\`\`\`typescript +// Before +if (response.status === 429) { await sleep(60000); } + +// After +const RATE_LIMITED = 429; +const RATE_LIMIT_WAIT_MS = 60000; +if (response.status === RATE_LIMITED) { await sleep(RATE_LIMIT_WAIT_MS); } +\`\`\` + +#### Simplify Conditionals +When: Nested conditionals are hard to follow +\`\`\`typescript +// Before +function getDiscount(user) { + if (user.isPremium) { + if (user.years > 5) { + return 0.3; + } else { + return 0.2; + } + } else { + return 0; + } +} + +// After (guard clauses) +function getDiscount(user) { + if (!user.isPremium) return 0; + if (user.years > 5) return 0.3; + return 0.2; +} +\`\`\` + +#### Decompose Conditional +When: Condition logic is complex +\`\`\`typescript +// Before +if (date.isBefore(SUMMER_START) || date.isAfter(SUMMER_END)) { + charge = quantity * winterRate + winterServiceCharge; +} else { + charge = quantity * summerRate; +} + +// After +const isWinter = date.isBefore(SUMMER_START) || date.isAfter(SUMMER_END); +charge = isWinter ? winterCharge(quantity) : summerCharge(quantity); +\`\`\` + +### Refactoring Priorities + +1. **Remove duplication**: DRY principle, but don't over-abstract +2. **Improve naming**: Names should reveal intent +3. **Reduce complexity**: Lower cyclomatic complexity +4. **Shrink functions**: Each function does one thing +5. **Flatten nesting**: Use early returns and guard clauses + +### Red Flags (Code Smells) + +- **Long functions**: > 20-30 lines usually need splitting +- **Deep nesting**: > 3 levels of indentation +- **Long parameter lists**: > 3-4 parameters +- **Duplicate code**: Same logic in multiple places +- **Feature envy**: Method uses more of another class's data +- **Data clumps**: Same group of variables passed together +- **Primitive obsession**: Using primitives instead of small objects +- **Shotgun surgery**: One change requires many file edits + +### Safety Guidelines + +1. **One refactoring at a time**: Don't combine multiple changes +2. **Run tests after each change**: Verify behavior preserved +3. **Commit frequently**: Each refactoring is a commit +4. **Don't mix refactoring with features**: Separate commits +5. **Keep changes reversible**: Avoid destructive transformations + +### Don't + +- Don't refactor and add features simultaneously +- Don't refactor without understanding the code first +- Don't refactor code without test coverage (add tests first) +- Don't over-engineer or add unnecessary abstractions +- Don't rename everything at once +- Don't assume refactoring is always beneficial`; + +export const REFACTORING_CONTEXT_TEMPLATE = ` +## Refactoring Context + +**Refactoring Type**: {{refactoringType}} +**Target**: {{target}} +**Goal**: {{goal}} +`; diff --git a/src/prompts/system/tools.ts b/src/prompts/system/tools.ts new file mode 100644 index 0000000..aae7eb8 --- /dev/null +++ b/src/prompts/system/tools.ts @@ -0,0 +1,103 @@ +/** + * Tool Instructions Prompt + * + * Detailed instructions for using each tool effectively. + */ + +export const BASH_TOOL_INSTRUCTIONS = `### Bash Tool + +Executes shell commands with optional timeout. + +**When to use**: Terminal operations like git, npm, docker, builds, tests. + +**When NOT to use**: File operations (reading, writing, editing, searching). + +**Guidelines**: +- Always quote file paths containing spaces with double quotes +- Use absolute paths when possible to maintain working directory +- For multiple independent commands, make parallel tool calls +- For dependent commands, chain with && in a single call +- Write a clear description of what the command does + +**Examples**: + +Good: pytest /foo/bar/tests +Bad: cd /foo/bar && pytest tests + + + +Good: git add specific-file.ts && git commit -m "message" +Bad: git add . (may include unwanted files) +`; + +export const READ_TOOL_INSTRUCTIONS = `### Read Tool + +Reads file contents from the filesystem. + +**When to use**: View file contents, understand existing code before editing. + +**Guidelines**: +- Can read any file by absolute path +- By default reads up to 2000 lines from the beginning +- Use offset and limit parameters for long files +- Can read images (PNG, JPG), PDFs, and Jupyter notebooks +- Line numbers start at 1 +- Prefer reading specific files over using Bash with cat/head/tail`; + +export const WRITE_TOOL_INSTRUCTIONS = `### Write Tool + +Creates or overwrites files. + +**When to use**: Create new files when absolutely necessary. + +**Guidelines**: +- ALWAYS prefer editing existing files to creating new ones +- Will overwrite existing files - read first if the file exists +- NEVER proactively create documentation (*.md, README) files +- Only use emojis in file content if the user explicitly requests it`; + +export const EDIT_TOOL_INSTRUCTIONS = `### Edit Tool + +Performs exact string replacements in files. + +**When to use**: Modify existing code, fix bugs, add features. + +**Guidelines**: +- MUST read the file first before editing +- Preserve exact indentation from the file +- The old_string must be unique in the file +- If not unique, provide more surrounding context +- Use replace_all: true to replace all occurrences (e.g., renaming) +- Prefer editing over writing new files`; + +export const GLOB_TOOL_INSTRUCTIONS = `### Glob Tool + +Fast file pattern matching. + +**When to use**: Find files by name patterns. + +**Examples**: +- "**/*.ts" - all TypeScript files +- "src/**/*.tsx" - all TSX files in src +- "**/test*.ts" - all test files`; + +export const GREP_TOOL_INSTRUCTIONS = `### Grep Tool + +Search file contents with regex. + +**When to use**: Find code patterns, search for implementations. + +**Guidelines**: +- Supports full regex syntax (e.g., "function\\s+\\w+") +- Use glob parameter to filter file types (e.g., "*.ts") +- Output modes: "content" (matching lines), "files_with_matches" (file paths), "count" +- Use multiline: true for patterns spanning multiple lines`; + +export const ALL_TOOL_INSTRUCTIONS = [ + BASH_TOOL_INSTRUCTIONS, + READ_TOOL_INSTRUCTIONS, + WRITE_TOOL_INSTRUCTIONS, + EDIT_TOOL_INSTRUCTIONS, + GLOB_TOOL_INSTRUCTIONS, + GREP_TOOL_INSTRUCTIONS, +].join("\n\n"); diff --git a/src/prompts/ui/help.ts b/src/prompts/ui/help.ts new file mode 100644 index 0000000..20b0296 --- /dev/null +++ b/src/prompts/ui/help.ts @@ -0,0 +1,49 @@ +/** + * Help Text Prompts + * + * User-facing help messages for the CLI interface. + */ + +export const HELP_TEXT = `Commands: + /help - Show this help + /clear - Clear conversation + /exit - Exit chat + /save - Save session + /context - Show context info + /usage - Show token usage + /model - Select AI model + /agent - Select agent + /remember - Save a learning about the project + /learnings - Show saved learnings + /whoami - Show logged in account + /login - Authenticate with provider + /logout - Sign out from provider + @file - Add file to context` as const; + +export const COMMAND_DESCRIPTIONS: Record = { + help: "Show this help message", + h: "Show this help message", + clear: "Clear conversation history", + c: "Clear conversation history", + exit: "Exit the chat", + quit: "Exit the chat", + q: "Exit the chat", + save: "Save current session", + s: "Save current session", + context: "Show current context size", + usage: "Show token usage statistics", + u: "Show token usage statistics", + model: "Select AI model", + models: "Show available models", + m: "Show available models", + agent: "Select agent", + a: "Select agent", + provider: "Switch to a different provider", + providers: "Show all providers status", + p: "Show all providers status", + remember: "Save a learning about the project", + learnings: "Show saved learnings", + whoami: "Show logged in account", + login: "Authenticate with provider", + logout: "Sign out from provider", +} as const; diff --git a/src/providers/chat.ts b/src/providers/chat.ts new file mode 100644 index 0000000..740aa98 --- /dev/null +++ b/src/providers/chat.ts @@ -0,0 +1,75 @@ +/** + * Provider chat functions + */ + +import { getProvider } from "@providers/registry"; +import type { + ProviderName, + Message, + ChatCompletionOptions, + ChatCompletionResponse, + StreamChunk, +} from "@/types/providers"; + +export const chat = async ( + providerName: ProviderName, + messages: Message[], + options?: ChatCompletionOptions, +): Promise => { + const provider = getProvider(providerName); + + const isConfigured = await provider.isConfigured(); + if (!isConfigured) { + throw new Error( + `Provider ${providerName} is not configured. Run: codetyper login ${providerName}`, + ); + } + + return provider.chat(messages, options); +}; + +/** + * Stream chat completion from provider + * Falls back to non-streaming if provider doesn't support it + */ +export const chatStream = async ( + providerName: ProviderName, + messages: Message[], + options: ChatCompletionOptions | undefined, + onChunk: (chunk: StreamChunk) => void, +): Promise => { + const provider = getProvider(providerName); + + const isConfigured = await provider.isConfigured(); + if (!isConfigured) { + throw new Error( + `Provider ${providerName} is not configured. Run: codetyper login ${providerName}`, + ); + } + + // Check if provider supports streaming + if (provider.chatStream) { + return provider.chatStream(messages, options, onChunk); + } + + // Fallback: use non-streaming and emit as single chunk + const response = await provider.chat(messages, options); + + if (response.content) { + onChunk({ type: "content", content: response.content }); + } + + if (response.toolCalls) { + for (const tc of response.toolCalls) { + onChunk({ type: "tool_call", toolCall: tc }); + } + } + + onChunk({ type: "done" }); +}; + +export const getDefaultModel = (providerName: ProviderName): string => + getProvider(providerName).getDefaultModel(); + +export const getModels = async (providerName: ProviderName) => + getProvider(providerName).getModels(); diff --git a/src/providers/copilot.ts b/src/providers/copilot.ts new file mode 100644 index 0000000..48e613f --- /dev/null +++ b/src/providers/copilot.ts @@ -0,0 +1,47 @@ +/** + * GitHub Copilot provider for CodeTyper CLI + * Authenticates via GitHub device flow (independent of Neovim) + * Falls back to copilot.lua/copilot.vim config if available + */ + +import { chat, chatStream } from "@providers/copilot/chat"; +import { getModels, getDefaultModel } from "@providers/copilot/models"; +import { + getCredentials, + setCredentials, + getUserInfo, + logout, + isConfigured, + validate, +} from "@providers/copilot/credentials"; +import { + initiateDeviceFlow, + pollForAccessToken, +} from "@providers/copilot/auth"; +import { + COPILOT_PROVIDER_NAME, + COPILOT_DISPLAY_NAME, +} from "@constants/copilot"; +import type { Provider } from "@/types/providers"; + +// Re-export auth functions for external use +export { initiateDeviceFlow, pollForAccessToken }; + +// Re-export types +export type { CopilotUserInfo } from "@/types/copilot"; + +export const copilotProvider: Provider = { + name: COPILOT_PROVIDER_NAME, + displayName: COPILOT_DISPLAY_NAME, + isConfigured, + validate, + getModels, + getDefaultModel, + chat, + chatStream, + getCredentials, + setCredentials, +}; + +// Export additional functions +export { getUserInfo as getCopilotUserInfo, logout as logoutCopilot }; diff --git a/src/providers/copilot/auth.ts b/src/providers/copilot/auth.ts new file mode 100644 index 0000000..de09f25 --- /dev/null +++ b/src/providers/copilot/auth.ts @@ -0,0 +1,93 @@ +/** + * Copilot authentication functions + */ + +import got from "got"; +import { + GITHUB_CLIENT_ID, + GITHUB_DEVICE_CODE_URL, + GITHUB_ACCESS_TOKEN_URL, +} from "@constants/copilot"; +import { sleep } from "@providers/copilot/utils"; +import type { DeviceCodeResponse, AccessTokenResponse } from "@/types/copilot"; + +export const initiateDeviceFlow = async (): Promise => { + const response = await got + .post(GITHUB_DEVICE_CODE_URL, { + headers: { + Accept: "application/json", + }, + form: { + client_id: GITHUB_CLIENT_ID, + scope: "read:user", + }, + }) + .json(); + + return response; +}; + +export const pollForAccessToken = async ( + deviceCode: string, + interval: number, + expiresIn: number, +): Promise => { + const startTime = Date.now(); + const expiresAt = startTime + expiresIn * 1000; + let pollInterval = interval; + + while (Date.now() < expiresAt) { + await sleep(pollInterval * 1000); + + try { + const response = await got + .post(GITHUB_ACCESS_TOKEN_URL, { + headers: { + Accept: "application/json", + }, + form: { + client_id: GITHUB_CLIENT_ID, + device_code: deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }, + }) + .json(); + + if (response.access_token) { + return response.access_token; + } + + const errorHandlers: Record void> = { + authorization_pending: () => {}, + slow_down: () => { + pollInterval += 5; + }, + expired_token: () => { + throw new Error("Authentication timed out. Please try again."); + }, + access_denied: () => { + throw new Error("Authentication was denied by the user."); + }, + }; + + if (response.error) { + const handler = errorHandlers[response.error]; + if (handler) { + handler(); + continue; + } + throw new Error( + response.error_description ?? + response.error ?? + "Authentication failed", + ); + } + } catch (error) { + if ((error as Error).message.includes("Authentication")) { + throw error; + } + } + } + + throw new Error("Authentication timed out. Please try again."); +}; diff --git a/src/providers/copilot/chat.ts b/src/providers/copilot/chat.ts new file mode 100644 index 0000000..b4edb92 --- /dev/null +++ b/src/providers/copilot/chat.ts @@ -0,0 +1,323 @@ +/** + * Copilot chat completion functions + */ + +import got from "got"; + +import { + COPILOT_MAX_RETRIES, + COPILOT_UNLIMITED_MODEL, +} from "@constants/copilot"; +import { refreshToken, buildHeaders } from "@providers/copilot/token"; +import { getDefaultModel, isModelUnlimited } from "@providers/copilot/models"; +import { + sleep, + isRateLimitError, + getRetryDelay, + isQuotaExceededError, +} from "@providers/copilot/utils"; +import type { CopilotToken } from "@/types/copilot"; +import type { + Message, + ChatCompletionOptions, + ChatCompletionResponse, + StreamChunk, +} from "@/types/providers"; + +interface FormattedMessage { + role: string; + content: string; + tool_call_id?: string; + tool_calls?: Message["tool_calls"]; +} + +const formatMessages = (messages: Message[]): FormattedMessage[] => + messages.map((msg) => { + const formatted: FormattedMessage = { + role: msg.role, + content: msg.content, + }; + + if (msg.tool_call_id) { + formatted.tool_call_id = msg.tool_call_id; + } + + if (msg.tool_calls) { + formatted.tool_calls = msg.tool_calls; + } + + return formatted; + }); + +interface ChatRequestBody { + model: string; + messages: FormattedMessage[]; + max_tokens: number; + temperature: number; + stream: boolean; + tools?: ChatCompletionOptions["tools"]; + tool_choice?: string; +} + +const buildRequestBody = ( + messages: Message[], + options: ChatCompletionOptions | undefined, + stream: boolean, + modelOverride?: string, +): ChatRequestBody => { + // Use model override if provided, otherwise use options model or default + const model = + modelOverride ?? + (options?.model && options.model !== "auto" + ? options.model + : getDefaultModel()); + + const body: ChatRequestBody = { + model, + messages: formatMessages(messages), + max_tokens: options?.maxTokens ?? 4096, + temperature: options?.temperature ?? 0.3, + stream, + }; + + if (options?.tools && options.tools.length > 0) { + body.tools = options.tools; + body.tool_choice = "auto"; + } + + return body; +}; + +export interface ChatResult extends ChatCompletionResponse { + modelSwitched?: boolean; + switchedFrom?: string; + switchedTo?: string; +} + +const getEndpoint = (token: CopilotToken): string => + (token.endpoints?.api ?? "https://api.githubcopilot.com") + + "/chat/completions"; + +const executeChatRequest = async ( + endpoint: string, + token: CopilotToken, + body: ChatRequestBody, +): Promise => { + const response = await got + .post(endpoint, { + headers: buildHeaders(token), + json: body, + }) + .json<{ + error?: { message?: string }; + choices?: Array<{ + message?: { content?: string; tool_calls?: Message["tool_calls"] }; + finish_reason?: ChatCompletionResponse["finishReason"]; + }>; + usage?: { + prompt_tokens?: number; + completion_tokens?: number; + total_tokens?: number; + }; + }>(); + + if (response.error) { + throw new Error(response.error.message ?? "Copilot API error"); + } + + const choice = response.choices?.[0]; + if (!choice) { + throw new Error("No response from Copilot"); + } + + const result: ChatCompletionResponse = { + content: choice.message?.content ?? null, + finishReason: choice.finish_reason, + }; + + if (choice.message?.tool_calls) { + result.toolCalls = choice.message.tool_calls; + } + + if (response.usage) { + result.usage = { + promptTokens: response.usage.prompt_tokens ?? 0, + completionTokens: response.usage.completion_tokens ?? 0, + totalTokens: response.usage.total_tokens ?? 0, + }; + } + + return result; +}; + +export const chat = async ( + messages: Message[], + options?: ChatCompletionOptions, +): Promise => { + const token = await refreshToken(); + const endpoint = getEndpoint(token); + const originalModel = + options?.model && options.model !== "auto" + ? options.model + : getDefaultModel(); + const body = buildRequestBody(messages, options, false); + + let lastError: unknown; + let switchedToUnlimited = false; + + for (let attempt = 0; attempt < COPILOT_MAX_RETRIES; attempt++) { + try { + const result = await executeChatRequest(endpoint, token, body); + + if (switchedToUnlimited) { + return { + ...result, + modelSwitched: true, + switchedFrom: originalModel, + switchedTo: COPILOT_UNLIMITED_MODEL, + }; + } + + return result; + } catch (error) { + lastError = error; + + // Check if quota exceeded and current model is not unlimited + if ( + isQuotaExceededError(error) && + !isModelUnlimited(body.model) && + !switchedToUnlimited + ) { + // Switch to unlimited model and retry + body.model = COPILOT_UNLIMITED_MODEL; + switchedToUnlimited = true; + continue; + } + + if (isRateLimitError(error) && attempt < COPILOT_MAX_RETRIES - 1) { + const delay = getRetryDelay(error, attempt); + await sleep(delay); + continue; + } + + throw error; + } + } + + throw lastError; +}; + +const executeStream = ( + endpoint: string, + token: CopilotToken, + body: ChatRequestBody, + onChunk: (chunk: StreamChunk) => void, +): Promise => + new Promise((resolve, reject) => { + const stream = got.stream.post(endpoint, { + headers: buildHeaders(token), + json: body, + }); + + let buffer = ""; + + stream.on("data", (data: Buffer) => { + buffer += data.toString(); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + + for (const line of lines) { + if (line.startsWith("data: ")) { + const jsonStr = line.slice(6).trim(); + if (jsonStr === "[DONE]") { + onChunk({ type: "done" }); + return; + } + + try { + const parsed = JSON.parse(jsonStr); + const delta = parsed.choices?.[0]?.delta; + + if (delta?.content) { + onChunk({ type: "content", content: delta.content }); + } + + if (delta?.tool_calls) { + for (const tc of delta.tool_calls) { + onChunk({ type: "tool_call", toolCall: tc }); + } + } + } catch { + // Ignore parse errors in stream + } + } + } + }); + + stream.on("error", (error: Error) => { + onChunk({ type: "error", error: error.message }); + reject(error); + }); + + stream.on("end", resolve); + }); + +export const chatStream = async ( + messages: Message[], + options: ChatCompletionOptions | undefined, + onChunk: (chunk: StreamChunk) => void, +): Promise => { + const token = await refreshToken(); + const endpoint = getEndpoint(token); + const originalModel = + options?.model && options.model !== "auto" + ? options.model + : getDefaultModel(); + const body = buildRequestBody(messages, options, true); + + let lastError: unknown; + let switchedToUnlimited = false; + + for (let attempt = 0; attempt < COPILOT_MAX_RETRIES; attempt++) { + try { + // Notify about model switch before streaming starts + if (switchedToUnlimited) { + onChunk({ + type: "model_switched", + modelSwitch: { + from: originalModel, + to: COPILOT_UNLIMITED_MODEL, + reason: "Quota exceeded - switched to unlimited model", + }, + }); + } + + await executeStream(endpoint, token, body, onChunk); + return; + } catch (error) { + lastError = error; + + // Check if quota exceeded and current model is not unlimited + if ( + isQuotaExceededError(error) && + !isModelUnlimited(body.model) && + !switchedToUnlimited + ) { + // Switch to unlimited model and retry + body.model = COPILOT_UNLIMITED_MODEL; + switchedToUnlimited = true; + continue; + } + + if (isRateLimitError(error) && attempt < COPILOT_MAX_RETRIES - 1) { + const delay = getRetryDelay(error, attempt); + await sleep(delay); + continue; + } + + throw error; + } + } + + throw lastError; +}; diff --git a/src/providers/copilot/credentials.ts b/src/providers/copilot/credentials.ts new file mode 100644 index 0000000..0eadf3b --- /dev/null +++ b/src/providers/copilot/credentials.ts @@ -0,0 +1,79 @@ +/** + * Copilot credentials management + */ + +import got from "got"; + +import { + setOAuthToken, + setGitHubToken, + setLoggedOut, + clearCredentials, +} from "@providers/copilot/state"; +import { getOAuthToken } from "@providers/copilot/token"; +import type { ProviderCredentials } from "@/types/providers"; +import type { CopilotUserInfo } from "@/types/copilot"; + +export const getCredentials = async (): Promise => { + const oauthToken = await getOAuthToken(); + return { oauthToken: oauthToken ?? undefined }; +}; + +export const setCredentials = async ( + credentials: ProviderCredentials, +): Promise => { + if (credentials.oauthToken) { + setOAuthToken(credentials.oauthToken); + setGitHubToken(null); + setLoggedOut(false); + } +}; + +export const getUserInfo = async (): Promise => { + const oauthToken = await getOAuthToken(); + if (!oauthToken) { + return null; + } + + try { + const response = await got + .get("https://api.github.com/user", { + headers: { + Authorization: `token ${oauthToken}`, + Accept: "application/json", + "User-Agent": "CodeTyper-CLI/1.0", + }, + }) + .json(); + + return { + login: response.login, + name: response.name, + email: response.email, + }; + } catch { + return null; + } +}; + +export const logout = (): void => { + clearCredentials(); +}; + +export const isConfigured = async (): Promise => { + const token = await getOAuthToken(); + return token !== null; +}; + +export const validate = async (): Promise<{ + valid: boolean; + error?: string; +}> => { + try { + const { refreshToken } = await import("@providers/copilot/token"); + const token = await refreshToken(); + return { valid: !!token.token }; + } catch (error) { + return { valid: false, error: (error as Error).message }; + } +}; diff --git a/src/providers/copilot/models.ts b/src/providers/copilot/models.ts new file mode 100644 index 0000000..4a1350d --- /dev/null +++ b/src/providers/copilot/models.ts @@ -0,0 +1,122 @@ +/** + * Copilot models management + */ + +import got from "got"; + +import { + COPILOT_MODELS_URL, + COPILOT_MODELS_CACHE_TTL, + COPILOT_DEFAULT_MODEL, + COPILOT_FALLBACK_MODELS, + MODEL_COST_MULTIPLIERS, + UNLIMITED_MODELS, + COPILOT_UNLIMITED_MODEL, +} from "@constants/copilot"; +import { getState, setModels } from "@providers/copilot/state"; +import { refreshToken } from "@providers/copilot/token"; +import type { ProviderModel } from "@/types/providers"; + +interface ModelBilling { + is_premium: boolean; + multiplier: number; + restricted_to?: string[]; +} + +interface ModelsApiResponse { + data: Array<{ + id: string; + name?: string; + model_picker_enabled?: boolean; + billing?: ModelBilling; + capabilities?: { + type?: string; + limits?: { + max_output_tokens?: number; + }; + supports?: { + tool_calls?: boolean; + streaming?: boolean; + }; + }; + }>; +} + +export const getModels = async (): Promise => { + const state = getState(); + + const isCacheValid = + state.models && + state.modelsFetchedAt && + Date.now() - state.modelsFetchedAt < COPILOT_MODELS_CACHE_TTL; + + if (isCacheValid && state.models) { + return state.models; + } + + try { + const token = await refreshToken(); + const response = await got + .get(COPILOT_MODELS_URL, { + headers: { + Authorization: `Bearer ${token.token}`, + Accept: "application/json", + "User-Agent": "GitHubCopilotChat/0.26.7", + "Editor-Version": "vscode/1.105.1", + "Editor-Plugin-Version": "copilot-chat/0.26.7", + }, + }) + .json(); + + const models: ProviderModel[] = []; + + if (response.data) { + for (const model of response.data) { + const isChatModel = model.capabilities?.type === "chat"; + const isPickerEnabled = model.model_picker_enabled; + + if (isChatModel && isPickerEnabled) { + // Get cost multiplier from API billing data, fallback to constants + const apiMultiplier = model.billing?.multiplier; + const costMultiplier = + apiMultiplier !== undefined + ? apiMultiplier + : (MODEL_COST_MULTIPLIERS[model.id] ?? 1.0); + + // Model is unlimited if API says not premium, or if multiplier is 0 + const isUnlimited = + model.billing?.is_premium === false || + costMultiplier === 0 || + UNLIMITED_MODELS.has(model.id); + + models.push({ + id: model.id, + name: model.name ?? model.id, + maxTokens: model.capabilities?.limits?.max_output_tokens, + supportsTools: model.capabilities?.supports?.tool_calls ?? false, + supportsStreaming: model.capabilities?.supports?.streaming ?? false, + costMultiplier, + isUnlimited, + }); + } + } + } + + const finalModels = models.length > 0 ? models : COPILOT_FALLBACK_MODELS; + setModels(finalModels); + + return finalModels; + } catch { + return COPILOT_FALLBACK_MODELS; + } +}; + +export const getDefaultModel = (): string => COPILOT_DEFAULT_MODEL; + +export const getUnlimitedModel = (): string => COPILOT_UNLIMITED_MODEL; + +export const isModelUnlimited = (modelId: string): boolean => + UNLIMITED_MODELS.has(modelId); + +export const getModelCostMultiplier = (modelId: string): number => + MODEL_COST_MULTIPLIERS[modelId] ?? 1.0; diff --git a/src/providers/copilot/state.ts b/src/providers/copilot/state.ts new file mode 100644 index 0000000..e2af014 --- /dev/null +++ b/src/providers/copilot/state.ts @@ -0,0 +1,50 @@ +/** + * Copilot provider state management + */ + +import type { CopilotState } from "@/types/copilot"; + +const createInitialState = (): CopilotState => ({ + oauthToken: null, + githubToken: null, + models: null, + modelsFetchedAt: null, + isLoggedOut: false, +}); + +let state: CopilotState = createInitialState(); + +export const getState = (): CopilotState => state; + +export const setState = (newState: Partial): void => { + state = { ...state, ...newState }; +}; + +export const resetState = (): void => { + state = createInitialState(); +}; + +export const setOAuthToken = (token: string | null): void => { + state.oauthToken = token; +}; + +export const setGitHubToken = (token: CopilotState["githubToken"]): void => { + state.githubToken = token; +}; + +export const setModels = (models: CopilotState["models"]): void => { + state.models = models; + state.modelsFetchedAt = models ? Date.now() : null; +}; + +export const setLoggedOut = (isLoggedOut: boolean): void => { + state.isLoggedOut = isLoggedOut; +}; + +export const clearCredentials = (): void => { + state.oauthToken = null; + state.githubToken = null; + state.models = null; + state.modelsFetchedAt = null; + state.isLoggedOut = true; +}; diff --git a/src/providers/copilot/token.ts b/src/providers/copilot/token.ts new file mode 100644 index 0000000..2a6548e --- /dev/null +++ b/src/providers/copilot/token.ts @@ -0,0 +1,123 @@ +/** + * Copilot token management + */ + +import { readFile } from "fs/promises"; +import { existsSync } from "fs"; +import { homedir, platform } from "os"; +import { join } from "path"; +import got from "got"; + +import { COPILOT_AUTH_URL } from "@constants/copilot"; +import { + getState, + setOAuthToken, + setGitHubToken, +} from "@providers/copilot/state"; +import type { CopilotToken } from "@/types/copilot"; + +const getConfigDir = (): string => { + const home = homedir(); + const os = platform(); + + if (process.env.XDG_CONFIG_HOME && existsSync(process.env.XDG_CONFIG_HOME)) { + return process.env.XDG_CONFIG_HOME; + } + + if (os === "linux" || os === "darwin") { + return join(home, ".config"); + } + + return join(home, "AppData", "Local"); +}; + +const loadTokenFromNeovimConfig = async (): Promise => { + const configDir = getConfigDir(); + const files = ["hosts.json", "apps.json"]; + + for (const filename of files) { + const filePath = join(configDir, "github-copilot", filename); + if (existsSync(filePath)) { + try { + const content = await readFile(filePath, "utf-8"); + const data = JSON.parse(content); + + for (const [key, value] of Object.entries(data)) { + if ( + key.includes("github.com") && + (value as { oauth_token?: string }).oauth_token + ) { + return (value as { oauth_token: string }).oauth_token; + } + } + } catch { + continue; + } + } + } + + return null; +}; + +export const getOAuthToken = async (): Promise => { + const state = getState(); + + if (state.oauthToken) { + return state.oauthToken; + } + + if (state.isLoggedOut) { + return null; + } + + return loadTokenFromNeovimConfig(); +}; + +export const refreshToken = async (): Promise => { + const state = getState(); + + if (!state.oauthToken) { + const token = await getOAuthToken(); + if (!token) { + throw new Error( + "Copilot not authenticated. Run: codetyper login copilot", + ); + } + setOAuthToken(token); + } + + const currentState = getState(); + + if ( + currentState.githubToken && + currentState.githubToken.expires_at > Date.now() / 1000 + ) { + return currentState.githubToken; + } + + const response = await got + .get(COPILOT_AUTH_URL, { + headers: { + Authorization: `token ${currentState.oauthToken}`, + Accept: "application/json", + }, + }) + .json(); + + if (!response.token) { + throw new Error("Failed to refresh Copilot token"); + } + + setGitHubToken(response); + return response; +}; + +export const buildHeaders = (token: CopilotToken): Record => ({ + Authorization: `Bearer ${token.token}`, + "Content-Type": "application/json", + "User-Agent": "GitHubCopilotChat/0.26.7", + "Editor-Version": "vscode/1.105.1", + "Editor-Plugin-Version": "copilot-chat/0.26.7", + "Copilot-Integration-Id": "vscode-chat", + "Openai-Intent": "conversation-edits", +}); diff --git a/src/providers/copilot/usage.ts b/src/providers/copilot/usage.ts new file mode 100644 index 0000000..2e6ac33 --- /dev/null +++ b/src/providers/copilot/usage.ts @@ -0,0 +1,34 @@ +/** + * Copilot usage/quota fetching + */ + +import got from "got"; + +import { getOAuthToken } from "@providers/copilot/token"; +import type { CopilotUsageResponse } from "@/types/copilot-usage"; + +const COPILOT_USER_URL = "https://api.github.com/copilot_internal/user"; + +export const getCopilotUsage = + async (): Promise => { + const oauthToken = await getOAuthToken(); + if (!oauthToken) { + return null; + } + + try { + const response = await got + .get(COPILOT_USER_URL, { + headers: { + Authorization: `token ${oauthToken}`, + Accept: "application/json", + "User-Agent": "CodeTyper-CLI/1.0", + }, + }) + .json(); + + return response; + } catch { + return null; + } + }; diff --git a/src/providers/copilot/utils.ts b/src/providers/copilot/utils.ts new file mode 100644 index 0000000..a14f03c --- /dev/null +++ b/src/providers/copilot/utils.ts @@ -0,0 +1,83 @@ +/** + * Copilot provider utility functions + */ + +import { COPILOT_INITIAL_RETRY_DELAY } from "@constants/copilot"; + +export const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +export const isRateLimitError = (error: unknown): boolean => { + if (error && typeof error === "object" && "response" in error) { + const response = (error as { response?: { statusCode?: number } }).response; + return response?.statusCode === 429; + } + return false; +}; + +interface ErrorResponse { + response?: { + statusCode?: number; + body?: string | { error?: { message?: string; code?: string } }; + }; + message?: string; +} + +const QUOTA_EXCEEDED_PATTERNS = [ + /quota/i, + /limit.*exceeded/i, + /usage.*limit/i, + /premium.*request/i, + /insufficient.*quota/i, + /rate.*limit.*exceeded/i, +]; + +export const isQuotaExceededError = (error: unknown): boolean => { + if (!error || typeof error !== "object") { + return false; + } + + const typedError = error as ErrorResponse; + const statusCode = typedError.response?.statusCode; + + if (statusCode === 403 || statusCode === 429 || statusCode === 402) { + const body = typedError.response?.body; + let errorMessage = ""; + + if (typeof body === "string") { + errorMessage = body; + } else if (body && typeof body === "object") { + errorMessage = body.error?.message ?? body.error?.code ?? ""; + } + + if (!errorMessage && typedError.message) { + errorMessage = typedError.message; + } + + return QUOTA_EXCEEDED_PATTERNS.some((pattern) => + pattern.test(errorMessage), + ); + } + + return false; +}; + +export const shouldSwitchToUnlimitedModel = (error: unknown): boolean => { + return isQuotaExceededError(error); +}; + +export const getRetryDelay = (error: unknown, attempt: number): number => { + if (error && typeof error === "object" && "response" in error) { + const response = ( + error as { response?: { headers?: Record } } + ).response; + const retryAfter = response?.headers?.["retry-after"]; + if (retryAfter) { + const seconds = parseInt(retryAfter, 10); + if (!isNaN(seconds)) { + return seconds * 1000; + } + } + } + return COPILOT_INITIAL_RETRY_DELAY * Math.pow(2, attempt); +}; diff --git a/src/providers/credentials.ts b/src/providers/credentials.ts new file mode 100644 index 0000000..484c3d0 --- /dev/null +++ b/src/providers/credentials.ts @@ -0,0 +1,32 @@ +/** + * Provider credentials management + */ + +import { readFile, writeFile, mkdir } from "fs/promises"; +import { existsSync } from "fs"; + +import { DIRS, FILES } from "@constants/paths"; +import { CREDENTIALS_FILE_MODE } from "@constants/providers"; +import type { StoredCredentials } from "@/types/providers"; + +export const loadCredentials = async (): Promise => { + if (!existsSync(FILES.credentials)) { + return {}; + } + + try { + const content = await readFile(FILES.credentials, "utf-8"); + return JSON.parse(content); + } catch { + return {}; + } +}; + +export const saveCredentials = async ( + credentials: StoredCredentials, +): Promise => { + await mkdir(DIRS.data, { recursive: true }); + await writeFile(FILES.credentials, JSON.stringify(credentials, null, 2), { + mode: CREDENTIALS_FILE_MODE, + }); +}; diff --git a/src/providers/index.ts b/src/providers/index.ts new file mode 100644 index 0000000..0f8e1bc --- /dev/null +++ b/src/providers/index.ts @@ -0,0 +1,46 @@ +/** + * Provider manager for CodeTyper CLI + * Handles provider selection, authentication, and credential management + */ + +// Re-export types +export * from "@/types/providers"; + +// Re-export registry functions +export { + getProvider, + getAllProviders, + getProviderNames, + isValidProvider, +} from "@providers/registry"; + +// Re-export status functions +export { getProviderStatus, displayProvidersStatus } from "@providers/status"; + +// Re-export login functions +export { + loginProvider, + logoutProvider, + initializeProviders, + completeCopilotLogin, +} from "@providers/login"; + +// Re-export chat functions +export { chat, chatStream, getDefaultModel, getModels } from "@providers/chat"; + +// Re-export copilot-specific functions +export { + initiateDeviceFlow, + pollForAccessToken, + getCopilotUserInfo as getCopilotUserInfoFn, +} from "@providers/copilot"; + +// Re-export getCopilotUserInfo with consistent naming +export const getCopilotUserInfo = async (): Promise<{ + login: string; + name?: string; + email?: string; +} | null> => { + const { getCopilotUserInfo: fn } = await import("@providers/copilot"); + return fn(); +}; diff --git a/src/providers/login.ts b/src/providers/login.ts new file mode 100644 index 0000000..5f5a29c --- /dev/null +++ b/src/providers/login.ts @@ -0,0 +1,12 @@ +/** + * Provider login handlers + */ + +export { loginProvider, logoutProvider } from "@providers/login/handlers"; +export { + initializeProviders, + completeCopilotLogin, +} from "@providers/login/initialize"; +export { displayModels } from "@providers/login/utils"; +export { loginCopilot } from "@providers/login/copilot-login"; +export { loginOllama } from "@providers/login/ollama-login"; diff --git a/src/providers/login/copilot-login.ts b/src/providers/login/copilot-login.ts new file mode 100644 index 0000000..f7e2931 --- /dev/null +++ b/src/providers/login/copilot-login.ts @@ -0,0 +1,126 @@ +/** + * Copilot login handler + */ + +import inquirer from "inquirer"; +import chalk from "chalk"; + +import { + LOGIN_MESSAGES, + LOGIN_PROMPTS, + AUTH_STEP_PREFIXES, +} from "@constants/login"; +import { getProvider } from "@providers/registry"; +import { getProviderStatus } from "@providers/status"; +import { loadCredentials, saveCredentials } from "@providers/credentials"; +import { initiateDeviceFlow, pollForAccessToken } from "@providers/copilot"; +import { displayModels } from "@providers/login/utils"; +import type { ProviderName, LoginHandler } from "@/types/providers"; + +const checkExistingAuth = async ( + name: ProviderName, +): Promise => { + const status = await getProviderStatus(name); + + if (!status.valid) { + return null; + } + + console.log(chalk.green(LOGIN_MESSAGES.COPILOT_ALREADY_CONFIGURED)); + + const { reconfigure } = await inquirer.prompt([ + { + type: "confirm", + name: "reconfigure", + message: LOGIN_PROMPTS.RECONFIGURE, + default: false, + }, + ]); + + return reconfigure ? null : true; +}; + +const displayAuthInstructions = ( + verificationUri: string, + userCode: string, +): void => { + console.log(chalk.bold(LOGIN_MESSAGES.COPILOT_AUTH_INSTRUCTIONS)); + console.log( + `${AUTH_STEP_PREFIXES.OPEN_URL} ${chalk.cyan.underline(verificationUri)}`, + ); + console.log( + `${AUTH_STEP_PREFIXES.ENTER_CODE} ${chalk.yellow.bold(userCode)}\n`, + ); + console.log(chalk.gray(LOGIN_MESSAGES.COPILOT_WAITING)); +}; + +const performDeviceFlowAuth = async (): Promise<{ + success: boolean; + accessToken?: string; +}> => { + console.log(chalk.cyan(LOGIN_MESSAGES.COPILOT_STARTING_AUTH)); + + try { + const deviceResponse = await initiateDeviceFlow(); + + displayAuthInstructions( + deviceResponse.verification_uri, + deviceResponse.user_code, + ); + + const accessToken = await pollForAccessToken( + deviceResponse.device_code, + deviceResponse.interval, + deviceResponse.expires_in, + ); + + return { success: true, accessToken }; + } catch (error) { + console.log( + chalk.red(`${LOGIN_MESSAGES.AUTH_FAILED} ${(error as Error).message}`), + ); + return { success: false }; + } +}; + +const saveAndValidateCredentials = async ( + name: ProviderName, + accessToken: string, +): Promise => { + const provider = getProvider(name); + + await provider.setCredentials({ oauthToken: accessToken }); + + const credentials = await loadCredentials(); + credentials[name] = { oauthToken: accessToken }; + await saveCredentials(credentials); + + const validation = await provider.validate(); + + if (validation.valid) { + console.log(chalk.green(LOGIN_MESSAGES.COPILOT_SUCCESS)); + await displayModels(provider); + return true; + } + + console.log( + chalk.red(`${LOGIN_MESSAGES.VALIDATION_FAILED} ${validation.error}`), + ); + return false; +}; + +export const loginCopilot: LoginHandler = async (name) => { + const existingAuth = await checkExistingAuth(name); + + if (existingAuth !== null) { + return existingAuth; + } + + const authResult = await performDeviceFlowAuth(); + + if (!authResult.success || !authResult.accessToken) { + return false; + } + + return saveAndValidateCredentials(name, authResult.accessToken); +}; diff --git a/src/providers/login/handlers.ts b/src/providers/login/handlers.ts new file mode 100644 index 0000000..8f2b743 --- /dev/null +++ b/src/providers/login/handlers.ts @@ -0,0 +1,59 @@ +/** + * Login and logout handler registries + */ + +import chalk from "chalk"; + +import { PROVIDER_INFO } from "@constants/providers"; +import { LOGIN_MESSAGES } from "@constants/login"; +import { getProvider } from "@providers/registry"; +import { loadCredentials, saveCredentials } from "@providers/credentials"; +import { logoutCopilot } from "@providers/copilot"; +import { loginCopilot } from "@providers/login/copilot-login"; +import { loginOllama } from "@providers/login/ollama-login"; +import type { + ProviderName, + LoginHandler, + LogoutHandler, +} from "@/types/providers"; + +const LOGIN_HANDLERS: Record = { + copilot: loginCopilot, + ollama: loginOllama, +}; + +const LOGOUT_HANDLERS: Record = { + copilot: logoutCopilot, + ollama: () => {}, +}; + +export const loginProvider = async (name: ProviderName): Promise => { + const provider = getProvider(name); + const info = PROVIDER_INFO[name]; + + console.log(`\n${chalk.bold(`Configure ${provider.displayName}`)}\n`); + console.log(chalk.gray(info.description)); + console.log(); + + const handler = LOGIN_HANDLERS[name]; + + if (!handler) { + console.log(chalk.red(`${LOGIN_MESSAGES.UNKNOWN_PROVIDER} ${name}`)); + return false; + } + + return handler(name); +}; + +export const logoutProvider = async (name: ProviderName): Promise => { + const handler = LOGOUT_HANDLERS[name]; + handler?.(); + + const credentials = await loadCredentials(); + credentials[name] = { loggedOut: "true" }; + await saveCredentials(credentials); +}; + +export const getLogoutHandler = ( + name: ProviderName, +): LogoutHandler | undefined => LOGOUT_HANDLERS[name]; diff --git a/src/providers/login/initialize.ts b/src/providers/login/initialize.ts new file mode 100644 index 0000000..4501ef4 --- /dev/null +++ b/src/providers/login/initialize.ts @@ -0,0 +1,45 @@ +/** + * Provider initialization + */ + +import { getProvider, isValidProvider } from "@providers/registry"; +import { loadCredentials, saveCredentials } from "@providers/credentials"; +import { getLogoutHandler } from "@providers/login/handlers"; +import type { ProviderName } from "@/types/providers"; + +export const initializeProviders = async (): Promise => { + const credentials = await loadCredentials(); + const providerNames = Object.keys(credentials); + + for (const name of providerNames) { + if (!isValidProvider(name)) { + continue; + } + + const creds = credentials[name]; + + if (creds.loggedOut === "true") { + const handler = getLogoutHandler(name as ProviderName); + handler?.(); + continue; + } + + try { + const provider = getProvider(name as ProviderName); + await provider.setCredentials(creds); + } catch { + // Ignore errors + } + } +}; + +export const completeCopilotLogin = async ( + accessToken: string, +): Promise => { + const provider = getProvider("copilot"); + await provider.setCredentials({ oauthToken: accessToken }); + + const credentials = await loadCredentials(); + credentials["copilot"] = { oauthToken: accessToken }; + await saveCredentials(credentials); +}; diff --git a/src/providers/login/ollama-login.ts b/src/providers/login/ollama-login.ts new file mode 100644 index 0000000..6d9682a --- /dev/null +++ b/src/providers/login/ollama-login.ts @@ -0,0 +1,65 @@ +/** + * Ollama login handler + */ + +import inquirer from "inquirer"; +import chalk from "chalk"; + +import { DEFAULT_OLLAMA_HOST } from "@constants/providers"; +import { LOGIN_MESSAGES, LOGIN_PROMPTS } from "@constants/login"; +import { getProvider } from "@providers/registry"; +import { loadCredentials, saveCredentials } from "@providers/credentials"; +import { displayModels } from "@providers/login/utils"; +import type { ProviderName, LoginHandler } from "@/types/providers"; + +const promptForHost = async (): Promise => { + const { host } = await inquirer.prompt([ + { + type: "input", + name: "host", + message: LOGIN_PROMPTS.OLLAMA_HOST, + default: process.env.OLLAMA_HOST ?? DEFAULT_OLLAMA_HOST, + }, + ]); + + return host; +}; + +const saveAndValidateOllama = async ( + name: ProviderName, + host: string, +): Promise => { + const provider = getProvider(name); + + await provider.setCredentials({ baseUrl: host }); + + const credentials = await loadCredentials(); + credentials[name] = { baseUrl: host }; + await saveCredentials(credentials); + + const validation = await provider.validate(); + + if (!validation.valid) { + console.log( + chalk.red(`${LOGIN_MESSAGES.CONNECTION_FAILED} ${validation.error}`), + ); + return false; + } + + console.log(chalk.green(LOGIN_MESSAGES.OLLAMA_SUCCESS)); + + const models = await provider.getModels(); + + if (models.length > 0) { + await displayModels(provider); + } else { + console.log(chalk.yellow(LOGIN_MESSAGES.OLLAMA_NO_MODELS)); + } + + return true; +}; + +export const loginOllama: LoginHandler = async (name) => { + const host = await promptForHost(); + return saveAndValidateOllama(name, host); +}; diff --git a/src/providers/login/utils.ts b/src/providers/login/utils.ts new file mode 100644 index 0000000..f2da568 --- /dev/null +++ b/src/providers/login/utils.ts @@ -0,0 +1,27 @@ +/** + * Login utility functions + */ + +import { MAX_MODELS_DISPLAY } from "@constants/providers"; +import { LOGIN_MESSAGES } from "@constants/login"; +import type { Provider } from "@/types/providers"; + +export const displayModels = async (provider: Provider): Promise => { + const models = await provider.getModels(); + + if (models.length === 0) { + return; + } + + console.log(LOGIN_MESSAGES.AVAILABLE_MODELS); + + const displayCount = Math.min(models.length, MAX_MODELS_DISPLAY); + + for (let i = 0; i < displayCount; i++) { + console.log(` - ${models[i].name}`); + } + + if (models.length > MAX_MODELS_DISPLAY) { + console.log(` ... and ${models.length - MAX_MODELS_DISPLAY} more`); + } +}; diff --git a/src/providers/ollama.ts b/src/providers/ollama.ts new file mode 100644 index 0000000..bf6f7b5 --- /dev/null +++ b/src/providers/ollama.ts @@ -0,0 +1,46 @@ +/** + * Ollama provider for CodeTyper CLI (local models) + */ + +import { OLLAMA_PROVIDER_NAME, OLLAMA_DISPLAY_NAME } from "@constants/ollama"; +import { + isOllamaConfigured, + validateOllama, +} from "@providers/ollama/validation"; +import { + getOllamaModels, + getDefaultOllamaModel, +} from "@providers/ollama/models"; +import { ollamaChat } from "@providers/ollama/chat"; +import { ollamaChatStream } from "@providers/ollama/stream"; +import { + getOllamaCredentials, + setOllamaCredentials, +} from "@providers/ollama/credentials"; +import { pullOllamaModel } from "@providers/ollama/pull"; +import type { Provider } from "@/types/providers"; + +export const ollamaProvider: Provider = { + name: OLLAMA_PROVIDER_NAME, + displayName: OLLAMA_DISPLAY_NAME, + isConfigured: isOllamaConfigured, + validate: validateOllama, + getModels: getOllamaModels, + getDefaultModel: getDefaultOllamaModel, + chat: ollamaChat, + chatStream: ollamaChatStream, + getCredentials: getOllamaCredentials, + setCredentials: setOllamaCredentials, +}; + +export { + isOllamaConfigured, + validateOllama, + getOllamaModels, + getDefaultOllamaModel, + ollamaChat, + ollamaChatStream, + getOllamaCredentials, + setOllamaCredentials, + pullOllamaModel, +}; diff --git a/src/providers/ollama/chat.ts b/src/providers/ollama/chat.ts new file mode 100644 index 0000000..de9c3ee --- /dev/null +++ b/src/providers/ollama/chat.ts @@ -0,0 +1,138 @@ +/** + * Ollama provider chat + */ + +import got from "got"; + +import { + OLLAMA_ENDPOINTS, + OLLAMA_TIMEOUTS, + OLLAMA_CHAT_OPTIONS, +} from "@constants/ollama"; +import { getOllamaBaseUrl } from "@providers/ollama/state"; +import { getDefaultOllamaModel } from "@providers/ollama/models"; +import type { + Message, + ChatCompletionOptions, + ChatCompletionResponse, + ToolCall, +} from "@/types/providers"; +import type { + OllamaChatRequest, + OllamaChatResponse, + OllamaToolCall, + OllamaToolDefinition, +} from "@/types/ollama"; + +const formatMessages = ( + messages: Message[], +): Array<{ role: string; content: string }> => + messages.map((msg) => ({ + role: msg.role, + content: msg.content, + })); + +const formatTools = ( + tools: ChatCompletionOptions["tools"], +): OllamaToolDefinition[] | undefined => { + if (!tools || tools.length === 0) return undefined; + + return tools.map((tool) => ({ + type: "function" as const, + function: { + name: tool.function.name, + description: tool.function.description, + parameters: tool.function.parameters, + }, + })); +}; + +const buildChatRequest = ( + messages: Message[], + options?: ChatCompletionOptions, + stream = false, +): OllamaChatRequest => { + // When model is "auto" or undefined, use the provider's default model + const model = + options?.model && options.model !== "auto" + ? options.model + : getDefaultOllamaModel(); + + const request: OllamaChatRequest = { + model, + messages: formatMessages(messages), + stream, + options: { + temperature: + options?.temperature ?? OLLAMA_CHAT_OPTIONS.DEFAULT_TEMPERATURE, + num_predict: options?.maxTokens || OLLAMA_CHAT_OPTIONS.DEFAULT_MAX_TOKENS, + }, + }; + + const formattedTools = formatTools(options?.tools); + if (formattedTools) { + request.tools = formattedTools; + } + + return request; +}; + +const mapToolCall = (tc: OllamaToolCall): ToolCall => ({ + id: tc.id || `call_${Date.now()}`, + type: "function", + function: { + name: tc.function.name, + arguments: + typeof tc.function.arguments === "string" + ? tc.function.arguments + : JSON.stringify(tc.function.arguments), + }, +}); + +const buildChatResponse = ( + response: OllamaChatResponse, +): ChatCompletionResponse => { + const result: ChatCompletionResponse = { + content: response.message?.content || null, + finishReason: response.done ? "stop" : "length", + }; + + if (response.message?.tool_calls) { + result.toolCalls = response.message.tool_calls.map(mapToolCall); + result.finishReason = "tool_calls"; + } + + if (response.prompt_eval_count || response.eval_count) { + result.usage = { + promptTokens: response.prompt_eval_count || 0, + completionTokens: response.eval_count || 0, + totalTokens: + (response.prompt_eval_count || 0) + (response.eval_count || 0), + }; + } + + return result; +}; + +export const ollamaChat = async ( + messages: Message[], + options?: ChatCompletionOptions, +): Promise => { + const baseUrl = getOllamaBaseUrl(); + const body = buildChatRequest(messages, options, false); + + const response = await got + .post(`${baseUrl}${OLLAMA_ENDPOINTS.CHAT}`, { + json: body, + timeout: { request: OLLAMA_TIMEOUTS.CHAT }, + }) + .json(); + + if (response.error) { + throw new Error(response.error); + } + + return buildChatResponse(response); +}; + +export { buildChatRequest, mapToolCall }; diff --git a/src/providers/ollama/credentials.ts b/src/providers/ollama/credentials.ts new file mode 100644 index 0000000..58cd085 --- /dev/null +++ b/src/providers/ollama/credentials.ts @@ -0,0 +1,18 @@ +/** + * Ollama provider credentials + */ + +import { getOllamaBaseUrl, setOllamaBaseUrl } from "@providers/ollama/state"; +import type { ProviderCredentials } from "@/types/providers"; + +export const getOllamaCredentials = async (): Promise => ({ + baseUrl: getOllamaBaseUrl(), +}); + +export const setOllamaCredentials = async ( + credentials: ProviderCredentials, +): Promise => { + if (credentials.baseUrl) { + setOllamaBaseUrl(credentials.baseUrl); + } +}; diff --git a/src/providers/ollama/models.ts b/src/providers/ollama/models.ts new file mode 100644 index 0000000..cacdae6 --- /dev/null +++ b/src/providers/ollama/models.ts @@ -0,0 +1,36 @@ +/** + * Ollama provider models + */ + +import got from "got"; + +import { OLLAMA_ENDPOINTS } from "@constants/ollama"; +import { + getOllamaBaseUrl, + getOllamaDefaultModel, +} from "@providers/ollama/state"; +import type { ProviderModel } from "@/types/providers"; +import type { OllamaTagsResponse, OllamaModelInfo } from "@/types/ollama"; + +const mapModelToProviderModel = (model: OllamaModelInfo): ProviderModel => ({ + id: model.name, + name: model.name, + supportsTools: true, + supportsStreaming: true, +}); + +export const getOllamaModels = async (): Promise => { + const baseUrl = getOllamaBaseUrl(); + + try { + const response = await got + .get(`${baseUrl}${OLLAMA_ENDPOINTS.TAGS}`) + .json(); + + return response.models.map(mapModelToProviderModel); + } catch { + return []; + } +}; + +export const getDefaultOllamaModel = (): string => getOllamaDefaultModel(); diff --git a/src/providers/ollama/pull.ts b/src/providers/ollama/pull.ts new file mode 100644 index 0000000..697226e --- /dev/null +++ b/src/providers/ollama/pull.ts @@ -0,0 +1,74 @@ +/** + * Ollama model pull functionality + */ + +import got from "got"; + +import { OLLAMA_ENDPOINTS } from "@constants/ollama"; +import { getOllamaBaseUrl } from "@providers/ollama/state"; +import type { + OllamaPullProgress, + OllamaProgressCallback, +} from "@/types/ollama"; + +const formatProgress = (parsed: OllamaPullProgress): string => { + if (parsed.completed && parsed.total) { + const percentage = Math.round((parsed.completed / parsed.total) * 100); + return `${parsed.status} (${percentage}%)`; + } + return parsed.status; +}; + +const parseProgressLine = ( + line: string, + onProgress?: OllamaProgressCallback, +): void => { + if (!line.trim() || !onProgress) return; + + try { + const parsed = JSON.parse(line) as OllamaPullProgress; + if (parsed.status) { + onProgress(formatProgress(parsed)); + } + } catch { + // Ignore parse errors + } +}; + +const processProgressData = ( + data: Buffer, + buffer: string, + onProgress?: OllamaProgressCallback, +): string => { + const combined = buffer + data.toString(); + const lines = combined.split("\n"); + const remaining = lines.pop() || ""; + + for (const line of lines) { + parseProgressLine(line, onProgress); + } + + return remaining; +}; + +export const pullOllamaModel = async ( + modelName: string, + onProgress?: OllamaProgressCallback, +): Promise => { + const baseUrl = getOllamaBaseUrl(); + + const stream = got.stream.post(`${baseUrl}${OLLAMA_ENDPOINTS.PULL}`, { + json: { name: modelName }, + }); + + let buffer = ""; + + stream.on("data", (data: Buffer) => { + buffer = processProgressData(data, buffer, onProgress); + }); + + return new Promise((resolve, reject) => { + stream.on("end", resolve); + stream.on("error", reject); + }); +}; diff --git a/src/providers/ollama/state.ts b/src/providers/ollama/state.ts new file mode 100644 index 0000000..abf2ed0 --- /dev/null +++ b/src/providers/ollama/state.ts @@ -0,0 +1,29 @@ +/** + * Ollama provider state management + */ + +import { createStore } from "zustand/vanilla"; + +import { OLLAMA_DEFAULTS } from "@constants/ollama"; +import type { OllamaState } from "@/types/ollama"; + +const store = createStore(() => ({ + baseUrl: OLLAMA_DEFAULTS.BASE_URL, + defaultModel: OLLAMA_DEFAULTS.MODEL, +})); + +export const getOllamaBaseUrl = (): string => + process.env.OLLAMA_HOST || store.getState().baseUrl; + +export const getOllamaDefaultModel = (): string => + store.getState().defaultModel; + +export const setOllamaBaseUrl = (url: string): void => { + store.setState({ baseUrl: url }); +}; + +export const setOllamaDefaultModel = (model: string): void => { + store.setState({ defaultModel: model }); +}; + +export const subscribeToOllama = store.subscribe; diff --git a/src/providers/ollama/stream.ts b/src/providers/ollama/stream.ts new file mode 100644 index 0000000..23be2cf --- /dev/null +++ b/src/providers/ollama/stream.ts @@ -0,0 +1,90 @@ +/** + * Ollama provider streaming + */ + +import got from "got"; + +import { OLLAMA_ENDPOINTS, OLLAMA_TIMEOUTS } from "@constants/ollama"; +import { getOllamaBaseUrl } from "@providers/ollama/state"; +import { buildChatRequest, mapToolCall } from "@providers/ollama/chat"; +import type { + Message, + ChatCompletionOptions, + StreamChunk, +} from "@/types/providers"; +import type { OllamaChatResponse } from "@/types/ollama"; + +const parseStreamLine = ( + line: string, + onChunk: (chunk: StreamChunk) => void, +): void => { + if (!line.trim()) return; + + try { + const parsed = JSON.parse(line) as OllamaChatResponse; + + if (parsed.message?.content) { + onChunk({ type: "content", content: parsed.message.content }); + } + + if (parsed.message?.tool_calls) { + for (const tc of parsed.message.tool_calls) { + onChunk({ + type: "tool_call", + toolCall: mapToolCall(tc), + }); + } + } + + if (parsed.done) { + onChunk({ type: "done" }); + } + } catch { + // Ignore parse errors + } +}; + +const processStreamData = ( + data: Buffer, + buffer: string, + onChunk: (chunk: StreamChunk) => void, +): string => { + const combined = buffer + data.toString(); + const lines = combined.split("\n"); + const remaining = lines.pop() || ""; + + for (const line of lines) { + parseStreamLine(line, onChunk); + } + + return remaining; +}; + +export const ollamaChatStream = async ( + messages: Message[], + options: ChatCompletionOptions | undefined, + onChunk: (chunk: StreamChunk) => void, +): Promise => { + const baseUrl = getOllamaBaseUrl(); + const body = buildChatRequest(messages, options, true); + + const stream = got.stream.post(`${baseUrl}${OLLAMA_ENDPOINTS.CHAT}`, { + json: body, + timeout: { request: OLLAMA_TIMEOUTS.CHAT }, + }); + + let buffer = ""; + + stream.on("data", (data: Buffer) => { + buffer = processStreamData(data, buffer, onChunk); + }); + + stream.on("error", (error: Error) => { + onChunk({ type: "error", error: error.message }); + }); + + return new Promise((resolve, reject) => { + stream.on("end", resolve); + stream.on("error", reject); + }); +}; diff --git a/src/providers/ollama/validation.ts b/src/providers/ollama/validation.ts new file mode 100644 index 0000000..deb108d --- /dev/null +++ b/src/providers/ollama/validation.ts @@ -0,0 +1,35 @@ +/** + * Ollama provider validation + */ + +import got from "got"; + +import { + OLLAMA_ENDPOINTS, + OLLAMA_TIMEOUTS, + OLLAMA_ERRORS, +} from "@constants/ollama"; +import { getOllamaBaseUrl } from "@providers/ollama/state"; + +export const isOllamaConfigured = async (): Promise => { + return true; +}; + +export const validateOllama = async (): Promise<{ + valid: boolean; + error?: string; +}> => { + const baseUrl = getOllamaBaseUrl(); + + try { + await got.get(`${baseUrl}${OLLAMA_ENDPOINTS.TAGS}`, { + timeout: { request: OLLAMA_TIMEOUTS.VALIDATION }, + }); + return { valid: true }; + } catch { + return { + valid: false, + error: OLLAMA_ERRORS.NOT_RUNNING(baseUrl), + }; + } +}; diff --git a/src/providers/registry.ts b/src/providers/registry.ts new file mode 100644 index 0000000..061b3b3 --- /dev/null +++ b/src/providers/registry.ts @@ -0,0 +1,28 @@ +/** + * Provider registry + */ + +import { copilotProvider } from "@providers/copilot"; +import { ollamaProvider } from "@providers/ollama"; +import type { Provider, ProviderName } from "@/types/providers"; + +const providers: Record = { + copilot: copilotProvider, + ollama: ollamaProvider, +}; + +export const getProvider = (name: ProviderName): Provider => { + const provider = providers[name]; + if (!provider) { + throw new Error(`Unknown provider: ${name}`); + } + return provider; +}; + +export const getAllProviders = (): Provider[] => Object.values(providers); + +export const getProviderNames = (): ProviderName[] => + Object.keys(providers) as ProviderName[]; + +export const isValidProvider = (name: string): name is ProviderName => + name in providers; diff --git a/src/providers/status.ts b/src/providers/status.ts new file mode 100644 index 0000000..4498cd9 --- /dev/null +++ b/src/providers/status.ts @@ -0,0 +1,71 @@ +/** + * Provider status functions + */ + +import chalk from "chalk"; + +import { PROVIDER_INFO } from "@constants/providers"; +import { getProvider, getProviderNames } from "@providers/registry"; +import type { ProviderName, ProviderStatus } from "@/types/providers"; + +export const getProviderStatus = async ( + name: ProviderName, +): Promise => { + const provider = getProvider(name); + + const configured = await provider.isConfigured(); + if (!configured) { + return { configured: false, valid: false }; + } + + const validation = await provider.validate(); + return { + configured: true, + valid: validation.valid, + error: validation.error, + }; +}; + +const getStatusIcon = (status: ProviderStatus): string => { + if (status.valid) return chalk.green("✓"); + if (status.configured) return chalk.yellow("!"); + return chalk.gray("○"); +}; + +const getStatusText = (status: ProviderStatus): string => { + if (status.valid) return chalk.green("Connected"); + if (status.configured) return chalk.yellow(status.error ?? "Invalid"); + return chalk.gray("Not configured"); +}; + +export const displayProvidersStatus = async ( + currentProvider: ProviderName, +): Promise => { + console.log("\n" + chalk.bold.underline("LLM Providers") + "\n"); + + for (const name of getProviderNames()) { + const provider = getProvider(name); + const status = await getProviderStatus(name); + const info = PROVIDER_INFO[name]; + + const marker = currentProvider === name ? chalk.cyan("→") : " "; + const statusIcon = getStatusIcon(status); + const statusText = getStatusText(status); + + console.log(`${marker} ${chalk.bold(provider.displayName)}`); + console.log(` Status: ${statusIcon} ${statusText}`); + console.log(` ${chalk.gray(info.description)}`); + + if (!status.configured) { + console.log(` ${chalk.gray(`Run: codetyper login ${name}`)}`); + } + console.log(); + } + + console.log(chalk.bold("Current:") + ` ${chalk.cyan(currentProvider)}`); + console.log( + chalk.gray( + "Use /provider to switch, or codetyper login to configure\n", + ), + ); +}; diff --git a/src/services/__tests__/agent-stream.test.ts b/src/services/__tests__/agent-stream.test.ts new file mode 100644 index 0000000..21bc838 --- /dev/null +++ b/src/services/__tests__/agent-stream.test.ts @@ -0,0 +1,203 @@ +/** + * Unit tests for Streaming Agent + */ + +import { describe, it, expect } from "bun:test"; + +import { + createInitialStreamingState, + createStreamAccumulator, +} from "@/types/streaming"; + +import type { + StreamingState, + StreamAccumulator, + PartialToolCall, +} from "@/types/streaming"; + +describe("Streaming Agent Types", () => { + describe("createInitialStreamingState", () => { + it("should create state with idle status", () => { + const state = createInitialStreamingState(); + + expect(state.status).toBe("idle"); + expect(state.content).toBe(""); + expect(state.pendingToolCalls).toHaveLength(0); + expect(state.completedToolCalls).toHaveLength(0); + expect(state.error).toBeNull(); + expect(state.modelSwitched).toBeNull(); + }); + }); + + describe("createStreamAccumulator", () => { + it("should create empty accumulator", () => { + const accumulator = createStreamAccumulator(); + + expect(accumulator.content).toBe(""); + expect(accumulator.toolCalls.size).toBe(0); + expect(accumulator.modelSwitch).toBeNull(); + }); + + it("should accumulate content", () => { + const accumulator = createStreamAccumulator(); + + accumulator.content += "Hello "; + accumulator.content += "World"; + + expect(accumulator.content).toBe("Hello World"); + }); + + it("should store partial tool calls", () => { + const accumulator = createStreamAccumulator(); + + const partial: PartialToolCall = { + index: 0, + id: "call_123", + name: "read", + argumentsBuffer: '{"path": "/test', + isComplete: false, + }; + + accumulator.toolCalls.set(0, partial); + + expect(accumulator.toolCalls.size).toBe(1); + expect(accumulator.toolCalls.get(0)?.name).toBe("read"); + }); + + it("should accumulate tool call arguments", () => { + const accumulator = createStreamAccumulator(); + + const partial: PartialToolCall = { + index: 0, + id: "call_123", + name: "read", + argumentsBuffer: "", + isComplete: false, + }; + + accumulator.toolCalls.set(0, partial); + + // Simulate streaming arguments + partial.argumentsBuffer += '{"path": '; + partial.argumentsBuffer += '"/test.ts"}'; + + expect(partial.argumentsBuffer).toBe('{"path": "/test.ts"}'); + + // Verify JSON is valid + const parsed = JSON.parse(partial.argumentsBuffer); + expect(parsed.path).toBe("/test.ts"); + }); + }); + + describe("StreamingState transitions", () => { + it("should represent idle to streaming transition", () => { + const state: StreamingState = { + ...createInitialStreamingState(), + status: "streaming", + content: "Processing your request", + }; + + expect(state.status).toBe("streaming"); + expect(state.content).toBe("Processing your request"); + }); + + it("should represent tool call accumulation", () => { + const partial: PartialToolCall = { + index: 0, + id: "call_456", + name: "bash", + argumentsBuffer: '{"command": "ls -la"}', + isComplete: false, + }; + + const state: StreamingState = { + ...createInitialStreamingState(), + status: "accumulating_tool", + pendingToolCalls: [partial], + }; + + expect(state.status).toBe("accumulating_tool"); + expect(state.pendingToolCalls).toHaveLength(1); + expect(state.pendingToolCalls[0].name).toBe("bash"); + }); + + it("should represent completion state", () => { + const state: StreamingState = { + ...createInitialStreamingState(), + status: "complete", + content: "Task completed successfully.", + completedToolCalls: [ + { id: "call_789", name: "write", arguments: { path: "/out.txt" } }, + ], + }; + + expect(state.status).toBe("complete"); + expect(state.completedToolCalls).toHaveLength(1); + }); + + it("should represent error state", () => { + const state: StreamingState = { + ...createInitialStreamingState(), + status: "error", + error: "Connection timeout", + }; + + expect(state.status).toBe("error"); + expect(state.error).toBe("Connection timeout"); + }); + + it("should represent model switch", () => { + const state: StreamingState = { + ...createInitialStreamingState(), + status: "streaming", + modelSwitched: { + from: "gpt-4", + to: "gpt-4-unlimited", + reason: "Quota exceeded", + }, + }; + + expect(state.modelSwitched).not.toBeNull(); + expect(state.modelSwitched?.from).toBe("gpt-4"); + expect(state.modelSwitched?.to).toBe("gpt-4-unlimited"); + }); + }); + + describe("Tool call finalization", () => { + it("should convert partial to complete tool call", () => { + const partial: PartialToolCall = { + index: 0, + id: "call_abc", + name: "edit", + argumentsBuffer: + '{"file_path": "/src/app.ts", "old_string": "foo", "new_string": "bar"}', + isComplete: true, + }; + + const args = JSON.parse(partial.argumentsBuffer); + + expect(args.file_path).toBe("/src/app.ts"); + expect(args.old_string).toBe("foo"); + expect(args.new_string).toBe("bar"); + }); + + it("should handle malformed JSON gracefully", () => { + const partial: PartialToolCall = { + index: 0, + id: "call_def", + name: "read", + argumentsBuffer: '{"path": "/incomplete', + isComplete: true, + }; + + let args: Record = {}; + try { + args = JSON.parse(partial.argumentsBuffer); + } catch { + args = {}; + } + + expect(args).toEqual({}); + }); + }); +}); diff --git a/src/services/agent-loader.ts b/src/services/agent-loader.ts new file mode 100644 index 0000000..c67fe29 --- /dev/null +++ b/src/services/agent-loader.ts @@ -0,0 +1,222 @@ +/** + * Agent loader service + * Loads agent configurations from markdown files + */ + +import fs from "fs/promises"; +import path from "path"; +import fg from "fast-glob"; +import type { + AgentConfig, + AgentFrontmatter, + AgentRegistry, +} from "@/types/agent-config"; + +const AGENT_PATTERNS = [ + ".codetyper/agent/**/*.agent.md", + ".codetyper/agents/**/*.agent.md", + ".coder/agent/**/*.agent.md", + ".coder/agents/**/*.agent.md", + "codetyper/agent/**/*.agent.md", + "codetyper/agents/**/*.agent.md", +]; + +const DEFAULT_AGENT: AgentConfig = { + id: "coder", + name: "Coder", + description: "General purpose coding assistant", + prompt: "", + filePath: "", + mode: "primary", +}; + +let cachedRegistry: AgentRegistry | null = null; + +const extractNameFromFile = (filePath: string): string => { + const basename = path.basename(filePath, ".agent.md"); + return basename + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +}; + +const extractIdFromFile = (filePath: string): string => { + return path.basename(filePath, ".agent.md").toLowerCase(); +}; + +/** + * Simple frontmatter parser + * Supports YAML frontmatter delimited by --- + */ +const parseFrontmatter = ( + content: string, +): { data: AgentFrontmatter; body: string } => { + const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/; + const match = content.match(frontmatterRegex); + + if (!match) { + return { data: {}, body: content }; + } + + const yamlContent = match[1]; + const body = match[2]; + const data: AgentFrontmatter = {}; + + // Simple YAML parsing for known fields + const lines = yamlContent.split("\n"); + for (const line of lines) { + const colonIndex = line.indexOf(":"); + if (colonIndex === -1) continue; + + const key = line.slice(0, colonIndex).trim(); + let value = line.slice(colonIndex + 1).trim(); + + // Remove quotes if present + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + + const keyMap: Record = { + name: "name", + description: "description", + mode: "mode", + model: "model", + temperature: "temperature", + topP: "topP", + top_p: "topP", + hidden: "hidden", + color: "color", + }; + + const mappedKey = keyMap[key]; + if (mappedKey) { + if (mappedKey === "temperature" || mappedKey === "topP") { + data[mappedKey] = parseFloat(value); + } else if (mappedKey === "hidden") { + data[mappedKey] = value === "true"; + } else if (mappedKey === "mode") { + data[mappedKey] = value as "primary" | "subagent" | "all"; + } else { + (data as Record)[mappedKey] = value; + } + } + } + + return { data, body }; +}; + +const parseAgentFile = async ( + filePath: string, +): Promise => { + try { + const content = await fs.readFile(filePath, "utf-8"); + const { data: frontmatter, body } = parseFrontmatter(content); + + const id = extractIdFromFile(filePath); + const name = frontmatter.name ?? extractNameFromFile(filePath); + + // Extract description from frontmatter or first line starting with "ROLE:" + let description = frontmatter.description ?? ""; + let prompt = body.trim(); + + if (!description) { + const roleMatch = body.match(/^ROLE:\s*(.+)$/m); + if (roleMatch) { + description = roleMatch[1].trim(); + } + } + + // If no prompt in body, use entire content + if (!prompt) { + prompt = content; + } + + return { + id, + name, + description, + prompt, + filePath, + mode: frontmatter.mode ?? "primary", + model: frontmatter.model, + temperature: frontmatter.temperature, + topP: frontmatter.topP, + hidden: frontmatter.hidden ?? false, + color: frontmatter.color, + }; + } catch { + return null; + } +}; + +export const loadAgents = async ( + workingDir: string, +): Promise => { + const agents = new Map(); + + // Add default agent + agents.set(DEFAULT_AGENT.id, DEFAULT_AGENT); + + // Find all agent files using fast-glob + const files = await fg(AGENT_PATTERNS, { + cwd: workingDir, + absolute: true, + onlyFiles: true, + }); + + // Parse each agent file + for (const filePath of files) { + const agent = await parseAgentFile(filePath); + if (agent && !agent.hidden) { + agents.set(agent.id, agent); + } + } + + cachedRegistry = { + agents, + defaultAgent: DEFAULT_AGENT.id, + }; + + return cachedRegistry; +}; + +export const getAgentRegistry = async ( + workingDir: string, +): Promise => { + if (cachedRegistry) { + return cachedRegistry; + } + return loadAgents(workingDir); +}; + +export const getAgent = async ( + agentId: string, + workingDir: string, +): Promise => { + const registry = await getAgentRegistry(workingDir); + return registry.agents.get(agentId) ?? null; +}; + +export const getAvailableAgents = async ( + workingDir: string, +): Promise => { + const registry = await getAgentRegistry(workingDir); + return Array.from(registry.agents.values()) + .filter((agent) => !agent.hidden && agent.mode !== "subagent") + .sort((a, b) => a.name.localeCompare(b.name)); +}; + +export const clearAgentCache = (): void => { + cachedRegistry = null; +}; + +export const agentLoader = { + loadAgents, + getAgentRegistry, + getAgent, + getAvailableAgents, + clearAgentCache, +}; diff --git a/src/services/agent-stream.ts b/src/services/agent-stream.ts new file mode 100644 index 0000000..d71eb0b --- /dev/null +++ b/src/services/agent-stream.ts @@ -0,0 +1,427 @@ +/** + * Streaming Agent Loop + * + * Agent loop that streams LLM responses in real-time to the TUI. + * Handles tool call accumulation mid-stream. + */ + +import { v4 as uuidv4 } from "uuid"; +import type { Message, StreamChunk } from "@/types/providers"; +import type { AgentOptions } from "@interfaces/AgentOptions"; +import type { AgentResult } from "@interfaces/AgentResult"; +import type { + AgentMessage, + ToolCallMessage, + ToolResultMessage, +} from "@/types/agent"; +import type { ToolCall, ToolResult, ToolContext } from "@/types/tools"; +import type { + StreamAccumulator, + PartialToolCall, + StreamCallbacks, +} from "@/types/streaming"; +import { chatStream } from "@providers/chat"; +import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index"; +import { initializePermissions } from "@services/permissions"; +import { MAX_ITERATIONS } from "@constants/agent"; +import { createStreamAccumulator } from "@/types/streaming"; + +// ============================================================================= +// Types +// ============================================================================= + +interface StreamAgentState { + sessionId: string; + workingDir: string; + abort: AbortController; + options: AgentOptions; + callbacks: Partial; +} + +// ============================================================================= +// State Creation +// ============================================================================= + +const createStreamAgentState = ( + workingDir: string, + options: AgentOptions, + callbacks: Partial, +): StreamAgentState => ({ + sessionId: uuidv4(), + workingDir, + abort: new AbortController(), + options, + callbacks, +}); + +// ============================================================================= +// Tool Call Accumulation +// ============================================================================= + +/** + * Process a stream chunk and update accumulator + * Returns true if a complete tool call was assembled + */ +const processStreamChunk = ( + chunk: StreamChunk, + accumulator: StreamAccumulator, + callbacks: Partial, +): ToolCall[] => { + const completedCalls: ToolCall[] = []; + + const chunkHandlers: Record void> = { + content: () => { + if (chunk.content) { + accumulator.content += chunk.content; + callbacks.onContentChunk?.(chunk.content); + } + }, + + tool_call: () => { + if (!chunk.toolCall) return; + + const tc = chunk.toolCall; + const index = tc.id ? getToolCallIndex(tc.id, accumulator) : 0; + + // Get or create partial tool call + let partial = accumulator.toolCalls.get(index); + if (!partial && tc.id) { + partial = { + index, + id: tc.id, + name: tc.function?.name ?? "", + argumentsBuffer: "", + isComplete: false, + }; + accumulator.toolCalls.set(index, partial); + callbacks.onToolCallStart?.(partial); + } + + if (partial) { + // Update name if provided + if (tc.function?.name) { + partial.name = tc.function.name; + } + + // Accumulate arguments + if (tc.function?.arguments) { + partial.argumentsBuffer += tc.function.arguments; + } + } + }, + + model_switched: () => { + if (chunk.modelSwitch) { + accumulator.modelSwitch = chunk.modelSwitch; + callbacks.onModelSwitch?.(chunk.modelSwitch); + } + }, + + done: () => { + // Finalize all pending tool calls + for (const partial of accumulator.toolCalls.values()) { + if (!partial.isComplete) { + partial.isComplete = true; + const toolCall = finalizeToolCall(partial); + completedCalls.push(toolCall); + callbacks.onToolCallComplete?.(toolCall); + } + } + callbacks.onComplete?.(); + }, + + error: () => { + if (chunk.error) { + callbacks.onError?.(chunk.error); + } + }, + }; + + const handler = chunkHandlers[chunk.type]; + if (handler) { + handler(); + } + + return completedCalls; +}; + +/** + * Get or assign index for a tool call by ID + */ +const getToolCallIndex = ( + id: string, + accumulator: StreamAccumulator, +): number => { + for (const [index, partial] of accumulator.toolCalls.entries()) { + if (partial.id === id) { + return index; + } + } + return accumulator.toolCalls.size; +}; + +/** + * Convert partial tool call to complete tool call + */ +const finalizeToolCall = (partial: PartialToolCall): ToolCall => { + let args: Record = {}; + try { + args = JSON.parse(partial.argumentsBuffer || "{}"); + } catch { + args = {}; + } + + return { + id: partial.id, + name: partial.name, + arguments: args, + }; +}; + +// ============================================================================= +// Tool Execution +// ============================================================================= + +const executeTool = async ( + state: StreamAgentState, + toolCall: ToolCall, +): Promise => { + const tool = getTool(toolCall.name); + + if (!tool) { + return { + success: false, + title: "Unknown tool", + output: "", + error: `Tool not found: ${toolCall.name}`, + }; + } + + const ctx: ToolContext = { + sessionId: state.sessionId, + messageId: uuidv4(), + workingDir: state.workingDir, + abort: state.abort, + autoApprove: state.options.autoApprove, + onMetadata: () => {}, + }; + + try { + const validatedArgs = tool.parameters.parse(toolCall.arguments); + return await tool.execute(validatedArgs, ctx); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + success: false, + title: "Tool error", + output: "", + error: errorMessage, + }; + } +}; + +// ============================================================================= +// Streaming LLM Call +// ============================================================================= + +const callLLMStream = async ( + state: StreamAgentState, + messages: AgentMessage[], +): Promise<{ + content: string; + toolCalls: ToolCall[]; +}> => { + const chatMode = state.options.chatMode ?? false; + const toolDefs = getToolsForApi(chatMode); + const accumulator = createStreamAccumulator(); + let streamError: string | null = null; + const completedToolCalls: ToolCall[] = []; + + // Convert messages to provider format + const providerMessages: Message[] = messages.map((msg) => { + if ("tool_calls" in msg) { + return { + role: "assistant" as const, + content: msg.content ?? "", + tool_calls: msg.tool_calls, + }; + } + if ("tool_call_id" in msg) { + return { + role: "tool" as const, + tool_call_id: msg.tool_call_id, + content: msg.content, + }; + } + return msg as Message; + }); + + await chatStream( + state.options.provider, + providerMessages, + { + model: state.options.model, + tools: toolDefs, + stream: true, + }, + (chunk: StreamChunk) => { + const completed = processStreamChunk(chunk, accumulator, state.callbacks); + completedToolCalls.push(...completed); + + if (chunk.type === "error") { + streamError = chunk.error ?? "Unknown stream error"; + } + }, + ); + + if (streamError) { + throw new Error(streamError); + } + + return { + content: accumulator.content, + toolCalls: completedToolCalls, + }; +}; + +// ============================================================================= +// Main Streaming Agent Loop +// ============================================================================= + +export const runAgentLoopStream = async ( + state: StreamAgentState, + messages: Message[], +): Promise => { + const maxIterations = state.options.maxIterations ?? MAX_ITERATIONS; + const allToolCalls: { call: ToolCall; result: ToolResult }[] = []; + let iterations = 0; + let finalResponse = ""; + + // Initialize + await initializePermissions(); + await refreshMCPTools(); + + const agentMessages: AgentMessage[] = [...messages]; + + while (iterations < maxIterations) { + iterations++; + + try { + const response = await callLLMStream(state, agentMessages); + + // Check if response has tool calls + if (response.toolCalls && response.toolCalls.length > 0) { + // Add assistant message with tool calls + const assistantMessage: ToolCallMessage = { + role: "assistant", + content: response.content || null, + tool_calls: response.toolCalls.map((tc) => ({ + id: tc.id, + type: "function" as const, + function: { + name: tc.name, + arguments: JSON.stringify(tc.arguments), + }, + })), + }; + agentMessages.push(assistantMessage); + + // Emit any text content + if (response.content) { + state.options.onText?.(response.content); + } + + // Execute each tool call + for (const toolCall of response.toolCalls) { + state.options.onToolCall?.(toolCall); + + const result = await executeTool(state, toolCall); + allToolCalls.push({ call: toolCall, result }); + + state.options.onToolResult?.(toolCall.id, result); + + // Add tool result message + const toolResultMessage: ToolResultMessage = { + role: "tool", + tool_call_id: toolCall.id, + content: result.error + ? `Error: ${result.error}\n\n${result.output}` + : result.output, + }; + agentMessages.push(toolResultMessage); + } + } else { + // No tool calls - this is the final response + finalResponse = response.content || ""; + state.options.onText?.(finalResponse); + break; + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + state.options.onError?.(`Agent error: ${errorMessage}`); + return { + success: false, + finalResponse: `Error: ${errorMessage}`, + iterations, + toolCalls: allToolCalls, + }; + } + } + + if (iterations >= maxIterations) { + state.options.onWarning?.(`Reached max iterations (${maxIterations})`); + } + + return { + success: true, + finalResponse, + iterations, + toolCalls: allToolCalls, + }; +}; + +// ============================================================================= +// Public API +// ============================================================================= + +/** + * Create and run a streaming agent with callbacks + */ +export const runStreamingAgent = async ( + prompt: string, + systemPrompt: string, + options: AgentOptions, + callbacks: Partial = {}, +): Promise => { + const messages: Message[] = [ + { role: "system", content: systemPrompt }, + { role: "user", content: prompt }, + ]; + + const state = createStreamAgentState(process.cwd(), options, callbacks); + return runAgentLoopStream(state, messages); +}; + +/** + * Create a streaming agent instance with stop capability + */ +export const createStreamingAgent = ( + workingDir: string, + options: AgentOptions, + callbacks: Partial = {}, +): { + run: (messages: Message[]) => Promise; + stop: () => void; + updateCallbacks: (newCallbacks: Partial) => void; +} => { + const state = createStreamAgentState(workingDir, options, callbacks); + + return { + run: (messages: Message[]) => runAgentLoopStream(state, messages), + stop: () => state.abort.abort(), + updateCallbacks: (newCallbacks: Partial) => { + Object.assign(state.callbacks, newCallbacks); + }, + }; +}; diff --git a/src/services/agent.ts b/src/services/agent.ts new file mode 100644 index 0000000..215fc3c --- /dev/null +++ b/src/services/agent.ts @@ -0,0 +1,325 @@ +/** + * Agent system for autonomous task execution + * + * This module handles the core agent loop: + * 1. Send messages to LLM with tools + * 2. Process tool calls from response + * 3. Execute tools and collect results + * 4. Send results back to LLM + * 5. Repeat until LLM responds with text only + */ + +import chalk from "chalk"; +import { v4 as uuidv4 } from "uuid"; +import type { Message } from "@/types/providers"; +import type { AgentOptions } from "@interfaces/AgentOptions"; +import type { AgentResult } from "@interfaces/AgentResult"; +import type { + AgentMessage, + ToolCallMessage, + ToolResultMessage, +} from "@/types/agent"; +import { chat as providerChat } from "@providers/index"; +import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index"; +import type { ToolContext, ToolCall, ToolResult } from "@/types/tools"; +import { initializePermissions } from "@services/permissions"; +import { MAX_ITERATIONS } from "@constants/agent"; +import { usageStore } from "@stores/usage-store"; + +/** + * Agent state interface + */ +interface AgentState { + sessionId: string; + workingDir: string; + abort: AbortController; + options: AgentOptions; +} + +/** + * Create a new agent state + */ +const createAgentState = ( + workingDir: string, + options: AgentOptions, +): AgentState => ({ + sessionId: uuidv4(), + workingDir, + abort: new AbortController(), + options, +}); + +/** + * Call the LLM with tools + */ +const callLLM = async ( + state: AgentState, + messages: AgentMessage[], +): Promise<{ + content: string | null; + toolCalls?: ToolCall[]; +}> => { + const toolDefs = getToolsForApi(); + + // Convert messages to provider format + const providerMessages: unknown[] = messages.map((msg) => { + if ("tool_calls" in msg) { + return { + role: "assistant", + content: msg.content, + tool_calls: msg.tool_calls, + }; + } + if ("tool_call_id" in msg) { + return { + role: "tool", + tool_call_id: msg.tool_call_id, + content: msg.content, + }; + } + return msg; + }); + + // Call provider with tools + const response = await providerChat( + state.options.provider, + providerMessages as Message[], + { + model: state.options.model, + tools: toolDefs, + }, + ); + + // Track usage if available + if (response.usage) { + usageStore.addUsage({ + promptTokens: response.usage.promptTokens, + completionTokens: response.usage.completionTokens, + totalTokens: response.usage.totalTokens, + model: state.options.model, + }); + } + + // Parse tool calls from response + const toolCalls: ToolCall[] = []; + + if (response.toolCalls) { + for (const tc of response.toolCalls) { + let args: Record; + try { + args = + typeof tc.function.arguments === "string" + ? JSON.parse(tc.function.arguments) + : tc.function.arguments; + } catch { + args = {}; + } + + toolCalls.push({ + id: tc.id, + name: tc.function.name, + arguments: args, + }); + } + } + + return { + content: response.content, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + }; +}; + +/** + * Execute a tool call + */ +const executeTool = async ( + state: AgentState, + toolCall: ToolCall, +): Promise => { + const tool = getTool(toolCall.name); + + if (!tool) { + return { + success: false, + title: "Unknown tool", + output: "", + error: `Tool not found: ${toolCall.name}`, + }; + } + + const ctx: ToolContext = { + sessionId: state.sessionId, + messageId: uuidv4(), + workingDir: state.workingDir, + abort: state.abort, + autoApprove: state.options.autoApprove, + onMetadata: (metadata) => { + if (state.options.verbose && metadata.output) { + // Already printed by the tool + } + }, + }; + + try { + // Validate arguments + const validatedArgs = tool.parameters.parse(toolCall.arguments); + return await tool.execute(validatedArgs, ctx); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + success: false, + title: "Tool error", + output: "", + error: errorMessage, + }; + } +}; + +/** + * Run the agent with the given messages + */ +export const runAgentLoop = async ( + state: AgentState, + messages: Message[], +): Promise => { + const maxIterations = state.options.maxIterations ?? MAX_ITERATIONS; + const allToolCalls: { call: ToolCall; result: ToolResult }[] = []; + let iterations = 0; + let finalResponse = ""; + + // Initialize permissions + await initializePermissions(); + + // Refresh MCP tools if available + await refreshMCPTools(); + + // Convert messages to agent format + const agentMessages: AgentMessage[] = [...messages]; + + while (iterations < maxIterations) { + iterations++; + + if (state.options.verbose) { + console.log(chalk.gray(`\n--- Iteration ${iterations} ---`)); + } + + try { + // Call LLM with tools + const response = await callLLM(state, agentMessages); + + // Check if response has tool calls + if (response.toolCalls && response.toolCalls.length > 0) { + // Add assistant message with tool calls + const assistantMessage: ToolCallMessage = { + role: "assistant", + content: response.content || null, + tool_calls: response.toolCalls.map((tc) => ({ + id: tc.id, + type: "function" as const, + function: { + name: tc.name, + arguments: JSON.stringify(tc.arguments), + }, + })), + }; + agentMessages.push(assistantMessage); + + // If there's text content, emit it + if (response.content) { + state.options.onText?.(response.content); + } + + // Execute each tool call + for (const toolCall of response.toolCalls) { + state.options.onToolCall?.(toolCall); + + if (state.options.verbose) { + console.log(chalk.cyan(`\nTool: ${toolCall.name}`)); + console.log( + chalk.gray(JSON.stringify(toolCall.arguments, null, 2)), + ); + } + + const result = await executeTool(state, toolCall); + allToolCalls.push({ call: toolCall, result }); + + state.options.onToolResult?.(toolCall.id, result); + + // Add tool result message + const toolResultMessage: ToolResultMessage = { + role: "tool", + tool_call_id: toolCall.id, + content: result.error + ? `Error: ${result.error}\n\n${result.output}` + : result.output, + }; + agentMessages.push(toolResultMessage); + } + } else { + // No tool calls - this is the final response + finalResponse = response.content || ""; + state.options.onText?.(finalResponse); + break; + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + state.options.onError?.(`Agent error: ${errorMessage}`); + return { + success: false, + finalResponse: `Error: ${errorMessage}`, + iterations, + toolCalls: allToolCalls, + }; + } + } + + if (iterations >= maxIterations) { + state.options.onWarning?.(`Reached max iterations (${maxIterations})`); + } + + return { + success: true, + finalResponse, + iterations, + toolCalls: allToolCalls, + }; +}; + +/** + * Create and run an agent with a single prompt + */ +export const runAgent = async ( + prompt: string, + systemPrompt: string, + options: AgentOptions, +): Promise => { + const messages: Message[] = [ + { role: "system", content: systemPrompt }, + { role: "user", content: prompt }, + ]; + + const state = createAgentState(process.cwd(), options); + return runAgentLoop(state, messages); +}; + +/** + * Create an agent instance with stop capability + */ +export const createAgent = ( + workingDir: string, + options: AgentOptions, +): { + run: (messages: Message[]) => Promise; + stop: () => void; +} => { + const state = createAgentState(workingDir, options); + + return { + run: (messages: Message[]) => runAgentLoop(state, messages), + stop: () => state.abort.abort(), + }; +}; + +// Re-export types +export type { AgentOptions, AgentResult }; diff --git a/src/services/auto-compaction.ts b/src/services/auto-compaction.ts new file mode 100644 index 0000000..5808366 --- /dev/null +++ b/src/services/auto-compaction.ts @@ -0,0 +1,282 @@ +/** + * Auto-Compaction Service + * Automatically compacts conversation context when approaching token limits + */ + +import type { Message } from "@/types/providers"; +import type { + CompressibleMessage, + CompressibleToolResult, + CompressionOutput, +} from "@/types/reasoning"; +import { + DEFAULT_MAX_CONTEXT_TOKENS, + COMPACTION_TRIGGER_PERCENT, +} from "@constants/reasoning"; +import { getModelContextSize, DEFAULT_CONTEXT_SIZE } from "@constants/copilot"; +import { + compressContext, + getPreservationCandidates, + markMessagesWithAge, + getMessagePriority, +} from "@services/reasoning/context-compression"; +import { estimateTokens, createEntityTable } from "@services/reasoning/utils"; + +export interface AutoCompactionConfig { + tokenLimit: number; + compressAt: number; + prioritizeFiles: boolean; + prioritizeImages: boolean; +} + +export interface CompactionResult { + compacted: boolean; + originalTokens: number; + newTokens: number; + tokensSaved: number; + messagesCompressed: number; + preservedContextFiles: number; + preservedImages: number; +} + +const DEFAULT_CONFIG: AutoCompactionConfig = { + tokenLimit: DEFAULT_MAX_CONTEXT_TOKENS, + compressAt: COMPACTION_TRIGGER_PERCENT, + prioritizeFiles: true, + prioritizeImages: true, +}; + +/** + * Get compaction config for a specific model + * Uses model's actual context window size + */ +export const getModelCompactionConfig = ( + modelId: string | undefined, + overrides: Partial = {}, +): AutoCompactionConfig => { + const contextSize = modelId + ? getModelContextSize(modelId) + : DEFAULT_CONTEXT_SIZE; + + return { + ...DEFAULT_CONFIG, + tokenLimit: contextSize.input, + ...overrides, + }; +}; + +const estimateMessageTokens = (message: Message): number => { + const content = + typeof message.content === "string" + ? message.content + : JSON.stringify(message.content); + const estimatedTokens = estimateTokens(content); + return estimatedTokens; +}; + +const estimateTotalTokens = (messages: Message[]): number => { + const messagesTokens = messages.reduce( + (total, msg) => total + estimateMessageTokens(msg), + 0, + ); + return messagesTokens; +}; + +const shouldCompact = ( + messages: Message[], + config: AutoCompactionConfig = DEFAULT_CONFIG, +): boolean => { + const totalTokens = estimateTotalTokens(messages); + const threshold = config.tokenLimit * config.compressAt; + return totalTokens >= threshold; +}; + +export interface CompactionCheckResult { + needsCompaction: boolean; + currentTokens: number; + threshold: number; + tokenLimit: number; +} + +export const checkCompactionNeeded = ( + messages: Message[], + config: AutoCompactionConfig = DEFAULT_CONFIG, +): CompactionCheckResult => { + const currentTokens = estimateTotalTokens(messages); + const threshold = config.tokenLimit * config.compressAt; + return { + needsCompaction: currentTokens >= threshold, + currentTokens, + threshold, + tokenLimit: config.tokenLimit, + }; +}; + +const convertToCompressibleMessage = ( + message: Message, + index: number, + totalMessages: number, + config: AutoCompactionConfig, +): CompressibleMessage => { + const content = + typeof message.content === "string" + ? message.content + : JSON.stringify(message.content); + + const hasFileContent = content.includes("File:") && content.includes("```"); + const hasImageReference = + content.includes("[image]") || + content.includes("data:image") || + /\.(png|jpg|jpeg|gif|webp|svg)/.test(content); + + return { + id: `msg-${index}`, + role: message.role as "user" | "assistant" | "tool" | "system", + content, + tokenCount: estimateTokens(content), + age: totalMessages - index, + isPreserved: false, + isContextFile: config.prioritizeFiles && hasFileContent, + isImage: config.prioritizeImages && hasImageReference, + }; +}; + +const convertToToolResult = ( + message: Message, + index: number, +): CompressibleToolResult | null => { + if (message.role !== "tool") { + return null; + } + + const content = + typeof message.content === "string" + ? message.content + : JSON.stringify(message.content); + + return { + id: `tool-${index}`, + toolName: "unknown", + content, + tokenCount: estimateTokens(content), + success: !content.toLowerCase().includes("error"), + }; +}; + +const convertFromCompressibleMessage = ( + compressible: CompressibleMessage, +): Message => ({ + role: compressible.role, + content: compressible.content, +}); + +export const compactConversation = ( + messages: Message[], + config: AutoCompactionConfig = DEFAULT_CONFIG, +): { messages: Message[]; result: CompactionResult } => { + const originalTokens = estimateTotalTokens(messages); + const compactMessages = shouldCompact(messages, config); + + if (!compactMessages) { + return { + messages, + result: { + compacted: false, + originalTokens, + newTokens: originalTokens, + tokensSaved: 0, + messagesCompressed: 0, + preservedContextFiles: 0, + preservedImages: 0, + }, + }; + } + + const compressibleMessages = messages.map((msg, idx) => + convertToCompressibleMessage(msg, idx, messages.length, config), + ); + + const toolResults = messages + .map((msg, idx) => convertToToolResult(msg, idx)) + .filter( + (toolResult): toolResult is CompressibleToolResult => toolResult !== null, + ); + + const preserveList = getPreservationCandidates(compressibleMessages); + + const agedMessages = markMessagesWithAge( + compressibleMessages, + messages.length, + ); + + const sortedByPriority = [...agedMessages].sort( + (a, b) => getMessagePriority(b) - getMessagePriority(a), + ); + + const compressionOutput: CompressionOutput = compressContext({ + messages: sortedByPriority, + toolResults, + entities: createEntityTable([]), + currentTokenCount: originalTokens, + tokenLimit: config.tokenLimit, + preserveList, + }); + + const outputMessages = compressionOutput.compressedMessages.map( + convertFromCompressibleMessage, + ); + + const preservedContextFiles = compressionOutput.compressedMessages.filter( + (m) => m.isContextFile, + ).length; + + const preservedImages = compressionOutput.compressedMessages.filter( + (m) => m.isImage, + ).length; + + const newTokens = estimateTotalTokens(outputMessages); + + return { + messages: outputMessages, + result: { + compacted: true, + originalTokens, + newTokens, + tokensSaved: compressionOutput.tokensSaved, + messagesCompressed: messages.length - outputMessages.length, + preservedContextFiles, + preservedImages, + }, + }; +}; + +export const createCompactionSummary = (result: CompactionResult): string => { + if (!result.compacted) { + return ""; + } + + const percentSaved = Math.round( + (result.tokensSaved / result.originalTokens) * 100, + ); + + const parts = [ + `Compacted conversation: ${result.tokensSaved.toLocaleString()} tokens saved (${percentSaved}%)`, + ]; + + if (result.preservedContextFiles > 0) { + parts.push(`${result.preservedContextFiles} context file(s) preserved`); + } + + if (result.preservedImages > 0) { + parts.push(`${result.preservedImages} image(s) preserved`); + } + + return parts.join(" | "); +}; + +export const getCompactionConfig = ( + overrides: Partial = {}, +): AutoCompactionConfig => ({ + ...DEFAULT_CONFIG, + ...overrides, +}); diff --git a/src/services/cascading-provider/availability.ts b/src/services/cascading-provider/availability.ts new file mode 100644 index 0000000..d6d4650 --- /dev/null +++ b/src/services/cascading-provider/availability.ts @@ -0,0 +1,122 @@ +/** + * Provider Availability Service + * + * Monitors and checks provider availability + */ + +import { spawn } from "child_process"; + +export interface ProviderStatus { + available: boolean; + error?: string; + lastChecked: number; +} + +const providerCache: Record = {}; +const CACHE_TTL_MS = 30000; // 30 seconds + +const isCacheValid = (providerId: string): boolean => { + const cached = providerCache[providerId]; + if (!cached) { + return false; + } + return Date.now() - cached.lastChecked < CACHE_TTL_MS; +}; + +export const checkOllamaAvailability = async (): Promise => { + if (isCacheValid("ollama")) { + return providerCache["ollama"]; + } + + return new Promise((resolve) => { + const proc = spawn("ollama", ["list"], { + stdio: ["ignore", "pipe", "pipe"], + timeout: 5000, + }); + + let resolved = false; + + proc.on("close", (code) => { + if (resolved) return; + resolved = true; + + const status: ProviderStatus = { + available: code === 0, + lastChecked: Date.now(), + error: code !== 0 ? "Ollama not running or not installed" : undefined, + }; + + providerCache["ollama"] = status; + resolve(status); + }); + + proc.on("error", (err) => { + if (resolved) return; + resolved = true; + + const status: ProviderStatus = { + available: false, + lastChecked: Date.now(), + error: err.message, + }; + + providerCache["ollama"] = status; + resolve(status); + }); + + setTimeout(() => { + if (resolved) return; + resolved = true; + proc.kill(); + + const status: ProviderStatus = { + available: false, + lastChecked: Date.now(), + error: "Ollama check timed out", + }; + + providerCache["ollama"] = status; + resolve(status); + }, 5000); + }); +}; + +export const checkCopilotAvailability = async (): Promise => { + if (isCacheValid("copilot")) { + return providerCache["copilot"]; + } + + // Copilot availability depends on authentication + // For now, assume available if token exists + const status: ProviderStatus = { + available: true, + lastChecked: Date.now(), + }; + + providerCache["copilot"] = status; + return status; +}; + +export const getProviderStatuses = async (): Promise< + Record +> => { + const [ollama, copilot] = await Promise.all([ + checkOllamaAvailability(), + checkCopilotAvailability(), + ]); + + return { + ollama, + copilot, + }; +}; + +export const clearProviderCache = (): void => { + Object.keys(providerCache).forEach((key) => { + delete providerCache[key]; + }); +}; + +export const invalidateProvider = (providerId: string): void => { + delete providerCache[providerId]; +}; diff --git a/src/services/cascading-provider/index.ts b/src/services/cascading-provider/index.ts new file mode 100644 index 0000000..f935e8d --- /dev/null +++ b/src/services/cascading-provider/index.ts @@ -0,0 +1,22 @@ +/** + * Cascading Provider Service + * + * Orchestrates multi-provider cascading with quality learning + */ + +export { + checkOllamaAvailability, + checkCopilotAvailability, + getProviderStatuses, + clearProviderCache, + invalidateProvider, + type ProviderStatus, +} from "./availability"; + +export { + executeCascade, + recordUserFeedback, + type CascadeCallbacks, + type CascadeOptions, + type ProviderCallFn, +} from "./orchestrator"; diff --git a/src/services/cascading-provider/orchestrator.ts b/src/services/cascading-provider/orchestrator.ts new file mode 100644 index 0000000..c49d0b4 --- /dev/null +++ b/src/services/cascading-provider/orchestrator.ts @@ -0,0 +1,174 @@ +/** + * Cascade Orchestrator Service + * + * Orchestrates the cascading provider flow + */ + +import type { + TaskType, + RoutingDecision, + CascadeResult, + AuditResult, +} from "@/types/provider-quality"; +import { PROVIDER_IDS } from "@constants/provider-quality"; +import { parseAuditResponse } from "@prompts/audit-prompt"; +import { + detectTaskType, + determineRoute, + recordAuditResult, + recordApproval, + recordRejection, +} from "@services/provider-quality"; +import { + checkOllamaAvailability, + checkCopilotAvailability, +} from "./availability"; + +export interface CascadeCallbacks { + onRouteDecided?: (decision: RoutingDecision, taskType: TaskType) => void; + onPrimaryStart?: (provider: string) => void; + onPrimaryComplete?: (response: string) => void; + onAuditStart?: () => void; + onAuditComplete?: (result: AuditResult) => void; + onCorrectionApplied?: (correctedResponse: string) => void; +} + +export interface CascadeOptions { + cascadeEnabled: boolean; + callbacks?: CascadeCallbacks; +} + +export interface ProviderCallFn { + (prompt: string, provider: "ollama" | "copilot", isAudit?: boolean): Promise; +} + +export const executeCascade = async ( + userPrompt: string, + callProvider: ProviderCallFn, + options: CascadeOptions, +): Promise => { + const taskType = detectTaskType(userPrompt); + const ollamaStatus = await checkOllamaAvailability(); + const copilotStatus = await checkCopilotAvailability(); + + const routingDecision = await determineRoute({ + taskType, + ollamaAvailable: ollamaStatus.available, + copilotAvailable: copilotStatus.available, + cascadeEnabled: options.cascadeEnabled, + }); + + options.callbacks?.onRouteDecided?.(routingDecision, taskType); + + if (routingDecision === "copilot_only") { + options.callbacks?.onPrimaryStart?.("copilot"); + const response = await callProvider(userPrompt, "copilot"); + options.callbacks?.onPrimaryComplete?.(response); + + return { + primaryResponse: response, + finalResponse: response, + routingDecision, + taskType, + }; + } + + if (routingDecision === "ollama_only") { + options.callbacks?.onPrimaryStart?.("ollama"); + const response = await callProvider(userPrompt, "ollama"); + options.callbacks?.onPrimaryComplete?.(response); + + return { + primaryResponse: response, + finalResponse: response, + routingDecision, + taskType, + }; + } + + // Cascade mode: Ollama first, then Copilot audit + options.callbacks?.onPrimaryStart?.("ollama"); + const primaryResponse = await callProvider(userPrompt, "ollama"); + options.callbacks?.onPrimaryComplete?.(primaryResponse); + + options.callbacks?.onAuditStart?.(); + const auditResponse = await callProvider( + createAuditMessage(userPrompt, primaryResponse), + "copilot", + true, + ); + + const auditResult = parseAuditResult(auditResponse); + options.callbacks?.onAuditComplete?.(auditResult); + + // Update quality scores based on audit + await recordAuditResult( + PROVIDER_IDS.OLLAMA, + taskType, + auditResult.approved, + auditResult.severity === "major" || auditResult.severity === "critical", + ); + + // Determine final response + let finalResponse = primaryResponse; + + if (!auditResult.approved && auditResult.severity !== "none") { + const parsed = parseAuditResponse(auditResponse); + if (parsed.correctedResponse) { + finalResponse = parsed.correctedResponse; + options.callbacks?.onCorrectionApplied?.(finalResponse); + } + } + + return { + primaryResponse, + auditResult, + finalResponse, + routingDecision, + taskType, + }; +}; + +const createAuditMessage = ( + userPrompt: string, + ollamaResponse: string, +): string => { + return `Please audit this AI response: + +## User Request +${userPrompt} + +## AI Response to Audit +${ollamaResponse} + +## Instructions +Evaluate the response for correctness, completeness, best practices, and security. +Respond with a JSON object containing: approved (boolean), severity (none/minor/major/critical), issues (array), suggestions (array), and correctedResponse (if needed).`; +}; + +const parseAuditResult = (auditResponse: string): AuditResult => { + const parsed = parseAuditResponse(auditResponse); + + return { + approved: parsed.approved, + issues: parsed.issues, + suggestions: parsed.suggestions, + severity: parsed.severity, + }; +}; + +export const recordUserFeedback = async ( + taskType: TaskType, + isPositive: boolean, + lastProvider: string, +): Promise => { + if (lastProvider !== PROVIDER_IDS.OLLAMA) { + return; + } + + if (isPositive) { + await recordApproval(PROVIDER_IDS.OLLAMA, taskType); + } else { + await recordRejection(PROVIDER_IDS.OLLAMA, taskType); + } +}; diff --git a/src/services/chat-tui-service.ts b/src/services/chat-tui-service.ts new file mode 100644 index 0000000..6fa718e --- /dev/null +++ b/src/services/chat-tui-service.ts @@ -0,0 +1,61 @@ +/** + * Chat TUI Service - Business logic for the chat interface + * + * This service handles all non-rendering logic: + * - Session management + * - Provider/model handling + * - Message processing + * - File context management + * - Command execution + * - Authentication flows + * - Agent execution + */ + +// Re-export types +export type { + ChatServiceState, + ChatServiceCallbacks, + DiffResult, + ToolCallInfo, + ProviderDisplayInfo, +} from "@/types/chat-service"; + +// Re-export initialization +export { initializeChatService } from "@services/chat-tui/initialize"; + +// Re-export message handling +export { handleMessage } from "@services/chat-tui/message-handler"; + +// Re-export command handling +export { executeCommand } from "@services/chat-tui/commands"; + +// Re-export model handling +export { + getProviderInfo, + loadModels, + handleModelSelect, +} from "@services/chat-tui/models"; + +// Re-export utilities +export { + truncateOutput, + detectDiffContent, + getToolDescription, +} from "@services/chat-tui/utils"; + +// Re-export file handling +export { + addContextFile, + processFileReferences, + buildContextMessage, +} from "@services/chat-tui/files"; + +// Re-export permission handling +export { + createPermissionHandler, + setupPermissionHandler, + cleanupPermissionHandler, +} from "@services/chat-tui/permissions"; + +// Re-export print mode +export { executePrintMode } from "@services/chat-tui/print-mode"; diff --git a/src/services/chat-tui/agents.ts b/src/services/chat-tui/agents.ts new file mode 100644 index 0000000..8fe769a --- /dev/null +++ b/src/services/chat-tui/agents.ts @@ -0,0 +1,72 @@ +/** + * Agent commands for TUI + */ + +import { agentLoader } from "@services/agent-loader"; +import type { + ChatServiceState, + ChatServiceCallbacks, +} from "@/types/chat-service"; + +export const showAgentsList = async ( + state: ChatServiceState, + callbacks: ChatServiceCallbacks, +): Promise => { + const agents = await agentLoader.getAvailableAgents(process.cwd()); + const currentAgent = + (state as ChatServiceState & { currentAgent?: string }).currentAgent ?? + "coder"; + + const lines: string[] = ["Available Agents", ""]; + + for (const agent of agents) { + const isCurrent = agent.id === currentAgent; + const marker = isCurrent ? "→ " : " "; + const nameDisplay = isCurrent ? `[${agent.name}]` : agent.name; + + lines.push(`${marker}${nameDisplay}`); + + if (agent.description) { + lines.push(` ${agent.description}`); + } + } + + lines.push(""); + lines.push("Use /agent to switch agents"); + + callbacks.onLog("system", lines.join("\n")); +}; + +export const switchAgentById = async ( + agentId: string, + state: ChatServiceState, +): Promise<{ success: boolean; message: string }> => { + const agents = await agentLoader.getAvailableAgents(process.cwd()); + const agent = agents.find((a) => a.id === agentId); + + if (!agent) { + return { success: false, message: `Agent not found: ${agentId}` }; + } + + // Store current agent on state + (state as ChatServiceState & { currentAgent?: string }).currentAgent = + agent.id; + + // Update system prompt with agent prompt + if (agent.prompt) { + const basePrompt = state.systemPrompt; + state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`; + + // Update the system message in messages array + if (state.messages.length > 0 && state.messages[0].role === "system") { + state.messages[0].content = state.systemPrompt; + } + } + + let message = `Switched to agent: ${agent.name}`; + if (agent.description) { + message += `\n${agent.description}`; + } + + return { success: true, message }; +}; diff --git a/src/services/chat-tui/auth.ts b/src/services/chat-tui/auth.ts new file mode 100644 index 0000000..0ac3ff4 --- /dev/null +++ b/src/services/chat-tui/auth.ts @@ -0,0 +1,161 @@ +/** + * Chat TUI authentication handling + */ + +import { AUTH_MESSAGES } from "@constants/chat-service"; +import { + getProviderStatus, + getCopilotUserInfo, + logoutProvider, + initiateDeviceFlow, + pollForAccessToken, + completeCopilotLogin, +} from "@providers/index"; +import { appStore } from "@tui/index"; +import { loadModels } from "@services/chat-tui/models"; +import type { + ChatServiceState, + ChatServiceCallbacks, +} from "@/types/chat-service"; + +const PROVIDER_AUTH_HANDLERS: Record< + string, + (state: ChatServiceState, callbacks: ChatServiceCallbacks) => Promise +> = { + copilot: handleCopilotLogin, + ollama: async (_, callbacks) => { + callbacks.onLog("system", AUTH_MESSAGES.NO_LOGIN_REQUIRED("ollama")); + }, +}; + +const PROVIDER_LOGOUT_HANDLERS: Record< + string, + (state: ChatServiceState, callbacks: ChatServiceCallbacks) => Promise +> = { + copilot: handleCopilotLogout, + ollama: async (state, callbacks) => { + callbacks.onLog("system", AUTH_MESSAGES.NO_LOGOUT_SUPPORT(state.provider)); + }, +}; + +const PROVIDER_WHOAMI_HANDLERS: Record< + string, + (state: ChatServiceState, callbacks: ChatServiceCallbacks) => Promise +> = { + copilot: handleCopilotWhoami, + ollama: async (_, callbacks) => { + callbacks.onLog("system", AUTH_MESSAGES.OLLAMA_NO_AUTH); + }, +}; + +async function handleCopilotLogin( + state: ChatServiceState, + callbacks: ChatServiceCallbacks, +): Promise { + const status = await getProviderStatus(state.provider); + if (status.valid) { + callbacks.onLog("system", AUTH_MESSAGES.ALREADY_LOGGED_IN); + return; + } + + try { + const deviceResponse = await initiateDeviceFlow(); + callbacks.onLog( + "system", + AUTH_MESSAGES.COPILOT_AUTH_INSTRUCTIONS( + deviceResponse.verification_uri, + deviceResponse.user_code, + ), + ); + + try { + const accessToken = await pollForAccessToken( + deviceResponse.device_code, + deviceResponse.interval, + deviceResponse.expires_in, + ); + + await completeCopilotLogin(accessToken); + + const models = await loadModels(state.provider); + appStore.setAvailableModels(models); + + callbacks.onLog("system", AUTH_MESSAGES.AUTH_SUCCESS); + + const userInfo = await getCopilotUserInfo(); + if (userInfo) { + callbacks.onLog( + "system", + AUTH_MESSAGES.LOGGED_IN_AS(userInfo.login, userInfo.name), + ); + } + } catch (pollError) { + callbacks.onLog( + "error", + AUTH_MESSAGES.AUTH_FAILED((pollError as Error).message), + ); + } + } catch (error) { + callbacks.onLog( + "error", + AUTH_MESSAGES.AUTH_START_FAILED((error as Error).message), + ); + } +} + +async function handleCopilotLogout( + _state: ChatServiceState, + callbacks: ChatServiceCallbacks, +): Promise { + await logoutProvider("copilot"); + callbacks.onLog("system", AUTH_MESSAGES.LOGGED_OUT); +} + +async function handleCopilotWhoami( + _state: ChatServiceState, + callbacks: ChatServiceCallbacks, +): Promise { + const userInfo = await getCopilotUserInfo(); + if (userInfo) { + let content = `Logged in as: ${userInfo.login}`; + if (userInfo.name) content += `\nName: ${userInfo.name}`; + if (userInfo.email) content += `\nEmail: ${userInfo.email}`; + callbacks.onLog("system", content); + } else { + callbacks.onLog("system", AUTH_MESSAGES.NOT_LOGGED_IN); + } +} + +export const handleLogin = async ( + state: ChatServiceState, + callbacks: ChatServiceCallbacks, +): Promise => { + const handler = PROVIDER_AUTH_HANDLERS[state.provider]; + if (handler) { + await handler(state, callbacks); + } else { + callbacks.onLog("system", AUTH_MESSAGES.NO_LOGIN_REQUIRED(state.provider)); + } +}; + +export const handleLogout = async ( + state: ChatServiceState, + callbacks: ChatServiceCallbacks, +): Promise => { + const handler = PROVIDER_LOGOUT_HANDLERS[state.provider]; + if (handler) { + await handler(state, callbacks); + } else { + callbacks.onLog("system", AUTH_MESSAGES.NO_LOGOUT_SUPPORT(state.provider)); + } +}; + +export const showWhoami = async ( + state: ChatServiceState, + callbacks: ChatServiceCallbacks, +): Promise => { + const handler = PROVIDER_WHOAMI_HANDLERS[state.provider]; + if (handler) { + await handler(state, callbacks); + } +}; diff --git a/src/services/chat-tui/commands.ts b/src/services/chat-tui/commands.ts new file mode 100644 index 0000000..54a532d --- /dev/null +++ b/src/services/chat-tui/commands.ts @@ -0,0 +1,155 @@ +/** + * Chat TUI command handling + */ + +import { saveSession as saveSessionSession } from "@services/session"; +import { appStore } from "@tui/index"; +import { + CHAT_MESSAGES, + HELP_TEXT, + type CommandName, +} from "@constants/chat-service"; +import { handleLogin, handleLogout, showWhoami } from "@services/chat-tui/auth"; +import { + handleRememberCommand, + handleLearningsCommand, +} from "@services/chat-tui/learnings"; +import { showUsageStats } from "@services/chat-tui/usage"; +import { + checkOllamaAvailability, + checkCopilotAvailability, +} from "@services/cascading-provider"; +import { getOverallScore } from "@services/provider-quality"; +import { PROVIDER_IDS } from "@constants/provider-quality"; +import type { + ChatServiceState, + ChatServiceCallbacks, +} from "@/types/chat-service"; + +type CommandHandler = ( + state: ChatServiceState, + callbacks: ChatServiceCallbacks, +) => Promise | void; + +const showHelp: CommandHandler = (_, callbacks) => { + callbacks.onLog("system", HELP_TEXT); +}; + +const clearConversation: CommandHandler = (state, callbacks) => { + state.messages = [{ role: "system", content: state.systemPrompt }]; + appStore.clearLogs(); + callbacks.onLog("system", CHAT_MESSAGES.CONVERSATION_CLEARED); +}; + +const saveSession: CommandHandler = async (_, callbacks) => { + await saveSessionSession(); + callbacks.onLog("system", CHAT_MESSAGES.SESSION_SAVED); +}; + +const showContext: CommandHandler = (state, callbacks) => { + const tokenEstimate = state.messages.reduce( + (sum, msg) => sum + Math.ceil(msg.content.length / 4), + 0, + ); + callbacks.onLog( + "system", + `Context: ${state.messages.length} messages, ~${tokenEstimate} tokens`, + ); +}; + +const selectModel: CommandHandler = () => { + appStore.setMode("model_select"); +}; + +const selectProvider: CommandHandler = () => { + appStore.setMode("provider_select"); +}; + +const showStatus: CommandHandler = async (state, callbacks) => { + const ollamaStatus = await checkOllamaAvailability(); + const copilotStatus = await checkCopilotAvailability(); + const ollamaScore = await getOverallScore(PROVIDER_IDS.OLLAMA); + const { cascadeEnabled } = appStore.getState(); + + const lines = [ + "═══ Provider Status ═══", + "", + `Current Provider: ${state.provider}`, + `Cascade Mode: ${cascadeEnabled ? "Enabled" : "Disabled"}`, + "", + "Ollama:", + ` Status: ${ollamaStatus.available ? "● Available" : "○ Not Available"}`, + ollamaStatus.error ? ` Error: ${ollamaStatus.error}` : null, + ` Quality Score: ${Math.round(ollamaScore * 100)}%`, + "", + "Copilot:", + ` Status: ${copilotStatus.available ? "● Available" : "○ Not Available"}`, + copilotStatus.error ? ` Error: ${copilotStatus.error}` : null, + "", + "Commands:", + " /provider - Select provider", + " /login - Authenticate with Copilot", + "", + "Config: ~/.config/codetyper/config.json", + ].filter(Boolean); + + callbacks.onLog("system", lines.join("\n")); +}; + +const selectAgent: CommandHandler = () => { + appStore.setMode("agent_select"); +}; + +const selectTheme: CommandHandler = () => { + appStore.setMode("theme_select"); +}; + +const selectMCP: CommandHandler = () => { + appStore.setMode("mcp_select"); +}; + +const selectMode: CommandHandler = () => { + appStore.setMode("mode_select"); +}; + +const COMMAND_HANDLERS: Record = { + help: showHelp, + h: showHelp, + clear: clearConversation, + c: clearConversation, + save: saveSession, + s: saveSession, + context: showContext, + usage: (state, callbacks) => showUsageStats(state, callbacks), + u: (state, callbacks) => showUsageStats(state, callbacks), + model: selectModel, + models: selectModel, + agent: selectAgent, + a: selectAgent, + theme: selectTheme, + mcp: selectMCP, + mode: selectMode, + whoami: showWhoami, + login: handleLogin, + logout: handleLogout, + provider: selectProvider, + p: selectProvider, + status: showStatus, + remember: handleRememberCommand, + learnings: (_, callbacks) => handleLearningsCommand(callbacks), +}; + +export const executeCommand = async ( + state: ChatServiceState, + command: string, + callbacks: ChatServiceCallbacks, +): Promise => { + const normalizedCommand = command.toLowerCase() as CommandName; + const handler = COMMAND_HANDLERS[normalizedCommand]; + + if (handler) { + await handler(state, callbacks); + } else { + callbacks.onLog("error", CHAT_MESSAGES.UNKNOWN_COMMAND(command)); + } +}; diff --git a/src/services/chat-tui/files.ts b/src/services/chat-tui/files.ts new file mode 100644 index 0000000..d9de607 --- /dev/null +++ b/src/services/chat-tui/files.ts @@ -0,0 +1,203 @@ +/** + * Chat TUI file handling + */ + +import { readFile, stat } from "fs/promises"; +import { resolve, basename, extname } from "path"; +import { existsSync } from "fs"; +import fg from "fast-glob"; + +import { + FILE_SIZE_LIMITS, + GLOB_IGNORE_PATTERNS, + CHAT_MESSAGES, +} from "@constants/chat-service"; +import { + BINARY_EXTENSIONS, + type BinaryExtension, +} from "@constants/file-picker"; +import { appStore } from "@tui/index"; +import type { ChatServiceState } from "@/types/chat-service"; + +const isBinaryFile = (filePath: string): boolean => { + const ext = extname(filePath).toLowerCase(); + return BINARY_EXTENSIONS.includes(ext as BinaryExtension); +}; + +const isExecutableWithoutExtension = async ( + filePath: string, +): Promise => { + const ext = extname(filePath); + if (ext) return false; + + try { + const buffer = Buffer.alloc(4); + const { open } = await import("fs/promises"); + const handle = await open(filePath, "r"); + await handle.read(buffer, 0, 4, 0); + await handle.close(); + + // Check for common binary signatures + // ELF (Linux executables) + if ( + buffer[0] === 0x7f && + buffer[1] === 0x45 && + buffer[2] === 0x4c && + buffer[3] === 0x46 + ) { + return true; + } + // Mach-O (macOS executables) + if ( + (buffer[0] === 0xfe && + buffer[1] === 0xed && + buffer[2] === 0xfa && + buffer[3] === 0xce) || + (buffer[0] === 0xfe && + buffer[1] === 0xed && + buffer[2] === 0xfa && + buffer[3] === 0xcf) || + (buffer[0] === 0xce && + buffer[1] === 0xfa && + buffer[2] === 0xed && + buffer[3] === 0xfe) || + (buffer[0] === 0xcf && + buffer[1] === 0xfa && + buffer[2] === 0xed && + buffer[3] === 0xfe) + ) { + return true; + } + // MZ (Windows executables) + if (buffer[0] === 0x4d && buffer[1] === 0x5a) { + return true; + } + return false; + } catch { + return false; + } +}; + +export const loadFile = async ( + state: ChatServiceState, + filePath: string, +): Promise => { + try { + if (isBinaryFile(filePath)) { + appStore.addLog({ + type: "error", + content: CHAT_MESSAGES.FILE_IS_BINARY(basename(filePath)), + }); + return; + } + + const stats = await stat(filePath); + if (stats.size > FILE_SIZE_LIMITS.MAX_CONTEXT_FILE_SIZE) { + appStore.addLog({ + type: "error", + content: CHAT_MESSAGES.FILE_TOO_LARGE(basename(filePath), stats.size), + }); + return; + } + + if (await isExecutableWithoutExtension(filePath)) { + appStore.addLog({ + type: "error", + content: CHAT_MESSAGES.FILE_IS_BINARY(basename(filePath)), + }); + return; + } + + const content = await readFile(filePath, "utf-8"); + state.contextFiles.set(filePath, content); + appStore.addLog({ + type: "system", + content: CHAT_MESSAGES.FILE_ADDED(basename(filePath)), + }); + } catch (error) { + appStore.addLog({ + type: "error", + content: CHAT_MESSAGES.FILE_READ_FAILED(error), + }); + } +}; + +export const addContextFile = async ( + state: ChatServiceState, + pattern: string, +): Promise => { + try { + const paths = await fg(pattern, { + cwd: process.cwd(), + absolute: true, + ignore: [...GLOB_IGNORE_PATTERNS], + }); + + if (paths.length === 0) { + const absolutePath = resolve(process.cwd(), pattern); + if (existsSync(absolutePath)) { + await loadFile(state, absolutePath); + } else { + appStore.addLog({ + type: "error", + content: CHAT_MESSAGES.FILE_NOT_FOUND(pattern), + }); + } + return; + } + + for (const filePath of paths) { + await loadFile(state, filePath); + } + } catch (error) { + appStore.addLog({ + type: "error", + content: CHAT_MESSAGES.FILE_ADD_FAILED(error), + }); + } +}; + +export const processFileReferences = async ( + state: ChatServiceState, + input: string, +): Promise => { + const filePattern = /@(?:"([^"]+)"|'([^']+)'|(\S+))/g; + let match; + const filesToAdd: string[] = []; + + while ((match = filePattern.exec(input)) !== null) { + const filePath = match[1] || match[2] || match[3]; + filesToAdd.push(filePath); + } + + for (const filePath of filesToAdd) { + await addContextFile(state, filePath); + } + + const textOnly = input.replace(filePattern, "").trim(); + if (!textOnly && filesToAdd.length > 0) { + return CHAT_MESSAGES.ANALYZE_FILES; + } + + return input; +}; + +export const buildContextMessage = ( + state: ChatServiceState, + message: string, +): string => { + if (state.contextFiles.size === 0) { + return message; + } + + const contextParts: string[] = []; + for (const [path, fileContent] of state.contextFiles) { + const ext = extname(path).slice(1) || "txt"; + contextParts.push( + `File: ${basename(path)}\n\`\`\`${ext}\n${fileContent}\n\`\`\``, + ); + } + + state.contextFiles.clear(); + return contextParts.join("\n\n") + "\n\n" + message; +}; diff --git a/src/services/chat-tui/initialize.ts b/src/services/chat-tui/initialize.ts new file mode 100644 index 0000000..1d50835 --- /dev/null +++ b/src/services/chat-tui/initialize.ts @@ -0,0 +1,211 @@ +/** + * Chat TUI initialization + */ + +import { errorMessage, infoMessage } from "@utils/terminal"; +import { + findSession, + loadSession, + createSession, + getMostRecentSession, +} from "@services/session"; +import { getConfig } from "@services/config"; +import { initializePermissions } from "@services/permissions"; +import { projectConfig } from "@services/project-config"; +import { getProviderStatus } from "@providers/index"; +import { appStore } from "@tui/index"; +import { themeActions } from "@stores/theme-store"; +import { + AGENTIC_SYSTEM_PROMPT, + buildAgenticPrompt, + buildSystemPromptWithRules, +} from "@prompts/index"; +import { initSuggestionService } from "@services/command-suggestion-service"; +import { addContextFile } from "@services/chat-tui/files"; +import type { ProviderName, Message } from "@/types/providers"; +import type { ChatSession } from "@/types/index"; +import type { ChatTUIOptions } from "@interfaces/ChatTUIOptions"; +import type { ChatServiceState } from "@/types/chat-service"; + +const createInitialState = async ( + options: ChatTUIOptions, +): Promise => { + const config = await getConfig(); + + return { + provider: (options.provider || config.get("provider")) as ProviderName, + model: options.model || config.get("model") || undefined, + messages: [], + contextFiles: new Map(), + systemPrompt: AGENTIC_SYSTEM_PROMPT, + verbose: options.verbose || false, + autoApprove: options.autoApprove || false, + }; +}; + +const validateProvider = async (state: ChatServiceState): Promise => { + const status = await getProviderStatus(state.provider); + if (!status.valid) { + errorMessage(`Provider ${state.provider} is not configured.`); + infoMessage(`Run: codetyper login ${state.provider}`); + process.exit(1); + } +}; + +const getGitContext = async (): Promise<{ + isGitRepo: boolean; + branch?: string; + status?: string; + recentCommits?: string[]; +}> => { + try { + const { execSync } = await import("child_process"); + const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim(); + const status = execSync("git status --short", { encoding: "utf-8" }).trim() || "(clean)"; + const commits = execSync("git log --oneline -5", { encoding: "utf-8" }) + .trim() + .split("\n") + .filter(Boolean); + return { isGitRepo: true, branch, status, recentCommits: commits }; + } catch { + return { isGitRepo: false }; + } +}; + +const buildSystemPrompt = async ( + state: ChatServiceState, + options: ChatTUIOptions, +): Promise => { + if (options.systemPrompt) { + state.systemPrompt = options.systemPrompt; + return; + } + + // Build agentic prompt with environment context + const gitContext = await getGitContext(); + const basePrompt = buildAgenticPrompt({ + workingDir: process.cwd(), + isGitRepo: gitContext.isGitRepo, + platform: process.platform, + today: new Date().toISOString().split("T")[0], + model: state.model, + gitBranch: gitContext.branch, + gitStatus: gitContext.status, + recentCommits: gitContext.recentCommits, + }); + + // Add project rules + const { prompt: promptWithRules, rulesPaths } = + await buildSystemPromptWithRules(basePrompt, process.cwd()); + state.systemPrompt = promptWithRules; + + if (rulesPaths.length > 0 && state.verbose) { + infoMessage(`Loaded ${rulesPaths.length} rule file(s):`); + for (const rulePath of rulesPaths) { + infoMessage(` - ${rulePath}`); + } + } + + const learningsContext = await projectConfig.buildLearningsContext(); + if (learningsContext) { + state.systemPrompt = state.systemPrompt + "\n\n" + learningsContext; + if (state.verbose) { + infoMessage("Loaded project learnings"); + } + } + + if (options.appendSystemPrompt) { + state.systemPrompt = + state.systemPrompt + "\n\n" + options.appendSystemPrompt; + } +}; + +const restoreMessagesFromSession = ( + state: ChatServiceState, + session: ChatSession, +): void => { + state.messages = [{ role: "system", content: state.systemPrompt }]; + + for (const msg of session.messages) { + if (msg.role !== "system") { + state.messages.push({ + role: msg.role as Message["role"], + content: msg.content, + }); + + appStore.addLog({ + type: msg.role === "user" ? "user" : "assistant", + content: msg.content, + }); + } + } +}; + +const initializeSession = async ( + state: ChatServiceState, + options: ChatTUIOptions, +): Promise => { + if (options.continueSession) { + const recent = await getMostRecentSession(process.cwd()); + if (recent) { + await loadSession(recent.id); + restoreMessagesFromSession(state, recent); + return recent; + } + return createSession("coder"); + } + + if (options.resumeSession) { + const found = await findSession(options.resumeSession); + if (found) { + await loadSession(found.id); + restoreMessagesFromSession(state, found); + return found; + } + errorMessage(`Session not found: ${options.resumeSession}`); + process.exit(1); + } + + return createSession("coder"); +}; + +const addInitialContextFiles = async ( + state: ChatServiceState, + files?: string[], +): Promise => { + if (!files) return; + + for (const file of files) { + await addContextFile(state, file); + } +}; + +const initializeTheme = async (): Promise => { + const config = await getConfig(); + const savedTheme = config.get("theme"); + if (savedTheme) { + themeActions.setTheme(savedTheme); + } +}; + +export const initializeChatService = async ( + options: ChatTUIOptions, +): Promise<{ state: ChatServiceState; session: ChatSession }> => { + const state = await createInitialState(options); + + await validateProvider(state); + await buildSystemPrompt(state, options); + await initializeTheme(); + + const session = await initializeSession(state, options); + + if (state.messages.length === 0) { + state.messages.push({ role: "system", content: state.systemPrompt }); + } + + await addInitialContextFiles(state, options.files); + await initializePermissions(); + initSuggestionService(process.cwd()); + + return { state, session }; +}; diff --git a/src/services/chat-tui/learnings.ts b/src/services/chat-tui/learnings.ts new file mode 100644 index 0000000..733c2f5 --- /dev/null +++ b/src/services/chat-tui/learnings.ts @@ -0,0 +1,118 @@ +/** + * Chat TUI learnings handling + */ + +import { + CHAT_MESSAGES, + LEARNING_CONFIDENCE_THRESHOLD, + MAX_LEARNINGS_DISPLAY, +} from "@constants/chat-service"; +import { + detectLearnings, + saveLearning, + getLearnings, + learningExists, +} from "@services/learning-service"; +import type { + ChatServiceState, + ChatServiceCallbacks, +} from "@/types/chat-service"; + +export const handleRememberCommand = async ( + state: ChatServiceState, + callbacks: ChatServiceCallbacks, +): Promise => { + const lastUserMsg = [...state.messages] + .reverse() + .find((m) => m.role === "user"); + const lastAssistantMsg = [...state.messages] + .reverse() + .find((m) => m.role === "assistant"); + + if (!lastUserMsg || !lastAssistantMsg) { + callbacks.onLog("system", CHAT_MESSAGES.NO_CONVERSATION); + return; + } + + const candidates = detectLearnings( + lastUserMsg.content, + lastAssistantMsg.content, + ); + + if (candidates.length === 0) { + callbacks.onLog("system", CHAT_MESSAGES.NO_LEARNINGS_DETECTED); + return; + } + + if (callbacks.onLearningDetected) { + const topCandidate = candidates[0]; + const response = await callbacks.onLearningDetected(topCandidate); + if (response.save && response.scope) { + await saveLearning( + response.editedContent || topCandidate.content, + topCandidate.context, + response.scope === "global", + ); + callbacks.onLog("system", CHAT_MESSAGES.LEARNING_SAVED(response.scope)); + } else { + callbacks.onLog("system", CHAT_MESSAGES.LEARNING_SKIPPED); + } + } else { + callbacks.onLog( + "system", + `Detected learnings:\n${candidates.map((c) => `- ${c.content}`).join("\n")}`, + ); + } +}; + +export const handleLearningsCommand = async ( + callbacks: ChatServiceCallbacks, +): Promise => { + const learnings = await getLearnings(); + + if (learnings.length === 0) { + callbacks.onLog("system", CHAT_MESSAGES.NO_LEARNINGS); + return; + } + + const formatted = learnings + .slice(0, MAX_LEARNINGS_DISPLAY) + .map((l, i) => `${i + 1}. ${l.content}`) + .join("\n"); + + callbacks.onLog( + "system", + `Saved learnings (${learnings.length} total):\n${formatted}${learnings.length > MAX_LEARNINGS_DISPLAY ? "\n... and more" : ""}`, + ); +}; + +export const processLearningsFromExchange = async ( + userMessage: string, + assistantResponse: string, + callbacks: ChatServiceCallbacks, +): Promise => { + if (!callbacks.onLearningDetected) return; + + const candidates = detectLearnings(userMessage, assistantResponse); + + for (const candidate of candidates) { + if (candidate.confidence >= LEARNING_CONFIDENCE_THRESHOLD) { + const exists = await learningExists(candidate.content); + if (!exists) { + const response = await callbacks.onLearningDetected(candidate); + if (response.save && response.scope) { + await saveLearning( + response.editedContent || candidate.content, + candidate.context, + response.scope === "global", + ); + callbacks.onLog( + "system", + CHAT_MESSAGES.LEARNING_SAVED(response.scope), + ); + } + break; + } + } + } +}; diff --git a/src/services/chat-tui/message-handler.ts b/src/services/chat-tui/message-handler.ts new file mode 100644 index 0000000..018a26b --- /dev/null +++ b/src/services/chat-tui/message-handler.ts @@ -0,0 +1,477 @@ +/** + * Chat TUI message handling + */ + +import { addMessage, saveSession } from "@services/session"; +import { createStreamingAgent } from "@services/agent-stream"; +import { CHAT_MESSAGES } from "@constants/chat-service"; +import { enrichMessageWithIssues } from "@services/github-issue-service"; +import { + checkGitHubCLI, + extractPRUrls, + fetchPR, + fetchPRComments, + formatPRContext, + formatPendingComments, +} from "@services/github-pr"; +import { + analyzeFileChange, + clearSuggestions, + getPendingSuggestions, + formatSuggestions, +} from "@services/command-suggestion-service"; +import { + processFileReferences, + buildContextMessage, +} from "@services/chat-tui/files"; +import { getToolDescription } from "@services/chat-tui/utils"; +import { processLearningsFromExchange } from "@services/chat-tui/learnings"; +import { + compactConversation, + createCompactionSummary, + getModelCompactionConfig, + checkCompactionNeeded, +} from "@services/auto-compaction"; +import { + detectTaskType, + determineRoute, + recordAuditResult, + isCorrection, + getRoutingExplanation, +} from "@services/provider-quality"; +import { + checkOllamaAvailability, + checkCopilotAvailability, +} from "@services/cascading-provider"; +import { chat } from "@providers/chat"; +import { AUDIT_SYSTEM_PROMPT, createAuditPrompt, parseAuditResponse } from "@prompts/audit-prompt"; +import { PROVIDER_IDS } from "@constants/provider-quality"; +import { appStore } from "@tui/index"; +import type { StreamCallbacks } from "@/types/streaming"; +import type { TaskType } from "@/types/provider-quality"; +import type { + ChatServiceState, + ChatServiceCallbacks, + ToolCallInfo, +} from "@/types/chat-service"; + +// Track last response for feedback learning +let lastResponseContext: { + taskType: TaskType; + provider: string; + response: string; +} | null = null; + +const FILE_MODIFYING_TOOLS = ["write", "edit"]; + +const createToolCallHandler = + ( + callbacks: ChatServiceCallbacks, + toolCallRef: { current: ToolCallInfo | null }, + ) => + (call: { id: string; name: string; arguments?: Record }) => { + const args = call.arguments; + if (FILE_MODIFYING_TOOLS.includes(call.name) && args?.path) { + toolCallRef.current = { name: call.name, path: String(args.path) }; + } else { + toolCallRef.current = { name: call.name }; + } + + callbacks.onModeChange("tool_execution"); + callbacks.onToolCall({ + id: call.id, + name: call.name, + description: getToolDescription(call), + args, + }); + }; + +const createToolResultHandler = + ( + callbacks: ChatServiceCallbacks, + toolCallRef: { current: ToolCallInfo | null }, + ) => + ( + _callId: string, + result: { + success: boolean; + title: string; + output?: string; + error?: string; + }, + ) => { + if (result.success && toolCallRef.current?.path) { + analyzeFileChange(toolCallRef.current.path); + } + + callbacks.onToolResult( + result.success, + result.title, + result.success ? result.output : undefined, + result.error, + ); + callbacks.onModeChange("thinking"); + }; + +/** + * Create streaming callbacks for TUI integration + */ +const createStreamCallbacks = (): StreamCallbacks => ({ + onContentChunk: (content: string) => { + appStore.appendStreamContent(content); + }, + + onToolCallStart: (toolCall) => { + appStore.setCurrentToolCall({ + id: toolCall.id, + name: toolCall.name, + description: `Calling ${toolCall.name}...`, + status: "pending", + }); + }, + + onToolCallComplete: (toolCall) => { + appStore.updateToolCall({ + id: toolCall.id, + name: toolCall.name, + status: "running", + }); + }, + + onModelSwitch: (info) => { + appStore.addLog({ + type: "system", + content: `Model switched: ${info.from} → ${info.to} (${info.reason})`, + }); + }, + + onComplete: () => { + appStore.completeStreaming(); + }, + + onError: (error: string) => { + appStore.cancelStreaming(); + appStore.addLog({ + type: "error", + content: error, + }); + }, +}); + +/** + * Run audit with Copilot on Ollama's response + */ +const runAudit = async ( + userPrompt: string, + ollamaResponse: string, + callbacks: ChatServiceCallbacks, +): Promise<{ approved: boolean; hasMajorIssues: boolean; correctedResponse?: string }> => { + try { + callbacks.onLog("system", "Auditing response with Copilot..."); + + const auditMessages = [ + { role: "system" as const, content: AUDIT_SYSTEM_PROMPT }, + { role: "user" as const, content: createAuditPrompt(userPrompt, ollamaResponse) }, + ]; + + const auditResponse = await chat("copilot", auditMessages, {}); + const parsed = parseAuditResponse(auditResponse.content ?? ""); + + if (parsed.approved) { + callbacks.onLog("system", "✓ Audit passed - response approved"); + } else { + const issueCount = parsed.issues.length; + callbacks.onLog( + "system", + `⚠ Audit found ${issueCount} issue(s): ${parsed.issues.slice(0, 2).join(", ")}${issueCount > 2 ? "..." : ""}`, + ); + } + + return { + approved: parsed.approved, + hasMajorIssues: parsed.severity === "major" || parsed.severity === "critical", + correctedResponse: parsed.correctedResponse, + }; + } catch (error) { + callbacks.onLog("system", "Audit skipped due to error"); + return { approved: true, hasMajorIssues: false }; + } +}; + +/** + * Check for user feedback on previous response and update quality scores + */ +const checkUserFeedback = async ( + message: string, + callbacks: ChatServiceCallbacks, +): Promise => { + if (!lastResponseContext) return; + + if (isCorrection(message)) { + callbacks.onLog( + "system", + `Learning: Recording correction feedback for ${lastResponseContext.provider}`, + ); + + await recordAuditResult( + lastResponseContext.provider, + lastResponseContext.taskType, + false, + true, + ); + } + + // Clear context after checking + lastResponseContext = null; +}; + +export const handleMessage = async ( + state: ChatServiceState, + message: string, + callbacks: ChatServiceCallbacks, +): Promise => { + // Check for feedback on previous response + await checkUserFeedback(message, callbacks); + + // Get interaction mode and cascade setting from app store + const { interactionMode, cascadeEnabled } = appStore.getState(); + const isReadOnlyMode = interactionMode === "ask" || interactionMode === "code-review"; + + if (isReadOnlyMode) { + const modeLabel = interactionMode === "ask" ? "Ask" : "Code Review"; + callbacks.onLog( + "system", + `${modeLabel} mode: Read-only responses (Ctrl+Tab to switch modes)`, + ); + } + + let processedMessage = await processFileReferences(state, message); + + // In code-review mode, check for PR URLs and enrich with PR context + if (interactionMode === "code-review") { + const prUrls = extractPRUrls(message); + + if (prUrls.length > 0) { + const ghStatus = await checkGitHubCLI(); + + if (!ghStatus.installed) { + callbacks.onLog( + "system", + "GitHub CLI (gh) is not installed. Install it to enable PR review features: https://cli.github.com/", + ); + } else if (!ghStatus.authenticated) { + callbacks.onLog( + "system", + "GitHub CLI is not authenticated. Run 'gh auth login' to enable PR review features.", + ); + } else { + // Fetch PR details for each URL + for (const prUrl of prUrls) { + callbacks.onLog( + "system", + `Fetching PR #${prUrl.prNumber} from ${prUrl.owner}/${prUrl.repo}...`, + ); + + const pr = await fetchPR(prUrl); + if (pr) { + const prContext = formatPRContext(pr); + const comments = await fetchPRComments(prUrl); + const commentsContext = + comments.length > 0 ? formatPendingComments(comments) : ""; + + processedMessage = `${processedMessage}\n\n---\n\n${prContext}${commentsContext ? `\n\n${commentsContext}` : ""}`; + + callbacks.onLog( + "system", + `Loaded PR #${pr.number}: ${pr.title} (+${pr.additions} -${pr.deletions}, ${comments.length} comment(s))`, + ); + } else { + callbacks.onLog( + "system", + `Could not fetch PR #${prUrl.prNumber}. Make sure you have access to the repository.`, + ); + } + } + } + } + } + + const { enrichedMessage, issues } = + await enrichMessageWithIssues(processedMessage); + + if (issues.length > 0) { + callbacks.onLog( + "system", + CHAT_MESSAGES.GITHUB_ISSUES_FOUND( + issues.length, + issues.map((i) => `#${i.number}`).join(", "), + ), + ); + } + + const userMessage = buildContextMessage(state, enrichedMessage); + + state.messages.push({ role: "user", content: userMessage }); + + clearSuggestions(); + + // Get model-specific compaction config based on context window size + const config = getModelCompactionConfig(state.model); + const { needsCompaction } = checkCompactionNeeded(state.messages, config); + + if (needsCompaction) { + appStore.setIsCompacting(true); + callbacks.onLog("system", CHAT_MESSAGES.COMPACTION_STARTING); + + const { messages: compactedMessages, result: compactionResult } = + compactConversation(state.messages, config); + + if (compactionResult.compacted) { + const summary = createCompactionSummary(compactionResult); + callbacks.onLog("system", summary); + state.messages = compactedMessages; + callbacks.onLog("system", CHAT_MESSAGES.COMPACTION_CONTINUING); + } + + appStore.setIsCompacting(false); + } + + const toolCallRef: { current: ToolCallInfo | null } = { current: null }; + + // Determine routing for cascade mode + const taskType = detectTaskType(message); + let effectiveProvider = state.provider; + let shouldAudit = false; + + if (cascadeEnabled && !isReadOnlyMode) { + const ollamaStatus = await checkOllamaAvailability(); + const copilotStatus = await checkCopilotAvailability(); + + // If Ollama not available, fallback to Copilot with a message + if (!ollamaStatus.available) { + effectiveProvider = "copilot"; + shouldAudit = false; + callbacks.onLog( + "system", + `Ollama not available (${ollamaStatus.error ?? "not running"}). Using Copilot.`, + ); + } else if (!copilotStatus.available) { + // If Copilot not available, use Ollama only + effectiveProvider = "ollama"; + shouldAudit = false; + callbacks.onLog("system", "Copilot not available. Using Ollama only."); + } else { + // Both available - use routing logic + const routingDecision = await determineRoute({ + taskType, + ollamaAvailable: ollamaStatus.available, + copilotAvailable: copilotStatus.available, + cascadeEnabled: true, + }); + + const explanation = await getRoutingExplanation(routingDecision, taskType); + callbacks.onLog("system", explanation); + + if (routingDecision === "ollama_only") { + effectiveProvider = "ollama"; + shouldAudit = false; + } else if (routingDecision === "copilot_only") { + effectiveProvider = "copilot"; + shouldAudit = false; + } else if (routingDecision === "cascade") { + effectiveProvider = "ollama"; + shouldAudit = true; + } + } + } + + // Start streaming UI + appStore.setMode("thinking"); + appStore.startThinking(); + appStore.startStreaming(); + + const streamCallbacks = createStreamCallbacks(); + const agent = createStreamingAgent( + process.cwd(), + { + provider: effectiveProvider, + model: state.model, + verbose: state.verbose, + autoApprove: state.autoApprove, + chatMode: isReadOnlyMode, + onToolCall: createToolCallHandler(callbacks, toolCallRef), + onToolResult: createToolResultHandler(callbacks, toolCallRef), + onError: (error) => { + callbacks.onLog("error", error); + }, + onWarning: (warning) => { + callbacks.onLog("system", warning); + }, + }, + streamCallbacks, + ); + + try { + const result = await agent.run(state.messages); + + // Stop thinking timer + appStore.stopThinking(); + + if (result.finalResponse) { + let finalResponse = result.finalResponse; + + // Run audit if cascade mode with Ollama + if (shouldAudit && effectiveProvider === "ollama") { + const auditResult = await runAudit(message, result.finalResponse, callbacks); + + // Record quality score based on audit + await recordAuditResult( + PROVIDER_IDS.OLLAMA, + taskType, + auditResult.approved, + auditResult.hasMajorIssues, + ); + + // Use corrected response if provided + if (!auditResult.approved && auditResult.correctedResponse) { + finalResponse = auditResult.correctedResponse; + callbacks.onLog("system", "Using corrected response from audit"); + } + } + + // Store context for feedback learning + lastResponseContext = { + taskType, + provider: effectiveProvider, + response: finalResponse, + }; + + state.messages.push({ + role: "assistant", + content: finalResponse, + }); + // Note: Don't call callbacks.onLog here - streaming already added the log entry + // via appendStreamContent/completeStreaming + + addMessage("user", message); + addMessage("assistant", finalResponse); + await saveSession(); + + await processLearningsFromExchange( + message, + finalResponse, + callbacks, + ); + + const suggestions = getPendingSuggestions(); + if (suggestions.length > 0) { + const formatted = formatSuggestions(suggestions); + callbacks.onLog("system", formatted); + } + } + } catch (error) { + appStore.cancelStreaming(); + appStore.stopThinking(); + callbacks.onLog("error", String(error)); + } +}; diff --git a/src/services/chat-tui/models.ts b/src/services/chat-tui/models.ts new file mode 100644 index 0000000..2b48060 --- /dev/null +++ b/src/services/chat-tui/models.ts @@ -0,0 +1,55 @@ +/** + * Chat TUI model handling + */ + +import { MODEL_MESSAGES } from "@constants/chat-service"; +import { getConfig } from "@services/config"; +import { + getProvider, + getDefaultModel, + getModels as getProviderModels, +} from "@providers/index"; +import { appStore } from "@tui/index"; +import type { ProviderName, ProviderModel } from "@/types/providers"; +import type { + ChatServiceState, + ChatServiceCallbacks, + ProviderDisplayInfo, +} from "@/types/chat-service"; + +export const getProviderInfo = ( + providerName: ProviderName, +): ProviderDisplayInfo => { + const provider = getProvider(providerName); + const model = getDefaultModel(providerName); + return { displayName: provider.displayName, model }; +}; + +export const loadModels = async ( + providerName: ProviderName, +): Promise => { + try { + return await getProviderModels(providerName); + } catch { + return []; + } +}; + +export const handleModelSelect = async ( + state: ChatServiceState, + model: string, + callbacks: ChatServiceCallbacks, +): Promise => { + if (model === "auto") { + state.model = undefined; + callbacks.onLog("system", MODEL_MESSAGES.MODEL_AUTO); + } else { + state.model = model; + callbacks.onLog("system", MODEL_MESSAGES.MODEL_CHANGED(model)); + } + appStore.setModel(model); + + const config = await getConfig(); + config.set("model", model === "auto" ? undefined : model); + await config.save(); +}; diff --git a/src/services/chat-tui/permissions.ts b/src/services/chat-tui/permissions.ts new file mode 100644 index 0000000..df8ed44 --- /dev/null +++ b/src/services/chat-tui/permissions.ts @@ -0,0 +1,45 @@ +/** + * Chat TUI permission handling + */ + +import { v4 as uuidv4 } from "uuid"; + +import { setPermissionHandler } from "@services/permissions"; +import type { + PermissionPromptRequest, + PermissionPromptResponse, +} from "@/types/permissions"; +import { appStore } from "@tui/index"; + +export const createPermissionHandler = (): (( + request: PermissionPromptRequest, +) => Promise) => { + return ( + request: PermissionPromptRequest, + ): Promise => { + return new Promise((resolve) => { + appStore.setMode("permission_prompt"); + + appStore.setPermissionRequest({ + id: uuidv4(), + type: request.type, + description: request.description, + command: request.command, + path: request.path, + resolve: (response) => { + appStore.setPermissionRequest(null); + appStore.setMode("tool_execution"); + resolve(response); + }, + }); + }); + }; +}; + +export const setupPermissionHandler = (): void => { + setPermissionHandler(createPermissionHandler()); +}; + +export const cleanupPermissionHandler = (): void => { + setPermissionHandler(null); +}; diff --git a/src/services/chat-tui/print-mode.ts b/src/services/chat-tui/print-mode.ts new file mode 100644 index 0000000..f39422b --- /dev/null +++ b/src/services/chat-tui/print-mode.ts @@ -0,0 +1,51 @@ +/** + * Chat TUI print mode (non-interactive) + */ + +import { createAgent } from "@services/agent"; +import { initializePermissions } from "@services/permissions"; +import { + processFileReferences, + buildContextMessage, +} from "@services/chat-tui/files"; +import type { ChatServiceState } from "@/types/chat-service"; + +export const executePrintMode = async ( + state: ChatServiceState, + prompt: string, +): Promise => { + const processedPrompt = await processFileReferences(state, prompt); + const userMessage = buildContextMessage(state, processedPrompt); + + state.messages.push({ role: "user", content: userMessage }); + + await initializePermissions(); + + const agent = createAgent(process.cwd(), { + provider: state.provider, + model: state.model, + verbose: state.verbose, + autoApprove: state.autoApprove, + onToolCall: (call) => { + console.error(`[Tool: ${call.name}]`); + }, + onToolResult: (_callId, result) => { + if (result.success) { + console.error(`✓ ${result.title}`); + } else { + console.error(`✗ ${result.title}: ${result.error}`); + } + }, + }); + + try { + const result = await agent.run(state.messages); + + if (result.finalResponse) { + console.log(result.finalResponse); + } + } catch (error) { + console.error(`Error: ${error}`); + process.exit(1); + } +}; diff --git a/src/services/chat-tui/streaming.ts b/src/services/chat-tui/streaming.ts new file mode 100644 index 0000000..86e59a3 --- /dev/null +++ b/src/services/chat-tui/streaming.ts @@ -0,0 +1,261 @@ +/** + * Streaming Chat TUI Integration + * + * Connects the streaming agent loop to the TUI store for real-time updates. + */ + +import type { Message } from "@/types/providers"; +import type { AgentOptions } from "@interfaces/AgentOptions"; +import type { AgentResult } from "@interfaces/AgentResult"; +import type { + StreamCallbacks, + PartialToolCall, + ModelSwitchInfo, +} from "@/types/streaming"; +import type { ToolCall, ToolResult } from "@/types/tools"; +import { createStreamingAgent } from "@services/agent-stream"; +import { appStore } from "@tui/index"; + +// ============================================================================= +// Types +// ============================================================================= + +export interface StreamingChatOptions extends AgentOptions { + onModelSwitch?: (info: ModelSwitchInfo) => void; +} + +// ============================================================================= +// TUI Streaming Callbacks +// ============================================================================= + +const createTUIStreamCallbacks = ( + options?: Partial, +): StreamCallbacks => ({ + onContentChunk: (content: string) => { + appStore.appendStreamContent(content); + }, + + onToolCallStart: (toolCall: PartialToolCall) => { + appStore.setCurrentToolCall({ + id: toolCall.id, + name: toolCall.name, + description: `Calling ${toolCall.name}...`, + status: "pending", + }); + }, + + onToolCallComplete: (toolCall: ToolCall) => { + appStore.updateToolCall({ + id: toolCall.id, + name: toolCall.name, + status: "running", + }); + }, + + onModelSwitch: (info: ModelSwitchInfo) => { + appStore.addLog({ + type: "system", + content: `Model switched: ${info.from} → ${info.to} (${info.reason})`, + }); + options?.onModelSwitch?.(info); + }, + + onComplete: () => { + appStore.completeStreaming(); + }, + + onError: (error: string) => { + appStore.cancelStreaming(); + appStore.addLog({ + type: "error", + content: error, + }); + }, +}); + +// ============================================================================= +// Agent Options with TUI Integration +// ============================================================================= + +const createAgentOptionsWithTUI = ( + options: StreamingChatOptions, +): AgentOptions => ({ + ...options, + + onText: (text: string) => { + // Text is handled by streaming callbacks, but we may want to notify + options.onText?.(text); + }, + + onToolCall: (toolCall: ToolCall) => { + appStore.setMode("tool_execution"); + appStore.setCurrentToolCall({ + id: toolCall.id, + name: toolCall.name, + description: `Executing ${toolCall.name}...`, + status: "running", + }); + + appStore.addLog({ + type: "tool", + content: `${toolCall.name}`, + metadata: { + toolName: toolCall.name, + toolStatus: "running", + toolDescription: JSON.stringify(toolCall.arguments), + }, + }); + + options.onToolCall?.(toolCall); + }, + + onToolResult: (toolCallId: string, result: ToolResult) => { + appStore.updateToolCall({ + status: result.success ? "success" : "error", + result: result.output, + error: result.error, + }); + + appStore.addLog({ + type: "tool", + content: result.output || result.error || "", + metadata: { + toolName: appStore.getState().currentToolCall?.name, + toolStatus: result.success ? "success" : "error", + toolDescription: result.title, + }, + }); + + appStore.setCurrentToolCall(null); + appStore.setMode("thinking"); + + options.onToolResult?.(toolCallId, result); + }, + + onError: (error: string) => { + appStore.setMode("idle"); + appStore.addLog({ + type: "error", + content: error, + }); + options.onError?.(error); + }, + + onWarning: (warning: string) => { + appStore.addLog({ + type: "system", + content: warning, + }); + options.onWarning?.(warning); + }, +}); + +// ============================================================================= +// Main API +// ============================================================================= + +/** + * Run a streaming chat session with TUI integration + */ +export const runStreamingChat = async ( + messages: Message[], + options: StreamingChatOptions, +): Promise => { + // Set up TUI state + appStore.setMode("thinking"); + appStore.startThinking(); + appStore.startStreaming(); + + // Create callbacks that update the TUI + const streamCallbacks = createTUIStreamCallbacks(options); + const agentOptions = createAgentOptionsWithTUI(options); + + // Create and run the streaming agent + const agent = createStreamingAgent( + process.cwd(), + agentOptions, + streamCallbacks, + ); + + try { + const result = await agent.run(messages); + + appStore.stopThinking(); + appStore.setMode("idle"); + + return result; + } catch (error) { + appStore.cancelStreaming(); + appStore.stopThinking(); + appStore.setMode("idle"); + + const errorMessage = error instanceof Error ? error.message : String(error); + appStore.addLog({ + type: "error", + content: errorMessage, + }); + + return { + success: false, + finalResponse: errorMessage, + iterations: 0, + toolCalls: [], + }; + } +}; + +/** + * Create a streaming chat instance with stop capability + */ +export const createStreamingChat = ( + options: StreamingChatOptions, +): { + run: (messages: Message[]) => Promise; + stop: () => void; +} => { + const streamCallbacks = createTUIStreamCallbacks(options); + const agentOptions = createAgentOptionsWithTUI(options); + + const agent = createStreamingAgent( + process.cwd(), + agentOptions, + streamCallbacks, + ); + + return { + run: async (messages: Message[]) => { + appStore.setMode("thinking"); + appStore.startThinking(); + appStore.startStreaming(); + + try { + const result = await agent.run(messages); + + appStore.stopThinking(); + appStore.setMode("idle"); + + return result; + } catch (error) { + appStore.cancelStreaming(); + appStore.stopThinking(); + appStore.setMode("idle"); + + const errorMessage = + error instanceof Error ? error.message : String(error); + return { + success: false, + finalResponse: errorMessage, + iterations: 0, + toolCalls: [], + }; + } + }, + + stop: () => { + agent.stop(); + appStore.cancelStreaming(); + appStore.stopThinking(); + appStore.setMode("idle"); + }, + }; +}; diff --git a/src/services/chat-tui/usage.ts b/src/services/chat-tui/usage.ts new file mode 100644 index 0000000..efe717f --- /dev/null +++ b/src/services/chat-tui/usage.ts @@ -0,0 +1,146 @@ +/** + * Usage statistics display for TUI + */ + +import { usageStore } from "@stores/usage-store"; +import { getUserInfo } from "@providers/copilot/credentials"; +import { getCopilotUsage } from "@providers/copilot/usage"; +import type { + ChatServiceState, + ChatServiceCallbacks, +} from "@/types/chat-service"; +import type { CopilotQuotaDetail } from "@/types/copilot-usage"; + +const BAR_WIDTH = 40; +const FILLED_CHAR = "█"; +const EMPTY_CHAR = "░"; + +const formatNumber = (num: number): string => { + return num.toLocaleString(); +}; + +const formatDuration = (ms: number): string => { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h ${minutes % 60}m`; + } + if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } + return `${seconds}s`; +}; + +const renderBar = (percent: number): string => { + const clampedPercent = Math.max(0, Math.min(100, percent)); + const filledWidth = Math.round((clampedPercent / 100) * BAR_WIDTH); + const emptyWidth = BAR_WIDTH - filledWidth; + return FILLED_CHAR.repeat(filledWidth) + EMPTY_CHAR.repeat(emptyWidth); +}; + +const formatQuotaBar = ( + name: string, + quota: CopilotQuotaDetail | undefined, + resetInfo?: string, +): string[] => { + const lines: string[] = []; + + if (!quota) { + lines.push(name); + lines.push("N/A"); + return lines; + } + + if (quota.unlimited) { + lines.push(name); + lines.push(FILLED_CHAR.repeat(BAR_WIDTH) + " Unlimited"); + return lines; + } + + const used = quota.entitlement - quota.remaining; + const percentUsed = + quota.entitlement > 0 ? (used / quota.entitlement) * 100 : 0; + + lines.push(name); + lines.push(`${renderBar(percentUsed)} ${percentUsed.toFixed(0)}% used`); + if (resetInfo) { + lines.push(resetInfo); + } + + return lines; +}; + +export const showUsageStats = async ( + state: ChatServiceState, + callbacks: ChatServiceCallbacks, +): Promise => { + const stats = usageStore.getStats(); + const sessionDuration = Date.now() - stats.sessionStartTime; + + const lines: string[] = []; + + if (state.provider === "copilot") { + const userInfo = await getUserInfo(); + const copilotUsage = await getCopilotUsage(); + + if (copilotUsage) { + const resetInfo = `Resets ${copilotUsage.quota_reset_date}`; + + lines.push( + ...formatQuotaBar( + "Premium Requests", + copilotUsage.quota_snapshots.premium_interactions, + resetInfo, + ), + ); + lines.push(""); + + lines.push( + ...formatQuotaBar("Chat", copilotUsage.quota_snapshots.chat, resetInfo), + ); + lines.push(""); + + lines.push( + ...formatQuotaBar( + "Completions", + copilotUsage.quota_snapshots.completions, + resetInfo, + ), + ); + lines.push(""); + } + + lines.push("Account"); + lines.push(`Provider: ${state.provider}`); + lines.push(`Model: ${state.model ?? "auto"}`); + if (userInfo) { + lines.push(`User: ${userInfo.login}`); + } + if (copilotUsage) { + lines.push(`Plan: ${copilotUsage.copilot_plan}`); + } + lines.push(""); + } else { + lines.push("Provider"); + lines.push(`Name: ${state.provider}`); + lines.push(`Model: ${state.model ?? "auto"}`); + lines.push(""); + } + + lines.push("Current Session"); + lines.push( + `Tokens: ${formatNumber(stats.totalTokens)} (${formatNumber(stats.promptTokens)} prompt + ${formatNumber(stats.completionTokens)} completion)`, + ); + lines.push(`Requests: ${formatNumber(stats.requestCount)}`); + lines.push(`Duration: ${formatDuration(sessionDuration)}`); + if (stats.requestCount > 0) { + const avgTokensPerRequest = Math.round( + stats.totalTokens / stats.requestCount, + ); + lines.push(`Avg tokens/request: ${formatNumber(avgTokensPerRequest)}`); + } + + callbacks.onLog("system", lines.join("\n")); +}; diff --git a/src/services/chat-tui/utils.ts b/src/services/chat-tui/utils.ts new file mode 100644 index 0000000..4d2fa18 --- /dev/null +++ b/src/services/chat-tui/utils.ts @@ -0,0 +1,78 @@ +/** + * Chat TUI utility functions + */ + +import { CHAT_TRUNCATE_DEFAULTS, DIFF_PATTERNS } from "@constants/chat-service"; +import type { DiffResult } from "@/types/chat-service"; + +export const stripAnsi = (str: string): string => + str.replace(/\x1b\[[0-9;]*m/g, ""); + +export const truncateOutput = ( + output: string, + maxLines = CHAT_TRUNCATE_DEFAULTS.MAX_LINES, + maxLength = CHAT_TRUNCATE_DEFAULTS.MAX_LENGTH, +): string => { + if (!output) return ""; + + const lines = output.split("\n"); + let truncated = lines.slice(0, maxLines).join("\n"); + + if (truncated.length > maxLength) { + truncated = truncated.slice(0, maxLength) + "..."; + } else if (lines.length > maxLines) { + truncated += `\n... (${lines.length - maxLines} more lines)`; + } + + return truncated; +}; + +export const detectDiffContent = (content: string): DiffResult => { + if (!content) { + return { isDiff: false, additions: 0, deletions: 0 }; + } + + const cleanContent = stripAnsi(content); + + const isDiff = DIFF_PATTERNS.some((pattern) => pattern.test(cleanContent)); + + if (!isDiff) { + return { isDiff: false, additions: 0, deletions: 0 }; + } + + const fileMatch = cleanContent.match(/\+\+\+\s+[ab]?\/(.+?)(?:\s|$)/m); + const filePath = fileMatch?.[1]?.trim(); + + const lines = cleanContent.split("\n"); + let additions = 0; + let deletions = 0; + + for (const line of lines) { + if (/^\+[^+]/.test(line) || line === "+") { + additions++; + } else if (/^-[^-]/.test(line) || line === "-") { + deletions++; + } + } + + return { isDiff, filePath, additions, deletions }; +}; + +const TOOL_DESCRIPTIONS: Record< + string, + (args: Record) => string +> = { + bash: (args) => String(args.command || "command"), + read: (args) => String(args.path || "file"), + write: (args) => String(args.path || "file"), + edit: (args) => String(args.path || "file"), +}; + +export const getToolDescription = (call: { + name: string; + arguments?: Record; +}): string => { + const args = call.arguments || {}; + const describer = TOOL_DESCRIPTIONS[call.name]; + return describer ? describer(args) : call.name; +}; diff --git a/src/services/code-review-service.ts b/src/services/code-review-service.ts new file mode 100644 index 0000000..f0d45ba --- /dev/null +++ b/src/services/code-review-service.ts @@ -0,0 +1,266 @@ +/** + * Code Review Detection Service + * + * Detects code review requests and provides review context. + */ + +import { + CODE_REVIEW_SYSTEM_PROMPT, + CODE_REVIEW_CONTEXT_TEMPLATE, +} from "@prompts/system/code-review"; + +export interface CodeReviewContext { + isReview: boolean; + reviewType: ReviewType; + focusArea?: ReviewFocusArea; + filesChanged?: string[]; +} + +export type ReviewType = + | "general" + | "security" + | "performance" + | "refactor" + | "pr" + | "diff" + | "none"; + +export type ReviewFocusArea = + | "security" + | "performance" + | "maintainability" + | "correctness" + | "style" + | "all"; + +const REVIEW_KEYWORDS: Record, string[]> = { + general: [ + "review", + "code review", + "look at this code", + "check this code", + "feedback on", + "what do you think", + "is this good", + "is this correct", + "review my code", + ], + security: [ + "security review", + "security audit", + "vulnerabilities", + "security check", + "is this secure", + "security issues", + "injection", + "xss", + "csrf", + ], + performance: [ + "performance review", + "optimize", + "slow", + "faster", + "efficiency", + "bottleneck", + "memory usage", + "cpu usage", + ], + refactor: [ + "refactor", + "refactoring", + "clean up", + "cleanup", + "improve this", + "better way", + "simplify", + "restructure", + ], + pr: [ + "pull request", + "pr review", + "merge request", + "mr review", + "review pr", + "review this pr", + ], + diff: [ + "review diff", + "review changes", + "what changed", + "review these changes", + "look at the diff", + ], +}; + +const FOCUS_KEYWORDS: Record = { + security: [ + "security", + "secure", + "vulnerability", + "injection", + "auth", + "authentication", + "authorization", + ], + performance: [ + "performance", + "speed", + "fast", + "slow", + "optimize", + "efficient", + "memory", + ], + maintainability: [ + "maintainability", + "readable", + "clean", + "organize", + "structure", + "modular", + ], + correctness: ["correct", "bug", "wrong", "error", "logic", "edge case"], + style: ["style", "formatting", "convention", "naming", "lint"], + all: [], +}; + +const detectReviewType = (input: string): ReviewType => { + const lowerInput = input.toLowerCase(); + + // Check specific review types first (more specific) + const typeOrder: Exclude[] = [ + "security", + "performance", + "pr", + "diff", + "refactor", + "general", + ]; + + for (const type of typeOrder) { + const keywords = REVIEW_KEYWORDS[type]; + for (const keyword of keywords) { + if (lowerInput.includes(keyword)) { + return type; + } + } + } + + return "none"; +}; + +const detectFocusArea = (input: string): ReviewFocusArea | undefined => { + const lowerInput = input.toLowerCase(); + + for (const [area, keywords] of Object.entries(FOCUS_KEYWORDS)) { + if (area === "all") continue; + for (const keyword of keywords) { + if (lowerInput.includes(keyword)) { + return area as ReviewFocusArea; + } + } + } + + return undefined; +}; + +const extractFileReferences = (input: string): string[] | undefined => { + const filePatterns = [ + // Common file extensions + /([a-zA-Z0-9_\-./]+\.(ts|tsx|js|jsx|py|go|rs|java|c|cpp|h|rb|php|swift|kt))/g, + // File paths + /(?:file|in|at)\s+[`"']?([^`"'\s]+)[`"']?/gi, + ]; + + const files: Set = new Set(); + + for (const pattern of filePatterns) { + const matches = input.matchAll(pattern); + for (const match of matches) { + if (match[1]) { + files.add(match[1]); + } + } + } + + return files.size > 0 ? Array.from(files) : undefined; +}; + +export const detectCodeReviewRequest = (input: string): CodeReviewContext => { + const reviewType = detectReviewType(input); + + if (reviewType === "none") { + return { + isReview: false, + reviewType: "none", + }; + } + + return { + isReview: true, + reviewType, + focusArea: detectFocusArea(input), + filesChanged: extractFileReferences(input), + }; +}; + +export const buildCodeReviewContext = (context: CodeReviewContext): string => { + if (!context.isReview) { + return ""; + } + + const reviewTypeLabels: Record = { + general: "General Code Review", + security: "Security Review", + performance: "Performance Review", + refactor: "Refactoring Review", + pr: "Pull Request Review", + diff: "Diff Review", + none: "None", + }; + + const focusAreaLabels: Record = { + security: "Security", + performance: "Performance", + maintainability: "Maintainability", + correctness: "Correctness", + style: "Style & Conventions", + all: "All Areas", + }; + + return CODE_REVIEW_CONTEXT_TEMPLATE.replace( + "{{reviewType}}", + reviewTypeLabels[context.reviewType], + ) + .replace( + "{{filesChanged}}", + context.filesChanged?.join(", ") || "Not specified", + ) + .replace( + "{{focusArea}}", + context.focusArea ? focusAreaLabels[context.focusArea] : "All Areas", + ); +}; + +export const getCodeReviewPrompt = (): string => { + return CODE_REVIEW_SYSTEM_PROMPT; +}; + +export const enhancePromptForCodeReview = ( + basePrompt: string, + userInput: string, +): { prompt: string; context: CodeReviewContext } => { + const context = detectCodeReviewRequest(userInput); + + if (!context.isReview) { + return { prompt: basePrompt, context }; + } + + const reviewPrompt = getCodeReviewPrompt(); + const reviewContext = buildCodeReviewContext(context); + + const enhancedPrompt = `${basePrompt}\n\n${reviewPrompt}\n${reviewContext}`; + + return { prompt: enhancedPrompt, context }; +}; diff --git a/src/services/command-suggestion-service.ts b/src/services/command-suggestion-service.ts new file mode 100644 index 0000000..a49cf02 --- /dev/null +++ b/src/services/command-suggestion-service.ts @@ -0,0 +1,43 @@ +/** + * Command Suggestion Service - Suggests follow-up commands after changes + * + * Detects patterns in file changes and tool executions to suggest + * commands the user might need to run (e.g., npm install, npm run build). + */ + +export { detectProjectContext } from "@services/command-suggestion/context"; +export { analyzeFileChange } from "@services/command-suggestion/analyze"; +export { + getPendingSuggestions, + clearSuggestions, + removeSuggestion, + formatSuggestions, + hasHighPrioritySuggestions, +} from "@services/command-suggestion/format"; +export { + setProjectContext, + getProjectContext, + addSuggestion, + removeSuggestion as removeSuggestionFromState, + clearSuggestions as clearSuggestionsFromState, + hasSuggestion, + getPendingSuggestionsMap, +} from "@services/command-suggestion/state"; +export type { + CommandSuggestion, + ProjectContext, + SuggestionPriority, + SuggestionPattern, +} from "@/types/command-suggestion"; + +import { detectProjectContext } from "@services/command-suggestion/context"; +import { + setProjectContext, + clearSuggestions as clearStore, +} from "@services/command-suggestion/state"; + +export const initSuggestionService = (cwd: string): void => { + const ctx = detectProjectContext(cwd); + setProjectContext(ctx); + clearStore(); +}; diff --git a/src/services/command-suggestion/analyze.ts b/src/services/command-suggestion/analyze.ts new file mode 100644 index 0000000..910a8cd --- /dev/null +++ b/src/services/command-suggestion/analyze.ts @@ -0,0 +1,68 @@ +/** + * File change analysis for command suggestions + */ + +import { basename } from "path"; + +import { detectProjectContext } from "@services/command-suggestion/context"; +import { SUGGESTION_PATTERNS } from "@services/command-suggestion/patterns"; +import { + getProjectContext, + setProjectContext, + addSuggestion, +} from "@services/command-suggestion/state"; +import type { + CommandSuggestion, + SuggestionPattern, +} from "@/types/command-suggestion"; + +const matchesFilePattern = ( + pattern: SuggestionPattern, + filePath: string, + fileName: string, +): boolean => + pattern.filePatterns.some((p) => p.test(filePath) || p.test(fileName)); + +const matchesContentPattern = ( + pattern: SuggestionPattern, + content?: string, +): boolean => { + if (!pattern.contentPatterns || !content) { + return true; + } + return pattern.contentPatterns.some((p) => p.test(content)); +}; + +export const analyzeFileChange = ( + filePath: string, + content?: string, +): CommandSuggestion[] => { + let ctx = getProjectContext(); + if (!ctx) { + ctx = detectProjectContext(process.cwd()); + setProjectContext(ctx); + } + + const newSuggestions: CommandSuggestion[] = []; + const fileName = basename(filePath); + + for (const pattern of SUGGESTION_PATTERNS) { + if (!matchesFilePattern(pattern, filePath, fileName)) { + continue; + } + + if (!matchesContentPattern(pattern, content)) { + continue; + } + + const suggestions = pattern.suggestions(ctx, filePath); + + for (const suggestion of suggestions) { + if (addSuggestion(suggestion)) { + newSuggestions.push(suggestion); + } + } + } + + return newSuggestions; +}; diff --git a/src/services/command-suggestion/context.ts b/src/services/command-suggestion/context.ts new file mode 100644 index 0000000..baafb53 --- /dev/null +++ b/src/services/command-suggestion/context.ts @@ -0,0 +1,41 @@ +/** + * Project context detection + */ + +import { existsSync } from "fs"; +import { join } from "path"; + +import { PROJECT_FILES } from "@constants/command-suggestion"; +import type { ProjectContext } from "@/types/command-suggestion"; + +export const detectProjectContext = (cwd: string): ProjectContext => ({ + hasPackageJson: existsSync(join(cwd, PROJECT_FILES.PACKAGE_JSON)), + hasYarnLock: existsSync(join(cwd, PROJECT_FILES.YARN_LOCK)), + hasPnpmLock: existsSync(join(cwd, PROJECT_FILES.PNPM_LOCK)), + hasBunLock: existsSync(join(cwd, PROJECT_FILES.BUN_LOCK)), + hasCargoToml: existsSync(join(cwd, PROJECT_FILES.CARGO_TOML)), + hasGoMod: existsSync(join(cwd, PROJECT_FILES.GO_MOD)), + hasPyproject: existsSync(join(cwd, PROJECT_FILES.PYPROJECT)), + hasRequirements: existsSync(join(cwd, PROJECT_FILES.REQUIREMENTS)), + hasMakefile: existsSync(join(cwd, PROJECT_FILES.MAKEFILE)), + hasDockerfile: existsSync(join(cwd, PROJECT_FILES.DOCKERFILE)), + cwd, +}); + +const PACKAGE_MANAGER_PRIORITY: Array<{ + check: (ctx: ProjectContext) => boolean; + manager: string; +}> = [ + { check: (ctx) => ctx.hasBunLock, manager: "bun" }, + { check: (ctx) => ctx.hasPnpmLock, manager: "pnpm" }, + { check: (ctx) => ctx.hasYarnLock, manager: "yarn" }, +]; + +export const getPackageManager = (ctx: ProjectContext): string => { + for (const { check, manager } of PACKAGE_MANAGER_PRIORITY) { + if (check(ctx)) { + return manager; + } + } + return "npm"; +}; diff --git a/src/services/command-suggestion/format.ts b/src/services/command-suggestion/format.ts new file mode 100644 index 0000000..8f8e008 --- /dev/null +++ b/src/services/command-suggestion/format.ts @@ -0,0 +1,41 @@ +/** + * Command suggestion formatting and retrieval + */ + +import { PRIORITY_ORDER, PRIORITY_ICONS } from "@constants/command-suggestion"; +import { + getPendingSuggestionsMap, + clearSuggestions as clearStore, + removeSuggestion as removeFromStore, +} from "@services/command-suggestion/state"; +import type { CommandSuggestion } from "@/types/command-suggestion"; + +export const getPendingSuggestions = (): CommandSuggestion[] => { + const suggestionsMap = getPendingSuggestionsMap(); + return Array.from(suggestionsMap.values()).sort( + (a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority], + ); +}; + +export const clearSuggestions = (): void => clearStore(); + +export const removeSuggestion = (command: string): void => + removeFromStore(command); + +export const formatSuggestions = (suggestions: CommandSuggestion[]): string => { + if (suggestions.length === 0) return ""; + + const lines = ["", "Suggested commands:"]; + + for (const s of suggestions) { + const icon = PRIORITY_ICONS[s.priority]; + lines.push(` ${icon} ${s.command} (${s.reason})`); + } + + return lines.join("\n"); +}; + +export const hasHighPrioritySuggestions = (): boolean => { + const suggestionsMap = getPendingSuggestionsMap(); + return Array.from(suggestionsMap.values()).some((s) => s.priority === "high"); +}; diff --git a/src/services/command-suggestion/patterns.ts b/src/services/command-suggestion/patterns.ts new file mode 100644 index 0000000..653cc0a --- /dev/null +++ b/src/services/command-suggestion/patterns.ts @@ -0,0 +1,225 @@ +/** + * Command suggestion patterns + */ + +import { + FILE_PATTERNS, + CONTENT_PATTERNS, + SUGGESTION_MESSAGES, + SUGGESTION_REASONS, +} from "@constants/command-suggestion"; +import { getPackageManager } from "@services/command-suggestion/context"; +import type { + SuggestionPattern, + ProjectContext, + CommandSuggestion, +} from "@/types/command-suggestion"; + +const createPackageJsonSuggestions = ( + ctx: ProjectContext, +): CommandSuggestion[] => { + const pm = getPackageManager(ctx); + return [ + { + command: `${pm} install`, + description: SUGGESTION_MESSAGES.INSTALL_DEPS, + priority: "high", + reason: SUGGESTION_REASONS.PACKAGE_JSON_MODIFIED, + }, + ]; +}; + +const createTsconfigSuggestions = ( + ctx: ProjectContext, +): CommandSuggestion[] => { + const pm = getPackageManager(ctx); + return [ + { + command: `${pm} run build`, + description: SUGGESTION_MESSAGES.REBUILD_PROJECT, + priority: "medium", + reason: SUGGESTION_REASONS.TSCONFIG_CHANGED, + }, + ]; +}; + +const createSourceFileSuggestions = ( + ctx: ProjectContext, + filePath: string, +): CommandSuggestion[] => { + const pm = getPackageManager(ctx); + const isTestFile = FILE_PATTERNS.TEST_FILE.test(filePath); + + return isTestFile + ? [ + { + command: `${pm} test`, + description: SUGGESTION_MESSAGES.RUN_TESTS, + priority: "high", + reason: SUGGESTION_REASONS.TEST_FILE_MODIFIED, + }, + ] + : [ + { + command: `${pm} run dev`, + description: SUGGESTION_MESSAGES.START_DEV, + priority: "low", + reason: SUGGESTION_REASONS.SOURCE_FILE_MODIFIED, + }, + ]; +}; + +const createCargoSuggestions = (): CommandSuggestion[] => [ + { + command: "cargo build", + description: SUGGESTION_MESSAGES.BUILD_RUST, + priority: "high", + reason: SUGGESTION_REASONS.CARGO_MODIFIED, + }, +]; + +const createGoModSuggestions = (): CommandSuggestion[] => [ + { + command: "go mod tidy", + description: SUGGESTION_MESSAGES.TIDY_GO, + priority: "high", + reason: SUGGESTION_REASONS.GO_MOD_MODIFIED, + }, +]; + +const createPythonSuggestions = (ctx: ProjectContext): CommandSuggestion[] => + ctx.hasPyproject + ? [ + { + command: "pip install -e .", + description: SUGGESTION_MESSAGES.INSTALL_PYTHON_EDITABLE, + priority: "high", + reason: SUGGESTION_REASONS.PYTHON_DEPS_CHANGED, + }, + ] + : [ + { + command: "pip install -r requirements.txt", + description: SUGGESTION_MESSAGES.INSTALL_PYTHON_DEPS, + priority: "high", + reason: SUGGESTION_REASONS.REQUIREMENTS_MODIFIED, + }, + ]; + +const createDockerSuggestions = ( + _ctx: ProjectContext, + filePath: string, +): CommandSuggestion[] => + filePath.includes("docker-compose") + ? [ + { + command: "docker-compose up --build", + description: SUGGESTION_MESSAGES.DOCKER_COMPOSE_BUILD, + priority: "medium", + reason: SUGGESTION_REASONS.DOCKER_COMPOSE_CHANGED, + }, + ] + : [ + { + command: "docker build -t app .", + description: SUGGESTION_MESSAGES.DOCKER_BUILD, + priority: "medium", + reason: SUGGESTION_REASONS.DOCKERFILE_MODIFIED, + }, + ]; + +const createMakefileSuggestions = (): CommandSuggestion[] => [ + { + command: "make", + description: SUGGESTION_MESSAGES.RUN_MAKE, + priority: "medium", + reason: SUGGESTION_REASONS.MAKEFILE_MODIFIED, + }, +]; + +const createMigrationSuggestions = ( + ctx: ProjectContext, +): CommandSuggestion[] => { + const pm = getPackageManager(ctx); + return [ + { + command: `${pm} run migrate`, + description: SUGGESTION_MESSAGES.RUN_MIGRATE, + priority: "high", + reason: SUGGESTION_REASONS.MIGRATION_MODIFIED, + }, + ]; +}; + +const createEnvSuggestions = (): CommandSuggestion[] => [ + { + command: "cp .env.example .env", + description: SUGGESTION_MESSAGES.CREATE_ENV, + priority: "medium", + reason: SUGGESTION_REASONS.ENV_TEMPLATE_MODIFIED, + }, +]; + +const createLinterSuggestions = (ctx: ProjectContext): CommandSuggestion[] => { + const pm = getPackageManager(ctx); + return [ + { + command: `${pm} run lint`, + description: SUGGESTION_MESSAGES.RUN_LINT, + priority: "low", + reason: SUGGESTION_REASONS.LINTER_CONFIG_CHANGED, + }, + ]; +}; + +export const SUGGESTION_PATTERNS: SuggestionPattern[] = [ + { + filePatterns: [FILE_PATTERNS.PACKAGE_JSON], + contentPatterns: [ + CONTENT_PATTERNS.DEPENDENCIES, + CONTENT_PATTERNS.DEV_DEPENDENCIES, + CONTENT_PATTERNS.PEER_DEPENDENCIES, + ], + suggestions: createPackageJsonSuggestions, + }, + { + filePatterns: [FILE_PATTERNS.TSCONFIG], + suggestions: createTsconfigSuggestions, + }, + { + filePatterns: [FILE_PATTERNS.SOURCE_FILES], + suggestions: createSourceFileSuggestions, + }, + { + filePatterns: [FILE_PATTERNS.CARGO_TOML], + suggestions: createCargoSuggestions, + }, + { + filePatterns: [FILE_PATTERNS.GO_MOD], + suggestions: createGoModSuggestions, + }, + { + filePatterns: [FILE_PATTERNS.PYTHON_DEPS], + suggestions: createPythonSuggestions, + }, + { + filePatterns: [FILE_PATTERNS.DOCKER], + suggestions: createDockerSuggestions, + }, + { + filePatterns: [FILE_PATTERNS.MAKEFILE], + suggestions: createMakefileSuggestions, + }, + { + filePatterns: [FILE_PATTERNS.MIGRATIONS], + suggestions: createMigrationSuggestions, + }, + { + filePatterns: [FILE_PATTERNS.ENV_EXAMPLE], + suggestions: createEnvSuggestions, + }, + { + filePatterns: [FILE_PATTERNS.LINTER_CONFIG], + suggestions: createLinterSuggestions, + }, +]; diff --git a/src/services/command-suggestion/state.ts b/src/services/command-suggestion/state.ts new file mode 100644 index 0000000..8e556a4 --- /dev/null +++ b/src/services/command-suggestion/state.ts @@ -0,0 +1,57 @@ +/** + * Command suggestion state management + */ + +import { createStore } from "zustand/vanilla"; + +import type { + CommandSuggestion, + ProjectContext, +} from "@/types/command-suggestion"; + +interface SuggestionState { + pendingSuggestions: Map; + projectContext: ProjectContext | null; +} + +const store = createStore(() => ({ + pendingSuggestions: new Map(), + projectContext: null, +})); + +export const getProjectContext = (): ProjectContext | null => + store.getState().projectContext; + +export const setProjectContext = (ctx: ProjectContext): void => { + store.setState({ projectContext: ctx }); +}; + +export const addSuggestion = (suggestion: CommandSuggestion): boolean => { + const { pendingSuggestions } = store.getState(); + if (pendingSuggestions.has(suggestion.command)) { + return false; + } + const newMap = new Map(pendingSuggestions); + newMap.set(suggestion.command, suggestion); + store.setState({ pendingSuggestions: newMap }); + return true; +}; + +export const removeSuggestion = (command: string): void => { + const { pendingSuggestions } = store.getState(); + const newMap = new Map(pendingSuggestions); + newMap.delete(command); + store.setState({ pendingSuggestions: newMap }); +}; + +export const clearSuggestions = (): void => { + store.setState({ pendingSuggestions: new Map() }); +}; + +export const getPendingSuggestionsMap = (): Map => + store.getState().pendingSuggestions; + +export const hasSuggestion = (command: string): boolean => + store.getState().pendingSuggestions.has(command); + +export const subscribeToSuggestions = store.subscribe; diff --git a/src/services/config.ts b/src/services/config.ts new file mode 100644 index 0000000..7b65925 --- /dev/null +++ b/src/services/config.ts @@ -0,0 +1,168 @@ +/** + * Configuration management for CodeTyper CLI + */ + +import fs from "fs/promises"; +import path from "path"; +import type { Config, Provider } from "@/types/index"; +import { DIRS, FILES } from "@constants/paths"; + +/** + * Default configuration values + */ +const getDefaults = (): Config => ({ + provider: "copilot", + model: "auto", + theme: "default", + maxIterations: 20, + timeout: 30000, + cascadeEnabled: true, + protectedPaths: [ + ".git", + "node_modules", + ".env", + ".env.local", + ".env.production", + "dist", + "build", + ".next", + "__pycache__", + "venv", + ".venv", + ], +}); + +/** + * Environment variable mapping for providers + */ +const PROVIDER_ENV_VARS: Record = { + copilot: "GITHUB_COPILOT_TOKEN", + ollama: "OLLAMA_HOST", +}; + +/** + * Config state (singleton pattern using closure) + */ +let configState: Config = getDefaults(); + +/** + * Load configuration from file + */ +export const loadConfig = async (): Promise => { + try { + const data = await fs.readFile(FILES.config, "utf-8"); + const loaded = JSON.parse(data); + + // Clean up deprecated keys + delete loaded.models; + + configState = { ...getDefaults(), ...loaded }; + } catch { + // Config file doesn't exist or is invalid, use defaults + configState = getDefaults(); + } +}; + +/** + * Save configuration to file + */ +export const saveConfig = async (): Promise => { + try { + await fs.mkdir(DIRS.config, { recursive: true }); + await fs.writeFile( + FILES.config, + JSON.stringify(configState, null, 2), + "utf-8", + ); + } catch (error) { + throw new Error(`Failed to save config: ${error}`); + } +}; + +/** + * Get configuration value + */ +export const getConfigValue = (key: K): Config[K] => { + return configState[key]; +}; + +/** + * Set configuration value + */ +export const setConfigValue = ( + key: K, + value: Config[K], +): void => { + configState[key] = value; +}; + +/** + * Get full configuration + */ +export const getAllConfig = (): Config => ({ ...configState }); + +/** + * Get API key for provider from environment + */ +export const getApiKey = (provider?: Provider): string | undefined => { + const targetProvider = provider ?? configState.provider; + const envVar = PROVIDER_ENV_VARS[targetProvider]; + return envVar ? process.env[envVar] : undefined; +}; + +/** + * Get configured model + */ +export const getModel = (): string => { + return configState.model ?? "auto"; +}; + +/** + * Get config file path + */ +export const getConfigPath = (): string => FILES.config; + +/** + * Check if a path is protected + */ +export const isProtectedPath = (filePath: string): boolean => { + const normalizedPath = path.normalize(filePath); + return configState.protectedPaths.some((protectedPath) => + normalizedPath.includes(protectedPath), + ); +}; + +/** + * Reset configuration to defaults + */ +export const resetConfig = (): void => { + configState = getDefaults(); +}; + +/** + * Initialize and get config (convenience function) + */ +export const getConfig = async (): Promise<{ + get: (key: K) => Config[K]; + set: (key: K, value: Config[K]) => void; + getAll: () => Config; + save: () => Promise; + getApiKey: (provider?: Provider) => string | undefined; + getModel: () => string; + getConfigPath: () => string; + isProtectedPath: (filePath: string) => boolean; + reset: () => void; +}> => { + await loadConfig(); + return { + get: getConfigValue, + set: setConfigValue, + getAll: getAllConfig, + save: saveConfig, + getApiKey, + getModel, + getConfigPath, + isProtectedPath, + reset: resetConfig, + }; +}; diff --git a/src/services/debugging-service.ts b/src/services/debugging-service.ts new file mode 100644 index 0000000..0129644 --- /dev/null +++ b/src/services/debugging-service.ts @@ -0,0 +1,208 @@ +/** + * Debugging Detection Service + * + * Detects debugging-related requests and provides debugging context. + */ + +import { + DEBUGGING_SYSTEM_PROMPT, + DEBUGGING_CONTEXT_TEMPLATE, +} from "@prompts/system/debugging"; + +export interface DebugContext { + isDebugging: boolean; + errorMessage?: string; + location?: string; + expected?: string; + actual?: string; + stackTrace?: string; + debugType: DebugType; +} + +export type DebugType = + | "error" + | "bug" + | "fix" + | "issue" + | "crash" + | "broken" + | "notworking" + | "none"; + +const DEBUG_KEYWORDS: Record = { + error: [ + "error", + "exception", + "throw", + "thrown", + "stack trace", + "stacktrace", + "traceback", + ], + bug: ["bug", "buggy", "glitch", "defect"], + fix: ["fix", "fixing", "repair", "resolve", "solved"], + issue: ["issue", "problem", "trouble", "wrong"], + crash: ["crash", "crashed", "crashing", "dies", "killed"], + broken: ["broken", "break", "breaks", "broke"], + notworking: [ + "not working", + "doesn't work", + "doesn't work", + "won't work", + "isn't working", + "stopped working", + "no longer works", + "fails", + "failing", + "failed", + ], + none: [], +}; + +const ERROR_PATTERNS = [ + // JavaScript/TypeScript errors + /(?:TypeError|ReferenceError|SyntaxError|RangeError|Error):\s*.+/i, + // Stack trace patterns + /at\s+\w+\s+\([^)]+:\d+:\d+\)/, + /^\s*at\s+.+\s+\(.+\)$/m, + // Common error messages + /cannot read propert(?:y|ies) of (?:undefined|null)/i, + /is not a function/i, + /is not defined/i, + /unexpected token/i, + /failed to/i, + /unable to/i, + // Exit codes + /exit(?:ed)? (?:with )?(?:code|status) \d+/i, + // HTTP errors + /(?:4|5)\d{2}\s+(?:error|bad|not found|internal|forbidden)/i, +]; + +const extractErrorMessage = (input: string): string | undefined => { + for (const pattern of ERROR_PATTERNS) { + const match = input.match(pattern); + if (match) { + return match[0]; + } + } + return undefined; +}; + +const extractStackTrace = (input: string): string | undefined => { + const lines = input.split("\n"); + const stackLines: string[] = []; + let inStack = false; + + for (const line of lines) { + if (/^\s*at\s+/.test(line) || /Error:/.test(line)) { + inStack = true; + stackLines.push(line); + } else if (inStack && line.trim() === "") { + break; + } else if (inStack) { + stackLines.push(line); + } + } + + return stackLines.length > 0 ? stackLines.join("\n") : undefined; +}; + +const extractLocation = (input: string): string | undefined => { + // Match file:line:column patterns + const patterns = [ + /([a-zA-Z0-9_\-./]+\.[a-zA-Z]+):(\d+)(?::(\d+))?/, + /(?:in|at|file)\s+['"]?([^'":\s]+)['"]?\s*(?:line\s*)?(\d+)?/i, + ]; + + for (const pattern of patterns) { + const match = input.match(pattern); + if (match) { + const file = match[1]; + const line = match[2]; + const col = match[3]; + return col ? `${file}:${line}:${col}` : line ? `${file}:${line}` : file; + } + } + return undefined; +}; + +const detectDebugType = (input: string): DebugType => { + const lowerInput = input.toLowerCase(); + + for (const [type, keywords] of Object.entries(DEBUG_KEYWORDS)) { + if (type === "none") continue; + for (const keyword of keywords) { + if (lowerInput.includes(keyword)) { + return type as DebugType; + } + } + } + + // Check for error patterns even without keywords + if (ERROR_PATTERNS.some((pattern) => pattern.test(input))) { + return "error"; + } + + return "none"; +}; + +export const detectDebuggingRequest = (input: string): DebugContext => { + const debugType = detectDebugType(input); + + if (debugType === "none") { + return { + isDebugging: false, + debugType: "none", + }; + } + + return { + isDebugging: true, + debugType, + errorMessage: extractErrorMessage(input), + location: extractLocation(input), + stackTrace: extractStackTrace(input), + }; +}; + +export const buildDebuggingContext = (context: DebugContext): string => { + if (!context.isDebugging) { + return ""; + } + + let debugContext = DEBUGGING_CONTEXT_TEMPLATE.replace( + "{{errorMessage}}", + context.errorMessage || "Not specified", + ) + .replace("{{location}}", context.location || "Not specified") + .replace("{{expected}}", context.expected || "Not specified") + .replace("{{actual}}", context.actual || "Not specified"); + + if (context.stackTrace) { + debugContext += `\n**Stack Trace**:\n\`\`\`\n${context.stackTrace}\n\`\`\`\n`; + } + + return debugContext; +}; + +export const getDebuggingPrompt = (): string => { + return DEBUGGING_SYSTEM_PROMPT; +}; + +export const enhancePromptForDebugging = ( + basePrompt: string, + userInput: string, +): { prompt: string; context: DebugContext } => { + const context = detectDebuggingRequest(userInput); + + if (!context.isDebugging) { + return { prompt: basePrompt, context }; + } + + const debugPrompt = getDebuggingPrompt(); + const debugContext = buildDebuggingContext(context); + + const enhancedPrompt = `${basePrompt}\n\n${debugPrompt}\n${debugContext}`; + + return { prompt: enhancedPrompt, context }; +}; diff --git a/src/services/environment-service.ts b/src/services/environment-service.ts new file mode 100644 index 0000000..bcd64f5 --- /dev/null +++ b/src/services/environment-service.ts @@ -0,0 +1,69 @@ +/** + * Environment Service + * + * Provides environment context for system prompts. + */ + +import { platform, release } from "os"; +import { existsSync } from "fs"; +import { join } from "path"; + +import { ENVIRONMENT_PROMPT_TEMPLATE } from "@prompts/system/environment"; + +export interface EnvironmentContext { + workingDirectory: string; + isGitRepo: boolean; + platform: string; + osVersion: string; + date: string; + model?: string; + provider?: string; +} + +const isGitRepository = (dir: string): boolean => { + return existsSync(join(dir, ".git")); +}; + +const formatDate = (): string => { + return new Date().toISOString().split("T")[0]; +}; + +const getPlatformName = (): string => { + const platformMap: Record = { + darwin: "macOS", + linux: "Linux", + win32: "Windows", + }; + return platformMap[platform()] || platform(); +}; + +export const getEnvironmentContext = ( + workingDirectory: string, +): EnvironmentContext => ({ + workingDirectory, + isGitRepo: isGitRepository(workingDirectory), + platform: getPlatformName(), + osVersion: release(), + date: formatDate(), +}); + +export const buildEnvironmentPrompt = ( + ctx: EnvironmentContext, + model?: string, + provider?: string, +): string => { + let prompt = ENVIRONMENT_PROMPT_TEMPLATE.replace( + "{{workingDirectory}}", + ctx.workingDirectory, + ) + .replace("{{isGitRepo}}", ctx.isGitRepo ? "Yes" : "No") + .replace("{{platform}}", ctx.platform) + .replace("{{osVersion}}", ctx.osVersion) + .replace("{{date}}", ctx.date); + + if (model && provider) { + prompt += `\n\nYou are powered by ${provider}/${model}.`; + } + + return prompt; +}; diff --git a/src/services/executor.ts b/src/services/executor.ts new file mode 100644 index 0000000..6bfd393 --- /dev/null +++ b/src/services/executor.ts @@ -0,0 +1,358 @@ +/** + * Command executor with permission system + */ + +import { exec, spawn } from "child_process"; +import { promisify } from "util"; +import chalk from "chalk"; +import fs from "fs/promises"; +import path from "path"; +import { promptBashPermission } from "@services/permissions"; +import type { ExecutionResult } from "@interfaces/ExecutionResult"; + +const execAsync = promisify(exec); + +/** + * Executor state + */ +let workingDir = process.cwd(); + +/** + * Set working directory + */ +export const setWorkingDir = (dir: string): void => { + workingDir = dir; +}; + +/** + * Get working directory + */ +export const getWorkingDir = (): string => workingDir; + +/** + * Execute a shell command with permission check + */ +export const executeCommand = async ( + command: string, + description?: string, + requirePermission = true, +): Promise => { + // Check permission + if (requirePermission) { + const { allowed } = await promptBashPermission( + command, + description ?? "Execute shell command", + ); + + if (!allowed) { + return { + success: false, + error: "Permission denied by user", + }; + } + } + + console.log(chalk.gray(`$ ${command}`)); + + try { + const { stdout, stderr } = await execAsync(command, { + cwd: workingDir, + maxBuffer: 10 * 1024 * 1024, // 10MB + timeout: 300000, // 5 minutes + }); + + return { + success: true, + stdout: stdout.trim(), + stderr: stderr.trim(), + exitCode: 0, + }; + } catch (error: unknown) { + const err = error as { + stdout?: string; + stderr?: string; + message: string; + code?: number; + }; + return { + success: false, + stdout: err.stdout?.trim(), + stderr: err.stderr?.trim(), + error: err.message, + exitCode: err.code, + }; + } +}; + +/** + * Execute a command with real-time output streaming + */ +export const executeStreamingCommand = async ( + command: string, + args: string[], + description?: string, + onOutput?: (data: string) => void, +): Promise => { + const fullCommand = `${command} ${args.join(" ")}`; + + const { allowed } = await promptBashPermission( + fullCommand, + description ?? "Execute shell command", + ); + + if (!allowed) { + return { + success: false, + error: "Permission denied by user", + }; + } + + console.log(chalk.gray(`$ ${fullCommand}`)); + + return new Promise((resolve) => { + const proc = spawn(command, args, { + cwd: workingDir, + shell: true, + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout?.on("data", (data) => { + const text = data.toString(); + stdout += text; + onOutput?.(text); + }); + + proc.stderr?.on("data", (data) => { + const text = data.toString(); + stderr += text; + onOutput?.(text); + }); + + proc.on("close", (code) => { + resolve({ + success: code === 0, + stdout: stdout.trim(), + stderr: stderr.trim(), + exitCode: code ?? 0, + }); + }); + + proc.on("error", (error) => { + resolve({ + success: false, + stdout: stdout.trim(), + stderr: stderr.trim(), + error: error.message, + }); + }); + }); +}; + +/** + * Read a file + */ +export const readFile = async ( + filePath: string, +): Promise<{ content: string | null; error?: string }> => { + const fullPath = path.isAbsolute(filePath) + ? filePath + : path.join(workingDir, filePath); + + try { + const content = await fs.readFile(fullPath, "utf-8"); + return { content }; + } catch (error: unknown) { + const err = error as { message: string }; + return { content: null, error: err.message }; + } +}; + +/** + * Write a file with permission check + */ +export const writeFile = async ( + filePath: string, + content: string, + description?: string, +): Promise<{ success: boolean; error?: string }> => { + const fullPath = path.isAbsolute(filePath) + ? filePath + : path.join(workingDir, filePath); + + const { allowed } = await promptBashPermission( + `write ${fullPath}`, + description ?? `Write to file: ${filePath}`, + ); + + if (!allowed) { + return { success: false, error: "Permission denied by user" }; + } + + try { + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, content, "utf-8"); + console.log(chalk.green(`✓ Wrote ${filePath}`)); + return { success: true }; + } catch (error: unknown) { + const err = error as { message: string }; + return { success: false, error: err.message }; + } +}; + +/** + * Edit a file with permission check + */ +export const editFile = async ( + filePath: string, + oldContent: string, + newContent: string, + description?: string, +): Promise<{ success: boolean; error?: string }> => { + const fullPath = path.isAbsolute(filePath) + ? filePath + : path.join(workingDir, filePath); + + const { allowed } = await promptBashPermission( + `edit ${fullPath}`, + description ?? `Edit file: ${filePath}`, + ); + + if (!allowed) { + return { success: false, error: "Permission denied by user" }; + } + + try { + const currentContent = await fs.readFile(fullPath, "utf-8"); + + if (!currentContent.includes(oldContent)) { + return { success: false, error: "Could not find content to replace" }; + } + + const updated = currentContent.replace(oldContent, newContent); + await fs.writeFile(fullPath, updated, "utf-8"); + console.log(chalk.green(`✓ Edited ${filePath}`)); + return { success: true }; + } catch (error: unknown) { + const err = error as { message: string }; + return { success: false, error: err.message }; + } +}; + +/** + * Delete a file with permission check + */ +export const deleteFile = async ( + filePath: string, +): Promise<{ success: boolean; error?: string }> => { + const fullPath = path.isAbsolute(filePath) + ? filePath + : path.join(workingDir, filePath); + + const { allowed } = await promptBashPermission( + `delete ${fullPath}`, + `Delete file: ${filePath}`, + ); + + if (!allowed) { + return { success: false, error: "Permission denied by user" }; + } + + try { + await fs.unlink(fullPath); + console.log(chalk.yellow(`✓ Deleted ${filePath}`)); + return { success: true }; + } catch (error: unknown) { + const err = error as { message: string }; + return { success: false, error: err.message }; + } +}; + +/** + * Create a directory with permission check + */ +export const createDirectory = async ( + dirPath: string, +): Promise<{ success: boolean; error?: string }> => { + const fullPath = path.isAbsolute(dirPath) + ? dirPath + : path.join(workingDir, dirPath); + + const { allowed } = await promptBashPermission( + `mkdir ${fullPath}`, + `Create directory: ${dirPath}`, + ); + + if (!allowed) { + return { success: false, error: "Permission denied by user" }; + } + + try { + await fs.mkdir(fullPath, { recursive: true }); + console.log(chalk.green(`✓ Created directory ${dirPath}`)); + return { success: true }; + } catch (error: unknown) { + const err = error as { message: string }; + return { success: false, error: err.message }; + } +}; + +/** + * List directory contents + */ +export const listDirectory = async (dirPath?: string): Promise => { + const fullPath = dirPath + ? path.isAbsolute(dirPath) + ? dirPath + : path.join(workingDir, dirPath) + : workingDir; + + try { + return await fs.readdir(fullPath); + } catch { + return []; + } +}; + +/** + * Check if path exists + */ +export const pathExists = async (targetPath: string): Promise => { + const fullPath = path.isAbsolute(targetPath) + ? targetPath + : path.join(workingDir, targetPath); + + try { + await fs.access(fullPath); + return true; + } catch { + return false; + } +}; + +/** + * Get file stats + */ +export const getStats = async ( + targetPath: string, +): Promise<{ isFile: boolean; isDirectory: boolean; size: number } | null> => { + const fullPath = path.isAbsolute(targetPath) + ? targetPath + : path.join(workingDir, targetPath); + + try { + const stats = await fs.stat(fullPath); + return { + isFile: stats.isFile(), + isDirectory: stats.isDirectory(), + size: stats.size, + }; + } catch { + return null; + } +}; + +// Re-export types +export type { ExecutionResult } from "@interfaces/ExecutionResult"; +export type { FileOperation } from "@interfaces/FileOperation"; diff --git a/src/services/file-picker-service.ts b/src/services/file-picker-service.ts new file mode 100644 index 0000000..3ac804d --- /dev/null +++ b/src/services/file-picker-service.ts @@ -0,0 +1,71 @@ +/** + * File Picker Service - Filesystem operations for file selection + * + * This service handles all filesystem access for the FilePicker component, + * keeping the TSX file free of side effects. + */ + +export { getFiles } from "@services/file-picker/files"; +export { fuzzyMatch } from "@services/file-picker/match"; +export { filterFiles } from "@services/file-picker/filter"; +export { + getCurrentDir, + getCwd, + getCachedFiles, + setCurrentDir, + setCachedFiles, + invalidateCache, + initializeFilePicker, + getFilesFromState, + filterFilesFromState, +} from "@services/file-picker/state"; +export type { + FileEntry, + FilePickerStore, + FilePickerStoreState, + FilePickerStoreActions, +} from "@/types/file-picker"; + +import { getFiles } from "@services/file-picker/files"; +import { filterFiles } from "@services/file-picker/filter"; +import { FILE_PICKER_DEFAULTS } from "@constants/file-picker"; +import type { FileEntry } from "@/types/file-picker"; + +/** + * Create a file picker state manager (legacy API for backward compatibility) + */ +export const createFilePickerState = (cwd: string) => { + let currentDir = cwd; + let cachedFiles: FileEntry[] | null = null; + + return { + getCurrentDir: () => currentDir, + + setCurrentDir: (dir: string) => { + currentDir = dir; + cachedFiles = null; + }, + + getFiles: (): FileEntry[] => { + if (cachedFiles === null) { + cachedFiles = getFiles(currentDir, cwd); + } + return cachedFiles; + }, + + filterFiles: ( + query: string, + maxResults = FILE_PICKER_DEFAULTS.MAX_RESULTS, + ): FileEntry[] => { + const files = cachedFiles ?? getFiles(currentDir, cwd); + if (cachedFiles === null) cachedFiles = files; + return filterFiles(files, query, maxResults); + }, + + invalidateCache: () => { + cachedFiles = null; + }, + }; +}; + +export type FilePickerState = ReturnType; diff --git a/src/services/file-picker/files.ts b/src/services/file-picker/files.ts new file mode 100644 index 0000000..b1799e7 --- /dev/null +++ b/src/services/file-picker/files.ts @@ -0,0 +1,80 @@ +/** + * File system operations for file picker + */ + +import { readdirSync } from "fs"; +import { join, relative, extname } from "path"; + +import { + IGNORED_PATTERNS, + BINARY_EXTENSIONS, + FILE_PICKER_DEFAULTS, + type IgnoredPattern, + type BinaryExtension, +} from "@constants/file-picker"; +import type { FileEntry } from "@/types/file-picker"; + +const isIgnoredEntry = (name: string): boolean => + IGNORED_PATTERNS.includes(name as IgnoredPattern); + +const isHiddenAtDepth = (name: string, depth: number): boolean => + name.startsWith(".") && depth > 0; + +const isBinaryFile = (name: string): boolean => { + const ext = extname(name).toLowerCase(); + return BINARY_EXTENSIONS.includes(ext as BinaryExtension); +}; + +const shouldSkipEntry = ( + name: string, + depth: number, + isDirectory: boolean, +): boolean => + isIgnoredEntry(name) || + isHiddenAtDepth(name, depth) || + (!isDirectory && isBinaryFile(name)); + +const createFileEntry = ( + name: string, + dir: string, + cwd: string, + isDirectory: boolean, +): FileEntry => { + const fullPath = join(dir, name); + return { + name, + path: fullPath, + isDirectory, + relativePath: relative(cwd, fullPath), + }; +}; + +export const getFiles = ( + dir: string, + cwd: string, + maxDepth: number = FILE_PICKER_DEFAULTS.MAX_DEPTH, + currentDepth: number = FILE_PICKER_DEFAULTS.INITIAL_DEPTH, +): FileEntry[] => { + if (currentDepth > maxDepth) return []; + + try { + const entries = readdirSync(dir, { withFileTypes: true }); + const files: FileEntry[] = []; + + for (const entry of entries) { + const isDirectory = entry.isDirectory(); + if (shouldSkipEntry(entry.name, currentDepth, isDirectory)) continue; + + files.push(createFileEntry(entry.name, dir, cwd, isDirectory)); + + if (isDirectory && currentDepth < maxDepth) { + const fullPath = join(dir, entry.name); + files.push(...getFiles(fullPath, cwd, maxDepth, currentDepth + 1)); + } + } + + return files; + } catch { + return []; + } +}; diff --git a/src/services/file-picker/filter.ts b/src/services/file-picker/filter.ts new file mode 100644 index 0000000..e7c8d44 --- /dev/null +++ b/src/services/file-picker/filter.ts @@ -0,0 +1,19 @@ +/** + * File filtering utilities for file picker + */ + +import { FILE_PICKER_DEFAULTS } from "@constants/file-picker"; +import { fuzzyMatch } from "@services/file-picker/match"; +import type { FileEntry } from "@/types/file-picker"; + +export const filterFiles = ( + files: FileEntry[], + query: string, + maxResults = FILE_PICKER_DEFAULTS.MAX_RESULTS, +): FileEntry[] => { + if (!query) return files.slice(0, maxResults); + + return files + .filter((f) => fuzzyMatch(query, f.relativePath)) + .slice(0, maxResults); +}; diff --git a/src/services/file-picker/match.ts b/src/services/file-picker/match.ts new file mode 100644 index 0000000..4e47eb7 --- /dev/null +++ b/src/services/file-picker/match.ts @@ -0,0 +1,32 @@ +/** + * Fuzzy matching utilities for file picker + */ + +const containsMatch = (query: string, text: string): boolean => + text.includes(query); + +const fuzzyCharMatch = (query: string, text: string): boolean => { + let queryIndex = 0; + for ( + let textIndex = 0; + textIndex < text.length && queryIndex < query.length; + textIndex++ + ) { + if (text[textIndex] === query[queryIndex]) { + queryIndex++; + } + } + return queryIndex === query.length; +}; + +export const fuzzyMatch = (query: string, text: string): boolean => { + if (!query) return true; + + const lowerQuery = query.toLowerCase(); + const lowerText = text.toLowerCase(); + + return ( + containsMatch(lowerQuery, lowerText) || + fuzzyCharMatch(lowerQuery, lowerText) + ); +}; diff --git a/src/services/file-picker/state.ts b/src/services/file-picker/state.ts new file mode 100644 index 0000000..c70ece8 --- /dev/null +++ b/src/services/file-picker/state.ts @@ -0,0 +1,74 @@ +/** + * File picker state management with Zustand vanilla + */ + +import { createStore } from "zustand/vanilla"; + +import { FILE_PICKER_DEFAULTS } from "@constants/file-picker"; +import { getFiles } from "@services/file-picker/files"; +import { filterFiles } from "@services/file-picker/filter"; +import type { FileEntry } from "@/types/file-picker"; + +interface FilePickerState { + currentDir: string; + cachedFiles: FileEntry[] | null; + cwd: string; +} + +const store = createStore(() => ({ + currentDir: "", + cachedFiles: null, + cwd: "", +})); + +export const getCurrentDir = (): string => store.getState().currentDir; + +export const getCwd = (): string => store.getState().cwd; + +export const getCachedFiles = (): FileEntry[] | null => + store.getState().cachedFiles; + +export const setCurrentDir = (dir: string): void => { + store.setState({ currentDir: dir, cachedFiles: null }); +}; + +export const setCachedFiles = (files: FileEntry[] | null): void => { + store.setState({ cachedFiles: files }); +}; + +export const invalidateCache = (): void => { + store.setState({ cachedFiles: null }); +}; + +export const initializeFilePicker = (cwd: string): void => { + store.setState({ cwd, currentDir: cwd, cachedFiles: null }); +}; + +export const getFilesFromState = (): FileEntry[] => { + const { currentDir, cwd, cachedFiles } = store.getState(); + + if (cachedFiles !== null) { + return cachedFiles; + } + + const files = getFiles(currentDir, cwd); + setCachedFiles(files); + return files; +}; + +export const filterFilesFromState = ( + query: string, + maxResults = FILE_PICKER_DEFAULTS.MAX_RESULTS, +): FileEntry[] => { + const { currentDir, cwd, cachedFiles } = store.getState(); + + const files = cachedFiles ?? getFiles(currentDir, cwd); + + if (cachedFiles === null) { + setCachedFiles(files); + } + + return filterFiles(files, query, maxResults); +}; + +export const subscribeToFilePicker = store.subscribe; diff --git a/src/services/github-issue-service.ts b/src/services/github-issue-service.ts new file mode 100644 index 0000000..f74b98b --- /dev/null +++ b/src/services/github-issue-service.ts @@ -0,0 +1,18 @@ +/** + * GitHub Issue Service - Fetches issue details from the current repo + * + * Recognizes issue references in user messages and enriches them with + * actual issue content from GitHub. + */ + +export { extractIssueNumbers } from "@services/github-issue/extract"; +export { isGitHubRepo } from "@services/github-issue/repo"; +export { fetchIssue, fetchIssues } from "@services/github-issue/fetch"; +export { formatIssueContext } from "@services/github-issue/format"; +export { enrichMessageWithIssues } from "@services/github-issue/enrich"; +export type { + GitHubIssue, + GitHubIssueLabel, + GitHubIssueApiResponse, + EnrichedMessageResult, +} from "@/types/github-issue"; diff --git a/src/services/github-issue/enrich.ts b/src/services/github-issue/enrich.ts new file mode 100644 index 0000000..5f8191d --- /dev/null +++ b/src/services/github-issue/enrich.ts @@ -0,0 +1,50 @@ +/** + * Message enrichment with GitHub issue context + */ + +import { GITHUB_ISSUE_MESSAGES } from "@constants/github-issue"; +import { extractIssueNumbers } from "@services/github-issue/extract"; +import { isGitHubRepo } from "@services/github-issue/repo"; +import { fetchIssues } from "@services/github-issue/fetch"; +import { formatIssueContext } from "@services/github-issue/format"; +import type { EnrichedMessageResult } from "@/types/github-issue"; + +const createEmptyResult = (message: string): EnrichedMessageResult => ({ + enrichedMessage: message, + issues: [], +}); + +const buildEnrichedMessage = ( + issueContexts: string, + originalMessage: string, +): string => + `${GITHUB_ISSUE_MESSAGES.CONTEXT_HEADER}${GITHUB_ISSUE_MESSAGES.SECTION_SEPARATOR}${issueContexts}${GITHUB_ISSUE_MESSAGES.SECTION_SEPARATOR}${GITHUB_ISSUE_MESSAGES.USER_REQUEST_PREFIX}${originalMessage}`; + +export const enrichMessageWithIssues = async ( + message: string, +): Promise => { + const issueNumbers = extractIssueNumbers(message); + + if (issueNumbers.length === 0) { + return createEmptyResult(message); + } + + const isGitHub = await isGitHubRepo(); + if (!isGitHub) { + return createEmptyResult(message); + } + + const issues = await fetchIssues(issueNumbers); + + if (issues.length === 0) { + return createEmptyResult(message); + } + + const issueContexts = issues + .map(formatIssueContext) + .join(GITHUB_ISSUE_MESSAGES.SECTION_SEPARATOR); + + const enrichedMessage = buildEnrichedMessage(issueContexts, message); + + return { enrichedMessage, issues }; +}; diff --git a/src/services/github-issue/extract.ts b/src/services/github-issue/extract.ts new file mode 100644 index 0000000..3ef072a --- /dev/null +++ b/src/services/github-issue/extract.ts @@ -0,0 +1,37 @@ +/** + * Issue number extraction from text + */ + +import { ISSUE_PATTERNS, GITHUB_ISSUE_DEFAULTS } from "@constants/github-issue"; + +const isValidIssueNumber = (num: number): boolean => + num >= GITHUB_ISSUE_DEFAULTS.MIN_ISSUE_NUMBER && + num < GITHUB_ISSUE_DEFAULTS.MAX_ISSUE_NUMBER; + +const extractFromPattern = (pattern: RegExp, message: string): number[] => { + const numbers: number[] = []; + pattern.lastIndex = 0; + + let match; + while ((match = pattern.exec(message)) !== null) { + const num = parseInt(match[1], 10); + if (isValidIssueNumber(num)) { + numbers.push(num); + } + } + + return numbers; +}; + +export const extractIssueNumbers = (message: string): number[] => { + const numbers = new Set(); + + for (const pattern of ISSUE_PATTERNS) { + const extracted = extractFromPattern(pattern, message); + for (const num of extracted) { + numbers.add(num); + } + } + + return Array.from(numbers).sort((a, b) => a - b); +}; diff --git a/src/services/github-issue/fetch.ts b/src/services/github-issue/fetch.ts new file mode 100644 index 0000000..bb1cbfd --- /dev/null +++ b/src/services/github-issue/fetch.ts @@ -0,0 +1,44 @@ +/** + * GitHub issue fetching utilities + */ + +import { exec } from "child_process"; +import { promisify } from "util"; + +import { + GH_CLI_COMMANDS, + GITHUB_ISSUE_MESSAGES, +} from "@constants/github-issue"; +import type { GitHubIssue, GitHubIssueApiResponse } from "@/types/github-issue"; + +const execAsync = promisify(exec); + +const parseIssueResponse = (data: GitHubIssueApiResponse): GitHubIssue => ({ + number: data.number, + title: data.title, + state: data.state, + body: data.body ?? "", + author: data.author?.login ?? GITHUB_ISSUE_MESSAGES.UNKNOWN_AUTHOR, + labels: data.labels?.map((l) => l.name) ?? [], + url: data.url, +}); + +export const fetchIssue = async ( + issueNumber: number, +): Promise => { + try { + const { stdout } = await execAsync(GH_CLI_COMMANDS.VIEW_ISSUE(issueNumber)); + const data = JSON.parse(stdout) as GitHubIssueApiResponse; + return parseIssueResponse(data); + } catch { + return null; + } +}; + +export const fetchIssues = async ( + issueNumbers: number[], +): Promise => { + const results = await Promise.all(issueNumbers.map((num) => fetchIssue(num))); + + return results.filter((issue): issue is GitHubIssue => issue !== null); +}; diff --git a/src/services/github-issue/format.ts b/src/services/github-issue/format.ts new file mode 100644 index 0000000..6ef177e --- /dev/null +++ b/src/services/github-issue/format.ts @@ -0,0 +1,19 @@ +/** + * GitHub issue formatting utilities + */ + +import type { GitHubIssue } from "@/types/github-issue"; + +const formatLabels = (labels: string[]): string => + labels.length > 0 ? `\nLabels: ${labels.join(", ")}` : ""; + +export const formatIssueContext = (issue: GitHubIssue): string => { + const labelStr = formatLabels(issue.labels); + + return `## GitHub Issue #${issue.number}: ${issue.title} +State: ${issue.state} +Author: ${issue.author}${labelStr} +URL: ${issue.url} + +${issue.body}`; +}; diff --git a/src/services/github-issue/repo.ts b/src/services/github-issue/repo.ts new file mode 100644 index 0000000..a8e823c --- /dev/null +++ b/src/services/github-issue/repo.ts @@ -0,0 +1,22 @@ +/** + * Git repository detection utilities + */ + +import { exec } from "child_process"; +import { promisify } from "util"; + +import { + GH_CLI_COMMANDS, + GITHUB_REMOTE_IDENTIFIER, +} from "@constants/github-issue"; + +const execAsync = promisify(exec); + +export const isGitHubRepo = async (): Promise => { + try { + const { stdout } = await execAsync(GH_CLI_COMMANDS.GET_REMOTE_URL); + return stdout.includes(GITHUB_REMOTE_IDENTIFIER); + } catch { + return false; + } +}; diff --git a/src/services/github-pr/cli.ts b/src/services/github-pr/cli.ts new file mode 100644 index 0000000..b65dc00 --- /dev/null +++ b/src/services/github-pr/cli.ts @@ -0,0 +1,124 @@ +/** + * GitHub CLI Detection and Status + * + * Check if gh CLI is installed and authenticated. + */ + +import { spawn } from "child_process"; +import type { GitHubCLIStatus } from "@/types/github-pr"; + +let cachedStatus: GitHubCLIStatus | null = null; + +const runCommand = ( + command: string, + args: string[], +): Promise<{ exitCode: number; stdout: string; stderr: string }> => { + return new Promise((resolve) => { + const proc = spawn(command, args, { + stdio: ["pipe", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout?.on("data", (data: Buffer) => { + stdout += data.toString(); + }); + + proc.stderr?.on("data", (data: Buffer) => { + stderr += data.toString(); + }); + + proc.on("error", () => { + resolve({ exitCode: 1, stdout: "", stderr: "Command not found" }); + }); + + proc.on("close", (code) => { + resolve({ exitCode: code ?? 1, stdout, stderr }); + }); + }); +}; + +/** + * Check if GitHub CLI (gh) is installed and authenticated + */ +export const checkGitHubCLI = async (): Promise => { + if (cachedStatus) { + return cachedStatus; + } + + try { + // Check if gh is installed + const versionResult = await runCommand("gh", ["--version"]); + + if (versionResult.exitCode !== 0) { + cachedStatus = { + installed: false, + authenticated: false, + error: "GitHub CLI (gh) is not installed", + }; + return cachedStatus; + } + + // Extract version + const versionMatch = versionResult.stdout.match(/gh version ([\d.]+)/); + const version = versionMatch ? versionMatch[1] : undefined; + + // Check authentication status + const authResult = await runCommand("gh", ["auth", "status"]); + const authenticated = authResult.exitCode === 0; + + cachedStatus = { + installed: true, + authenticated, + version, + error: authenticated ? undefined : "Not authenticated with GitHub CLI", + }; + + return cachedStatus; + } catch (error) { + cachedStatus = { + installed: false, + authenticated: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + return cachedStatus; + } +}; + +/** + * Clear cached CLI status (useful for re-checking after login) + */ +export const clearCLIStatusCache = (): void => { + cachedStatus = null; +}; + +/** + * Execute a gh command and return the output + */ +export const executeGHCommand = async ( + args: string[], +): Promise<{ success: boolean; output: string; error?: string }> => { + try { + const result = await runCommand("gh", args); + + if (result.exitCode !== 0) { + return { + success: false, + output: "", + error: result.stderr || "Command failed", + }; + } + + return { + success: true, + output: result.stdout, + }; + } catch (error) { + return { + success: false, + output: "", + error: error instanceof Error ? error.message : "Unknown error", + }; + } +}; diff --git a/src/services/github-pr/fetch.ts b/src/services/github-pr/fetch.ts new file mode 100644 index 0000000..e5f136f --- /dev/null +++ b/src/services/github-pr/fetch.ts @@ -0,0 +1,321 @@ +/** + * GitHub PR Data Fetching + * + * Fetch PR details, comments, reviews, and diffs using the gh CLI. + */ + +import { spawn } from "child_process"; +import { executeGHCommand } from "@services/github-pr/cli"; +import type { + GitHubPR, + GitHubPRComment, + GitHubPRReview, + GitHubPRDiff, + GitHubPRFile, + PRUrlParts, +} from "@/types/github-pr"; + +const parseJSON = (output: string): T | null => { + try { + return JSON.parse(output) as T; + } catch { + return null; + } +}; + +const runGitCommand = ( + args: string[], +): Promise<{ exitCode: number; stdout: string }> => { + return new Promise((resolve) => { + const proc = spawn("git", args, { + stdio: ["pipe", "pipe", "pipe"], + }); + + let stdout = ""; + + proc.stdout?.on("data", (data: Buffer) => { + stdout += data.toString(); + }); + + proc.on("error", () => { + resolve({ exitCode: 1, stdout: "" }); + }); + + proc.on("close", (code) => { + resolve({ exitCode: code ?? 1, stdout }); + }); + }); +}; + +/** + * Fetch PR details using gh CLI + */ +export const fetchPR = async ( + parts: PRUrlParts, +): Promise => { + const { owner, repo, prNumber } = parts; + + const result = await executeGHCommand([ + "pr", + "view", + String(prNumber), + "--repo", + `${owner}/${repo}`, + "--json", + "number,title,state,body,author,headRefName,baseRefName,url,additions,deletions,changedFiles,isDraft,mergeable,labels", + ]); + + if (!result.success) { + return null; + } + + interface PRApiResponse { + number: number; + title: string; + state: string; + body: string; + author: { login: string }; + headRefName: string; + baseRefName: string; + url: string; + additions: number; + deletions: number; + changedFiles: number; + isDraft: boolean; + mergeable?: string; + labels: Array<{ name: string }>; + } + + const data = parseJSON(result.output); + if (!data) { + return null; + } + + return { + number: data.number, + title: data.title, + state: data.state as GitHubPR["state"], + body: data.body || "", + author: data.author?.login || "unknown", + headRef: data.headRefName, + baseRef: data.baseRefName, + url: data.url, + additions: data.additions, + deletions: data.deletions, + changedFiles: data.changedFiles, + isDraft: data.isDraft, + mergeable: data.mergeable === "MERGEABLE", + labels: data.labels?.map((l) => l.name) || [], + }; +}; + +/** + * Fetch PR review comments (comments on specific lines of code) + */ +export const fetchPRComments = async ( + parts: PRUrlParts, +): Promise => { + const { owner, repo, prNumber } = parts; + + const result = await executeGHCommand([ + "api", + `repos/${owner}/${repo}/pulls/${prNumber}/comments`, + "--jq", + ".[] | {id: .id, author: .user.login, body: .body, path: .path, line: .line, createdAt: .created_at, diffHunk: .diff_hunk}", + ]); + + if (!result.success) { + return []; + } + + // Parse newline-delimited JSON objects + const lines = result.output.trim().split("\n").filter(Boolean); + const comments: GitHubPRComment[] = []; + + for (const line of lines) { + const data = parseJSON<{ + id: number; + author: string; + body: string; + path?: string; + line?: number; + createdAt: string; + diffHunk?: string; + }>(line); + + if (data) { + comments.push({ + id: data.id, + author: data.author, + body: data.body, + path: data.path, + line: data.line, + createdAt: data.createdAt, + diffHunk: data.diffHunk, + }); + } + } + + return comments; +}; + +/** + * Fetch PR reviews (overall reviews with comments) + */ +export const fetchPRReviews = async ( + parts: PRUrlParts, +): Promise => { + const { owner, repo, prNumber } = parts; + + const result = await executeGHCommand([ + "api", + `repos/${owner}/${repo}/pulls/${prNumber}/reviews`, + "--jq", + ".[] | {id: .id, author: .user.login, state: .state, body: .body, submittedAt: .submitted_at}", + ]); + + if (!result.success) { + return []; + } + + const lines = result.output.trim().split("\n").filter(Boolean); + const reviews: GitHubPRReview[] = []; + + for (const line of lines) { + const data = parseJSON<{ + id: number; + author: string; + state: GitHubPRReview["state"]; + body: string; + submittedAt: string; + }>(line); + + if (data) { + reviews.push({ + id: data.id, + author: data.author, + state: data.state, + body: data.body || "", + submittedAt: data.submittedAt, + comments: [], // Comments fetched separately if needed + }); + } + } + + return reviews; +}; + +/** + * Fetch PR diff + */ +export const fetchPRDiff = async ( + parts: PRUrlParts, +): Promise => { + const { owner, repo, prNumber } = parts; + + // Get the diff content + const diffResult = await executeGHCommand([ + "pr", + "diff", + String(prNumber), + "--repo", + `${owner}/${repo}`, + ]); + + if (!diffResult.success) { + return null; + } + + // Get file list with stats + const filesResult = await executeGHCommand([ + "api", + `repos/${owner}/${repo}/pulls/${prNumber}/files`, + "--jq", + ".[] | {filename: .filename, status: .status, additions: .additions, deletions: .deletions, patch: .patch}", + ]); + + const files: GitHubPRFile[] = []; + + if (filesResult.success) { + const lines = filesResult.output.trim().split("\n").filter(Boolean); + + for (const line of lines) { + const data = parseJSON<{ + filename: string; + status: string; + additions: number; + deletions: number; + patch?: string; + }>(line); + + if (data) { + files.push({ + filename: data.filename, + status: data.status as GitHubPRFile["status"], + additions: data.additions, + deletions: data.deletions, + patch: data.patch, + }); + } + } + } + + return { + prNumber, + diff: diffResult.output, + files, + }; +}; + +/** + * Get current repo's default branch for comparing PRs + */ +export const getDefaultBranch = async (): Promise => { + const result = await executeGHCommand([ + "repo", + "view", + "--json", + "defaultBranchRef", + "--jq", + ".defaultBranchRef.name", + ]); + + if (!result.success) { + return null; + } + + return result.output.trim() || null; +}; + +/** + * Get current branch name + */ +export const getCurrentBranch = async (): Promise => { + const result = await runGitCommand(["branch", "--show-current"]); + + if (result.exitCode !== 0) { + return null; + } + + return result.stdout.trim() || null; +}; + +/** + * Get diff between current branch and default branch + */ +export const getBranchDiff = async ( + baseBranch?: string, +): Promise => { + const base = baseBranch || (await getDefaultBranch()); + if (!base) { + return null; + } + + const result = await runGitCommand(["diff", `${base}...HEAD`]); + + if (result.exitCode !== 0) { + return null; + } + + return result.stdout; +}; diff --git a/src/services/github-pr/format.ts b/src/services/github-pr/format.ts new file mode 100644 index 0000000..533df04 --- /dev/null +++ b/src/services/github-pr/format.ts @@ -0,0 +1,199 @@ +/** + * GitHub PR Formatting + * + * Format PR data for display and context injection. + */ + +import type { + GitHubPR, + GitHubPRComment, + GitHubPRReview, + GitHubPRDiff, +} from "@/types/github-pr"; + +/** + * Format PR details for context + */ +export const formatPRContext = (pr: GitHubPR): string => { + const lines: string[] = [ + `## Pull Request #${pr.number}: ${pr.title}`, + "", + `**Author:** ${pr.author}`, + `**State:** ${pr.state}${pr.isDraft ? " (Draft)" : ""}`, + `**Branch:** ${pr.headRef} → ${pr.baseRef}`, + `**Changes:** +${pr.additions} -${pr.deletions} in ${pr.changedFiles} file(s)`, + ]; + + if (pr.labels.length > 0) { + lines.push(`**Labels:** ${pr.labels.join(", ")}`); + } + + if (pr.body) { + lines.push("", "### Description", "", pr.body); + } + + return lines.join("\n"); +}; + +/** + * Format PR comments for context + */ +export const formatPRComments = (comments: GitHubPRComment[]): string => { + if (comments.length === 0) { + return "No review comments on this PR."; + } + + const lines: string[] = ["## Review Comments", ""]; + + for (const comment of comments) { + lines.push(`### Comment by ${comment.author}`); + if (comment.path) { + lines.push(`**File:** ${comment.path}${comment.line ? `:${comment.line}` : ""}`); + } + lines.push(`**Date:** ${new Date(comment.createdAt).toLocaleDateString()}`); + lines.push(""); + + if (comment.diffHunk) { + lines.push("```diff"); + lines.push(comment.diffHunk); + lines.push("```"); + lines.push(""); + } + + lines.push(comment.body); + lines.push(""); + lines.push("---"); + lines.push(""); + } + + return lines.join("\n"); +}; + +/** + * Format PR reviews for context + */ +export const formatPRReviews = (reviews: GitHubPRReview[]): string => { + if (reviews.length === 0) { + return "No reviews on this PR."; + } + + const stateEmojis: Record = { + PENDING: "⏳", + COMMENTED: "💬", + APPROVED: "✅", + CHANGES_REQUESTED: "🔄", + DISMISSED: "🚫", + }; + + const lines: string[] = ["## Reviews", ""]; + + for (const review of reviews) { + const emoji = stateEmojis[review.state] || ""; + lines.push(`### ${emoji} ${review.state} by ${review.author}`); + lines.push(`**Date:** ${new Date(review.submittedAt).toLocaleDateString()}`); + + if (review.body) { + lines.push(""); + lines.push(review.body); + } + + lines.push(""); + lines.push("---"); + lines.push(""); + } + + return lines.join("\n"); +}; + +/** + * Format PR diff summary + */ +export const formatPRDiffSummary = (diff: GitHubPRDiff): string => { + const lines: string[] = [ + "## Changed Files", + "", + "| File | Status | Changes |", + "|------|--------|---------|", + ]; + + for (const file of diff.files) { + const changes = `+${file.additions} -${file.deletions}`; + lines.push(`| ${file.filename} | ${file.status} | ${changes} |`); + } + + return lines.join("\n"); +}; + +/** + * Format a single comment for solving + */ +export const formatCommentForSolving = (comment: GitHubPRComment): string => { + const lines: string[] = [ + `## Review Comment to Address`, + "", + `**Author:** ${comment.author}`, + ]; + + if (comment.path) { + lines.push(`**File:** ${comment.path}`); + if (comment.line) { + lines.push(`**Line:** ${comment.line}`); + } + } + + lines.push(""); + lines.push("### Comment:"); + lines.push(comment.body); + + if (comment.diffHunk) { + lines.push(""); + lines.push("### Code Context:"); + lines.push("```diff"); + lines.push(comment.diffHunk); + lines.push("```"); + } + + lines.push(""); + lines.push("Please address this comment by making the necessary changes to the code."); + + return lines.join("\n"); +}; + +/** + * Format all comments that need addressing + */ +export const formatPendingComments = (comments: GitHubPRComment[]): string => { + if (comments.length === 0) { + return "No pending comments to address."; + } + + const lines: string[] = [ + `## ${comments.length} Comment(s) to Address`, + "", + ]; + + for (let i = 0; i < comments.length; i++) { + const comment = comments[i]; + lines.push(`### ${i + 1}. Comment by ${comment.author}`); + + if (comment.path) { + lines.push(`**File:** ${comment.path}${comment.line ? `:${comment.line}` : ""}`); + } + + lines.push(""); + lines.push(comment.body); + + if (comment.diffHunk) { + lines.push(""); + lines.push("```diff"); + lines.push(comment.diffHunk); + lines.push("```"); + } + + lines.push(""); + lines.push("---"); + lines.push(""); + } + + return lines.join("\n"); +}; diff --git a/src/services/github-pr/index.ts b/src/services/github-pr/index.ts new file mode 100644 index 0000000..59a6dcc --- /dev/null +++ b/src/services/github-pr/index.ts @@ -0,0 +1,43 @@ +/** + * GitHub PR Service + * + * Service for interacting with GitHub Pull Requests using the gh CLI. + */ + +export { checkGitHubCLI, clearCLIStatusCache, executeGHCommand } from "@services/github-pr/cli"; + +export { + parsePRUrl, + containsPRUrl, + extractPRUrls, + buildPRUrl, +} from "@services/github-pr/url"; + +export { + fetchPR, + fetchPRComments, + fetchPRReviews, + fetchPRDiff, + getDefaultBranch, + getCurrentBranch, + getBranchDiff, +} from "@services/github-pr/fetch"; + +export { + formatPRContext, + formatPRComments, + formatPRReviews, + formatPRDiffSummary, + formatCommentForSolving, + formatPendingComments, +} from "@services/github-pr/format"; + +export type { + GitHubPR, + GitHubPRComment, + GitHubPRReview, + GitHubPRDiff, + GitHubPRFile, + GitHubCLIStatus, + PRUrlParts, +} from "@/types/github-pr"; diff --git a/src/services/github-pr/url.ts b/src/services/github-pr/url.ts new file mode 100644 index 0000000..a29dd79 --- /dev/null +++ b/src/services/github-pr/url.ts @@ -0,0 +1,76 @@ +/** + * GitHub URL Parsing + * + * Parse GitHub PR URLs to extract owner, repo, and PR number. + */ + +import type { PRUrlParts } from "@/types/github-pr"; + +/** + * GitHub PR URL patterns + * Matches: + * - https://github.com/owner/repo/pull/123 + * - https://github.com/owner/repo/pull/123/files + * - https://github.com/owner/repo/pull/123/commits + * - github.com/owner/repo/pull/123 + */ +const PR_URL_PATTERN = + /(?:https?:\/\/)?github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/i; + +/** + * Parse a GitHub PR URL to extract owner, repo, and PR number + */ +export const parsePRUrl = (url: string): PRUrlParts | null => { + const match = url.match(PR_URL_PATTERN); + + if (!match) { + return null; + } + + const [, owner, repo, prNumberStr] = match; + const prNumber = parseInt(prNumberStr, 10); + + if (isNaN(prNumber)) { + return null; + } + + return { + owner, + repo, + prNumber, + }; +}; + +/** + * Check if a string contains a GitHub PR URL + */ +export const containsPRUrl = (text: string): boolean => { + return PR_URL_PATTERN.test(text); +}; + +/** + * Extract all PR URLs from a text string + */ +export const extractPRUrls = (text: string): PRUrlParts[] => { + const results: PRUrlParts[] = []; + const globalPattern = new RegExp(PR_URL_PATTERN.source, "gi"); + let match; + + while ((match = globalPattern.exec(text)) !== null) { + const [, owner, repo, prNumberStr] = match; + const prNumber = parseInt(prNumberStr, 10); + + if (!isNaN(prNumber)) { + results.push({ owner, repo, prNumber }); + } + } + + return results; +}; + +/** + * Build a GitHub PR URL from parts + */ +export const buildPRUrl = (parts: PRUrlParts): string => { + return `https://github.com/${parts.owner}/${parts.repo}/pull/${parts.prNumber}`; +}; diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..a77540c --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,10 @@ +/** + * Services Module - Business logic extracted from UI components + */ + +export * from "@services/file-picker-service"; +export * from "@services/chat-tui-service"; +export * from "@services/github-issue-service"; +export * from "@services/command-suggestion-service"; +export * from "@services/learning-service"; +export * from "@services/rules-service"; diff --git a/src/services/learning-service.ts b/src/services/learning-service.ts new file mode 100644 index 0000000..ef4daad --- /dev/null +++ b/src/services/learning-service.ts @@ -0,0 +1,32 @@ +/** + * Learning Service - Detects and manages project learnings + * + * Identifies patterns in conversations that should be remembered: + * - User preferences (language, file types, coding style) + * - Project conventions (architecture, patterns) + * - Workflow decisions + * - Common patterns to follow + */ + +export { detectLearnings } from "@services/learning/detect"; +export { analyzeMessage } from "@services/learning/analyze"; +export { analyzeAssistantResponse } from "@services/learning/assistant"; +export { categorizePattern } from "@services/learning/categorize"; +export { deduplicateCandidates } from "@services/learning/deduplicate"; +export { + extractLearningContent, + extractLearningFromAcknowledgment, +} from "@services/learning/extract"; +export { formatLearningForPrompt } from "@services/learning/format"; +export { + saveLearning, + getLearnings, + learningExists, +} from "@services/learning/persistence"; +export type { + LearningCandidate, + LearningCategory, + StoredLearning, + MessageSource, + LearningPatternMatch, +} from "@/types/learning"; diff --git a/src/services/learning/__tests__/vector-store.test.ts b/src/services/learning/__tests__/vector-store.test.ts new file mode 100644 index 0000000..3d14931 --- /dev/null +++ b/src/services/learning/__tests__/vector-store.test.ts @@ -0,0 +1,231 @@ +/** + * Unit tests for Vector Store + */ + +import { describe, it, expect } from "bun:test"; + +import { + cosineSimilarity, + euclideanDistance, + upsertEmbedding, + removeEmbedding, + hasEmbedding, + getEmbedding, + findSimilar, + findAboveThreshold, + getIndexStats, +} from "@services/learning/vector-store"; + +import { createEmptyIndex } from "@/types/embeddings"; + +describe("Vector Store", () => { + describe("cosineSimilarity", () => { + it("should return 1 for identical vectors", () => { + const a = [1, 0, 0]; + const b = [1, 0, 0]; + + expect(cosineSimilarity(a, b)).toBeCloseTo(1); + }); + + it("should return 0 for orthogonal vectors", () => { + const a = [1, 0, 0]; + const b = [0, 1, 0]; + + expect(cosineSimilarity(a, b)).toBeCloseTo(0); + }); + + it("should return -1 for opposite vectors", () => { + const a = [1, 0, 0]; + const b = [-1, 0, 0]; + + expect(cosineSimilarity(a, b)).toBeCloseTo(-1); + }); + + it("should handle normalized vectors", () => { + const a = [0.6, 0.8, 0]; + const b = [0.8, 0.6, 0]; + + const similarity = cosineSimilarity(a, b); + expect(similarity).toBeGreaterThan(0); + expect(similarity).toBeLessThan(1); + }); + + it("should return 0 for mismatched lengths", () => { + const a = [1, 0, 0]; + const b = [1, 0]; + + expect(cosineSimilarity(a, b)).toBe(0); + }); + + it("should handle zero vectors", () => { + const a = [0, 0, 0]; + const b = [1, 0, 0]; + + expect(cosineSimilarity(a, b)).toBe(0); + }); + }); + + describe("euclideanDistance", () => { + it("should return 0 for identical vectors", () => { + const a = [1, 2, 3]; + const b = [1, 2, 3]; + + expect(euclideanDistance(a, b)).toBe(0); + }); + + it("should compute correct distance", () => { + const a = [0, 0, 0]; + const b = [3, 4, 0]; + + expect(euclideanDistance(a, b)).toBe(5); + }); + + it("should return Infinity for mismatched lengths", () => { + const a = [1, 0, 0]; + const b = [1, 0]; + + expect(euclideanDistance(a, b)).toBe(Infinity); + }); + }); + + describe("Index Operations", () => { + it("should create empty index", () => { + const index = createEmptyIndex("test-model"); + + expect(index.version).toBe(1); + expect(index.model).toBe("test-model"); + expect(Object.keys(index.embeddings)).toHaveLength(0); + }); + + it("should upsert embedding", () => { + let index = createEmptyIndex("test-model"); + const embedding = [0.1, 0.2, 0.3]; + + index = upsertEmbedding(index, "learn_1", embedding); + + expect(hasEmbedding(index, "learn_1")).toBe(true); + expect(getEmbedding(index, "learn_1")?.embedding).toEqual(embedding); + }); + + it("should update existing embedding", () => { + let index = createEmptyIndex("test-model"); + const embedding1 = [0.1, 0.2, 0.3]; + const embedding2 = [0.4, 0.5, 0.6]; + + index = upsertEmbedding(index, "learn_1", embedding1); + index = upsertEmbedding(index, "learn_1", embedding2); + + expect(getEmbedding(index, "learn_1")?.embedding).toEqual(embedding2); + }); + + it("should remove embedding", () => { + let index = createEmptyIndex("test-model"); + index = upsertEmbedding(index, "learn_1", [0.1, 0.2, 0.3]); + index = upsertEmbedding(index, "learn_2", [0.4, 0.5, 0.6]); + + index = removeEmbedding(index, "learn_1"); + + expect(hasEmbedding(index, "learn_1")).toBe(false); + expect(hasEmbedding(index, "learn_2")).toBe(true); + }); + + it("should return null for missing embedding", () => { + const index = createEmptyIndex("test-model"); + + expect(getEmbedding(index, "nonexistent")).toBeNull(); + }); + + it("should track index stats", () => { + let index = createEmptyIndex("test-model"); + index = upsertEmbedding(index, "learn_1", [0.1, 0.2, 0.3]); + index = upsertEmbedding(index, "learn_2", [0.4, 0.5, 0.6]); + + const stats = getIndexStats(index); + + expect(stats.count).toBe(2); + expect(stats.model).toBe("test-model"); + }); + }); + + describe("Similarity Search", () => { + it("should find similar embeddings", () => { + let index = createEmptyIndex("test-model"); + + // Add embeddings with known similarities + index = upsertEmbedding(index, "a", [1, 0, 0]); + index = upsertEmbedding(index, "b", [0.9, 0.1, 0]); + index = upsertEmbedding(index, "c", [0, 1, 0]); + + const query = [1, 0, 0]; + const results = findSimilar(index, query, 2, 0); + + expect(results).toHaveLength(2); + expect(results[0].id).toBe("a"); + expect(results[0].score).toBeCloseTo(1); + expect(results[1].id).toBe("b"); + }); + + it("should respect minSimilarity threshold", () => { + let index = createEmptyIndex("test-model"); + + index = upsertEmbedding(index, "a", [1, 0, 0]); + index = upsertEmbedding(index, "b", [0, 1, 0]); + + const query = [1, 0, 0]; + const results = findSimilar(index, query, 10, 0.5); + + expect(results).toHaveLength(1); + expect(results[0].id).toBe("a"); + }); + + it("should limit results to topK", () => { + let index = createEmptyIndex("test-model"); + + for (let i = 0; i < 10; i++) { + const embedding = [Math.random(), Math.random(), Math.random()]; + index = upsertEmbedding(index, `learn_${i}`, embedding); + } + + const query = [0.5, 0.5, 0.5]; + const results = findSimilar(index, query, 3, 0); + + expect(results.length).toBeLessThanOrEqual(3); + }); + + it("should find all above threshold", () => { + let index = createEmptyIndex("test-model"); + + index = upsertEmbedding(index, "a", [1, 0, 0]); + index = upsertEmbedding(index, "b", [0.95, 0.05, 0]); + index = upsertEmbedding(index, "c", [0.9, 0.1, 0]); + index = upsertEmbedding(index, "d", [0, 1, 0]); + + const query = [1, 0, 0]; + const results = findAboveThreshold(index, query, 0.85); + + expect(results.length).toBe(3); + expect(results.map((r) => r.id)).toContain("a"); + expect(results.map((r) => r.id)).toContain("b"); + expect(results.map((r) => r.id)).toContain("c"); + }); + + it("should return empty array for no matches", () => { + let index = createEmptyIndex("test-model"); + + index = upsertEmbedding(index, "a", [1, 0, 0]); + + const query = [-1, 0, 0]; + const results = findSimilar(index, query, 10, 0.5); + + expect(results).toHaveLength(0); + }); + + it("should handle empty index", () => { + const index = createEmptyIndex("test-model"); + const query = [1, 0, 0]; + const results = findSimilar(index, query, 10, 0); + + expect(results).toHaveLength(0); + }); + }); +}); diff --git a/src/services/learning/analyze.ts b/src/services/learning/analyze.ts new file mode 100644 index 0000000..c97149f --- /dev/null +++ b/src/services/learning/analyze.ts @@ -0,0 +1,86 @@ +/** + * Message analysis for learning detection + */ + +import { + LEARNING_PATTERNS, + LEARNING_KEYWORDS, + LEARNING_DEFAULTS, + LEARNING_CONTEXTS, +} from "@constants/learning"; +import { categorizePattern } from "@services/learning/categorize"; +import { extractLearningContent } from "@services/learning/extract"; +import type { LearningCandidate, MessageSource } from "@/types/learning"; + +const getContextForSource = (source: MessageSource): string => + source === "user" + ? LEARNING_CONTEXTS.USER_PREFERENCE + : LEARNING_CONTEXTS.CONVENTION_IDENTIFIED; + +const findPatternMatches = ( + message: string, + source: MessageSource, +): LearningCandidate[] => { + const candidates: LearningCandidate[] = []; + + for (const pattern of LEARNING_PATTERNS) { + const match = message.match(pattern); + if (match) { + candidates.push({ + content: extractLearningContent(message, match), + context: getContextForSource(source), + confidence: LEARNING_DEFAULTS.BASE_PATTERN_CONFIDENCE, + category: categorizePattern(pattern), + }); + } + } + + return candidates; +}; + +const countKeywords = (text: string): number => + LEARNING_KEYWORDS.filter((keyword) => text.includes(keyword)).length; + +const extractKeywordSentences = (message: string): LearningCandidate[] => { + const candidates: LearningCandidate[] = []; + const sentences = message.split(/[.!?]+/).filter((s) => s.trim()); + + for (const sentence of sentences) { + const sentenceLower = sentence.toLowerCase(); + const keywordCount = countKeywords(sentenceLower); + + if (keywordCount >= LEARNING_DEFAULTS.MIN_KEYWORDS_FOR_LEARNING) { + const confidence = + LEARNING_DEFAULTS.BASE_KEYWORD_CONFIDENCE + + keywordCount * LEARNING_DEFAULTS.KEYWORD_CONFIDENCE_INCREMENT; + + candidates.push({ + content: sentence.trim(), + context: LEARNING_CONTEXTS.MULTIPLE_INDICATORS, + confidence, + category: "general", + }); + } + } + + return candidates; +}; + +export const analyzeMessage = ( + message: string, + source: MessageSource, +): LearningCandidate[] => { + const candidates: LearningCandidate[] = []; + const lowerMessage = message.toLowerCase(); + + const patternMatches = findPatternMatches(message, source); + candidates.push(...patternMatches); + + const keywordCount = countKeywords(lowerMessage); + if (keywordCount >= LEARNING_DEFAULTS.MIN_KEYWORDS_FOR_LEARNING) { + const keywordSentences = extractKeywordSentences(message); + candidates.push(...keywordSentences); + } + + return candidates; +}; diff --git a/src/services/learning/assistant.ts b/src/services/learning/assistant.ts new file mode 100644 index 0000000..ff835a7 --- /dev/null +++ b/src/services/learning/assistant.ts @@ -0,0 +1,72 @@ +/** + * Assistant response analysis for learning detection + */ + +import { + ACKNOWLEDGMENT_PATTERNS, + ACKNOWLEDGMENT_PHRASES, + LEARNING_DEFAULTS, + LEARNING_CONTEXTS, +} from "@constants/learning"; +import { analyzeMessage } from "@services/learning/analyze"; +import { extractLearningFromAcknowledgment } from "@services/learning/extract"; +import type { LearningCandidate } from "@/types/learning"; + +const findAcknowledgmentMatches = ( + userMessage: string, + assistantResponse: string, +): LearningCandidate[] => { + const candidates: LearningCandidate[] = []; + + for (const pattern of ACKNOWLEDGMENT_PATTERNS) { + const match = assistantResponse.match(pattern); + if (match) { + candidates.push({ + content: extractLearningFromAcknowledgment(userMessage), + context: LEARNING_CONTEXTS.CONVENTION_CONFIRMED, + confidence: LEARNING_DEFAULTS.ACKNOWLEDGMENT_CONFIDENCE, + category: "convention", + }); + } + } + + return candidates; +}; + +const hasAcknowledgmentPhrase = (response: string): boolean => + ACKNOWLEDGMENT_PHRASES.some((phrase) => response.includes(phrase)); + +const boostConfidence = (candidate: LearningCandidate): LearningCandidate => ({ + ...candidate, + confidence: Math.min( + candidate.confidence + LEARNING_DEFAULTS.CONFIDENCE_BOOST, + LEARNING_DEFAULTS.MAX_CONFIDENCE, + ), + context: LEARNING_CONTEXTS.PREFERENCE_ACKNOWLEDGED, +}); + +const getAcknowledgedLearnings = (userMessage: string): LearningCandidate[] => { + const userLearnings = analyzeMessage(userMessage, "user"); + return userLearnings.map(boostConfidence); +}; + +export const analyzeAssistantResponse = ( + userMessage: string, + assistantResponse: string, +): LearningCandidate[] => { + const candidates: LearningCandidate[] = []; + const lowerResponse = assistantResponse.toLowerCase(); + + const acknowledgmentMatches = findAcknowledgmentMatches( + userMessage, + assistantResponse, + ); + candidates.push(...acknowledgmentMatches); + + if (hasAcknowledgmentPhrase(lowerResponse)) { + const acknowledgedLearnings = getAcknowledgedLearnings(userMessage); + candidates.push(...acknowledgedLearnings); + } + + return candidates; +}; diff --git a/src/services/learning/categorize.ts b/src/services/learning/categorize.ts new file mode 100644 index 0000000..dffcc12 --- /dev/null +++ b/src/services/learning/categorize.ts @@ -0,0 +1,20 @@ +/** + * Learning pattern categorization + */ + +import { CATEGORY_PATTERNS } from "@constants/learning"; +import type { LearningCategory } from "@/types/learning"; + +const findMatchingCategory = (patternStr: string): LearningCategory | null => { + for (const [keyword, category] of Object.entries(CATEGORY_PATTERNS)) { + if (patternStr.includes(keyword)) { + return category; + } + } + return null; +}; + +export const categorizePattern = (pattern: RegExp): LearningCategory => { + const patternStr = pattern.toString().toLowerCase(); + return findMatchingCategory(patternStr) ?? "general"; +}; diff --git a/src/services/learning/deduplicate.ts b/src/services/learning/deduplicate.ts new file mode 100644 index 0000000..8c4197b --- /dev/null +++ b/src/services/learning/deduplicate.ts @@ -0,0 +1,25 @@ +/** + * Learning candidate deduplication + */ + +import type { LearningCandidate } from "@/types/learning"; + +const normalizeContent = (content: string): string => + content.toLowerCase().trim(); + +export const deduplicateCandidates = ( + candidates: LearningCandidate[], +): LearningCandidate[] => { + const seen = new Set(); + + return candidates.filter((candidate) => { + const key = normalizeContent(candidate.content); + + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); +}; diff --git a/src/services/learning/detect.ts b/src/services/learning/detect.ts new file mode 100644 index 0000000..f0aff75 --- /dev/null +++ b/src/services/learning/detect.ts @@ -0,0 +1,29 @@ +/** + * Learning detection orchestration + */ + +import { analyzeMessage } from "@services/learning/analyze"; +import { analyzeAssistantResponse } from "@services/learning/assistant"; +import { deduplicateCandidates } from "@services/learning/deduplicate"; +import type { LearningCandidate } from "@/types/learning"; + +const sortByConfidence = (a: LearningCandidate, b: LearningCandidate): number => + b.confidence - a.confidence; + +export const detectLearnings = ( + userMessage: string, + assistantResponse: string, +): LearningCandidate[] => { + const candidates: LearningCandidate[] = []; + + const userLearnings = analyzeMessage(userMessage, "user"); + candidates.push(...userLearnings); + + const assistantLearnings = analyzeAssistantResponse( + userMessage, + assistantResponse, + ); + candidates.push(...assistantLearnings); + + return deduplicateCandidates(candidates).sort(sortByConfidence); +}; diff --git a/src/services/learning/embeddings.ts b/src/services/learning/embeddings.ts new file mode 100644 index 0000000..9135587 --- /dev/null +++ b/src/services/learning/embeddings.ts @@ -0,0 +1,240 @@ +/** + * Embedding Service + * + * Generates text embeddings using Ollama for semantic search + */ + +import got from "got"; + +import { + EMBEDDING_DEFAULTS, + EMBEDDING_ENDPOINTS, + EMBEDDING_TIMEOUTS, +} from "@constants/embeddings"; +import { getOllamaBaseUrl } from "@providers/ollama/state"; + +import type { + EmbeddingVector, + EmbeddingResult, + EmbeddingError, + EmbeddingServiceState, + OllamaEmbedRequest, + OllamaEmbedResponse, +} from "@/types/embeddings"; + +// ============================================================================= +// Service State +// ============================================================================= + +let serviceState: EmbeddingServiceState = { + initialized: false, + model: null, + available: false, + error: null, +}; + +// ============================================================================= +// Ollama API +// ============================================================================= + +const callOllamaEmbed = async ( + texts: string[], + model: string, +): Promise => { + const baseUrl = getOllamaBaseUrl(); + const endpoint = `${baseUrl}${EMBEDDING_ENDPOINTS.EMBED}`; + + const request: OllamaEmbedRequest = { + model, + input: texts, + }; + + const response = await got + .post(endpoint, { + json: request, + timeout: { request: EMBEDDING_TIMEOUTS.EMBED }, + }) + .json(); + + return response.embeddings; +}; + +// ============================================================================= +// Model Detection +// ============================================================================= + +const checkModelAvailable = async (model: string): Promise => { + try { + // Try to embed a simple test string + await callOllamaEmbed(["test"], model); + return true; + } catch { + return false; + } +}; + +const findAvailableModel = async (): Promise => { + const modelsToTry = [ + EMBEDDING_DEFAULTS.MODEL, + EMBEDDING_DEFAULTS.FALLBACK_MODEL, + "mxbai-embed-large", + "snowflake-arctic-embed", + ]; + + for (const model of modelsToTry) { + const available = await checkModelAvailable(model); + if (available) { + return model; + } + } + + return null; +}; + +// ============================================================================= +// Service Initialization +// ============================================================================= + +export const initializeEmbeddingService = + async (): Promise => { + if (serviceState.initialized) { + return serviceState; + } + + try { + const model = await findAvailableModel(); + + if (model) { + serviceState = { + initialized: true, + model, + available: true, + error: null, + }; + } else { + serviceState = { + initialized: true, + model: null, + available: false, + error: { + code: "MODEL_NOT_FOUND", + message: `No embedding model found. Install one with: ollama pull ${EMBEDDING_DEFAULTS.MODEL}`, + }, + }; + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const isConnectionError = + message.includes("ECONNREFUSED") || message.includes("connect"); + + serviceState = { + initialized: true, + model: null, + available: false, + error: { + code: isConnectionError ? "OLLAMA_NOT_RUNNING" : "EMBEDDING_FAILED", + message: isConnectionError + ? "Ollama is not running. Start it with: ollama serve" + : `Embedding service error: ${message}`, + }, + }; + } + + return serviceState; + }; + +// ============================================================================= +// Core Embedding Functions +// ============================================================================= + +/** + * Generate embedding for a single text + */ +export const embed = async (text: string): Promise => { + if (!serviceState.initialized) { + await initializeEmbeddingService(); + } + + if (!serviceState.available || !serviceState.model) { + return null; + } + + try { + const embeddings = await callOllamaEmbed([text], serviceState.model); + + if (embeddings.length === 0) { + return null; + } + + return { + text, + embedding: embeddings[0], + model: serviceState.model, + dimensions: embeddings[0].length, + }; + } catch { + return null; + } +}; + +/** + * Generate embeddings for multiple texts (batch) + */ +export const embedBatch = async ( + texts: string[], +): Promise<(EmbeddingResult | null)[]> => { + if (!serviceState.initialized) { + await initializeEmbeddingService(); + } + + if (!serviceState.available || !serviceState.model) { + return texts.map(() => null); + } + + try { + const embeddings = await callOllamaEmbed(texts, serviceState.model); + + return texts.map((text, i) => { + const embedding = embeddings[i]; + if (!embedding) { + return null; + } + + return { + text, + embedding, + model: serviceState.model!, + dimensions: embedding.length, + }; + }); + } catch { + return texts.map(() => null); + } +}; + +// ============================================================================= +// Service State Accessors +// ============================================================================= + +export const isEmbeddingAvailable = (): boolean => serviceState.available; + +export const getEmbeddingModel = (): string | null => serviceState.model; + +export const getEmbeddingError = (): EmbeddingError | null => + serviceState.error; + +export const getServiceState = (): EmbeddingServiceState => ({ + ...serviceState, +}); + +/** + * Reset service state (for testing) + */ +export const resetEmbeddingService = (): void => { + serviceState = { + initialized: false, + model: null, + available: false, + error: null, + }; +}; diff --git a/src/services/learning/extract.ts b/src/services/learning/extract.ts new file mode 100644 index 0000000..d49bb2d --- /dev/null +++ b/src/services/learning/extract.ts @@ -0,0 +1,38 @@ +/** + * Learning content extraction utilities + */ + +import { LEARNING_DEFAULTS } from "@constants/learning"; + +const splitIntoSentences = (text: string): string[] => + text.split(/[.!?]+/).filter((s) => s.trim()); + +const findMatchingSentence = ( + sentences: string[], + matchText: string, +): string | null => { + const lowerMatch = matchText.toLowerCase(); + return sentences.find((s) => s.toLowerCase().includes(lowerMatch)) ?? null; +}; + +export const extractLearningContent = ( + message: string, + match: RegExpMatchArray, +): string => { + const sentences = splitIntoSentences(message); + const matchingSentence = findMatchingSentence(sentences, match[0]); + + return matchingSentence?.trim() ?? match[0]; +}; + +export const extractLearningFromAcknowledgment = ( + userMessage: string, +): string => { + const sentences = splitIntoSentences(userMessage); + + if (sentences.length > 0) { + return sentences[0].trim(); + } + + return userMessage.slice(0, LEARNING_DEFAULTS.MAX_SLICE_LENGTH); +}; diff --git a/src/services/learning/format.ts b/src/services/learning/format.ts new file mode 100644 index 0000000..9cdea4c --- /dev/null +++ b/src/services/learning/format.ts @@ -0,0 +1,13 @@ +/** + * Learning formatting utilities + */ + +import { LEARNING_DEFAULTS } from "@constants/learning"; +import type { LearningCandidate } from "@/types/learning"; + +export const formatLearningForPrompt = ( + candidate: LearningCandidate, +): string => + candidate.content.length > LEARNING_DEFAULTS.MAX_CONTENT_LENGTH + ? candidate.content.slice(0, LEARNING_DEFAULTS.TRUNCATE_LENGTH) + "..." + : candidate.content; diff --git a/src/services/learning/index.ts b/src/services/learning/index.ts new file mode 100644 index 0000000..c093910 --- /dev/null +++ b/src/services/learning/index.ts @@ -0,0 +1,67 @@ +/** + * Learning Service Exports + * + * Central export point for all learning-related functionality + */ + +// Core persistence +export { + saveLearning, + getLearnings, + learningExists, +} from "@services/learning/persistence"; + +// Embedding service +export { + initializeEmbeddingService, + embed, + embedBatch, + isEmbeddingAvailable, + getEmbeddingModel, + getEmbeddingError, + getServiceState, + resetEmbeddingService, +} from "@services/learning/embeddings"; + +// Vector store +export { + cosineSimilarity, + euclideanDistance, + loadIndex, + saveIndex, + upsertEmbedding, + removeEmbedding, + hasEmbedding, + getEmbedding, + findSimilar, + findAboveThreshold, + getIndexStats, +} from "@services/learning/vector-store"; + +// Semantic search +export { + indexLearning, + unindexLearning, + isLearningIndexed, + searchLearnings, + rebuildIndex, + clearIndexCache, + getIndexStatistics, +} from "@services/learning/semantic-search"; + +// Re-export types +export type { + StoredLearning, + LearningCandidate, + LearningCategory, +} from "@/types/learning"; + +export type { + EmbeddingVector, + EmbeddingResult, + EmbeddingIndex, + StoredEmbedding, + SimilarityResult, + SemanticSearchResult, + SemanticSearchOptions, +} from "@/types/embeddings"; diff --git a/src/services/learning/persistence.ts b/src/services/learning/persistence.ts new file mode 100644 index 0000000..d330e66 --- /dev/null +++ b/src/services/learning/persistence.ts @@ -0,0 +1,48 @@ +/** + * Learning persistence operations + */ + +import { projectConfig } from "@services/project-config"; +import type { StoredLearning } from "@/types/learning"; +import { indexLearning } from "@services/learning/semantic-search"; + +export const saveLearning = async ( + content: string, + context?: string, + global = false, +): Promise => { + // Save the learning + const learning = await projectConfig.addLearning(content, context, global); + + // Index for semantic search (non-blocking, don't fail if embeddings unavailable) + if (learning) { + indexLearning(learning, global).catch(() => { + // Silently ignore embedding failures + }); + } +}; + +export const getLearnings = async (): Promise => + projectConfig.getLearnings(); + +const normalizeForComparison = (text: string): string => + text.toLowerCase().trim(); + +const isSimilarContent = (existing: string, newContent: string): boolean => { + const normalizedExisting = normalizeForComparison(existing); + const normalizedNew = normalizeForComparison(newContent); + + return ( + normalizedExisting === normalizedNew || + normalizedExisting.includes(normalizedNew) || + normalizedNew.includes(normalizedExisting) + ); +}; + +export const learningExists = async (content: string): Promise => { + const learnings = await getLearnings(); + + return learnings.some((learning) => + isSimilarContent(learning.content, content), + ); +}; diff --git a/src/services/learning/semantic-search.ts b/src/services/learning/semantic-search.ts new file mode 100644 index 0000000..e24d6ad --- /dev/null +++ b/src/services/learning/semantic-search.ts @@ -0,0 +1,386 @@ +/** + * Semantic Search Service + * + * High-level API for semantic learning retrieval + */ + +import * as path from "path"; + +import { EMBEDDING_SEARCH } from "@constants/embeddings"; +import { + getGlobalConfigDir, + getLocalConfigDir, +} from "@services/project-config"; + +import type { StoredLearning } from "@/types/learning"; +import type { + EmbeddingIndex, + SemanticSearchResult, + SemanticSearchOptions, + SimilarityResult, +} from "@/types/embeddings"; + +import { + embed, + isEmbeddingAvailable, + initializeEmbeddingService, + getEmbeddingModel, +} from "@services/learning/embeddings"; + +import { + loadIndex, + saveIndex, + upsertEmbedding, + removeEmbedding, + findSimilar, + hasEmbedding, +} from "@services/learning/vector-store"; + +// ============================================================================= +// Index Management +// ============================================================================= + +let globalIndex: EmbeddingIndex | null = null; +let localIndex: EmbeddingIndex | null = null; + +const getGlobalIndexDir = (): string => + path.join(getGlobalConfigDir(), "learnings"); + +const getLocalIndexDir = (): string => + path.join(getLocalConfigDir(), "learnings"); + +/** + * Initialize or get the global embedding index + */ +const getGlobalIndex = async (): Promise => { + if (!isEmbeddingAvailable()) { + return null; + } + + if (globalIndex) { + return globalIndex; + } + + const model = getEmbeddingModel(); + if (!model) { + return null; + } + + globalIndex = await loadIndex(getGlobalIndexDir(), model); + return globalIndex; +}; + +/** + * Initialize or get the local embedding index + */ +const getLocalIndex = async (): Promise => { + if (!isEmbeddingAvailable()) { + return null; + } + + if (localIndex) { + return localIndex; + } + + const model = getEmbeddingModel(); + if (!model) { + return null; + } + + localIndex = await loadIndex(getLocalIndexDir(), model); + return localIndex; +}; + +/** + * Save both indexes to disk + */ +const persistIndexes = async (): Promise => { + if (globalIndex) { + await saveIndex(getGlobalIndexDir(), globalIndex); + } + if (localIndex) { + await saveIndex(getLocalIndexDir(), localIndex); + } +}; + +// ============================================================================= +// Embedding Management +// ============================================================================= + +/** + * Add embedding for a learning + */ +export const indexLearning = async ( + learning: StoredLearning, + global: boolean, +): Promise => { + await initializeEmbeddingService(); + + if (!isEmbeddingAvailable()) { + return false; + } + + const result = await embed(learning.content); + if (!result) { + return false; + } + + const index = global ? await getGlobalIndex() : await getLocalIndex(); + if (!index) { + return false; + } + + const updatedIndex = upsertEmbedding(index, learning.id, result.embedding); + + if (global) { + globalIndex = updatedIndex; + } else { + localIndex = updatedIndex; + } + + await persistIndexes(); + return true; +}; + +/** + * Remove embedding for a learning + */ +export const unindexLearning = async ( + learningId: string, + global: boolean, +): Promise => { + const index = global ? await getGlobalIndex() : await getLocalIndex(); + if (!index) { + return; + } + + const updatedIndex = removeEmbedding(index, learningId); + + if (global) { + globalIndex = updatedIndex; + } else { + localIndex = updatedIndex; + } + + await persistIndexes(); +}; + +/** + * Check if a learning has an embedding + */ +export const isLearningIndexed = async ( + learningId: string, +): Promise => { + const gIndex = await getGlobalIndex(); + const lIndex = await getLocalIndex(); + + return ( + (gIndex !== null && hasEmbedding(gIndex, learningId)) || + (lIndex !== null && hasEmbedding(lIndex, learningId)) + ); +}; + +// ============================================================================= +// Semantic Search +// ============================================================================= + +/** + * Search learnings by semantic similarity + */ +export const searchLearnings = async ( + query: string, + learnings: StoredLearning[], + options: SemanticSearchOptions = {}, +): Promise[]> => { + const { + topK = EMBEDDING_SEARCH.TOP_K, + minSimilarity = EMBEDDING_SEARCH.MIN_SIMILARITY, + } = options; + + await initializeEmbeddingService(); + + if (!isEmbeddingAvailable()) { + // Fallback to keyword matching + return fallbackKeywordSearch(query, learnings, topK); + } + + // Embed the query + const queryResult = await embed(query); + if (!queryResult) { + return fallbackKeywordSearch(query, learnings, topK); + } + + // Search both indexes + const gIndex = await getGlobalIndex(); + const lIndex = await getLocalIndex(); + + const allResults: SimilarityResult[] = []; + + if (gIndex) { + allResults.push( + ...findSimilar(gIndex, queryResult.embedding, topK * 2, minSimilarity), + ); + } + + if (lIndex) { + allResults.push( + ...findSimilar(lIndex, queryResult.embedding, topK * 2, minSimilarity), + ); + } + + // Deduplicate and sort + const seen = new Set(); + const uniqueResults: SimilarityResult[] = []; + + for (const result of allResults.sort((a, b) => b.score - a.score)) { + if (!seen.has(result.id)) { + seen.add(result.id); + uniqueResults.push(result); + } + } + + // Map results to learnings + const learningMap = new Map(learnings.map((l) => [l.id, l])); + const searchResults: SemanticSearchResult[] = []; + + for (let i = 0; i < Math.min(uniqueResults.length, topK); i++) { + const result = uniqueResults[i]; + const learning = learningMap.get(result.id); + + if (learning) { + searchResults.push({ + item: learning, + score: result.score, + rank: i + 1, + }); + } + } + + return searchResults; +}; + +// ============================================================================= +// Fallback Search +// ============================================================================= + +/** + * Simple keyword-based search as fallback when embeddings unavailable + */ +const fallbackKeywordSearch = ( + query: string, + learnings: StoredLearning[], + topK: number, +): SemanticSearchResult[] => { + const queryTokens = tokenize(query); + + if (queryTokens.length === 0) { + // Return most recent if no query tokens + return learnings.slice(0, topK).map((item, i) => ({ + item, + score: 1 - i * 0.05, + rank: i + 1, + })); + } + + // Score each learning by token overlap + const scored = learnings.map((learning) => { + const contentTokens = tokenize(learning.content); + const overlap = queryTokens.filter((t) => contentTokens.includes(t)).length; + const score = overlap / Math.max(queryTokens.length, 1); + + return { learning, score }; + }); + + // Sort by score and return top K + return scored + .filter((s) => s.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, topK) + .map((s, i) => ({ + item: s.learning, + score: s.score, + rank: i + 1, + })); +}; + +/** + * Simple tokenizer for fallback search + */ +const tokenize = (text: string): string[] => + text + .toLowerCase() + .split(/\W+/) + .filter((t) => t.length > 2); + +// ============================================================================= +// Index Rebuilding +// ============================================================================= + +/** + * Rebuild all embeddings for existing learnings + */ +export const rebuildIndex = async ( + learnings: StoredLearning[], + global: boolean, + onProgress?: (current: number, total: number) => void, +): Promise<{ indexed: number; failed: number }> => { + await initializeEmbeddingService(); + + if (!isEmbeddingAvailable()) { + return { indexed: 0, failed: learnings.length }; + } + + let indexed = 0; + let failed = 0; + + for (let i = 0; i < learnings.length; i++) { + const learning = learnings[i]; + const success = await indexLearning(learning, global); + + if (success) { + indexed++; + } else { + failed++; + } + + onProgress?.(i + 1, learnings.length); + } + + return { indexed, failed }; +}; + +// ============================================================================= +// Cache Management +// ============================================================================= + +/** + * Clear in-memory index cache + */ +export const clearIndexCache = (): void => { + globalIndex = null; + localIndex = null; +}; + +/** + * Get index statistics + */ +export const getIndexStatistics = async (): Promise<{ + global: { count: number; model: string } | null; + local: { count: number; model: string } | null; + embeddingsAvailable: boolean; +}> => { + await initializeEmbeddingService(); + + const gIndex = await getGlobalIndex(); + const lIndex = await getLocalIndex(); + + return { + global: gIndex + ? { count: Object.keys(gIndex.embeddings).length, model: gIndex.model } + : null, + local: lIndex + ? { count: Object.keys(lIndex.embeddings).length, model: lIndex.model } + : null, + embeddingsAvailable: isEmbeddingAvailable(), + }; +}; diff --git a/src/services/learning/vector-store.ts b/src/services/learning/vector-store.ts new file mode 100644 index 0000000..629fe1a --- /dev/null +++ b/src/services/learning/vector-store.ts @@ -0,0 +1,243 @@ +/** + * Vector Store + * + * Stores and searches embeddings for semantic retrieval + */ + +import * as fs from "fs/promises"; +import * as path from "path"; + +import { EMBEDDING_STORAGE, EMBEDDING_SEARCH } from "@constants/embeddings"; + +import type { + EmbeddingVector, + EmbeddingIndex, + StoredEmbedding, + SimilarityResult, +} from "@/types/embeddings"; + +import { createEmptyIndex } from "@/types/embeddings"; + +// ============================================================================= +// Vector Math +// ============================================================================= + +/** + * Compute cosine similarity between two vectors + * Returns value between -1 and 1 (1 = identical, 0 = orthogonal, -1 = opposite) + */ +export const cosineSimilarity = ( + a: EmbeddingVector, + b: EmbeddingVector, +): number => { + if (a.length !== b.length) { + return 0; + } + + let dotProduct = 0; + let normA = 0; + let normB = 0; + + for (let i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + + const magnitude = Math.sqrt(normA) * Math.sqrt(normB); + + if (magnitude === 0) { + return 0; + } + + return dotProduct / magnitude; +}; + +/** + * Compute Euclidean distance between two vectors + */ +export const euclideanDistance = ( + a: EmbeddingVector, + b: EmbeddingVector, +): number => { + if (a.length !== b.length) { + return Infinity; + } + + let sum = 0; + for (let i = 0; i < a.length; i++) { + const diff = a[i] - b[i]; + sum += diff * diff; + } + + return Math.sqrt(sum); +}; + +// ============================================================================= +// Index File Operations +// ============================================================================= + +const getIndexPath = (baseDir: string): string => + path.join(baseDir, EMBEDDING_STORAGE.INDEX_FILE); + +/** + * Load embedding index from disk + */ +export const loadIndex = async ( + baseDir: string, + model: string, +): Promise => { + const indexPath = getIndexPath(baseDir); + + try { + const data = await fs.readFile(indexPath, "utf-8"); + const index = JSON.parse(data) as EmbeddingIndex; + + // Check version and model compatibility + if (index.version !== EMBEDDING_STORAGE.VERSION || index.model !== model) { + // Index is incompatible, create new one + return createEmptyIndex(model); + } + + return index; + } catch { + // Index doesn't exist or is invalid + return createEmptyIndex(model); + } +}; + +/** + * Save embedding index to disk + */ +export const saveIndex = async ( + baseDir: string, + index: EmbeddingIndex, +): Promise => { + const indexPath = getIndexPath(baseDir); + + await fs.mkdir(baseDir, { recursive: true }); + await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf-8"); +}; + +// ============================================================================= +// Index Operations +// ============================================================================= + +/** + * Add or update an embedding in the index + */ +export const upsertEmbedding = ( + index: EmbeddingIndex, + id: string, + embedding: EmbeddingVector, +): EmbeddingIndex => { + const stored: StoredEmbedding = { + id, + embedding, + model: index.model, + createdAt: Date.now(), + }; + + return { + ...index, + embeddings: { + ...index.embeddings, + [id]: stored, + }, + lastUpdated: Date.now(), + }; +}; + +/** + * Remove an embedding from the index + */ +export const removeEmbedding = ( + index: EmbeddingIndex, + id: string, +): EmbeddingIndex => { + const { [id]: _, ...remaining } = index.embeddings; + + return { + ...index, + embeddings: remaining, + lastUpdated: Date.now(), + }; +}; + +/** + * Check if an embedding exists in the index + */ +export const hasEmbedding = (index: EmbeddingIndex, id: string): boolean => + id in index.embeddings; + +/** + * Get an embedding from the index + */ +export const getEmbedding = ( + index: EmbeddingIndex, + id: string, +): StoredEmbedding | null => index.embeddings[id] ?? null; + +// ============================================================================= +// Similarity Search +// ============================================================================= + +/** + * Find the most similar embeddings to a query vector + */ +export const findSimilar = ( + index: EmbeddingIndex, + queryVector: EmbeddingVector, + topK: number = EMBEDDING_SEARCH.TOP_K, + minSimilarity: number = EMBEDDING_SEARCH.MIN_SIMILARITY, +): SimilarityResult[] => { + const results: SimilarityResult[] = []; + + for (const [id, stored] of Object.entries(index.embeddings)) { + const score = cosineSimilarity(queryVector, stored.embedding); + + if (score >= minSimilarity) { + results.push({ id, score }); + } + } + + // Sort by score descending and take top K + return results.sort((a, b) => b.score - a.score).slice(0, topK); +}; + +/** + * Find all embeddings above a similarity threshold + */ +export const findAboveThreshold = ( + index: EmbeddingIndex, + queryVector: EmbeddingVector, + threshold: number, +): SimilarityResult[] => { + const results: SimilarityResult[] = []; + + for (const [id, stored] of Object.entries(index.embeddings)) { + const score = cosineSimilarity(queryVector, stored.embedding); + + if (score >= threshold) { + results.push({ id, score }); + } + } + + return results.sort((a, b) => b.score - a.score); +}; + +// ============================================================================= +// Index Statistics +// ============================================================================= + +export const getIndexStats = ( + index: EmbeddingIndex, +): { + count: number; + model: string; + lastUpdated: number; +} => ({ + count: Object.keys(index.embeddings).length, + model: index.model, + lastUpdated: index.lastUpdated, +}); diff --git a/src/services/mcp/client.ts b/src/services/mcp/client.ts new file mode 100644 index 0000000..4a59cbd --- /dev/null +++ b/src/services/mcp/client.ts @@ -0,0 +1,373 @@ +/** + * MCP Client - Manages connection to a single MCP server + */ + +import { spawn, type ChildProcess } from "child_process"; +import type { + MCPServerConfig, + MCPServerInstance, + MCPToolDefinition, + MCPResourceDefinition, + MCPToolCallResult, + MCPConnectionState, +} from "@/types/mcp"; + +/** + * JSON-RPC message types + */ +interface JsonRpcRequest { + jsonrpc: "2.0"; + id: number; + method: string; + params?: unknown; +} + +interface JsonRpcResponse { + jsonrpc: "2.0"; + id: number; + result?: unknown; + error?: { + code: number; + message: string; + data?: unknown; + }; +} + +/** + * MCP Client class for managing a single server connection + */ +export class MCPClient { + private config: MCPServerConfig; + private process: ChildProcess | null = null; + private state: MCPConnectionState = "disconnected"; + private tools: MCPToolDefinition[] = []; + private resources: MCPResourceDefinition[] = []; + private error: string | undefined; + private requestId = 0; + private pendingRequests: Map< + number, + { + resolve: (value: unknown) => void; + reject: (error: Error) => void; + } + > = new Map(); + private buffer = ""; + + constructor(config: MCPServerConfig) { + this.config = config; + } + + /** + * Get server instance state + */ + getInstance(): MCPServerInstance { + return { + config: this.config, + state: this.state, + tools: this.tools, + resources: this.resources, + error: this.error, + pid: this.process?.pid, + }; + } + + /** + * Connect to the MCP server + */ + async connect(): Promise { + if (this.state === "connected" || this.state === "connecting") { + return; + } + + this.state = "connecting"; + this.error = undefined; + + try { + if (this.config.transport === "stdio" || !this.config.transport) { + await this.connectStdio(); + } else { + throw new Error( + `Transport type '${this.config.transport}' not yet supported`, + ); + } + + // Initialize the connection + await this.initialize(); + + // Discover tools and resources + await this.discoverCapabilities(); + + this.state = "connected"; + } catch (err) { + this.state = "error"; + this.error = err instanceof Error ? err.message : String(err); + throw err; + } + } + + /** + * Connect via stdio transport + */ + private async connectStdio(): Promise { + return new Promise((resolve, reject) => { + const env = { + ...process.env, + ...this.config.env, + }; + + this.process = spawn(this.config.command, this.config.args || [], { + stdio: ["pipe", "pipe", "pipe"], + env, + }); + + if (!this.process.stdout || !this.process.stdin) { + reject(new Error("Failed to create stdio pipes")); + return; + } + + this.process.stdout.on("data", (data: Buffer) => { + this.handleData(data.toString()); + }); + + this.process.stderr?.on("data", (data: Buffer) => { + console.error(`[MCP ${this.config.name}] stderr:`, data.toString()); + }); + + this.process.on("error", (err) => { + this.state = "error"; + this.error = err.message; + reject(err); + }); + + this.process.on("exit", (code) => { + this.state = "disconnected"; + if (code !== 0) { + this.error = `Process exited with code ${code}`; + } + }); + + // Give the process a moment to start + setTimeout(resolve, 100); + }); + } + + /** + * Handle incoming data from the server + */ + private handleData(data: string): void { + this.buffer += data; + + // Process complete JSON-RPC messages (newline-delimited) + const lines = this.buffer.split("\n"); + this.buffer = lines.pop() || ""; + + for (const line of lines) { + if (line.trim()) { + try { + const message = JSON.parse(line) as JsonRpcResponse; + this.handleMessage(message); + } catch { + // Ignore parse errors for incomplete messages + } + } + } + } + + /** + * Handle a JSON-RPC message + */ + private handleMessage(message: JsonRpcResponse): void { + const pending = this.pendingRequests.get(message.id); + if (pending) { + this.pendingRequests.delete(message.id); + if (message.error) { + pending.reject(new Error(message.error.message)); + } else { + pending.resolve(message.result); + } + } + } + + /** + * Send a JSON-RPC request + */ + private async sendRequest( + method: string, + params?: unknown, + ): Promise { + if (!this.process?.stdin) { + throw new Error("Not connected"); + } + + const id = ++this.requestId; + const request: JsonRpcRequest = { + jsonrpc: "2.0", + id, + method, + params, + }; + + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { resolve, reject }); + + const timeout = setTimeout(() => { + this.pendingRequests.delete(id); + reject(new Error("Request timeout")); + }, 30000); + + this.process!.stdin!.write(JSON.stringify(request) + "\n", (err) => { + if (err) { + clearTimeout(timeout); + this.pendingRequests.delete(id); + reject(err); + } + }); + }); + } + + /** + * Initialize the MCP connection + */ + private async initialize(): Promise { + await this.sendRequest("initialize", { + protocolVersion: "2024-11-05", + capabilities: { + tools: {}, + resources: {}, + }, + clientInfo: { + name: "codetyper", + version: "0.1.0", + }, + }); + + // Send initialized notification + if (this.process?.stdin) { + this.process.stdin.write( + JSON.stringify({ + jsonrpc: "2.0", + method: "notifications/initialized", + }) + "\n", + ); + } + } + + /** + * Discover available tools and resources + */ + private async discoverCapabilities(): Promise { + // Get tools + try { + const toolsResult = (await this.sendRequest("tools/list")) as { + tools?: MCPToolDefinition[]; + }; + this.tools = toolsResult?.tools || []; + } catch { + this.tools = []; + } + + // Get resources + try { + const resourcesResult = (await this.sendRequest("resources/list")) as { + resources?: MCPResourceDefinition[]; + }; + this.resources = resourcesResult?.resources || []; + } catch { + this.resources = []; + } + } + + /** + * Call a tool on the server + */ + async callTool( + toolName: string, + args: Record, + ): Promise { + if (this.state !== "connected") { + return { + success: false, + error: "Not connected to server", + }; + } + + try { + const result = await this.sendRequest("tools/call", { + name: toolName, + arguments: args, + }); + + return { + success: true, + content: result, + }; + } catch (err) { + return { + success: false, + error: err instanceof Error ? err.message : String(err), + }; + } + } + + /** + * Read a resource from the server + */ + async readResource( + uri: string, + ): Promise<{ content: string; mimeType?: string } | null> { + if (this.state !== "connected") { + return null; + } + + try { + const result = (await this.sendRequest("resources/read", { uri })) as { + contents?: Array<{ text?: string; mimeType?: string }>; + }; + + if (result?.contents?.[0]) { + return { + content: result.contents[0].text || "", + mimeType: result.contents[0].mimeType, + }; + } + return null; + } catch { + return null; + } + } + + /** + * Disconnect from the server + */ + async disconnect(): Promise { + if (this.process) { + this.process.kill(); + this.process = null; + } + this.state = "disconnected"; + this.tools = []; + this.resources = []; + this.pendingRequests.clear(); + } + + /** + * Get connection state + */ + getState(): MCPConnectionState { + return this.state; + } + + /** + * Get available tools + */ + getTools(): MCPToolDefinition[] { + return this.tools; + } + + /** + * Get available resources + */ + getResources(): MCPResourceDefinition[] { + return this.resources; + } +} diff --git a/src/services/mcp/index.ts b/src/services/mcp/index.ts new file mode 100644 index 0000000..950798d --- /dev/null +++ b/src/services/mcp/index.ts @@ -0,0 +1,37 @@ +/** + * MCP Service - Model Context Protocol integration + * + * Provides connectivity to MCP servers for extensible tool integration. + */ + +export { MCPClient } from "@services/mcp/client"; + +export { + initializeMCP, + loadMCPConfig, + saveMCPConfig, + connectServer, + disconnectServer, + connectAllServers, + disconnectAllServers, + getServerInstances, + getAllTools, + callTool, + addServer, + removeServer, + getMCPConfig, + isMCPAvailable, +} from "@services/mcp/manager"; + +export type { + MCPConfig, + MCPServerConfig, + MCPServerInstance, + MCPToolDefinition, + MCPResourceDefinition, + MCPToolCallRequest, + MCPToolCallResult, + MCPConnectionState, + MCPTransportType, + MCPManagerState, +} from "@/types/mcp"; diff --git a/src/services/mcp/manager.ts b/src/services/mcp/manager.ts new file mode 100644 index 0000000..8486265 --- /dev/null +++ b/src/services/mcp/manager.ts @@ -0,0 +1,297 @@ +/** + * MCP Manager - Manages multiple MCP server connections + */ + +import fs from "fs/promises"; +import path from "path"; +import os from "os"; +import { MCPClient } from "@services/mcp/client"; +import type { + MCPConfig, + MCPServerConfig, + MCPServerInstance, + MCPToolDefinition, + MCPToolCallResult, +} from "@/types/mcp"; + +/** + * MCP Configuration file locations + */ +const CONFIG_LOCATIONS = { + global: path.join(os.homedir(), ".codetyper", "mcp.json"), + local: path.join(process.cwd(), ".codetyper", "mcp.json"), +}; + +/** + * MCP Manager State + */ +interface MCPManagerState { + clients: Map; + config: MCPConfig; + initialized: boolean; +} + +/** + * MCP Manager singleton state + */ +const state: MCPManagerState = { + clients: new Map(), + config: { servers: {} }, + initialized: false, +}; + +/** + * Load MCP configuration from file + */ +const loadConfigFile = async (filePath: string): Promise => { + try { + const content = await fs.readFile(filePath, "utf-8"); + return JSON.parse(content) as MCPConfig; + } catch { + return null; + } +}; + +/** + * Load MCP configuration (merges global + local) + */ +export const loadMCPConfig = async (): Promise => { + const globalConfig = await loadConfigFile(CONFIG_LOCATIONS.global); + const localConfig = await loadConfigFile(CONFIG_LOCATIONS.local); + + const merged: MCPConfig = { + servers: { + ...(globalConfig?.servers || {}), + ...(localConfig?.servers || {}), + }, + }; + + return merged; +}; + +/** + * Save MCP configuration + */ +export const saveMCPConfig = async ( + config: MCPConfig, + global = false, +): Promise => { + const filePath = global ? CONFIG_LOCATIONS.global : CONFIG_LOCATIONS.local; + const dir = path.dirname(filePath); + + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(config, null, 2), "utf-8"); +}; + +/** + * Initialize MCP Manager + */ +export const initializeMCP = async (): Promise => { + if (state.initialized) return; + + state.config = await loadMCPConfig(); + state.initialized = true; +}; + +/** + * Connect to a specific MCP server + */ +export const connectServer = async ( + serverName: string, +): Promise => { + await initializeMCP(); + + const serverConfig = state.config.servers[serverName]; + if (!serverConfig) { + throw new Error(`Server '${serverName}' not found in configuration`); + } + + // Check if already connected + let client = state.clients.get(serverName); + if (client && client.getState() === "connected") { + return client.getInstance(); + } + + // Create new client + client = new MCPClient({ + ...serverConfig, + name: serverName, + }); + + state.clients.set(serverName, client); + + await client.connect(); + return client.getInstance(); +}; + +/** + * Disconnect from a specific MCP server + */ +export const disconnectServer = async (serverName: string): Promise => { + const client = state.clients.get(serverName); + if (client) { + await client.disconnect(); + state.clients.delete(serverName); + } +}; + +/** + * Connect to all enabled servers + */ +export const connectAllServers = async (): Promise< + Map +> => { + await initializeMCP(); + + const results = new Map(); + + for (const [name, config] of Object.entries(state.config.servers)) { + if (config.enabled === false) continue; + + try { + const instance = await connectServer(name); + results.set(name, instance); + } catch (err) { + results.set(name, { + config: { ...config, name }, + state: "error", + tools: [], + resources: [], + error: err instanceof Error ? err.message : String(err), + }); + } + } + + return results; +}; + +/** + * Disconnect from all servers + */ +export const disconnectAllServers = async (): Promise => { + for (const [name] of state.clients) { + await disconnectServer(name); + } +}; + +/** + * Get all server instances + */ +export const getServerInstances = (): Map => { + const instances = new Map(); + + for (const [name, client] of state.clients) { + instances.set(name, client.getInstance()); + } + + // Include configured but not connected servers + for (const [name, config] of Object.entries(state.config.servers)) { + if (!instances.has(name)) { + instances.set(name, { + config: { ...config, name }, + state: "disconnected", + tools: [], + resources: [], + }); + } + } + + return instances; +}; + +/** + * Get all available tools from all connected servers + */ +export const getAllTools = (): Array<{ + server: string; + tool: MCPToolDefinition; +}> => { + const tools: Array<{ server: string; tool: MCPToolDefinition }> = []; + + for (const [name, client] of state.clients) { + if (client.getState() === "connected") { + for (const tool of client.getTools()) { + tools.push({ server: name, tool }); + } + } + } + + return tools; +}; + +/** + * Call a tool on a specific server + */ +export const callTool = async ( + serverName: string, + toolName: string, + args: Record, +): Promise => { + const client = state.clients.get(serverName); + if (!client) { + return { + success: false, + error: `Server '${serverName}' not connected`, + }; + } + + return client.callTool(toolName, args); +}; + +/** + * Add a server to configuration + */ +export const addServer = async ( + name: string, + config: Omit, + global = false, +): Promise => { + await initializeMCP(); + + const targetConfig = global + ? (await loadConfigFile(CONFIG_LOCATIONS.global)) || { servers: {} } + : (await loadConfigFile(CONFIG_LOCATIONS.local)) || { servers: {} }; + + targetConfig.servers[name] = { ...config, name }; + + await saveMCPConfig(targetConfig, global); + + // Update in-memory config + state.config.servers[name] = { ...config, name }; +}; + +/** + * Remove a server from configuration + */ +export const removeServer = async ( + name: string, + global = false, +): Promise => { + await disconnectServer(name); + + const filePath = global ? CONFIG_LOCATIONS.global : CONFIG_LOCATIONS.local; + const config = await loadConfigFile(filePath); + + if (config?.servers[name]) { + delete config.servers[name]; + await saveMCPConfig(config, global); + } + + delete state.config.servers[name]; +}; + +/** + * Get MCP configuration + */ +export const getMCPConfig = async (): Promise => { + await initializeMCP(); + return state.config; +}; + +/** + * Check if MCP is available (has any configured servers) + */ +export const isMCPAvailable = async (): Promise => { + await initializeMCP(); + return Object.keys(state.config.servers).length > 0; +}; diff --git a/src/services/mcp/tools.ts b/src/services/mcp/tools.ts new file mode 100644 index 0000000..007d948 --- /dev/null +++ b/src/services/mcp/tools.ts @@ -0,0 +1,171 @@ +/** + * MCP Tools Integration + * + * Wraps MCP tools to work with the codetyper tool system. + */ + +import { z } from "zod"; +import { + getAllTools, + callTool, + connectAllServers, +} from "@services/mcp/manager"; +import type { MCPToolDefinition } from "@/types/mcp"; + +/** + * Tool definition compatible with codetyper's tool system + */ +export interface ToolDefinition { + name: string; + description: string; + parameters: z.ZodSchema; + execute: ( + args: Record, + ctx: unknown, + ) => Promise<{ + success: boolean; + output: string; + error?: string; + title: string; + }>; +} + +/** + * Convert JSON Schema to Zod schema (simplified) + */ +const jsonSchemaToZod = ( + _schema: MCPToolDefinition["inputSchema"], +): z.ZodSchema => { + // Create a passthrough object schema that accepts any properties + // In a full implementation, we'd parse the JSON Schema properly + return z.object({}).passthrough(); +}; + +/** + * Create a codetyper tool from an MCP tool definition + */ +const createToolFromMCP = ( + serverName: string, + mcpTool: MCPToolDefinition, +): ToolDefinition => { + const fullName = `mcp_${serverName}_${mcpTool.name}`; + + return { + name: fullName, + description: + mcpTool.description || `MCP tool: ${mcpTool.name} from ${serverName}`, + parameters: jsonSchemaToZod(mcpTool.inputSchema), + execute: async (args) => { + const result = await callTool(serverName, mcpTool.name, args); + + if (result.success) { + return { + success: true, + output: + typeof result.content === "string" + ? result.content + : JSON.stringify(result.content, null, 2), + title: `${serverName}/${mcpTool.name}`, + }; + } + + return { + success: false, + output: "", + error: result.error || "Unknown error", + title: `${serverName}/${mcpTool.name}`, + }; + }, + }; +}; + +/** + * Get all MCP tools as codetyper tool definitions + */ +export const getMCPTools = async (): Promise => { + // Ensure servers are connected + await connectAllServers(); + + const mcpTools = getAllTools(); + + return mcpTools.map(({ server, tool }) => createToolFromMCP(server, tool)); +}; + +/** + * Get MCP tools for API (OpenAI function format) + */ +export const getMCPToolsForApi = async (): Promise< + Array<{ + type: "function"; + function: { + name: string; + description: string; + parameters: { + type: "object"; + properties: Record; + required?: string[]; + }; + }; + }> +> => { + await connectAllServers(); + + const mcpTools = getAllTools(); + + return mcpTools.map(({ server, tool }) => ({ + type: "function" as const, + function: { + name: `mcp_${server}_${tool.name}`, + description: tool.description || `MCP tool: ${tool.name} from ${server}`, + parameters: { + type: "object" as const, + properties: tool.inputSchema.properties || {}, + required: tool.inputSchema.required, + }, + }, + })); +}; + +/** + * Execute an MCP tool by full name (mcp_server_toolname) + */ +export const executeMCPTool = async ( + fullName: string, + args: Record, +): Promise<{ success: boolean; output: string; error?: string }> => { + // Parse the full name: mcp_servername_toolname + const match = fullName.match(/^mcp_([^_]+)_(.+)$/); + if (!match) { + return { + success: false, + output: "", + error: `Invalid MCP tool name: ${fullName}`, + }; + } + + const [, serverName, toolName] = match; + const result = await callTool(serverName, toolName, args); + + if (result.success) { + return { + success: true, + output: + typeof result.content === "string" + ? result.content + : JSON.stringify(result.content, null, 2), + }; + } + + return { + success: false, + output: "", + error: result.error, + }; +}; + +/** + * Check if a tool name is an MCP tool + */ +export const isMCPTool = (toolName: string): boolean => { + return toolName.startsWith("mcp_"); +}; diff --git a/src/services/memory-service.ts b/src/services/memory-service.ts new file mode 100644 index 0000000..f733cb0 --- /dev/null +++ b/src/services/memory-service.ts @@ -0,0 +1,388 @@ +/** + * Memory Service + * + * Manages persistent memory across sessions. + * Builds on the existing learning infrastructure. + */ + +import { + MEMORY_SYSTEM_PROMPT, + MEMORY_CONTEXT_TEMPLATE, + MEMORY_RETRIEVAL_PROMPT, +} from "@prompts/system/memory"; +import { + getLearnings, + addLearning, + buildLearningsContext, +} from "@services/project-config"; +import type { LearningEntry } from "@/types/project-config"; + +export interface MemoryContext { + isMemoryCommand: boolean; + commandType: MemoryCommandType; + content?: string; + query?: string; +} + +export type MemoryCommandType = "store" | "forget" | "query" | "list" | "none"; + +export type MemoryCategory = + | "preference" + | "convention" + | "architecture" + | "workflow" + | "context" + | "general"; + +interface MemoryMatch { + memory: LearningEntry; + relevance: number; +} + +const STORE_PATTERNS = [ + /remember\s+(?:that\s+)?(.+)/i, + /always\s+(.+)/i, + /never\s+(.+)/i, + /from now on[,]?\s+(.+)/i, + /in this project[,]?\s+(.+)/i, + /i (?:prefer|want|like)\s+(.+)/i, + /use\s+(.+)\s+(?:for|when|instead)/i, + /don't\s+(?:ever\s+)?(.+)/i, + /make sure (?:to\s+)?(.+)/i, +]; + +const FORGET_PATTERNS = [ + /forget\s+(?:about\s+)?(.+)/i, + /remove\s+(?:the\s+)?memory\s+(?:about\s+)?(.+)/i, + /delete\s+(?:the\s+)?memory\s+(?:about\s+)?(.+)/i, + /stop remembering\s+(.+)/i, + /don't remember\s+(.+)/i, +]; + +const QUERY_PATTERNS = [ + /what do you (?:remember|know) about\s+(.+)/i, + /do you remember\s+(.+)/i, + /what (?:are|is) (?:my|the) (?:preference|convention)s?\s*(?:for\s+)?(.+)?/i, + /show (?:me\s+)?(?:my\s+)?memories?\s*(?:about\s+)?(.+)?/i, + /list (?:my\s+)?memories/i, + /what have you learned/i, +]; + +const CATEGORY_KEYWORDS: Record = { + preference: ["prefer", "like", "want", "always", "never", "style", "format"], + convention: ["convention", "pattern", "naming", "standard", "rule"], + architecture: [ + "architecture", + "structure", + "design", + "layer", + "module", + "component", + ], + workflow: ["workflow", "process", "deploy", "test", "build", "ci", "cd"], + context: ["context", "background", "project", "about", "note"], + general: [], +}; + +/** + * Detect if input is a memory-related command + */ +export const detectMemoryCommand = (input: string): MemoryContext => { + const lowerInput = input.toLowerCase(); + + // Check for store commands + for (const pattern of STORE_PATTERNS) { + const match = input.match(pattern); + if (match) { + return { + isMemoryCommand: true, + commandType: "store", + content: match[1]?.trim(), + }; + } + } + + // Check for forget commands + for (const pattern of FORGET_PATTERNS) { + const match = input.match(pattern); + if (match) { + return { + isMemoryCommand: true, + commandType: "forget", + query: match[1]?.trim(), + }; + } + } + + // Check for query commands + for (const pattern of QUERY_PATTERNS) { + const match = input.match(pattern); + if (match) { + const isListCommand = /list|show.*memories|what have you learned/i.test( + lowerInput, + ); + return { + isMemoryCommand: true, + commandType: isListCommand ? "list" : "query", + query: match[1]?.trim(), + }; + } + } + + return { + isMemoryCommand: false, + commandType: "none", + }; +}; + +/** + * Detect category from content + */ +export const detectCategory = (content: string): MemoryCategory => { + const lowerContent = content.toLowerCase(); + + for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) { + if (category === "general") continue; + for (const keyword of keywords) { + if (lowerContent.includes(keyword)) { + return category as MemoryCategory; + } + } + } + + return "general"; +}; + +/** + * Calculate relevance score between memory and query + */ +const calculateRelevance = (memory: LearningEntry, query: string): number => { + const lowerContent = memory.content.toLowerCase(); + const lowerQuery = query.toLowerCase(); + const queryWords = lowerQuery.split(/\s+/).filter((w) => w.length > 2); + + let score = 0; + + // Exact substring match + if (lowerContent.includes(lowerQuery)) { + score += 10; + } + + // Word overlap + for (const word of queryWords) { + if (lowerContent.includes(word)) { + score += 2; + } + } + + // Context match + if (memory.context) { + const lowerContext = memory.context.toLowerCase(); + if (lowerContext.includes(lowerQuery)) { + score += 5; + } + for (const word of queryWords) { + if (lowerContext.includes(word)) { + score += 1; + } + } + } + + // Recency bonus (newer memories slightly preferred) + const ageInDays = (Date.now() - memory.createdAt) / (1000 * 60 * 60 * 24); + if (ageInDays < 7) { + score += 1; + } + + return score; +}; + +/** + * Store a new memory + */ +export const storeMemory = async ( + content: string, + context?: string, + global = false, +): Promise => { + await addLearning(content, context, global); +}; + +/** + * Get all memories + */ +export const getMemories = async (): Promise => { + return getLearnings(); +}; + +/** + * Find memories matching a query + */ +export const findMemories = async (query: string): Promise => { + const memories = await getMemories(); + + if (!query || query.trim() === "") { + return memories.slice(0, 10); + } + + const matches: MemoryMatch[] = memories.map((memory) => ({ + memory, + relevance: calculateRelevance(memory, query), + })); + + return matches + .filter((m) => m.relevance > 0) + .sort((a, b) => b.relevance - a.relevance) + .slice(0, 10) + .map((m) => m.memory); +}; + +/** + * Get relevant memories for a user input (auto-retrieval) + */ +export const getRelevantMemories = async ( + userInput: string, +): Promise => { + const memories = await getMemories(); + + if (memories.length === 0) { + return []; + } + + // Extract key terms from user input + const keyTerms = userInput + .toLowerCase() + .split(/\s+/) + .filter((word) => word.length > 3) + .slice(0, 10); + + if (keyTerms.length === 0) { + return []; + } + + const matches: MemoryMatch[] = memories.map((memory) => ({ + memory, + relevance: calculateRelevance(memory, keyTerms.join(" ")), + })); + + // Only return memories with meaningful relevance + return matches + .filter((m) => m.relevance >= 3) + .sort((a, b) => b.relevance - a.relevance) + .slice(0, 5) + .map((m) => m.memory); +}; + +/** + * Build memory context for system prompt + */ +export const buildMemoryContext = async (): Promise => { + const context = await buildLearningsContext(); + if (!context) { + return ""; + } + + return MEMORY_CONTEXT_TEMPLATE.replace("{{memories}}", context); +}; + +/** + * Build relevant memory prompt for a user message + */ +export const buildRelevantMemoryPrompt = async ( + userInput: string, +): Promise => { + const relevant = await getRelevantMemories(userInput); + + if (relevant.length === 0) { + return ""; + } + + const memoryList = relevant.map((m) => `- ${m.content}`).join("\n"); + + return MEMORY_RETRIEVAL_PROMPT.replace("{{relevantMemories}}", memoryList); +}; + +/** + * Get memory system prompt + */ +export const getMemoryPrompt = (): string => { + return MEMORY_SYSTEM_PROMPT; +}; + +/** + * Format memories for display + */ +export const formatMemoriesForDisplay = (memories: LearningEntry[]): string => { + if (memories.length === 0) { + return "No memories stored yet."; + } + + return memories + .map((m, i) => { + const date = new Date(m.createdAt).toLocaleDateString(); + const context = m.context ? ` (${m.context})` : ""; + return `${i + 1}. ${m.content}${context} - ${date}`; + }) + .join("\n"); +}; + +/** + * Process memory command and return response + */ +export const processMemoryCommand = async ( + context: MemoryContext, +): Promise<{ success: boolean; message: string }> => { + const handlers: Record< + MemoryCommandType, + () => Promise<{ success: boolean; message: string }> + > = { + store: async () => { + if (!context.content) { + return { success: false, message: "No content to remember." }; + } + const category = detectCategory(context.content); + await storeMemory(context.content, category); + return { + success: true, + message: `Remembered: "${context.content}" (category: ${category})`, + }; + }, + + forget: async () => { + // Note: Full forget implementation would need deletion support + return { + success: false, + message: + "Forget functionality not yet implemented. Memories can be manually removed from .codetyper/learnings/", + }; + }, + + query: async () => { + const memories = await findMemories(context.query || ""); + if (memories.length === 0) { + return { + success: true, + message: `No memories found${context.query ? ` about "${context.query}"` : ""}.`, + }; + } + return { + success: true, + message: `Found ${memories.length} memories:\n${formatMemoriesForDisplay(memories)}`, + }; + }, + + list: async () => { + const memories = await getMemories(); + return { + success: true, + message: `All memories (${memories.length}):\n${formatMemoriesForDisplay(memories)}`, + }; + }, + + none: async () => { + return { success: false, message: "Not a memory command." }; + }, + }; + + return handlers[context.commandType](); +}; diff --git a/src/services/permissions.ts b/src/services/permissions.ts new file mode 100644 index 0000000..6a9ae48 --- /dev/null +++ b/src/services/permissions.ts @@ -0,0 +1,706 @@ +/** + * Permission system for command execution + * + * Uses pattern matching format: Bash(command:args), Read(*), Write(*), Edit(*) + * + * Pattern examples: + * - Bash(git:status) - matches "git status" + * - Bash(git:*) - matches any git command + * - Bash(npm install:*) - matches npm install with any args + * - Bash(mkdir:*) - matches mkdir with any args + * - Read(*) - allow all file reads + * - Write(src/*) - allow writes to src directory + * - Edit(*.ts) - allow editing TypeScript files + */ + +import fs from "fs/promises"; +import path from "path"; +import chalk from "chalk"; +import { DIRS, FILES, LOCAL_CONFIG_DIR } from "@constants/paths"; +import type { + ToolType, + PermissionPattern, + PermissionsConfig, + PermissionHandler, +} from "@/types/permissions"; + +/** + * Multi-word command prefixes for pattern generation + */ +const MULTI_WORD_PREFIXES = [ + "git", + "npm", + "yarn", + "pnpm", + "docker", + "kubectl", + "make", +]; + +/** + * Simple tools that don't need arguments + */ +const SIMPLE_TOOLS: ToolType[] = ["WebSearch"]; + +/** + * Permission state + */ +let globalAllowPatterns: string[] = []; +let globalDenyPatterns: string[] = []; +let localAllowPatterns: string[] = []; +let localDenyPatterns: string[] = []; +let sessionAllowPatterns: string[] = []; +let initialized = false; +let workingDir = process.cwd(); +let permissionHandler: PermissionHandler | null = null; + +/** + * Set working directory + */ +export const setWorkingDir = (dir: string): void => { + workingDir = dir; +}; + +/** + * Set a custom permission handler (for TUI mode) + */ +export const setPermissionHandler = ( + handler: PermissionHandler | null, +): void => { + permissionHandler = handler; +}; + +/** + * Load permissions from file + */ +const loadPermissionsFile = async ( + filePath: string, +): Promise => { + try { + const data = await fs.readFile(filePath, "utf-8"); + return JSON.parse(data); + } catch { + return { permissions: { allow: [] } }; + } +}; + +/** + * Initialize permissions + */ +export const initializePermissions = async (): Promise => { + if (initialized) return; + + // Load global permissions + const globalConfig = await loadPermissionsFile(FILES.settings); + globalAllowPatterns = globalConfig.permissions?.allow ?? []; + globalDenyPatterns = globalConfig.permissions?.deny ?? []; + + // Load local permissions + const localConfig = await loadPermissionsFile( + path.join(workingDir, LOCAL_CONFIG_DIR, "settings.json"), + ); + localAllowPatterns = localConfig.permissions?.allow ?? []; + localDenyPatterns = localConfig.permissions?.deny ?? []; + + sessionAllowPatterns = []; + initialized = true; +}; + +/** + * Parse a permission pattern string + */ +export const parsePattern = (pattern: string): PermissionPattern | null => { + // Match patterns like: Bash(command:args), Read(path), WebFetch(domain:example.com) + const match = pattern.match(/^(\w+)\(([^)]*)\)$/); + if (!match) { + // Simple patterns like "WebSearch" + if (SIMPLE_TOOLS.includes(pattern as ToolType)) { + return { tool: pattern as ToolType }; + } + return null; + } + + const tool = match[1] as ToolType; + const content = match[2]; + + if (tool === "Bash") { + // Format: Bash(command:args) or Bash(command with spaces:args) + const colonIdx = content.lastIndexOf(":"); + if (colonIdx === -1) { + return { tool, command: content, args: "*" }; + } + const command = content.slice(0, colonIdx); + const args = content.slice(colonIdx + 1); + return { tool, command, args }; + } + + if (tool === "WebFetch") { + if (content.startsWith("domain:")) { + return { tool, domain: content.slice(7) }; + } + return { tool, path: content }; + } + + // Read, Write, Edit - just path pattern + return { tool, path: content }; +}; + +/** + * Check if a command matches a parsed pattern + */ +export const matchesBashPattern = ( + command: string, + pattern: PermissionPattern, +): boolean => { + if (pattern.tool !== "Bash") return false; + + const patternCmd = pattern.command ?? ""; + const patternArgs = pattern.args ?? "*"; + + if (!command.startsWith(patternCmd)) { + return false; + } + + if (patternArgs === "*") { + return command === patternCmd || command.startsWith(patternCmd + " "); + } + + const cmdArgs = command.slice(patternCmd.length).trim(); + + if (patternArgs.endsWith("*")) { + const prefix = patternArgs.slice(0, -1); + return cmdArgs.startsWith(prefix); + } + + return cmdArgs === patternArgs; +}; + +/** + * Check if a file path matches a pattern + */ +export const matchesPathPattern = ( + filePath: string, + pattern: string, +): boolean => { + if (pattern === "*") return true; + + const normalizedPath = path.normalize(filePath); + const normalizedPattern = path.normalize(pattern); + + if (pattern.endsWith("*")) { + const prefix = normalizedPattern.slice(0, -1); + return normalizedPath.startsWith(prefix); + } + + if (pattern.startsWith("*.")) { + const ext = pattern.slice(1); + return normalizedPath.endsWith(ext); + } + + return ( + normalizedPath === normalizedPattern || + normalizedPath.includes(normalizedPattern) + ); +}; + +/** + * Check if a Bash command is allowed + */ +export const isBashAllowed = (command: string): boolean => { + const allPatterns = [ + ...sessionAllowPatterns, + ...localAllowPatterns, + ...globalAllowPatterns, + ]; + + for (const patternStr of allPatterns) { + const pattern = parsePattern(patternStr); + if ( + pattern && + pattern.tool === "Bash" && + matchesBashPattern(command, pattern) + ) { + return true; + } + } + + return false; +}; + +/** + * Check if a Bash command is denied + */ +export const isBashDenied = (command: string): boolean => { + const denyPatterns = [...localDenyPatterns, ...globalDenyPatterns]; + + for (const patternStr of denyPatterns) { + const pattern = parsePattern(patternStr); + if ( + pattern && + pattern.tool === "Bash" && + matchesBashPattern(command, pattern) + ) { + return true; + } + } + + return false; +}; + +/** + * Check if a file operation is allowed + */ +export const isFileOpAllowed = ( + tool: "Read" | "Write" | "Edit", + filePath: string, +): boolean => { + const allPatterns = [ + ...sessionAllowPatterns, + ...localAllowPatterns, + ...globalAllowPatterns, + ]; + + for (const patternStr of allPatterns) { + const pattern = parsePattern(patternStr); + if (pattern && pattern.tool === tool) { + if (pattern.path && matchesPathPattern(filePath, pattern.path)) { + return true; + } + } + } + + return false; +}; + +/** + * Generate a pattern for the given command + */ +export const generateBashPattern = (command: string): string => { + const parts = command.trim().split(/\s+/); + + if (parts.length === 0) return `Bash(${command}:*)`; + + const firstWord = parts[0]; + + if (MULTI_WORD_PREFIXES.includes(firstWord) && parts.length > 1) { + const cmdPrefix = `${parts[0]} ${parts[1]}`; + return `Bash(${cmdPrefix}:*)`; + } + + return `Bash(${firstWord}:*)`; +}; + +/** + * Add a pattern to session allow list + */ +export const addSessionPattern = (pattern: string): void => { + if (!sessionAllowPatterns.includes(pattern)) { + sessionAllowPatterns.push(pattern); + } +}; + +/** + * Save global permissions + */ +const saveGlobalPermissions = async (): Promise => { + let config: Record = {}; + try { + const data = await fs.readFile(FILES.settings, "utf-8"); + config = JSON.parse(data); + } catch { + // File doesn't exist + } + + config.permissions = { + allow: globalAllowPatterns, + deny: globalDenyPatterns.length > 0 ? globalDenyPatterns : undefined, + }; + + await fs.mkdir(DIRS.config, { recursive: true }); + await fs.writeFile(FILES.settings, JSON.stringify(config, null, 2), "utf-8"); +}; + +/** + * Add a pattern to global allow list + */ +export const addGlobalPattern = async (pattern: string): Promise => { + if (!globalAllowPatterns.includes(pattern)) { + globalAllowPatterns.push(pattern); + await saveGlobalPermissions(); + } +}; + +/** + * Save local permissions + */ +const saveLocalPermissions = async (): Promise => { + const filePath = path.join(workingDir, LOCAL_CONFIG_DIR, "settings.json"); + + let config: Record = {}; + try { + const data = await fs.readFile(filePath, "utf-8"); + config = JSON.parse(data); + } catch { + // File doesn't exist + } + + config.permissions = { + allow: localAllowPatterns, + deny: localDenyPatterns.length > 0 ? localDenyPatterns : undefined, + }; + + const configDir = path.join(workingDir, LOCAL_CONFIG_DIR); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(config, null, 2), "utf-8"); +}; + +/** + * Add a pattern to local allow list + */ +export const addLocalPattern = async (pattern: string): Promise => { + if (!localAllowPatterns.includes(pattern)) { + localAllowPatterns.push(pattern); + await saveLocalPermissions(); + } +}; + +/** + * List all patterns + */ +export const listPatterns = (): { + global: string[]; + local: string[]; + session: string[]; +} => ({ + global: [...globalAllowPatterns], + local: [...localAllowPatterns], + session: [...sessionAllowPatterns], +}); + +/** + * Clear session patterns + */ +export const clearSessionPatterns = (): void => { + sessionAllowPatterns = []; +}; + +/** + * Handle permission scope + */ +const handlePermissionScope = async ( + scope: string, + pattern: string, +): Promise => { + const scopeHandlers: Record Promise | void> = { + session: () => addSessionPattern(pattern), + local: () => addLocalPattern(pattern), + global: () => addGlobalPattern(pattern), + }; + + const handler = scopeHandlers[scope]; + if (handler) { + await handler(); + } +}; + +/** + * Prompt user for permission to execute a bash command + */ +export const promptBashPermission = async ( + command: string, + description: string, +): Promise<{ allowed: boolean; remember?: "session" | "global" | "local" }> => { + if (isBashDenied(command)) { + return { allowed: false }; + } + + if (isBashAllowed(command)) { + return { allowed: true }; + } + + const suggestedPattern = generateBashPattern(command); + + // Use custom handler if set (TUI mode) + if (permissionHandler) { + const response = await permissionHandler({ + type: "bash", + command, + description, + pattern: suggestedPattern, + }); + + if (response.allowed && response.scope) { + await handlePermissionScope(response.scope, suggestedPattern); + } + + return { + allowed: response.allowed, + remember: response.scope === "once" ? undefined : response.scope, + }; + } + + // Fall back to readline-based prompt + console.log("\n" + chalk.yellow("Permission Required")); + console.log(chalk.gray("─".repeat(50))); + console.log(chalk.cyan("Command: ") + chalk.white(command)); + if (description) { + console.log(chalk.cyan("Description: ") + chalk.gray(description)); + } + console.log(chalk.cyan("Pattern: ") + chalk.magenta(suggestedPattern)); + console.log(chalk.gray("─".repeat(50))); + console.log(""); + console.log(" " + chalk.green("[y]") + " Yes, allow this command"); + console.log( + " " + chalk.blue("[s]") + " Yes, and allow pattern for this session", + ); + console.log( + " " + chalk.cyan("[l]") + " Yes, and allow pattern for this project", + ); + console.log(" " + chalk.magenta("[g]") + " Yes, and allow pattern globally"); + console.log(" " + chalk.red("[n]") + " No, deny this command"); + console.log(""); + + return new Promise((resolve) => { + process.stdout.write(chalk.yellow("Choice [y/s/l/g/n]: ")); + + const handleInput = async (data: Buffer) => { + const answer = data.toString().trim().toLowerCase(); + process.stdin.removeListener("data", handleInput); + process.stdin.setRawMode?.(false); + + const responseMap: Record Promise> = { + y: async () => resolve({ allowed: true }), + yes: async () => resolve({ allowed: true }), + s: async () => { + addSessionPattern(suggestedPattern); + console.log( + chalk.blue(`\n✓ Added session pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "session" }); + }, + session: async () => { + addSessionPattern(suggestedPattern); + console.log( + chalk.blue(`\n✓ Added session pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "session" }); + }, + l: async () => { + await addLocalPattern(suggestedPattern); + console.log( + chalk.cyan(`\n✓ Added project pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "local" }); + }, + local: async () => { + await addLocalPattern(suggestedPattern); + console.log( + chalk.cyan(`\n✓ Added project pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "local" }); + }, + project: async () => { + await addLocalPattern(suggestedPattern); + console.log( + chalk.cyan(`\n✓ Added project pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "local" }); + }, + g: async () => { + await addGlobalPattern(suggestedPattern); + console.log( + chalk.magenta(`\n✓ Added global pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "global" }); + }, + global: async () => { + await addGlobalPattern(suggestedPattern); + console.log( + chalk.magenta(`\n✓ Added global pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "global" }); + }, + }; + + const handler = responseMap[answer]; + if (handler) { + await handler(); + } else { + resolve({ allowed: false }); + } + }; + + process.stdin.setRawMode?.(true); + process.stdin.resume(); + process.stdin.once("data", handleInput); + }); +}; + +/** + * Prompt user for permission to perform a file operation + */ +export const promptFilePermission = async ( + tool: "Read" | "Write" | "Edit", + filePath: string, + description?: string, +): Promise<{ allowed: boolean; remember?: "session" | "global" | "local" }> => { + if (isFileOpAllowed(tool, filePath)) { + return { allowed: true }; + } + + const ext = path.extname(filePath); + const suggestedPattern = ext + ? `${tool}(*${ext})` + : `${tool}(${path.basename(filePath)})`; + + if (permissionHandler) { + const response = await permissionHandler({ + type: tool.toLowerCase() as "read" | "write" | "edit", + path: filePath, + description: description ?? `${tool} ${filePath}`, + pattern: suggestedPattern, + }); + + if (response.allowed && response.scope) { + await handlePermissionScope(response.scope, suggestedPattern); + } + + return { + allowed: response.allowed, + remember: response.scope === "once" ? undefined : response.scope, + }; + } + + // Fall back to readline-based prompt + console.log("\n" + chalk.yellow("Permission Required")); + console.log(chalk.gray("─".repeat(50))); + console.log(chalk.cyan(`${tool}: `) + chalk.white(filePath)); + if (description) { + console.log(chalk.cyan("Description: ") + chalk.gray(description)); + } + console.log(chalk.cyan("Pattern: ") + chalk.magenta(suggestedPattern)); + console.log(chalk.gray("─".repeat(50))); + console.log(""); + console.log( + " " + chalk.green("[y]") + ` Yes, allow this ${tool.toLowerCase()}`, + ); + console.log( + " " + chalk.blue("[s]") + " Yes, and allow pattern for this session", + ); + console.log( + " " + chalk.cyan("[l]") + " Yes, and allow pattern for this project", + ); + console.log(" " + chalk.magenta("[g]") + " Yes, and allow pattern globally"); + console.log(" " + chalk.red("[n]") + ` No, deny this ${tool.toLowerCase()}`); + console.log(""); + + return new Promise((resolve) => { + process.stdout.write(chalk.yellow("Choice [y/s/l/g/n]: ")); + + const handleInput = async (data: Buffer) => { + const answer = data.toString().trim().toLowerCase(); + process.stdin.removeListener("data", handleInput); + process.stdin.setRawMode?.(false); + + const responseMap: Record Promise> = { + y: async () => resolve({ allowed: true }), + yes: async () => resolve({ allowed: true }), + s: async () => { + addSessionPattern(suggestedPattern); + console.log( + chalk.blue(`\n✓ Added session pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "session" }); + }, + session: async () => { + addSessionPattern(suggestedPattern); + console.log( + chalk.blue(`\n✓ Added session pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "session" }); + }, + l: async () => { + await addLocalPattern(suggestedPattern); + console.log( + chalk.cyan(`\n✓ Added project pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "local" }); + }, + local: async () => { + await addLocalPattern(suggestedPattern); + console.log( + chalk.cyan(`\n✓ Added project pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "local" }); + }, + project: async () => { + await addLocalPattern(suggestedPattern); + console.log( + chalk.cyan(`\n✓ Added project pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "local" }); + }, + g: async () => { + await addGlobalPattern(suggestedPattern); + console.log( + chalk.magenta(`\n✓ Added global pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "global" }); + }, + global: async () => { + await addGlobalPattern(suggestedPattern); + console.log( + chalk.magenta(`\n✓ Added global pattern: ${suggestedPattern}`), + ); + resolve({ allowed: true, remember: "global" }); + }, + }; + + const handler = responseMap[answer]; + if (handler) { + await handler(); + } else { + resolve({ allowed: false }); + } + }; + + process.stdin.setRawMode?.(true); + process.stdin.resume(); + process.stdin.once("data", handleInput); + }); +}; + +/** + * Legacy method for backwards compatibility + */ +export const promptPermission = async ( + command: string, + description: string, +): Promise<{ allowed: boolean; remember?: "session" | "global" }> => { + const result = await promptBashPermission(command, description); + return { + allowed: result.allowed, + remember: result.remember === "local" ? "global" : result.remember, + }; +}; + +/** + * Legacy method + */ +export const getPermissionLevel = ( + command: string, +): "ask" | "allow_session" | "allow_global" | "deny" => { + if (isBashDenied(command)) return "deny"; + if (isBashAllowed(command)) return "allow_global"; + return "ask"; +}; + +// Re-export types +export type { + ToolType, + PermissionPattern, + PermissionsConfig, + PermissionPromptRequest, + PermissionPromptResponse, + PermissionHandler, +} from "@/types/permissions"; diff --git a/src/services/permissions/__tests__/bash-matcher.test.ts b/src/services/permissions/__tests__/bash-matcher.test.ts new file mode 100644 index 0000000..d710122 --- /dev/null +++ b/src/services/permissions/__tests__/bash-matcher.test.ts @@ -0,0 +1,152 @@ +/** + * Unit tests for Bash Pattern Matcher + */ + +import { describe, it, expect } from "bun:test"; + +import { + matchesBashPattern, + isBashAllowedByIndex, + findMatchingBashPatterns, + generateBashPattern, + extractCommandPrefix, +} from "@services/permissions/matchers/bash"; +import { buildPatternIndex } from "@services/permissions/pattern-index"; +import type { PermissionPattern } from "@/types/permissions"; + +describe("Bash Pattern Matcher", () => { + describe("matchesBashPattern", () => { + it("should match exact command with wildcard args", () => { + const pattern: PermissionPattern = { + tool: "Bash", + command: "git", + args: "*", + }; + + expect(matchesBashPattern("git", pattern)).toBe(true); + expect(matchesBashPattern("git status", pattern)).toBe(true); + expect(matchesBashPattern("git commit -m 'msg'", pattern)).toBe(true); + }); + + it("should not match different command", () => { + const pattern: PermissionPattern = { + tool: "Bash", + command: "git", + args: "*", + }; + + expect(matchesBashPattern("npm install", pattern)).toBe(false); + expect(matchesBashPattern("gitx status", pattern)).toBe(false); + }); + + it("should match command with specific args prefix", () => { + const pattern: PermissionPattern = { + tool: "Bash", + command: "git", + args: "status*", + }; + + expect(matchesBashPattern("git status", pattern)).toBe(true); + expect(matchesBashPattern("git status --short", pattern)).toBe(true); + expect(matchesBashPattern("git commit", pattern)).toBe(false); + }); + + it("should match exact args", () => { + const pattern: PermissionPattern = { + tool: "Bash", + command: "npm", + args: "install", + }; + + expect(matchesBashPattern("npm install", pattern)).toBe(true); + expect(matchesBashPattern("npm install lodash", pattern)).toBe(false); + }); + + it("should reject non-Bash patterns", () => { + const pattern: PermissionPattern = { + tool: "Read", + path: "*", + }; + + expect(matchesBashPattern("ls", pattern)).toBe(false); + }); + }); + + describe("isBashAllowedByIndex", () => { + it("should check against index patterns", () => { + const index = buildPatternIndex(["Bash(git:*)", "Bash(npm install:*)"]); + + expect(isBashAllowedByIndex("git status", index)).toBe(true); + expect(isBashAllowedByIndex("git commit", index)).toBe(true); + expect(isBashAllowedByIndex("npm install lodash", index)).toBe(true); + expect(isBashAllowedByIndex("npm run build", index)).toBe(false); + expect(isBashAllowedByIndex("rm -rf /", index)).toBe(false); + }); + + it("should return false for empty index", () => { + const index = buildPatternIndex([]); + + expect(isBashAllowedByIndex("git status", index)).toBe(false); + }); + }); + + describe("findMatchingBashPatterns", () => { + it("should find all matching patterns", () => { + const index = buildPatternIndex([ + "Bash(git:*)", + "Bash(git status:*)", + "Bash(npm:*)", + ]); + + const matches = findMatchingBashPatterns("git status", index); + + expect(matches.length).toBe(2); + expect(matches.map((m) => m.raw)).toContain("Bash(git:*)"); + expect(matches.map((m) => m.raw)).toContain("Bash(git status:*)"); + }); + + it("should return empty for no matches", () => { + const index = buildPatternIndex(["Bash(git:*)"]); + + const matches = findMatchingBashPatterns("npm install", index); + + expect(matches).toHaveLength(0); + }); + }); + + describe("generateBashPattern", () => { + it("should generate pattern for multi-word commands", () => { + expect(generateBashPattern("git status")).toBe("Bash(git status:*)"); + expect(generateBashPattern("npm install lodash")).toBe( + "Bash(npm install:*)", + ); + expect(generateBashPattern("docker run nginx")).toBe( + "Bash(docker run:*)", + ); + }); + + it("should generate pattern for single commands", () => { + expect(generateBashPattern("ls")).toBe("Bash(ls:*)"); + expect(generateBashPattern("pwd")).toBe("Bash(pwd:*)"); + }); + + it("should handle commands with many args", () => { + expect(generateBashPattern("git commit -m 'message'")).toBe( + "Bash(git commit:*)", + ); + }); + }); + + describe("extractCommandPrefix", () => { + it("should extract multi-word prefix", () => { + expect(extractCommandPrefix("git status")).toBe("git status"); + expect(extractCommandPrefix("npm install lodash")).toBe("npm install"); + expect(extractCommandPrefix("bun test --watch")).toBe("bun test"); + }); + + it("should extract single word for non-recognized commands", () => { + expect(extractCommandPrefix("ls -la")).toBe("ls"); + expect(extractCommandPrefix("cat file.txt")).toBe("cat"); + }); + }); +}); diff --git a/src/services/permissions/__tests__/path-matcher.test.ts b/src/services/permissions/__tests__/path-matcher.test.ts new file mode 100644 index 0000000..04b366d --- /dev/null +++ b/src/services/permissions/__tests__/path-matcher.test.ts @@ -0,0 +1,158 @@ +/** + * Unit tests for Path Pattern Matcher + */ + +import { describe, it, expect } from "bun:test"; + +import { + matchesPathPattern, + matchesFilePattern, + isFileOpAllowedByIndex, + findMatchingFilePatterns, + generateFilePattern, + normalizePath, + isPathInDirectory, +} from "@services/permissions/matchers/path"; +import { buildPatternIndex } from "@services/permissions/pattern-index"; +import type { PermissionPattern } from "@/types/permissions"; + +describe("Path Pattern Matcher", () => { + describe("matchesPathPattern", () => { + it("should match wildcard pattern", () => { + expect(matchesPathPattern("/any/path/file.ts", "*")).toBe(true); + expect(matchesPathPattern("relative/file.js", "*")).toBe(true); + }); + + it("should match directory prefix pattern", () => { + expect(matchesPathPattern("src/file.ts", "src/*")).toBe(true); + expect(matchesPathPattern("src/nested/file.ts", "src/*")).toBe(true); + expect(matchesPathPattern("tests/file.ts", "src/*")).toBe(false); + }); + + it("should match extension pattern", () => { + expect(matchesPathPattern("file.ts", "*.ts")).toBe(true); + expect(matchesPathPattern("src/nested/file.ts", "*.ts")).toBe(true); + expect(matchesPathPattern("file.js", "*.ts")).toBe(false); + }); + + it("should match exact path", () => { + expect(matchesPathPattern("src/file.ts", "src/file.ts")).toBe(true); + expect(matchesPathPattern("src/other.ts", "src/file.ts")).toBe(false); + }); + + it("should match substring", () => { + expect( + matchesPathPattern("/path/to/config/settings.json", "config"), + ).toBe(true); + }); + }); + + describe("matchesFilePattern", () => { + it("should match with parsed pattern", () => { + const pattern: PermissionPattern = { + tool: "Read", + path: "*.ts", + }; + + expect(matchesFilePattern("file.ts", pattern)).toBe(true); + expect(matchesFilePattern("file.js", pattern)).toBe(false); + }); + + it("should return false for pattern without path", () => { + const pattern: PermissionPattern = { + tool: "Bash", + command: "git", + }; + + expect(matchesFilePattern("file.ts", pattern)).toBe(false); + }); + }); + + describe("isFileOpAllowedByIndex", () => { + it("should check Read operations", () => { + const index = buildPatternIndex(["Read(*.ts)", "Read(src/*)"]); + + expect(isFileOpAllowedByIndex("Read", "file.ts", index)).toBe(true); + expect(isFileOpAllowedByIndex("Read", "src/nested.js", index)).toBe(true); + expect(isFileOpAllowedByIndex("Read", "tests/file.js", index)).toBe( + false, + ); + }); + + it("should check Write operations separately", () => { + const index = buildPatternIndex(["Read(*)", "Write(src/*)"]); + + expect(isFileOpAllowedByIndex("Read", "any/file.ts", index)).toBe(true); + expect(isFileOpAllowedByIndex("Write", "any/file.ts", index)).toBe(false); + expect(isFileOpAllowedByIndex("Write", "src/file.ts", index)).toBe(true); + }); + + it("should return false for empty index", () => { + const index = buildPatternIndex([]); + + expect(isFileOpAllowedByIndex("Read", "file.ts", index)).toBe(false); + }); + }); + + describe("findMatchingFilePatterns", () => { + it("should find all matching patterns", () => { + const index = buildPatternIndex(["Read(*)", "Read(*.ts)", "Read(src/*)"]); + + const matches = findMatchingFilePatterns("Read", "src/file.ts", index); + + expect(matches.length).toBe(3); + }); + + it("should return empty for no matches", () => { + const index = buildPatternIndex(["Read(src/*)"]); + + const matches = findMatchingFilePatterns("Read", "tests/file.ts", index); + + expect(matches).toHaveLength(0); + }); + }); + + describe("generateFilePattern", () => { + it("should generate extension-based pattern for common extensions", () => { + expect(generateFilePattern("Read", "file.ts")).toBe("Read(*.ts)"); + expect(generateFilePattern("Write", "file.json")).toBe("Write(*.json)"); + expect(generateFilePattern("Edit", "file.tsx")).toBe("Edit(*.tsx)"); + }); + + it("should generate directory-based pattern when appropriate", () => { + expect(generateFilePattern("Read", "src/file.xyz")).toBe("Read(src/*)"); + }); + + it("should fall back to basename", () => { + expect(generateFilePattern("Read", "Makefile")).toBe("Read(Makefile)"); + }); + }); + + describe("normalizePath", () => { + it("should normalize path separators", () => { + expect(normalizePath("src/file.ts")).toBe("src/file.ts"); + expect(normalizePath("src//file.ts")).toBe("src/file.ts"); + expect(normalizePath("./src/file.ts")).toBe("src/file.ts"); + }); + }); + + describe("isPathInDirectory", () => { + it("should check if path is in directory", () => { + expect(isPathInDirectory("/project/src/file.ts", "/project/src")).toBe( + true, + ); + expect( + isPathInDirectory("/project/src/nested/file.ts", "/project/src"), + ).toBe(true); + expect(isPathInDirectory("/project/tests/file.ts", "/project/src")).toBe( + false, + ); + }); + + it("should not match partial directory names", () => { + expect( + isPathInDirectory("/project/src-backup/file.ts", "/project/src"), + ).toBe(false); + }); + }); +}); diff --git a/src/services/permissions/__tests__/pattern-index.test.ts b/src/services/permissions/__tests__/pattern-index.test.ts new file mode 100644 index 0000000..be1d066 --- /dev/null +++ b/src/services/permissions/__tests__/pattern-index.test.ts @@ -0,0 +1,186 @@ +/** + * Unit tests for Permission Pattern Index + */ + +import { describe, it, expect } from "bun:test"; + +import { + createPatternIndex, + buildPatternIndex, + addToIndex, + removeFromIndex, + getPatternsForTool, + hasPattern, + getRawPatterns, + mergeIndexes, + getIndexStats, +} from "@services/permissions/pattern-index"; + +describe("Permission Pattern Index", () => { + describe("createPatternIndex", () => { + it("should create empty index", () => { + const index = createPatternIndex(); + + expect(index.all).toHaveLength(0); + expect(index.byTool.size).toBe(0); + }); + }); + + describe("buildPatternIndex", () => { + it("should build index from patterns", () => { + const patterns = [ + "Bash(git:*)", + "Bash(npm install:*)", + "Read(*)", + "Write(src/*)", + ]; + + const index = buildPatternIndex(patterns); + + expect(index.all).toHaveLength(4); + expect(index.byTool.get("Bash")).toHaveLength(2); + expect(index.byTool.get("Read")).toHaveLength(1); + expect(index.byTool.get("Write")).toHaveLength(1); + }); + + it("should skip invalid patterns", () => { + const patterns = ["Bash(git:*)", "invalid pattern", "Read(*)"]; + + const index = buildPatternIndex(patterns); + + expect(index.all).toHaveLength(2); + }); + + it("should handle empty array", () => { + const index = buildPatternIndex([]); + + expect(index.all).toHaveLength(0); + }); + }); + + describe("addToIndex", () => { + it("should add pattern to index", () => { + let index = createPatternIndex(); + index = addToIndex(index, "Bash(git:*)"); + + expect(index.all).toHaveLength(1); + expect(hasPattern(index, "Bash(git:*)")).toBe(true); + }); + + it("should not duplicate patterns", () => { + let index = buildPatternIndex(["Bash(git:*)"]); + index = addToIndex(index, "Bash(git:*)"); + + expect(index.all).toHaveLength(1); + }); + + it("should add to correct tool bucket", () => { + let index = createPatternIndex(); + index = addToIndex(index, "Read(src/*)"); + + expect(getPatternsForTool(index, "Read")).toHaveLength(1); + expect(getPatternsForTool(index, "Bash")).toHaveLength(0); + }); + }); + + describe("removeFromIndex", () => { + it("should remove pattern from index", () => { + let index = buildPatternIndex(["Bash(git:*)", "Read(*)"]); + index = removeFromIndex(index, "Bash(git:*)"); + + expect(index.all).toHaveLength(1); + expect(hasPattern(index, "Bash(git:*)")).toBe(false); + expect(hasPattern(index, "Read(*)")).toBe(true); + }); + + it("should handle non-existent pattern", () => { + const index = buildPatternIndex(["Bash(git:*)"]); + const result = removeFromIndex(index, "Read(*)"); + + expect(result.all).toHaveLength(1); + }); + }); + + describe("getPatternsForTool", () => { + it("should return patterns for specific tool", () => { + const index = buildPatternIndex([ + "Bash(git:*)", + "Bash(npm:*)", + "Read(*)", + ]); + + const bashPatterns = getPatternsForTool(index, "Bash"); + const readPatterns = getPatternsForTool(index, "Read"); + const writePatterns = getPatternsForTool(index, "Write"); + + expect(bashPatterns).toHaveLength(2); + expect(readPatterns).toHaveLength(1); + expect(writePatterns).toHaveLength(0); + }); + }); + + describe("getRawPatterns", () => { + it("should return all raw pattern strings", () => { + const patterns = ["Bash(git:*)", "Read(*)"]; + const index = buildPatternIndex(patterns); + + const raw = getRawPatterns(index); + + expect(raw).toEqual(patterns); + }); + }); + + describe("mergeIndexes", () => { + it("should merge multiple indexes", () => { + const index1 = buildPatternIndex(["Bash(git:*)"]); + const index2 = buildPatternIndex(["Read(*)"]); + const index3 = buildPatternIndex(["Write(src/*)"]); + + const merged = mergeIndexes(index1, index2, index3); + + expect(merged.all).toHaveLength(3); + expect(getPatternsForTool(merged, "Bash")).toHaveLength(1); + expect(getPatternsForTool(merged, "Read")).toHaveLength(1); + expect(getPatternsForTool(merged, "Write")).toHaveLength(1); + }); + + it("should preserve duplicates from different indexes", () => { + const index1 = buildPatternIndex(["Bash(git:*)"]); + const index2 = buildPatternIndex(["Bash(git:*)"]); + + const merged = mergeIndexes(index1, index2); + + // Duplicates preserved (session might override global) + expect(merged.all).toHaveLength(2); + }); + + it("should handle empty indexes", () => { + const index1 = createPatternIndex(); + const index2 = buildPatternIndex(["Read(*)"]); + + const merged = mergeIndexes(index1, index2); + + expect(merged.all).toHaveLength(1); + }); + }); + + describe("getIndexStats", () => { + it("should return correct statistics", () => { + const index = buildPatternIndex([ + "Bash(git:*)", + "Bash(npm:*)", + "Read(*)", + "Write(src/*)", + "Edit(*.ts)", + ]); + + const stats = getIndexStats(index); + + expect(stats.total).toBe(5); + expect(stats.byTool["Bash"]).toBe(2); + expect(stats.byTool["Read"]).toBe(1); + expect(stats.byTool["Write"]).toBe(1); + expect(stats.byTool["Edit"]).toBe(1); + }); + }); +}); diff --git a/src/services/permissions/index.ts b/src/services/permissions/index.ts new file mode 100644 index 0000000..cf57730 --- /dev/null +++ b/src/services/permissions/index.ts @@ -0,0 +1,75 @@ +/** + * Permission System Index + * + * Provides optimized permission checking with cached patterns and indexed lookups. + * Maintains backward compatibility with existing API. + */ + +// Re-export optimized implementation +export { + initializePermissions, + resetPermissions, + setWorkingDir, + setPermissionHandler, + isBashAllowed, + isBashDenied, + isFileOpAllowed, + addSessionPattern, + addGlobalPattern, + addLocalPattern, + listPatterns, + clearSessionPatterns, + getPermissionStats, + generateBashPattern, + generateFilePattern, + parsePattern, + matchesBashPattern, + matchesPathPattern, + promptBashPermission, + promptFilePermission, + promptPermission, + getPermissionLevel, +} from "@services/permissions/optimized"; + +// Re-export pattern cache utilities +export { + clearPatternCache, + getCacheStats, + warmCache, +} from "@services/permissions/pattern-cache"; + +// Re-export pattern index utilities +export { + buildPatternIndex, + addToIndex, + removeFromIndex, + getPatternsForTool, + hasPattern, + getRawPatterns, + mergeIndexes, + getIndexStats, + type PatternIndex, + type PatternEntry, + type IndexStats, +} from "@services/permissions/pattern-index"; + +// Re-export matchers +export { + isBashAllowedByIndex, + findMatchingBashPatterns, + extractCommandPrefix, + isFileOpAllowedByIndex, + findMatchingFilePatterns, + normalizePath, + isPathInDirectory, +} from "@services/permissions/matchers"; + +// Re-export types +export type { + ToolType, + PermissionPattern, + PermissionsConfig, + PermissionPromptRequest, + PermissionPromptResponse, + PermissionHandler, +} from "@/types/permissions"; diff --git a/src/services/permissions/matchers/bash.ts b/src/services/permissions/matchers/bash.ts new file mode 100644 index 0000000..475b5c7 --- /dev/null +++ b/src/services/permissions/matchers/bash.ts @@ -0,0 +1,142 @@ +/** + * Bash Command Pattern Matcher + * + * Optimized matching for Bash command patterns + */ + +import type { PermissionPattern } from "@/types/permissions"; +import type { + PatternEntry, + PatternIndex, +} from "@services/permissions/pattern-index"; +import { getPatternsForTool } from "@services/permissions/pattern-index"; + +// ============================================================================= +// Pattern Matching +// ============================================================================= + +/** + * Check if a command matches a parsed Bash pattern + */ +export const matchesBashPattern = ( + command: string, + pattern: PermissionPattern, +): boolean => { + if (pattern.tool !== "Bash") return false; + + const patternCmd = pattern.command ?? ""; + const patternArgs = pattern.args ?? "*"; + + // Command must start with pattern command + if (!command.startsWith(patternCmd)) { + return false; + } + + // Wildcard args: match exact command or command with space + if (patternArgs === "*") { + return command === patternCmd || command.startsWith(patternCmd + " "); + } + + // Extract actual args from command + const cmdArgs = command.slice(patternCmd.length).trim(); + + // Prefix match for args ending with * + if (patternArgs.endsWith("*")) { + const prefix = patternArgs.slice(0, -1); + return cmdArgs.startsWith(prefix); + } + + // Exact match on args + return cmdArgs === patternArgs; +}; + +// ============================================================================= +// Index-Based Matching +// ============================================================================= + +/** + * Check if a command is allowed by any pattern in the index + */ +export const isBashAllowedByIndex = ( + command: string, + index: PatternIndex, +): boolean => { + const bashPatterns = getPatternsForTool(index, "Bash"); + + for (const entry of bashPatterns) { + if (matchesBashPattern(command, entry.parsed)) { + return true; + } + } + + return false; +}; + +/** + * Find all matching patterns for a command + */ +export const findMatchingBashPatterns = ( + command: string, + index: PatternIndex, +): PatternEntry[] => { + const bashPatterns = getPatternsForTool(index, "Bash"); + return bashPatterns.filter((entry) => + matchesBashPattern(command, entry.parsed), + ); +}; + +// ============================================================================= +// Pattern Generation +// ============================================================================= + +const MULTI_WORD_PREFIXES = [ + "git", + "npm", + "yarn", + "pnpm", + "bun", + "docker", + "kubectl", + "make", + "cargo", + "go", +]; + +/** + * Generate a pattern suggestion for a command + */ +export const generateBashPattern = (command: string): string => { + const parts = command.trim().split(/\s+/); + + if (parts.length === 0) { + return `Bash(${command}:*)`; + } + + const firstWord = parts[0]; + + // For multi-word commands like "git status", use "git status" as prefix + if (MULTI_WORD_PREFIXES.includes(firstWord) && parts.length > 1) { + const cmdPrefix = `${parts[0]} ${parts[1]}`; + return `Bash(${cmdPrefix}:*)`; + } + + // For single commands, use just the command name + return `Bash(${firstWord}:*)`; +}; + +/** + * Extract command prefix for indexing/grouping + */ +export const extractCommandPrefix = (command: string): string => { + const parts = command.trim().split(/\s+/); + + if (parts.length === 0) return command; + + const firstWord = parts[0]; + + if (MULTI_WORD_PREFIXES.includes(firstWord) && parts.length > 1) { + return `${parts[0]} ${parts[1]}`; + } + + return firstWord; +}; diff --git a/src/services/permissions/matchers/index.ts b/src/services/permissions/matchers/index.ts new file mode 100644 index 0000000..fc82c91 --- /dev/null +++ b/src/services/permissions/matchers/index.ts @@ -0,0 +1,23 @@ +/** + * Permission Matchers Index + * + * Re-exports all matchers for convenient access + */ + +export { + matchesBashPattern, + isBashAllowedByIndex, + findMatchingBashPatterns, + generateBashPattern, + extractCommandPrefix, +} from "@services/permissions/matchers/bash"; + +export { + matchesPathPattern, + matchesFilePattern, + isFileOpAllowedByIndex, + findMatchingFilePatterns, + generateFilePattern, + normalizePath, + isPathInDirectory, +} from "@services/permissions/matchers/path"; diff --git a/src/services/permissions/matchers/path.ts b/src/services/permissions/matchers/path.ts new file mode 100644 index 0000000..1203871 --- /dev/null +++ b/src/services/permissions/matchers/path.ts @@ -0,0 +1,190 @@ +/** + * File Path Pattern Matcher + * + * Optimized matching for Read/Write/Edit patterns + */ + +import * as path from "path"; + +import type { PermissionPattern } from "@/types/permissions"; +import type { + PatternEntry, + PatternIndex, +} from "@services/permissions/pattern-index"; +import { getPatternsForTool } from "@services/permissions/pattern-index"; + +// ============================================================================= +// Pattern Matching +// ============================================================================= + +/** + * Check if a file path matches a pattern string + */ +export const matchesPathPattern = ( + filePath: string, + pattern: string, +): boolean => { + // Wildcard: match everything + if (pattern === "*") return true; + + const normalizedPath = path.normalize(filePath); + const normalizedPattern = path.normalize(pattern); + + // Directory prefix: src/* matches src/foo.ts + if (pattern.endsWith("/*") || pattern.endsWith("*")) { + const prefix = normalizedPattern.slice(0, -1); + if (normalizedPath.startsWith(prefix)) { + return true; + } + } + + // Extension pattern: *.ts matches foo.ts + if (pattern.startsWith("*.")) { + const ext = pattern.slice(1); // .ts + if (normalizedPath.endsWith(ext)) { + return true; + } + } + + // Exact match + if (normalizedPath === normalizedPattern) { + return true; + } + + // Substring match (for partial paths) + if (normalizedPath.includes(normalizedPattern)) { + return true; + } + + return false; +}; + +/** + * Check if a file path matches a parsed pattern + */ +export const matchesFilePattern = ( + filePath: string, + pattern: PermissionPattern, +): boolean => { + if (!pattern.path) return false; + return matchesPathPattern(filePath, pattern.path); +}; + +// ============================================================================= +// Index-Based Matching +// ============================================================================= + +type FileOpTool = "Read" | "Write" | "Edit"; + +/** + * Check if a file operation is allowed by patterns in the index + */ +export const isFileOpAllowedByIndex = ( + tool: FileOpTool, + filePath: string, + index: PatternIndex, +): boolean => { + const patterns = getPatternsForTool(index, tool); + + for (const entry of patterns) { + if (entry.parsed.path && matchesPathPattern(filePath, entry.parsed.path)) { + return true; + } + } + + return false; +}; + +/** + * Find all matching patterns for a file operation + */ +export const findMatchingFilePatterns = ( + tool: FileOpTool, + filePath: string, + index: PatternIndex, +): PatternEntry[] => { + const patterns = getPatternsForTool(index, tool); + return patterns.filter( + (entry) => + entry.parsed.path && matchesPathPattern(filePath, entry.parsed.path), + ); +}; + +// ============================================================================= +// Pattern Generation +// ============================================================================= + +/** + * Generate a pattern suggestion for a file operation + */ +export const generateFilePattern = ( + tool: FileOpTool, + filePath: string, +): string => { + const ext = path.extname(filePath); + + // Prefer extension-based patterns for common extensions + if (ext && isCommonExtension(ext)) { + return `${tool}(*${ext})`; + } + + // Directory-based pattern + const dir = path.dirname(filePath); + if (dir && dir !== ".") { + return `${tool}(${dir}/*)`; + } + + // Fall back to basename + return `${tool}(${path.basename(filePath)})`; +}; + +/** + * Check if extension is common enough to suggest extension-based pattern + */ +const isCommonExtension = (ext: string): boolean => { + const common = [ + ".ts", + ".tsx", + ".js", + ".jsx", + ".json", + ".md", + ".css", + ".scss", + ".html", + ".yml", + ".yaml", + ".toml", + ".lua", + ".py", + ".go", + ".rs", + ".java", + ".c", + ".cpp", + ".h", + ]; + return common.includes(ext.toLowerCase()); +}; + +// ============================================================================= +// Path Utilities +// ============================================================================= + +/** + * Normalize a file path for consistent matching + */ +export const normalizePath = (filePath: string): string => + path.normalize(filePath); + +/** + * Check if a path is within a directory + */ +export const isPathInDirectory = ( + filePath: string, + directory: string, +): boolean => { + const normalizedFile = path.normalize(path.resolve(filePath)); + const normalizedDir = path.normalize(path.resolve(directory)); + return normalizedFile.startsWith(normalizedDir + path.sep); +}; diff --git a/src/services/permissions/optimized.ts b/src/services/permissions/optimized.ts new file mode 100644 index 0000000..ea1552a --- /dev/null +++ b/src/services/permissions/optimized.ts @@ -0,0 +1,448 @@ +/** + * Optimized Permission System + * + * Uses indexed patterns for O(n/k) lookup instead of O(n) + * Caches parsed patterns for zero re-parsing overhead + */ + +import fs from "fs/promises"; +import path from "path"; + +import { DIRS, FILES, LOCAL_CONFIG_DIR } from "@constants/paths"; +import type { PermissionsConfig, PermissionHandler } from "@/types/permissions"; + +import { + clearPatternCache, + warmCache, +} from "@services/permissions/pattern-cache"; +import { + buildPatternIndex, + addToIndex, + getRawPatterns, + mergeIndexes, + getIndexStats, + type PatternIndex, + type IndexStats, +} from "@services/permissions/pattern-index"; +import { + isBashAllowedByIndex, + generateBashPattern, +} from "@services/permissions/matchers/bash"; +import { + isFileOpAllowedByIndex, + generateFilePattern, +} from "@services/permissions/matchers/path"; + +// ============================================================================= +// State +// ============================================================================= + +interface PermissionState { + globalAllow: PatternIndex; + globalDeny: PatternIndex; + localAllow: PatternIndex; + localDeny: PatternIndex; + sessionAllow: PatternIndex; + initialized: boolean; + workingDir: string; + permissionHandler: PermissionHandler | null; +} + +let state: PermissionState = { + globalAllow: buildPatternIndex([]), + globalDeny: buildPatternIndex([]), + localAllow: buildPatternIndex([]), + localDeny: buildPatternIndex([]), + sessionAllow: buildPatternIndex([]), + initialized: false, + workingDir: process.cwd(), + permissionHandler: null, +}; + +// ============================================================================= +// Initialization +// ============================================================================= + +/** + * Load permissions from a file + */ +const loadPermissionsFile = async ( + filePath: string, +): Promise => { + try { + const data = await fs.readFile(filePath, "utf-8"); + return JSON.parse(data); + } catch { + return { permissions: { allow: [] } }; + } +}; + +/** + * Initialize the permission system + */ +export const initializePermissions = async (): Promise => { + if (state.initialized) return; + + // Load global permissions + const globalConfig = await loadPermissionsFile(FILES.settings); + const globalAllowPatterns = globalConfig.permissions?.allow ?? []; + const globalDenyPatterns = globalConfig.permissions?.deny ?? []; + + // Load local permissions + const localConfig = await loadPermissionsFile( + path.join(state.workingDir, LOCAL_CONFIG_DIR, "settings.json"), + ); + const localAllowPatterns = localConfig.permissions?.allow ?? []; + const localDenyPatterns = localConfig.permissions?.deny ?? []; + + // Pre-warm the pattern cache + warmCache([ + ...globalAllowPatterns, + ...globalDenyPatterns, + ...localAllowPatterns, + ...localDenyPatterns, + ]); + + // Build indexes + state.globalAllow = buildPatternIndex(globalAllowPatterns); + state.globalDeny = buildPatternIndex(globalDenyPatterns); + state.localAllow = buildPatternIndex(localAllowPatterns); + state.localDeny = buildPatternIndex(localDenyPatterns); + state.sessionAllow = buildPatternIndex([]); + state.initialized = true; +}; + +/** + * Reset initialization (for testing) + */ +export const resetPermissions = (): void => { + state = { + globalAllow: buildPatternIndex([]), + globalDeny: buildPatternIndex([]), + localAllow: buildPatternIndex([]), + localDeny: buildPatternIndex([]), + sessionAllow: buildPatternIndex([]), + initialized: false, + workingDir: process.cwd(), + permissionHandler: null, + }; + clearPatternCache(); +}; + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * Set working directory + */ +export const setWorkingDir = (dir: string): void => { + state.workingDir = dir; +}; + +/** + * Set custom permission handler (for TUI mode) + */ +export const setPermissionHandler = ( + handler: PermissionHandler | null, +): void => { + state.permissionHandler = handler; +}; + +// ============================================================================= +// Permission Checking (Optimized) +// ============================================================================= + +/** + * Get merged allow index (session > local > global priority) + */ +const getMergedAllowIndex = (): PatternIndex => + mergeIndexes(state.sessionAllow, state.localAllow, state.globalAllow); + +/** + * Get merged deny index (local > global) + */ +const getMergedDenyIndex = (): PatternIndex => + mergeIndexes(state.localDeny, state.globalDeny); + +/** + * Check if a Bash command is allowed + */ +export const isBashAllowed = (command: string): boolean => { + const allowIndex = getMergedAllowIndex(); + return isBashAllowedByIndex(command, allowIndex); +}; + +/** + * Check if a Bash command is denied + */ +export const isBashDenied = (command: string): boolean => { + const denyIndex = getMergedDenyIndex(); + return isBashAllowedByIndex(command, denyIndex); +}; + +/** + * Check if a file operation is allowed + */ +export const isFileOpAllowed = ( + tool: "Read" | "Write" | "Edit", + filePath: string, +): boolean => { + const allowIndex = getMergedAllowIndex(); + return isFileOpAllowedByIndex(tool, filePath, allowIndex); +}; + +// ============================================================================= +// Pattern Management +// ============================================================================= + +/** + * Add a pattern to session allow list + */ +export const addSessionPattern = (pattern: string): void => { + state.sessionAllow = addToIndex(state.sessionAllow, pattern); +}; + +/** + * Save global permissions to disk + */ +const saveGlobalPermissions = async (): Promise => { + let config: Record = {}; + try { + const data = await fs.readFile(FILES.settings, "utf-8"); + config = JSON.parse(data); + } catch { + // File doesn't exist + } + + const allowPatterns = getRawPatterns(state.globalAllow); + const denyPatterns = getRawPatterns(state.globalDeny); + + config.permissions = { + allow: allowPatterns, + deny: denyPatterns.length > 0 ? denyPatterns : undefined, + }; + + await fs.mkdir(DIRS.config, { recursive: true }); + await fs.writeFile(FILES.settings, JSON.stringify(config, null, 2), "utf-8"); +}; + +/** + * Add a pattern to global allow list + */ +export const addGlobalPattern = async (pattern: string): Promise => { + state.globalAllow = addToIndex(state.globalAllow, pattern); + await saveGlobalPermissions(); +}; + +/** + * Save local permissions to disk + */ +const saveLocalPermissions = async (): Promise => { + const filePath = path.join( + state.workingDir, + LOCAL_CONFIG_DIR, + "settings.json", + ); + + let config: Record = {}; + try { + const data = await fs.readFile(filePath, "utf-8"); + config = JSON.parse(data); + } catch { + // File doesn't exist + } + + const allowPatterns = getRawPatterns(state.localAllow); + const denyPatterns = getRawPatterns(state.localDeny); + + config.permissions = { + allow: allowPatterns, + deny: denyPatterns.length > 0 ? denyPatterns : undefined, + }; + + const configDir = path.join(state.workingDir, LOCAL_CONFIG_DIR); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(config, null, 2), "utf-8"); +}; + +/** + * Add a pattern to local allow list + */ +export const addLocalPattern = async (pattern: string): Promise => { + state.localAllow = addToIndex(state.localAllow, pattern); + await saveLocalPermissions(); +}; + +/** + * List all patterns + */ +export const listPatterns = (): { + global: string[]; + local: string[]; + session: string[]; +} => ({ + global: getRawPatterns(state.globalAllow), + local: getRawPatterns(state.localAllow), + session: getRawPatterns(state.sessionAllow), +}); + +/** + * Clear session patterns + */ +export const clearSessionPatterns = (): void => { + state.sessionAllow = buildPatternIndex([]); +}; + +// ============================================================================= +// Statistics +// ============================================================================= + +/** + * Get permission system statistics + */ +export const getPermissionStats = (): { + global: IndexStats; + local: IndexStats; + session: IndexStats; + initialized: boolean; +} => ({ + global: getIndexStats(state.globalAllow), + local: getIndexStats(state.localAllow), + session: getIndexStats(state.sessionAllow), + initialized: state.initialized, +}); + +// ============================================================================= +// Permission Prompts +// ============================================================================= + +/** + * Handle permission scope after user response + */ +const handlePermissionScope = async ( + scope: string, + pattern: string, +): Promise => { + const scopeHandlers: Record Promise | void> = { + session: () => addSessionPattern(pattern), + local: () => addLocalPattern(pattern), + global: () => addGlobalPattern(pattern), + }; + + const handler = scopeHandlers[scope]; + if (handler) { + await handler(); + } +}; + +/** + * Prompt user for permission to execute a bash command + */ +export const promptBashPermission = async ( + command: string, + description: string, +): Promise<{ allowed: boolean; remember?: "session" | "global" | "local" }> => { + if (isBashDenied(command)) { + return { allowed: false }; + } + + if (isBashAllowed(command)) { + return { allowed: true }; + } + + const suggestedPattern = generateBashPattern(command); + + // Use custom handler if set (TUI mode) + if (state.permissionHandler) { + const response = await state.permissionHandler({ + type: "bash", + command, + description, + pattern: suggestedPattern, + }); + + if (response.allowed && response.scope) { + await handlePermissionScope(response.scope, suggestedPattern); + } + + return { + allowed: response.allowed, + remember: response.scope === "once" ? undefined : response.scope, + }; + } + + // No handler - default deny (TUI should always set handler) + return { allowed: false }; +}; + +/** + * Prompt user for permission to perform a file operation + */ +export const promptFilePermission = async ( + tool: "Read" | "Write" | "Edit", + filePath: string, + description?: string, +): Promise<{ allowed: boolean; remember?: "session" | "global" | "local" }> => { + if (isFileOpAllowed(tool, filePath)) { + return { allowed: true }; + } + + const suggestedPattern = generateFilePattern(tool, filePath); + + if (state.permissionHandler) { + const response = await state.permissionHandler({ + type: tool.toLowerCase() as "read" | "write" | "edit", + path: filePath, + description: description ?? `${tool} ${filePath}`, + pattern: suggestedPattern, + }); + + if (response.allowed && response.scope) { + await handlePermissionScope(response.scope, suggestedPattern); + } + + return { + allowed: response.allowed, + remember: response.scope === "once" ? undefined : response.scope, + }; + } + + // No handler - default deny + return { allowed: false }; +}; + +/** + * Legacy method for backwards compatibility + */ +export const promptPermission = async ( + command: string, + description: string, +): Promise<{ allowed: boolean; remember?: "session" | "global" }> => { + const result = await promptBashPermission(command, description); + return { + allowed: result.allowed, + remember: result.remember === "local" ? "global" : result.remember, + }; +}; + +/** + * Legacy method + */ +export const getPermissionLevel = ( + command: string, +): "ask" | "allow_session" | "allow_global" | "deny" => { + if (isBashDenied(command)) return "deny"; + if (isBashAllowed(command)) return "allow_global"; + return "ask"; +}; + +// ============================================================================= +// Re-exports for convenience +// ============================================================================= + +export { generateBashPattern } from "@services/permissions/matchers/bash"; +export { generateFilePattern } from "@services/permissions/matchers/path"; +export { parsePattern } from "@services/permissions/pattern-cache"; +export { matchesBashPattern } from "@services/permissions/matchers/bash"; +export { matchesPathPattern } from "@services/permissions/matchers/path"; diff --git a/src/services/permissions/pattern-cache.ts b/src/services/permissions/pattern-cache.ts new file mode 100644 index 0000000..3a4aa66 --- /dev/null +++ b/src/services/permissions/pattern-cache.ts @@ -0,0 +1,129 @@ +/** + * Permission Pattern Cache + * + * Parses patterns once and caches the result for fast lookup + */ + +import type { ToolType, PermissionPattern } from "@/types/permissions"; + +// ============================================================================= +// Constants +// ============================================================================= + +const SIMPLE_TOOLS: ToolType[] = ["WebSearch"]; + +// ============================================================================= +// Cache State +// ============================================================================= + +const patternCache = new Map(); + +// ============================================================================= +// Pattern Parsing +// ============================================================================= + +/** + * Parse a pattern string into a structured object + * Results are cached for subsequent lookups + */ +export const parsePattern = (pattern: string): PermissionPattern | null => { + // Check cache first + const cached = patternCache.get(pattern); + if (cached !== undefined) { + return cached; + } + + const parsed = parsePatternInternal(pattern); + patternCache.set(pattern, parsed); + return parsed; +}; + +/** + * Internal parsing logic (not cached) + */ +const parsePatternInternal = (pattern: string): PermissionPattern | null => { + // Match patterns like: Bash(command:args), Read(path), WebFetch(domain:example.com) + const match = pattern.match(/^(\w+)\(([^)]*)\)$/); + + if (!match) { + // Simple patterns like "WebSearch" + if (SIMPLE_TOOLS.includes(pattern as ToolType)) { + return { tool: pattern as ToolType }; + } + return null; + } + + const tool = match[1] as ToolType; + const content = match[2]; + + const toolParsers: Partial PermissionPattern>> = { + Bash: () => parseBashPattern(content), + WebFetch: () => parseWebFetchPattern(content), + Read: () => ({ tool, path: content }), + Write: () => ({ tool, path: content }), + Edit: () => ({ tool, path: content }), + }; + + const parser = toolParsers[tool]; + if (parser) { + return parser(); + } + + // Default: treat as path pattern + return { tool, path: content }; +}; + +/** + * Parse Bash pattern content + */ +const parseBashPattern = (content: string): PermissionPattern => { + const colonIdx = content.lastIndexOf(":"); + + if (colonIdx === -1) { + return { tool: "Bash", command: content, args: "*" }; + } + + return { + tool: "Bash", + command: content.slice(0, colonIdx), + args: content.slice(colonIdx + 1), + }; +}; + +/** + * Parse WebFetch pattern content + */ +const parseWebFetchPattern = (content: string): PermissionPattern => { + if (content.startsWith("domain:")) { + return { tool: "WebFetch", domain: content.slice(7) }; + } + return { tool: "WebFetch", path: content }; +}; + +// ============================================================================= +// Cache Management +// ============================================================================= + +/** + * Clear the pattern cache + */ +export const clearPatternCache = (): void => { + patternCache.clear(); +}; + +/** + * Get cache statistics + */ +export const getCacheStats = (): { size: number; hits: number } => ({ + size: patternCache.size, + hits: 0, // Could track hits if needed +}); + +/** + * Pre-warm cache with patterns + */ +export const warmCache = (patterns: string[]): void => { + for (const pattern of patterns) { + parsePattern(pattern); + } +}; diff --git a/src/services/permissions/pattern-index.ts b/src/services/permissions/pattern-index.ts new file mode 100644 index 0000000..27674a1 --- /dev/null +++ b/src/services/permissions/pattern-index.ts @@ -0,0 +1,198 @@ +/** + * Permission Pattern Index + * + * Indexes patterns by tool type for O(n/k) lookup instead of O(n) + */ + +import type { ToolType, PermissionPattern } from "@/types/permissions"; +import { parsePattern } from "@services/permissions/pattern-cache"; + +// ============================================================================= +// Types +// ============================================================================= + +export interface PatternEntry { + raw: string; + parsed: PermissionPattern; +} + +export interface PatternIndex { + byTool: Map; + all: PatternEntry[]; + lastUpdated: number; +} + +// ============================================================================= +// Index Creation +// ============================================================================= + +/** + * Create an empty pattern index + */ +export const createPatternIndex = (): PatternIndex => ({ + byTool: new Map(), + all: [], + lastUpdated: Date.now(), +}); + +/** + * Build an index from a list of pattern strings + */ +export const buildPatternIndex = (patterns: string[]): PatternIndex => { + const index = createPatternIndex(); + + for (const raw of patterns) { + const parsed = parsePattern(raw); + if (!parsed) continue; + + const entry: PatternEntry = { raw, parsed }; + + // Add to all + index.all.push(entry); + + // Add to tool-specific list + const toolList = index.byTool.get(parsed.tool); + if (toolList) { + toolList.push(entry); + } else { + index.byTool.set(parsed.tool, [entry]); + } + } + + index.lastUpdated = Date.now(); + return index; +}; + +// ============================================================================= +// Index Operations +// ============================================================================= + +/** + * Add a pattern to an existing index + */ +export const addToIndex = ( + index: PatternIndex, + pattern: string, +): PatternIndex => { + const parsed = parsePattern(pattern); + if (!parsed) return index; + + // Check if already exists + if (index.all.some((e) => e.raw === pattern)) { + return index; + } + + const entry: PatternEntry = { raw: pattern, parsed }; + const newAll = [...index.all, entry]; + + const newByTool = new Map(index.byTool); + const toolList = newByTool.get(parsed.tool) ?? []; + newByTool.set(parsed.tool, [...toolList, entry]); + + return { + byTool: newByTool, + all: newAll, + lastUpdated: Date.now(), + }; +}; + +/** + * Remove a pattern from an index + */ +export const removeFromIndex = ( + index: PatternIndex, + pattern: string, +): PatternIndex => { + const newAll = index.all.filter((e) => e.raw !== pattern); + + if (newAll.length === index.all.length) { + return index; // Pattern wasn't in index + } + + const newByTool = new Map(); + + for (const [tool, entries] of index.byTool) { + const filtered = entries.filter((e) => e.raw !== pattern); + if (filtered.length > 0) { + newByTool.set(tool, filtered); + } + } + + return { + byTool: newByTool, + all: newAll, + lastUpdated: Date.now(), + }; +}; + +/** + * Get patterns for a specific tool + */ +export const getPatternsForTool = ( + index: PatternIndex, + tool: ToolType, +): PatternEntry[] => index.byTool.get(tool) ?? []; + +/** + * Check if a pattern exists in the index + */ +export const hasPattern = (index: PatternIndex, pattern: string): boolean => + index.all.some((e) => e.raw === pattern); + +/** + * Get all raw pattern strings + */ +export const getRawPatterns = (index: PatternIndex): string[] => + index.all.map((e) => e.raw); + +// ============================================================================= +// Index Statistics +// ============================================================================= + +export interface IndexStats { + total: number; + byTool: Record; + lastUpdated: number; +} + +export const getIndexStats = (index: PatternIndex): IndexStats => { + const byTool: Record = {}; + + for (const [tool, entries] of index.byTool) { + byTool[tool] = entries.length; + } + + return { + total: index.all.length, + byTool, + lastUpdated: index.lastUpdated, + }; +}; + +// ============================================================================= +// Index Merging +// ============================================================================= + +/** + * Merge multiple indexes (for combining session, local, global) + * Later indexes take precedence (no deduplication, just concatenation) + */ +export const mergeIndexes = (...indexes: PatternIndex[]): PatternIndex => { + const merged = createPatternIndex(); + + for (const index of indexes) { + for (const entry of index.all) { + merged.all.push(entry); + + const toolList = merged.byTool.get(entry.parsed.tool); + if (toolList) { + toolList.push(entry); + } else { + merged.byTool.set(entry.parsed.tool, [entry]); + } + } + } + + merged.lastUpdated = Date.now(); + return merged; +}; diff --git a/src/services/plan-service.ts b/src/services/plan-service.ts new file mode 100644 index 0000000..c14701e --- /dev/null +++ b/src/services/plan-service.ts @@ -0,0 +1,148 @@ +/** + * Plan Service - Manages agent-generated task plans + * + * Provides functions for agents to create and update plans + */ + +import { todoStore } from "@stores/todo-store"; +import type { TodoStatus } from "@/types/todo"; + +/** + * Create a new plan with tasks + */ +export const createPlan = ( + title: string, + tasks: Array<{ title: string; description?: string }>, +): string => { + return todoStore.createPlan(title, tasks); +}; + +/** + * Add a task to the current plan + */ +export const addTask = (title: string, description?: string): string | null => { + return todoStore.addItem(title, description); +}; + +/** + * Mark the current task as completed and move to next + */ +export const completeCurrentTask = (): void => { + const current = todoStore.getCurrentItem(); + if (current) { + todoStore.updateItemStatus(current.id, "completed"); + } +}; + +/** + * Mark a specific task as completed + */ +export const completeTask = (taskId: string): void => { + todoStore.updateItemStatus(taskId, "completed"); +}; + +/** + * Mark a task as failed + */ +export const failTask = (taskId: string): void => { + todoStore.updateItemStatus(taskId, "failed"); +}; + +/** + * Update task status + */ +export const updateTaskStatus = (taskId: string, status: TodoStatus): void => { + todoStore.updateItemStatus(taskId, status); +}; + +/** + * Clear the current plan + */ +export const clearPlan = (): void => { + todoStore.clearPlan(); +}; + +/** + * Check if there's an active plan + */ +export const hasPlan = (): boolean => { + return todoStore.hasPlan(); +}; + +/** + * Get the current task being worked on + */ +export const getCurrentTask = () => { + return todoStore.getCurrentItem(); +}; + +/** + * Get plan progress + */ +export const getProgress = () => { + return todoStore.getProgress(); +}; + +/** + * Get the full current plan + */ +export const getPlan = () => { + return todoStore.getPlan(); +}; + +/** + * Parse a plan from agent response text + * Looks for numbered lists or task patterns + */ +export const parsePlanFromText = ( + text: string, +): Array<{ title: string; description?: string }> | null => { + const tasks: Array<{ title: string; description?: string }> = []; + + // Pattern 1: Numbered list (1. Task, 2. Task, etc.) + const numberedPattern = /^\s*(\d+)\.\s+(.+)$/gm; + let match; + + while ((match = numberedPattern.exec(text)) !== null) { + tasks.push({ title: match[2].trim() }); + } + + if (tasks.length > 0) { + return tasks; + } + + // Pattern 2: Checkbox list (- [ ] Task, - [x] Task) + const checkboxPattern = /^\s*-\s*\[[ x]\]\s+(.+)$/gm; + + while ((match = checkboxPattern.exec(text)) !== null) { + tasks.push({ title: match[1].trim() }); + } + + if (tasks.length > 0) { + return tasks; + } + + // Pattern 3: Bullet list with "Step" or "Task" keywords + const stepPattern = /^\s*[-*]\s*(Step|Task)\s*\d*:?\s*(.+)$/gim; + + while ((match = stepPattern.exec(text)) !== null) { + tasks.push({ title: match[2].trim() }); + } + + return tasks.length > 0 ? tasks : null; +}; + +/** + * Detect if text contains a plan + */ +export const detectPlan = (text: string): boolean => { + const planKeywords = [ + /here'?s? (my |the |a )?plan/i, + /i'?ll? (do|perform|execute) the following/i, + /steps? to (complete|accomplish|do)/i, + /let me (outline|break down)/i, + /the (plan|approach|strategy) is/i, + ]; + + return planKeywords.some((pattern) => pattern.test(text)); +}; diff --git a/src/services/planner.ts b/src/services/planner.ts new file mode 100644 index 0000000..71756de --- /dev/null +++ b/src/services/planner.ts @@ -0,0 +1,320 @@ +/** + * Planning functionality for task execution + */ + +import chalk from "chalk"; +import { chat as providerChat } from "@providers/index"; +import type { Message, ProviderName } from "@/types/providers"; +import type { + Plan, + PlanStep, + PlanStepStatus, + PlanStepType, +} from "@/types/planner"; +import { PLAN_SYSTEM_PROMPT } from "@prompts/index"; + +/** + * Status icon mapping + */ +const STATUS_ICONS: Record = { + pending: chalk.gray("○"), + in_progress: chalk.yellow("◐"), + completed: chalk.green("●"), + failed: chalk.red("✗"), + skipped: chalk.gray("◌"), +}; + +/** + * Type color mapping + */ +const TYPE_COLORS: Record string> = { + read: chalk.blue, + write: chalk.green, + edit: chalk.yellow, + execute: chalk.magenta, + verify: chalk.cyan, + analyze: chalk.gray, +}; + +/** + * Step type detection patterns + */ +const STEP_TYPE_PATTERNS: Array<{ pattern: RegExp; type: PlanStepType }> = [ + { pattern: /\[READ\]/i, type: "read" }, + { pattern: /\[WRITE\]/i, type: "write" }, + { pattern: /\[EDIT\]/i, type: "edit" }, + { pattern: /\[EXECUTE\]/i, type: "execute" }, + { pattern: /\[RUN\]/i, type: "execute" }, + { pattern: /\[VERIFY\]/i, type: "verify" }, + { pattern: /\[TEST\]/i, type: "verify" }, + { pattern: /read|examine|check|look|review/i, type: "read" }, + { pattern: /create|write|add new/i, type: "write" }, + { pattern: /edit|modify|update|change/i, type: "edit" }, + { pattern: /run|execute|npm|yarn|command/i, type: "execute" }, + { pattern: /verify|test|confirm|ensure/i, type: "verify" }, +]; + +/** + * Current plan state + */ +let currentPlan: Plan | null = null; + +/** + * Generate unique plan ID + */ +const generatePlanId = (): string => + `plan_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`; + +/** + * Detect step type from content + */ +const detectStepType = (content: string): PlanStepType => { + for (const { pattern, type } of STEP_TYPE_PATTERNS) { + if (pattern.test(content)) { + return type; + } + } + return "analyze"; +}; + +/** + * Extract target from step content + */ +const extractTarget = (content: string): string | undefined => { + const fileMatch = content.match(/`([^`]+)`/); + return fileMatch?.[1]; +}; + +/** + * Extract dependencies from step content + */ +const extractDependencies = (content: string): number[] => { + const depMatch = content.match( + /depends?\s*(?:on)?\s*(?:step)?\s*(\d+(?:\s*,\s*\d+)*)/i, + ); + if (!depMatch) return []; + + return depMatch[1] + .split(",") + .map((d) => parseInt(d.trim(), 10)) + .filter((d) => !isNaN(d)); +}; + +/** + * Clean step description + */ +const cleanDescription = (content: string): string => + content + .replace(/\[(READ|WRITE|EDIT|EXECUTE|RUN|VERIFY|TEST|ANALYZE)\]/gi, "") + .trim(); + +/** + * Parse LLM response into a structured plan + */ +const parsePlanResponse = (content: string, task: string): Plan => { + const lines = content.split("\n"); + const steps: PlanStep[] = []; + let title = task; + let description = ""; + let stepId = 0; + let collectDescription = true; + + for (const line of lines) { + const trimmed = line.trim(); + + // Extract title (first heading or first line) + if (trimmed.startsWith("#")) { + title = trimmed.replace(/^#+\s*/, ""); + continue; + } + + // Look for numbered steps + const stepMatch = trimmed.match(/^(\d+)[.)]\s*(.+)/); + if (stepMatch) { + collectDescription = false; + stepId++; + + const stepContent = stepMatch[2]; + const type = detectStepType(stepContent); + const target = extractTarget(stepContent); + const dependencies = extractDependencies(stepContent); + + steps.push({ + id: stepId, + description: cleanDescription(stepContent), + type, + target, + dependencies: dependencies.length > 0 ? dependencies : undefined, + status: "pending", + }); + } else if (collectDescription && trimmed && !trimmed.startsWith("-")) { + description += (description ? " " : "") + trimmed; + } + } + + return { + id: generatePlanId(), + title, + description, + steps, + createdAt: Date.now(), + updatedAt: Date.now(), + status: "draft", + }; +}; + +/** + * Generate a plan for a task + */ +export const createPlan = async ( + task: string, + context?: string, + provider?: ProviderName, + model?: string, +): Promise => { + const messages: Message[] = [ + { role: "system", content: PLAN_SYSTEM_PROMPT }, + { + role: "user", + content: context + ? `Context:\n${context}\n\nTask: ${task}` + : `Task: ${task}`, + }, + ]; + + console.log(chalk.gray("Generating plan...")); + + const targetProvider = provider ?? "copilot"; + const response = await providerChat(targetProvider, messages, { model }); + const planContent = response.content ?? ""; + + const plan = parsePlanResponse(planContent, task); + currentPlan = plan; + + return plan; +}; + +/** + * Display a plan in the terminal + */ +export const displayPlan = (plan: Plan): void => { + console.log("\n" + chalk.bold.cyan("═══ Execution Plan ═══")); + console.log(chalk.bold(plan.title)); + if (plan.description) { + console.log(chalk.gray(plan.description)); + } + console.log(""); + + for (const step of plan.steps) { + const statusIcon = STATUS_ICONS[step.status] ?? chalk.gray("?"); + const typeColor = TYPE_COLORS[step.type] ?? chalk.gray; + + console.log( + `${statusIcon} ${chalk.bold(`Step ${step.id}:`)} ${typeColor(`[${step.type.toUpperCase()}]`)} ${step.description}`, + ); + + if (step.target) { + console.log(` ${chalk.gray("Target:")} ${step.target}`); + } + + if (step.dependencies && step.dependencies.length > 0) { + console.log( + ` ${chalk.gray("Depends on:")} ${step.dependencies.map((d) => `Step ${d}`).join(", ")}`, + ); + } + + if (step.error) { + console.log(` ${chalk.red("Error:")} ${step.error}`); + } + } + + console.log("\n" + chalk.gray("─".repeat(50))); + console.log(chalk.gray(`Plan ID: ${plan.id}`)); + console.log(chalk.gray(`Status: ${plan.status}`)); + console.log(""); +}; + +/** + * Get current plan + */ +export const getCurrentPlan = (): Plan | null => currentPlan; + +/** + * Approve current plan + */ +export const approvePlan = (): void => { + if (currentPlan) { + currentPlan.status = "approved"; + currentPlan.updatedAt = Date.now(); + } +}; + +/** + * Update step status + */ +export const updateStepStatus = ( + stepId: number, + status: PlanStepStatus, + result?: string, + error?: string, +): void => { + if (!currentPlan) return; + + const step = currentPlan.steps.find((s) => s.id === stepId); + if (step) { + step.status = status; + if (result) step.result = result; + if (error) step.error = error; + currentPlan.updatedAt = Date.now(); + + // Update plan status + const allCompleted = currentPlan.steps.every( + (s) => s.status === "completed" || s.status === "skipped", + ); + const anyFailed = currentPlan.steps.some((s) => s.status === "failed"); + const anyInProgress = currentPlan.steps.some( + (s) => s.status === "in_progress", + ); + + if (allCompleted) { + currentPlan.status = "completed"; + } else if (anyFailed) { + currentPlan.status = "failed"; + } else if (anyInProgress) { + currentPlan.status = "in_progress"; + } + } +}; + +/** + * Get next executable step + */ +export const getNextStep = (): PlanStep | null => { + if (!currentPlan) return null; + + for (const step of currentPlan.steps) { + if (step.status !== "pending") continue; + + // Check if dependencies are satisfied + if (step.dependencies) { + const depsCompleted = step.dependencies.every((depId) => { + const dep = currentPlan!.steps.find((s) => s.id === depId); + return dep && (dep.status === "completed" || dep.status === "skipped"); + }); + + if (!depsCompleted) continue; + } + + return step; + } + + return null; +}; + +// Re-export types +export type { + Plan, + PlanStep, + PlanStepType, + PlanStepStatus, +} from "@/types/planner"; diff --git a/src/services/project-config.ts b/src/services/project-config.ts new file mode 100644 index 0000000..e94246d --- /dev/null +++ b/src/services/project-config.ts @@ -0,0 +1,558 @@ +/** + * Project configuration management + * + * Supports both local (.codetyper/) and global (~/.codetyper/) configurations + */ + +import fs from "fs/promises"; +import path from "path"; +import os from "os"; +import type { + AgentConfig, + SkillConfig, + RuleConfig, + LearningEntry, + ProjectSettings, +} from "@/types/project-config"; + +const GLOBAL_CONFIG_DIR = path.join(os.homedir(), ".codetyper"); +const LOCAL_CONFIG_DIR = ".codetyper"; + +/** + * State + */ +let workingDir = process.cwd(); +let initialized = false; + +/** + * Get local config directory + */ +export const getLocalConfigDir = (): string => + path.join(workingDir, LOCAL_CONFIG_DIR); + +/** + * Get global config directory + */ +export const getGlobalConfigDir = (): string => GLOBAL_CONFIG_DIR; + +/** + * Helper: Load JSON file + */ +const loadJsonFile = async ( + filePath: string, + defaultValue: T, +): Promise => { + try { + const data = await fs.readFile(filePath, "utf-8"); + return JSON.parse(data); + } catch { + return defaultValue; + } +}; + +/** + * Helper: List files with extension + */ +const listFiles = async (dir: string, extension: string): Promise => { + try { + const files = await fs.readdir(dir); + return files.filter((f) => f.endsWith(extension)); + } catch { + return []; + } +}; + +/** + * Helper: Load rules from directory + */ +const loadRulesFromDir = async (dir: string): Promise => { + const rules: RuleConfig[] = []; + const files = await listFiles(dir, ".md"); + + for (const file of files) { + try { + const content = await fs.readFile(path.join(dir, file), "utf-8"); + const name = path.basename(file, ".md"); + rules.push({ name, content }); + } catch { + // Skip on error + } + } + + return rules; +}; + +/** + * Helper: Load JSON configs from directory + */ +const loadJsonConfigs = async (dir: string): Promise => { + const configs: T[] = []; + const files = await listFiles(dir, ".json"); + + for (const file of files) { + try { + const content = await fs.readFile(path.join(dir, file), "utf-8"); + configs.push(JSON.parse(content)); + } catch { + // Skip on error + } + } + + return configs; +}; + +/** + * Ensure global config exists + */ +const ensureGlobalConfig = async (): Promise => { + const dirs = [ + getGlobalConfigDir(), + path.join(getGlobalConfigDir(), "rules"), + path.join(getGlobalConfigDir(), "agents"), + path.join(getGlobalConfigDir(), "skills"), + path.join(getGlobalConfigDir(), "learnings"), + ]; + + for (const dir of dirs) { + await fs.mkdir(dir, { recursive: true }); + } + + const settingsFile = path.join(getGlobalConfigDir(), "settings.json"); + try { + await fs.access(settingsFile); + } catch { + await fs.writeFile( + settingsFile, + JSON.stringify( + { + defaultModel: "gpt-4.1-mini", + defaultProvider: "copilot", + permissions: { + allow: [ + "Bash(ls:*)", + "Bash(pwd:*)", + "Bash(cat:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(grep:*)", + "Bash(find:*)", + "Bash(tree:*)", + "Bash(wc:*)", + "Bash(echo:*)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git ls-files:*)", + "Bash(node --version:*)", + "Bash(npm --version:*)", + "Bash(npm ls:*)", + ], + }, + }, + null, + 2, + ), + "utf-8", + ); + } +}; + +/** + * Ensure local config exists + */ +const ensureLocalConfig = async (): Promise => { + const dirs = [ + getLocalConfigDir(), + path.join(getLocalConfigDir(), "rules"), + path.join(getLocalConfigDir(), "agents"), + path.join(getLocalConfigDir(), "skills"), + path.join(getLocalConfigDir(), "learnings"), + ]; + + for (const dir of dirs) { + await fs.mkdir(dir, { recursive: true }); + } + + const settingsFile = path.join(getLocalConfigDir(), "settings.json"); + try { + await fs.access(settingsFile); + } catch { + await fs.writeFile( + settingsFile, + JSON.stringify( + { + permissions: { allow: [] }, + ignorePaths: ["node_modules", ".git", "dist"], + }, + null, + 2, + ), + "utf-8", + ); + } +}; + +/** + * Auto-initialize config directories + */ +export const autoInitialize = async (): Promise => { + if (initialized) return; + await ensureGlobalConfig(); + await ensureLocalConfig(); + initialized = true; +}; + +/** + * Initialize project config directory + */ +export const initProject = async (): Promise => { + const dirs = [ + getLocalConfigDir(), + path.join(getLocalConfigDir(), "rules"), + path.join(getLocalConfigDir(), "agents"), + path.join(getLocalConfigDir(), "skills"), + path.join(getLocalConfigDir(), "learnings"), + ]; + + for (const dir of dirs) { + await fs.mkdir(dir, { recursive: true }); + } + + const settingsFile = path.join(getLocalConfigDir(), "settings.json"); + try { + await fs.access(settingsFile); + } catch { + await fs.writeFile( + settingsFile, + JSON.stringify( + { + defaultModel: "gpt-5-mini", + defaultProvider: "copilot", + autoApprove: [], + ignorePaths: ["node_modules", ".git", "dist"], + } as ProjectSettings, + null, + 2, + ), + "utf-8", + ); + } + + const exampleRule = path.join(getLocalConfigDir(), "rules", "code-style.md"); + try { + await fs.access(exampleRule); + } catch { + await fs.writeFile( + exampleRule, + `# Code Style Rules + +These rules apply to all code generation in this project. + +- Follow existing code conventions +- Use TypeScript strict mode +- Add JSDoc comments for public APIs +`, + "utf-8", + ); + } +}; + +/** + * Initialize global config directory + */ +export const initGlobal = async (): Promise => { + const dirs = [ + getGlobalConfigDir(), + path.join(getGlobalConfigDir(), "rules"), + path.join(getGlobalConfigDir(), "agents"), + path.join(getGlobalConfigDir(), "skills"), + path.join(getGlobalConfigDir(), "learnings"), + ]; + + for (const dir of dirs) { + await fs.mkdir(dir, { recursive: true }); + } +}; + +/** + * Get project settings (merged local + global) + */ +export const getSettings = async (): Promise => { + const globalSettings = await loadJsonFile( + path.join(getGlobalConfigDir(), "settings.json"), + {}, + ); + const localSettings = await loadJsonFile( + path.join(getLocalConfigDir(), "settings.json"), + {}, + ); + + return { ...globalSettings, ...localSettings }; +}; + +/** + * Save project settings + */ +export const saveSettings = async ( + settings: Partial, + global = false, +): Promise => { + const configDir = global ? getGlobalConfigDir() : getLocalConfigDir(); + const filePath = path.join(configDir, "settings.json"); + + const existing = await loadJsonFile(filePath, {}); + const merged = { ...existing, ...settings }; + + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(merged, null, 2), "utf-8"); +}; + +/** + * Get all rules (merged local + global) + */ +export const getRules = async (): Promise => { + const globalRules = await loadRulesFromDir( + path.join(getGlobalConfigDir(), "rules"), + ); + const localRules = await loadRulesFromDir( + path.join(getLocalConfigDir(), "rules"), + ); + + return [...globalRules, ...localRules].sort( + (a, b) => (b.priority ?? 0) - (a.priority ?? 0), + ); +}; + +/** + * Add a rule + */ +export const addRule = async ( + name: string, + content: string, + global = false, +): Promise => { + const configDir = global ? getGlobalConfigDir() : getLocalConfigDir(); + const rulesDir = path.join(configDir, "rules"); + + await fs.mkdir(rulesDir, { recursive: true }); + const fileName = name.endsWith(".md") ? name : `${name}.md`; + await fs.writeFile(path.join(rulesDir, fileName), content, "utf-8"); +}; + +/** + * Get all agents (merged local + global) + */ +export const getAgents = async (): Promise => { + const globalAgents = await loadJsonConfigs( + path.join(getGlobalConfigDir(), "agents"), + ); + const localAgents = await loadJsonConfigs( + path.join(getLocalConfigDir(), "agents"), + ); + + const agentMap = new Map(); + for (const agent of [...globalAgents, ...localAgents]) { + agentMap.set(agent.name, agent); + } + + return Array.from(agentMap.values()); +}; + +/** + * Add an agent + */ +export const addAgent = async ( + agent: AgentConfig, + global = false, +): Promise => { + const configDir = global ? getGlobalConfigDir() : getLocalConfigDir(); + const agentsDir = path.join(configDir, "agents"); + + await fs.mkdir(agentsDir, { recursive: true }); + await fs.writeFile( + path.join(agentsDir, `${agent.name}.json`), + JSON.stringify(agent, null, 2), + "utf-8", + ); +}; + +/** + * Get all skills (merged local + global) + */ +export const getSkills = async (): Promise => { + const globalSkills = await loadJsonConfigs( + path.join(getGlobalConfigDir(), "skills"), + ); + const localSkills = await loadJsonConfigs( + path.join(getLocalConfigDir(), "skills"), + ); + + const skillMap = new Map(); + for (const skill of [...globalSkills, ...localSkills]) { + skillMap.set(skill.name, skill); + } + + return Array.from(skillMap.values()); +}; + +/** + * Add a skill + */ +export const addSkill = async ( + skill: SkillConfig, + global = false, +): Promise => { + const configDir = global ? getGlobalConfigDir() : getLocalConfigDir(); + const skillsDir = path.join(configDir, "skills"); + + await fs.mkdir(skillsDir, { recursive: true }); + await fs.writeFile( + path.join(skillsDir, `${skill.name}.json`), + JSON.stringify(skill, null, 2), + "utf-8", + ); +}; + +/** + * Get all learnings + */ +export const getLearnings = async (): Promise => { + const globalLearnings = await loadJsonConfigs( + path.join(getGlobalConfigDir(), "learnings"), + ); + const localLearnings = await loadJsonConfigs( + path.join(getLocalConfigDir(), "learnings"), + ); + + return [...globalLearnings, ...localLearnings].sort( + (a, b) => b.createdAt - a.createdAt, + ); +}; + +/** + * Add a learning + */ +export const addLearning = async ( + content: string, + context?: string, + global = false, +): Promise => { + const configDir = global ? getGlobalConfigDir() : getLocalConfigDir(); + const learningsDir = path.join(configDir, "learnings"); + + await fs.mkdir(learningsDir, { recursive: true }); + + const entry: LearningEntry = { + id: `learning_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`, + content, + context, + createdAt: Date.now(), + }; + + await fs.writeFile( + path.join(learningsDir, `${entry.id}.json`), + JSON.stringify(entry, null, 2), + "utf-8", + ); + + return entry; +}; + +/** + * Build system prompt additions from rules + */ +export const buildRulesPrompt = async (): Promise => { + const rules = await getRules(); + if (rules.length === 0) return ""; + + return rules.map((r) => `## ${r.name}\n\n${r.content}`).join("\n\n---\n\n"); +}; + +/** + * Build learnings context (simple, most recent) + */ +export const buildLearningsContext = async (): Promise => { + const learnings = await getLearnings(); + if (learnings.length === 0) return ""; + + return ( + "## Project Learnings\n\n" + + learnings + .slice(0, 20) + .map((l) => `- ${l.content}`) + .join("\n") + ); +}; + +/** + * Build learnings context with semantic search + */ +export const buildRelevantLearningsContext = async ( + query: string, + maxLearnings = 10, +): Promise => { + const { searchLearnings } = + await import("@services/learning/semantic-search"); + const learnings = await getLearnings(); + + if (learnings.length === 0) return ""; + + const results = await searchLearnings(query, learnings, { + topK: maxLearnings, + minSimilarity: 0.3, + }); + + if (results.length === 0) { + // Fallback to most recent + return ( + "## Project Learnings\n\n" + + learnings + .slice(0, maxLearnings) + .map((l) => `- ${l.content}`) + .join("\n") + ); + } + + return ( + "## Relevant Project Learnings\n\n" + + results.map((r) => `- ${r.item.content}`).join("\n") + ); +}; + +/** + * Set working directory + */ +export const setWorkingDir = (dir: string): void => { + workingDir = dir; +}; + +// Re-export types +export type { + AgentConfig, + SkillConfig, + RuleConfig, + LearningEntry, + ProjectSettings, +} from "@/types/project-config"; + +/** + * Legacy compatibility - project config object + */ +export const projectConfig = { + autoInitialize, + initProject, + initGlobal, + getSettings, + saveSettings, + getRules, + addRule, + getAgents, + addAgent, + getSkills, + addSkill, + getLearnings, + addLearning, + buildRulesPrompt, + buildLearningsContext, +}; diff --git a/src/services/provider-quality/feedback-detector.ts b/src/services/provider-quality/feedback-detector.ts new file mode 100644 index 0000000..e046963 --- /dev/null +++ b/src/services/provider-quality/feedback-detector.ts @@ -0,0 +1,70 @@ +/** + * Feedback Detection Service + * + * Detects user feedback from messages to update quality scores + */ + +import { + NEGATIVE_FEEDBACK_PATTERNS, + POSITIVE_FEEDBACK_PATTERNS, +} from "@constants/provider-quality"; + +export type FeedbackType = "positive" | "negative" | "neutral"; + +export interface FeedbackResult { + type: FeedbackType; + confidence: number; + matchedPatterns: string[]; +} + +export const detectFeedback = (message: string): FeedbackResult => { + const normalizedMessage = message.toLowerCase(); + const negativeMatches: string[] = []; + const positiveMatches: string[] = []; + + for (const pattern of NEGATIVE_FEEDBACK_PATTERNS) { + const match = normalizedMessage.match(pattern); + if (match) { + negativeMatches.push(match[0]); + } + } + + for (const pattern of POSITIVE_FEEDBACK_PATTERNS) { + const match = normalizedMessage.match(pattern); + if (match) { + positiveMatches.push(match[0]); + } + } + + if (negativeMatches.length > positiveMatches.length) { + return { + type: "negative", + confidence: Math.min(0.5 + negativeMatches.length * 0.2, 1.0), + matchedPatterns: negativeMatches, + }; + } + + if (positiveMatches.length > negativeMatches.length) { + return { + type: "positive", + confidence: Math.min(0.5 + positiveMatches.length * 0.2, 1.0), + matchedPatterns: positiveMatches, + }; + } + + return { + type: "neutral", + confidence: 1.0, + matchedPatterns: [], + }; +}; + +export const isCorrection = (message: string): boolean => { + const feedback = detectFeedback(message); + return feedback.type === "negative" && feedback.confidence >= 0.6; +}; + +export const isApproval = (message: string): boolean => { + const feedback = detectFeedback(message); + return feedback.type === "positive" && feedback.confidence >= 0.6; +}; diff --git a/src/services/provider-quality/index.ts b/src/services/provider-quality/index.ts new file mode 100644 index 0000000..22f434b --- /dev/null +++ b/src/services/provider-quality/index.ts @@ -0,0 +1,38 @@ +/** + * Provider Quality Service + * + * Manages provider quality scores, routing, and learning + */ + +export { detectTaskType, getTaskTypeConfidence } from "./task-detector"; +export { + detectFeedback, + isCorrection, + isApproval, + type FeedbackType, + type FeedbackResult, +} from "./feedback-detector"; +export { + loadQualityData, + saveQualityData, + getProviderQuality, + updateProviderQuality, + calculateOverallScore, +} from "./persistence"; +export { + updateQualityScore, + getTaskScore, + getOverallScore, + recordApproval, + recordCorrection, + recordRejection, + recordAuditResult, + type Outcome, + type ScoreUpdate, +} from "./score-manager"; +export { + determineRoute, + shouldAudit, + getRoutingExplanation, + type RoutingContext, +} from "./router"; diff --git a/src/services/provider-quality/persistence.ts b/src/services/provider-quality/persistence.ts new file mode 100644 index 0000000..4c38d76 --- /dev/null +++ b/src/services/provider-quality/persistence.ts @@ -0,0 +1,128 @@ +/** + * Provider Quality Persistence Service + * + * Saves and loads provider quality data to/from disk + */ + +import { join } from "path"; +import { homedir } from "os"; +import type { ProviderQualityData, TaskType, QualityScore } from "@/types/provider-quality"; +import { QUALITY_THRESHOLDS } from "@constants/provider-quality"; + +const QUALITY_DATA_DIR = join(homedir(), ".config", "codetyper"); +const QUALITY_DATA_FILE = "provider-quality.json"; + +const getQualityDataPath = (): string => { + return join(QUALITY_DATA_DIR, QUALITY_DATA_FILE); +}; + +const ensureDataDir = async (): Promise => { + const fs = await import("fs/promises"); + try { + await fs.mkdir(QUALITY_DATA_DIR, { recursive: true }); + } catch { + // Directory exists + } +}; + +const createDefaultScores = (): Record => { + const scores: Partial> = {}; + const taskTypes: TaskType[] = [ + "code_generation", + "refactoring", + "bug_fix", + "documentation", + "testing", + "explanation", + "review", + "general", + ]; + + for (const taskType of taskTypes) { + scores[taskType] = { + taskType, + successCount: 0, + correctionCount: 0, + userRejectionCount: 0, + lastUpdated: Date.now(), + }; + } + + return scores as Record; +}; + +export const loadQualityData = async (): Promise< + Record +> => { + const fs = await import("fs/promises"); + const filePath = getQualityDataPath(); + + try { + const content = await fs.readFile(filePath, "utf-8"); + return JSON.parse(content) as Record; + } catch { + return {}; + } +}; + +export const saveQualityData = async ( + data: Record, +): Promise => { + const fs = await import("fs/promises"); + await ensureDataDir(); + const filePath = getQualityDataPath(); + await fs.writeFile(filePath, JSON.stringify(data, null, 2)); +}; + +export const getProviderQuality = async ( + providerId: string, +): Promise => { + const allData = await loadQualityData(); + + if (allData[providerId]) { + return allData[providerId]; + } + + return { + providerId, + scores: createDefaultScores(), + overallScore: QUALITY_THRESHOLDS.INITIAL, + }; +}; + +export const updateProviderQuality = async ( + data: ProviderQualityData, +): Promise => { + const allData = await loadQualityData(); + allData[data.providerId] = data; + await saveQualityData(allData); +}; + +export const calculateOverallScore = ( + scores: Record, +): number => { + const taskTypes = Object.keys(scores) as TaskType[]; + let totalWeight = 0; + let weightedScore = 0; + + for (const taskType of taskTypes) { + const score = scores[taskType]; + const total = + score.successCount + score.correctionCount + score.userRejectionCount; + + if (total === 0) { + continue; + } + + const successRate = score.successCount / total; + const weight = total; + weightedScore += successRate * weight; + totalWeight += weight; + } + + if (totalWeight === 0) { + return QUALITY_THRESHOLDS.INITIAL; + } + + return weightedScore / totalWeight; +}; diff --git a/src/services/provider-quality/router.ts b/src/services/provider-quality/router.ts new file mode 100644 index 0000000..bc205b8 --- /dev/null +++ b/src/services/provider-quality/router.ts @@ -0,0 +1,84 @@ +/** + * Provider Router Service + * + * Determines which provider(s) to use based on quality scores + */ + +import type { TaskType, RoutingDecision } from "@/types/provider-quality"; +import { CASCADE_CONFIG, PROVIDER_IDS } from "@constants/provider-quality"; +import { getTaskScore, getOverallScore } from "./score-manager"; + +export interface RoutingContext { + taskType: TaskType; + ollamaAvailable: boolean; + copilotAvailable: boolean; + cascadeEnabled: boolean; +} + +export const determineRoute = async ( + context: RoutingContext, +): Promise => { + const { taskType, ollamaAvailable, copilotAvailable, cascadeEnabled } = context; + + if (!ollamaAvailable && !copilotAvailable) { + throw new Error("No providers available"); + } + + if (!ollamaAvailable) { + return "copilot_only"; + } + + if (!copilotAvailable) { + return "ollama_only"; + } + + if (!cascadeEnabled) { + return "ollama_only"; + } + + const ollamaScore = await getTaskScore(PROVIDER_IDS.OLLAMA, taskType); + const ollamaOverall = await getOverallScore(PROVIDER_IDS.OLLAMA); + + if (ollamaScore >= CASCADE_CONFIG.MIN_AUDIT_THRESHOLD) { + return "ollama_only"; + } + + if (ollamaOverall >= CASCADE_CONFIG.MIN_AUDIT_THRESHOLD) { + return "ollama_only"; + } + + if (ollamaScore <= CASCADE_CONFIG.MAX_SKIP_THRESHOLD) { + return "copilot_only"; + } + + return "cascade"; +}; + +export const shouldAudit = async ( + taskType: TaskType, + ollamaAvailable: boolean, +): Promise => { + if (!ollamaAvailable) { + return false; + } + + const ollamaScore = await getTaskScore(PROVIDER_IDS.OLLAMA, taskType); + + return ollamaScore < CASCADE_CONFIG.MIN_AUDIT_THRESHOLD; +}; + +export const getRoutingExplanation = async ( + decision: RoutingDecision, + taskType: TaskType, +): Promise => { + const ollamaScore = await getTaskScore(PROVIDER_IDS.OLLAMA, taskType); + const scorePercent = Math.round(ollamaScore * 100); + + const explanations: Record = { + ollama_only: `Using Ollama (score: ${scorePercent}% - trusted for ${taskType})`, + copilot_only: `Using Copilot (Ollama score: ${scorePercent}% - needs improvement for ${taskType})`, + cascade: `Using Ollama with Copilot audit (score: ${scorePercent}% - building trust for ${taskType})`, + }; + + return explanations[decision]; +}; diff --git a/src/services/provider-quality/score-manager.ts b/src/services/provider-quality/score-manager.ts new file mode 100644 index 0000000..69a600f --- /dev/null +++ b/src/services/provider-quality/score-manager.ts @@ -0,0 +1,99 @@ +/** + * Score Manager Service + * + * Manages quality scores for providers based on outcomes + */ + +import type { TaskType, QualityScore } from "@/types/provider-quality"; +import { + getProviderQuality, + updateProviderQuality, + calculateOverallScore, +} from "./persistence"; + +export type Outcome = "approved" | "corrected" | "rejected" | "minor_issue" | "major_issue"; + +export interface ScoreUpdate { + providerId: string; + taskType: TaskType; + outcome: Outcome; +} + +export const updateQualityScore = async (update: ScoreUpdate): Promise => { + const { providerId, taskType, outcome } = update; + const data = await getProviderQuality(providerId); + const score = data.scores[taskType]; + + const outcomeCounters: Record = { + approved: "successCount", + corrected: "correctionCount", + rejected: "userRejectionCount", + minor_issue: "correctionCount", + major_issue: "correctionCount", + }; + + const counterKey = outcomeCounters[outcome]; + (score[counterKey] as number)++; + score.lastUpdated = Date.now(); + + data.overallScore = calculateOverallScore(data.scores); + await updateProviderQuality(data); +}; + +export const getTaskScore = async ( + providerId: string, + taskType: TaskType, +): Promise => { + const data = await getProviderQuality(providerId); + const score = data.scores[taskType]; + + const total = + score.successCount + score.correctionCount + score.userRejectionCount; + + if (total === 0) { + return 0.5; + } + + return score.successCount / total; +}; + +export const getOverallScore = async (providerId: string): Promise => { + const data = await getProviderQuality(providerId); + return data.overallScore; +}; + +export const recordApproval = async ( + providerId: string, + taskType: TaskType, +): Promise => { + await updateQualityScore({ providerId, taskType, outcome: "approved" }); +}; + +export const recordCorrection = async ( + providerId: string, + taskType: TaskType, +): Promise => { + await updateQualityScore({ providerId, taskType, outcome: "corrected" }); +}; + +export const recordRejection = async ( + providerId: string, + taskType: TaskType, +): Promise => { + await updateQualityScore({ providerId, taskType, outcome: "rejected" }); +}; + +export const recordAuditResult = async ( + providerId: string, + taskType: TaskType, + approved: boolean, + hasMajorIssues: boolean, +): Promise => { + if (approved) { + await recordApproval(providerId, taskType); + return; + } + + const outcome: Outcome = hasMajorIssues ? "major_issue" : "minor_issue"; + await updateQualityScore({ providerId, taskType, outcome }); +}; diff --git a/src/services/provider-quality/task-detector.ts b/src/services/provider-quality/task-detector.ts new file mode 100644 index 0000000..8e657b7 --- /dev/null +++ b/src/services/provider-quality/task-detector.ts @@ -0,0 +1,51 @@ +/** + * Task Type Detection Service + * + * Detects the type of task from user prompts to route appropriately + */ + +import type { TaskType } from "@/types/provider-quality"; +import { TASK_TYPE_PATTERNS } from "@constants/provider-quality"; + +const TASK_TYPES: TaskType[] = [ + "code_generation", + "refactoring", + "bug_fix", + "documentation", + "testing", + "explanation", + "review", + "general", +]; + +export const detectTaskType = (prompt: string): TaskType => { + const normalizedPrompt = prompt.toLowerCase(); + + for (const taskType of TASK_TYPES) { + const patterns = TASK_TYPE_PATTERNS[taskType]; + + for (const pattern of patterns) { + if (pattern.test(normalizedPrompt)) { + return taskType; + } + } + } + + return "general"; +}; + +export const getTaskTypeConfidence = ( + prompt: string, + detectedType: TaskType, +): number => { + const patterns = TASK_TYPE_PATTERNS[detectedType]; + + if (patterns.length === 0) { + return 0.3; + } + + const matchCount = patterns.filter((p) => p.test(prompt.toLowerCase())).length; + const confidence = Math.min(0.5 + matchCount * 0.15, 1.0); + + return confidence; +}; diff --git a/src/services/reasoning-agent.ts b/src/services/reasoning-agent.ts new file mode 100644 index 0000000..585a786 --- /dev/null +++ b/src/services/reasoning-agent.ts @@ -0,0 +1,569 @@ +/** + * Reasoning-Enhanced Agent + * + * Extends the base agent with cognitive control layers that provide + * intelligent control flow independent of the underlying model. + * + * This agent wraps the standard agent loop with: + * - Quality evaluation of responses + * - Automatic retry with reframing + * - Context compression + * - Memory relevance selection + * - Termination confidence detection + */ + +import { v4 as uuidv4 } from "uuid"; +import type { Message } from "@/types/providers"; +import type { AgentOptions } from "@interfaces/AgentOptions"; +import type { AgentResult } from "@interfaces/AgentResult"; +import type { + AgentMessage, + ToolCallMessage, + ToolResultMessage, +} from "@/types/agent"; +import { chat as providerChat } from "@providers/index"; +import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index"; +import type { ToolContext, ToolCall, ToolResult } from "@/types/tools"; +import { initializePermissions } from "@services/permissions"; +import { MAX_ITERATIONS } from "@constants/agent"; +import { usageStore } from "@stores/usage-store"; +import type { + TaskConstraints, + CompressibleMessage, + AttemptRecord, + ReasoningControlState, + QualityEvalOutput, +} from "@/types/reasoning"; + +import { + createInitialState, + createMemoryStore, + addMemory, + createMemoryItem, + evaluateResponseQuality, + decideRetry, + compressContext, + markMessagesWithAge, + getPreservationCandidates, + checkTermination, + estimateTokens, + createTimestamp, +} from "@services/reasoning"; + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface ReasoningAgentOptions extends AgentOptions { + enableReasoning?: boolean; + reasoningConfig?: { + tokenBudget?: number; + autoValidate?: boolean; + maxRetries?: number; + }; + onQualityEval?: (output: QualityEvalOutput) => void; + onRetry?: (attempt: number, reason: string) => void; + onCompression?: (tokensSaved: number) => void; +} + +interface ReasoningAgentState { + sessionId: string; + workingDir: string; + abort: AbortController; + options: ReasoningAgentOptions; + reasoningState: ReasoningControlState; + memoryStore: ReturnType; + taskConstraints: TaskConstraints; +} + +// ============================================================================= +// STATE INITIALIZATION +// ============================================================================= + +const createReasoningAgentState = ( + workingDir: string, + options: ReasoningAgentOptions, + taskConstraints?: Partial, +): ReasoningAgentState => ({ + sessionId: uuidv4(), + workingDir, + abort: new AbortController(), + options, + reasoningState: createInitialState(), + memoryStore: createMemoryStore(500), + taskConstraints: { + requiredOutputs: [], + expectedToolCalls: [], + maxResponseTokens: 4000, + requiresCode: false, + ...taskConstraints, + }, +}); + +// ============================================================================= +// LLM INTERACTION +// ============================================================================= + +const callLLMWithReasoning = async ( + state: ReasoningAgentState, + messages: AgentMessage[], +): Promise<{ + content: string | null; + toolCalls?: ToolCall[]; + tokenCount: number; +}> => { + const toolDefs = getToolsForApi(); + + const providerMessages: unknown[] = messages.map((msg) => { + if ("tool_calls" in msg) { + return { + role: "assistant", + content: msg.content, + tool_calls: msg.tool_calls, + }; + } + if ("tool_call_id" in msg) { + return { + role: "tool", + tool_call_id: msg.tool_call_id, + content: msg.content, + }; + } + return msg; + }); + + const response = await providerChat( + state.options.provider, + providerMessages as Message[], + { + model: state.options.model, + tools: toolDefs, + }, + ); + + if (response.usage) { + usageStore.addUsage({ + promptTokens: response.usage.promptTokens, + completionTokens: response.usage.completionTokens, + totalTokens: response.usage.totalTokens, + model: state.options.model, + }); + } + + const toolCalls: ToolCall[] = []; + + if (response.toolCalls) { + for (const tc of response.toolCalls) { + let args: Record; + try { + args = + typeof tc.function.arguments === "string" + ? JSON.parse(tc.function.arguments) + : tc.function.arguments; + } catch { + args = {}; + } + + toolCalls.push({ + id: tc.id, + name: tc.function.name, + arguments: args, + }); + } + } + + const tokenCount = + response.usage?.completionTokens || estimateTokens(response.content || ""); + + return { + content: response.content, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + tokenCount, + }; +}; + +// ============================================================================= +// TOOL EXECUTION +// ============================================================================= + +const executeToolWithTracking = async ( + state: ReasoningAgentState, + toolCall: ToolCall, +): Promise => { + const startTime = createTimestamp(); + const tool = getTool(toolCall.name); + + if (!tool) { + return { + success: false, + title: "Unknown tool", + output: "", + error: `Tool not found: ${toolCall.name}`, + executionTime: createTimestamp() - startTime, + }; + } + + const ctx: ToolContext = { + sessionId: state.sessionId, + messageId: uuidv4(), + workingDir: state.workingDir, + abort: state.abort, + autoApprove: state.options.autoApprove, + }; + + try { + const validatedArgs = tool.parameters.parse(toolCall.arguments); + const result = await tool.execute(validatedArgs, ctx); + return { + ...result, + executionTime: createTimestamp() - startTime, + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + success: false, + title: "Tool error", + output: "", + error: errorMessage, + executionTime: createTimestamp() - startTime, + }; + } +}; + +// ============================================================================= +// CONTEXT MANAGEMENT +// ============================================================================= + +const convertToCompressibleMessages = ( + messages: AgentMessage[], + currentTurn: number, +): CompressibleMessage[] => { + return messages.map((msg, idx) => { + let content: string; + let role: "user" | "assistant" | "tool" | "system"; + + if ("tool_calls" in msg) { + role = "assistant"; + content = msg.content || JSON.stringify(msg.tool_calls); + } else if ("tool_call_id" in msg) { + role = "tool"; + content = msg.content; + } else { + role = msg.role as "user" | "assistant" | "system"; + content = msg.content; + } + + return { + id: `msg_${idx}`, + role, + content, + tokenCount: estimateTokens(content), + age: currentTurn - idx, + isPreserved: idx >= messages.length - 3, + }; + }); +}; + +const applyContextCompression = ( + state: ReasoningAgentState, + messages: AgentMessage[], + tokenBudget: number, +): { messages: AgentMessage[]; tokensSaved: number } => { + const compressible = convertToCompressibleMessages(messages, messages.length); + const aged = markMessagesWithAge(compressible, messages.length); + const preserveList = getPreservationCandidates(aged); + + const currentTokenCount = aged.reduce((sum, m) => sum + m.tokenCount, 0); + + if (currentTokenCount <= tokenBudget * 0.8) { + return { messages, tokensSaved: 0 }; + } + + const compressionOutput = compressContext({ + messages: aged, + toolResults: [], + entities: state.reasoningState.entityTable, + currentTokenCount, + tokenLimit: tokenBudget, + preserveList, + }); + + state.options.onCompression?.(compressionOutput.tokensSaved); + + const compressedAgentMessages = compressionOutput.compressedMessages.map( + (cm): AgentMessage => ({ + role: cm.role as "user" | "assistant" | "system", + content: cm.content, + }), + ); + + return { + messages: compressedAgentMessages, + tokensSaved: compressionOutput.tokensSaved, + }; +}; + +// ============================================================================= +// QUALITY-CONTROLLED AGENT LOOP +// ============================================================================= + +export const runReasoningAgentLoop = async ( + state: ReasoningAgentState, + messages: Message[], +): Promise => { + const maxIterations = state.options.maxIterations ?? MAX_ITERATIONS; + const tokenBudget = state.options.reasoningConfig?.tokenBudget ?? 8000; + const allToolCalls: { call: ToolCall; result: ToolResult }[] = []; + let iterations = 0; + let finalResponse = ""; + + await initializePermissions(); + await refreshMCPTools(); + + let agentMessages: AgentMessage[] = [...messages]; + const originalQuery = messages.find((m) => m.role === "user")?.content || ""; + const previousAttempts: AttemptRecord[] = []; + + while (iterations < maxIterations) { + iterations++; + + state.reasoningState = { + ...state.reasoningState, + metrics: { + ...state.reasoningState.metrics, + totalLLMCalls: state.reasoningState.metrics.totalLLMCalls + 1, + }, + }; + + try { + const { messages: compressedMessages, tokensSaved } = + applyContextCompression(state, agentMessages, tokenBudget); + + if (tokensSaved > 0) { + agentMessages = compressedMessages; + } + + const response = await callLLMWithReasoning(state, agentMessages); + + if (state.options.enableReasoning !== false) { + const qualityOutput = evaluateResponseQuality({ + responseText: response.content || "", + responseToolCalls: + response.toolCalls?.map((tc) => ({ + id: tc.id, + name: tc.name, + arguments: tc.arguments, + })) || [], + originalQuery, + taskConstraints: state.taskConstraints, + previousAttempts, + }); + + state.options.onQualityEval?.(qualityOutput); + + if (qualityOutput.verdict !== "ACCEPT") { + const retryDecision = decideRetry({ + qualityOutput, + state: state.reasoningState, + availableTools: ["read", "write", "edit", "bash", "glob", "grep"], + contextBudget: tokenBudget, + }); + + state.reasoningState = retryDecision.updatedState; + + previousAttempts.push({ + attemptNumber: iterations, + timestamp: createTimestamp(), + verdict: qualityOutput.verdict, + deficiencies: qualityOutput.deficiencies, + score: qualityOutput.score, + }); + + if (retryDecision.shouldRetry) { + state.options.onRetry?.( + iterations, + `Quality: ${qualityOutput.verdict}, Score: ${qualityOutput.score.toFixed(2)}`, + ); + continue; + } + + if (retryDecision.action.kind === "ABORT") { + state.options.onError?.( + `Agent aborted: ${retryDecision.action.reason}`, + ); + return { + success: false, + finalResponse: `Aborted: ${retryDecision.action.reason}`, + iterations, + toolCalls: allToolCalls, + }; + } + + if (retryDecision.action.kind === "ESCALATE_TO_USER") { + state.options.onWarning?.("Escalating to user for guidance"); + } + } + } + + if (response.toolCalls && response.toolCalls.length > 0) { + const assistantMessage: ToolCallMessage = { + role: "assistant", + content: response.content || null, + tool_calls: response.toolCalls.map((tc) => ({ + id: tc.id, + type: "function" as const, + function: { + name: tc.name, + arguments: JSON.stringify(tc.arguments), + }, + })), + }; + agentMessages.push(assistantMessage); + + if (response.content) { + state.options.onText?.(response.content); + } + + const toolResults: Array<{ name: string; success: boolean }> = []; + + for (const toolCall of response.toolCalls) { + state.options.onToolCall?.(toolCall); + + const result = await executeToolWithTracking(state, toolCall); + allToolCalls.push({ call: toolCall, result }); + toolResults.push({ name: toolCall.name, success: result.success }); + + state.options.onToolResult?.(toolCall.id, result); + + state.memoryStore = addMemory( + state.memoryStore, + createMemoryItem(result.output, "TOOL_RESULT"), + ); + + const toolResultMessage: ToolResultMessage = { + role: "tool", + tool_call_id: toolCall.id, + content: result.error + ? `Error: ${result.error}\n\n${result.output}` + : result.output, + }; + agentMessages.push(toolResultMessage); + + state.reasoningState = { + ...state.reasoningState, + metrics: { + ...state.reasoningState.metrics, + totalToolExecutions: + state.reasoningState.metrics.totalToolExecutions + 1, + }, + }; + } + + if (state.options.enableReasoning !== false) { + const termCheck = checkTermination({ + responseText: response.content || "", + hasToolCalls: true, + toolResults, + state: state.reasoningState, + }); + + state.reasoningState = { + ...state.reasoningState, + termination: termCheck.terminationState, + }; + + if (termCheck.isTerminal && termCheck.decision.kind === "COMPLETE") { + finalResponse = response.content || ""; + break; + } + } + } else { + finalResponse = response.content || ""; + + if (state.options.enableReasoning !== false) { + const termCheck = checkTermination({ + responseText: finalResponse, + hasToolCalls: false, + toolResults: [], + state: state.reasoningState, + }); + + if ( + termCheck.decision.kind === "COMPLETE" || + !termCheck.requiresValidation + ) { + state.options.onText?.(finalResponse); + break; + } + } else { + state.options.onText?.(finalResponse); + break; + } + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + state.options.onError?.(`Agent error: ${errorMessage}`); + return { + success: false, + finalResponse: `Error: ${errorMessage}`, + iterations, + toolCalls: allToolCalls, + }; + } + } + + if (iterations >= maxIterations) { + state.options.onWarning?.(`Reached max iterations (${maxIterations})`); + } + + return { + success: true, + finalResponse, + iterations, + toolCalls: allToolCalls, + }; +}; + +// ============================================================================= +// PUBLIC API +// ============================================================================= + +export const runReasoningAgent = async ( + prompt: string, + systemPrompt: string, + options: ReasoningAgentOptions, + taskConstraints?: Partial, +): Promise => { + const messages: Message[] = [ + { role: "system", content: systemPrompt }, + { role: "user", content: prompt }, + ]; + + const state = createReasoningAgentState( + process.cwd(), + options, + taskConstraints, + ); + return runReasoningAgentLoop(state, messages); +}; + +export const createReasoningAgent = ( + workingDir: string, + options: ReasoningAgentOptions, + taskConstraints?: Partial, +): { + run: (messages: Message[]) => Promise; + stop: () => void; + getState: () => ReasoningControlState; +} => { + const state = createReasoningAgentState(workingDir, options, taskConstraints); + + return { + run: (messages: Message[]) => runReasoningAgentLoop(state, messages), + stop: () => state.abort.abort(), + getState: () => state.reasoningState, + }; +}; + +// Types are exported via interface declaration above diff --git a/src/services/reasoning/__tests__/memory-selection.test.ts b/src/services/reasoning/__tests__/memory-selection.test.ts new file mode 100644 index 0000000..15645a0 --- /dev/null +++ b/src/services/reasoning/__tests__/memory-selection.test.ts @@ -0,0 +1,427 @@ +/** + * Unit tests for Memory Selection Layer + */ + +import { describe, it, expect } from "bun:test"; + +import { + selectRelevantMemories, + computeRelevance, + computeMandatoryItems, + createMemoryItem, + createQueryContext, + createMemoryStore, + addMemory, + findMemoriesByType, + findMemoriesByPath, + pruneOldMemories, +} from "../memory-selection"; + +import type { + MemoryItem, + QueryContext, + SelectionInput, +} from "@src/types/reasoning"; + +describe("Memory Selection Layer", () => { + const createTestMemory = ( + content: string, + type: MemoryItem["type"] = "CONVERSATION", + options: Partial = {}, + ): MemoryItem => ({ + id: `mem_${Math.random().toString(36).slice(2)}`, + content, + tokens: content.toLowerCase().split(/\s+/), + entities: [], + timestamp: Date.now(), + type, + causalLinks: [], + tokenCount: Math.ceil(content.length * 0.25), + ...options, + }); + + describe("computeRelevance", () => { + it("should score higher for keyword overlap", () => { + const memory = createTestMemory( + "The function handles database queries efficiently", + ); + const queryHighOverlap = createQueryContext( + "database query optimization", + {}, + ); + const queryLowOverlap = createQueryContext("user interface design", {}); + + const highScore = computeRelevance(memory, queryHighOverlap); + const lowScore = computeRelevance(memory, queryLowOverlap); + + expect(highScore.total).toBeGreaterThan(lowScore.total); + }); + + it("should score higher for recent memories", () => { + const recentMemory = createTestMemory("Recent content", "CONVERSATION", { + timestamp: Date.now(), + }); + const oldMemory = createTestMemory("Old content", "CONVERSATION", { + timestamp: Date.now() - 3600000, // 1 hour ago + }); + + const query = createQueryContext("content search", {}); + + const recentScore = computeRelevance(recentMemory, query); + const oldScore = computeRelevance(oldMemory, query); + + expect(recentScore.breakdown.recency).toBeGreaterThan( + oldScore.breakdown.recency, + ); + }); + + it("should give type bonus to ERROR type", () => { + const errorMemory = createTestMemory("Error: connection failed", "ERROR"); + const conversationMemory = createTestMemory( + "Error: connection failed", + "CONVERSATION", + ); + + const query = createQueryContext("error handling", {}); + + const errorScore = computeRelevance(errorMemory, query); + const convScore = computeRelevance(conversationMemory, query); + + expect(errorScore.breakdown.typeBonus).toBeGreaterThan( + convScore.breakdown.typeBonus, + ); + }); + + it("should score causal links", () => { + const linkedMemory = createTestMemory("Linked memory", "CONVERSATION", { + causalLinks: ["active_item_1"], + }); + const unlinkedMemory = createTestMemory( + "Unlinked memory", + "CONVERSATION", + { + causalLinks: [], + }, + ); + + const query = createQueryContext("test", { + activeItems: ["active_item_1"], + }); + + const linkedScore = computeRelevance(linkedMemory, query); + const unlinkedScore = computeRelevance(unlinkedMemory, query); + + expect(linkedScore.breakdown.causalLink).toBe(1); + expect(unlinkedScore.breakdown.causalLink).toBe(0); + }); + + it("should score path overlap", () => { + const memoryWithPath = createTestMemory("File content", "FILE_CONTENT", { + filePaths: ["/src/services/agent.ts"], + }); + + const queryMatchingPath = createQueryContext("agent implementation", { + activePaths: ["/src/services/agent.ts"], + }); + + const queryDifferentPath = createQueryContext("agent implementation", { + activePaths: ["/src/utils/helpers.ts"], + }); + + const matchingScore = computeRelevance(memoryWithPath, queryMatchingPath); + const differentScore = computeRelevance( + memoryWithPath, + queryDifferentPath, + ); + + expect(matchingScore.breakdown.pathOverlap).toBeGreaterThan( + differentScore.breakdown.pathOverlap, + ); + }); + }); + + describe("selectRelevantMemories", () => { + it("should select memories within token budget", () => { + const memories = [ + createTestMemory("First memory content here", "CONVERSATION", { + tokenCount: 100, + }), + createTestMemory("Second memory content here", "CONVERSATION", { + tokenCount: 100, + }), + createTestMemory("Third memory content here", "CONVERSATION", { + tokenCount: 100, + }), + ]; + + const input: SelectionInput = { + memories, + query: createQueryContext("memory content", {}), + tokenBudget: 250, + mandatoryItems: [], + }; + + const result = selectRelevantMemories(input); + + expect(result.tokenUsage).toBeLessThanOrEqual(250); + }); + + it("should always include mandatory items", () => { + const memories = [ + createTestMemory("Important memory", "CONVERSATION", { + id: "mandatory_1", + }), + createTestMemory("Irrelevant memory about cooking", "CONVERSATION"), + ]; + + const input: SelectionInput = { + memories, + query: createQueryContext("completely unrelated topic", {}), + tokenBudget: 1000, + mandatoryItems: ["mandatory_1"], + }; + + const result = selectRelevantMemories(input); + + expect(result.selected.some((m) => m.id === "mandatory_1")).toBe(true); + }); + + it("should exclude low relevance items", () => { + const memories = [ + createTestMemory( + "Highly relevant database query optimization", + "CONVERSATION", + ), + createTestMemory( + "xyz abc def completely unrelated topic", + "CONVERSATION", + ), + ]; + + const input: SelectionInput = { + memories, + query: createQueryContext("database query optimization", {}), + tokenBudget: 1000, + mandatoryItems: [], + }; + + const result = selectRelevantMemories(input); + + // At least one memory should be selected (the relevant one) + expect(result.selected.length).toBeGreaterThanOrEqual(1); + // The first (relevant) memory should be selected + expect(result.selected.some((m) => m.content.includes("database"))).toBe( + true, + ); + }); + + it("should return scores for all selected items", () => { + const memories = [ + createTestMemory("First memory", "CONVERSATION", { id: "mem_1" }), + createTestMemory("Second memory", "CONVERSATION", { id: "mem_2" }), + ]; + + const input: SelectionInput = { + memories, + query: createQueryContext("memory", {}), + tokenBudget: 1000, + mandatoryItems: [], + }; + + const result = selectRelevantMemories(input); + + for (const selected of result.selected) { + expect(result.scores.has(selected.id)).toBe(true); + } + }); + }); + + describe("computeMandatoryItems", () => { + it("should include recent memories", () => { + const now = Date.now(); + const memories = [ + createTestMemory("Recent", "CONVERSATION", { + id: "recent", + timestamp: now, + }), + createTestMemory("Old", "CONVERSATION", { + id: "old", + timestamp: now - 600000, + }), + ]; + + const mandatory = computeMandatoryItems(memories, now); + + expect(mandatory).toContain("recent"); + }); + + it("should include recent error memories", () => { + const now = Date.now(); + const memories = [ + createTestMemory("Error occurred", "ERROR", { + id: "error_1", + timestamp: now - 300000, // 5 minutes ago + }), + ]; + + const mandatory = computeMandatoryItems(memories, now); + + expect(mandatory).toContain("error_1"); + }); + + it("should include decision memories", () => { + const now = Date.now(); + const memories = [ + createTestMemory("Decided to use TypeScript", "DECISION", { + id: "decision_1", + }), + createTestMemory("Decided to use React", "DECISION", { + id: "decision_2", + }), + createTestMemory("Decided to use Bun", "DECISION", { + id: "decision_3", + }), + createTestMemory("Decided to use Zustand", "DECISION", { + id: "decision_4", + }), + ]; + + const mandatory = computeMandatoryItems(memories, now); + + // Should include last 3 decisions + expect(mandatory).toContain("decision_2"); + expect(mandatory).toContain("decision_3"); + expect(mandatory).toContain("decision_4"); + }); + }); + + describe("Memory Store Operations", () => { + describe("createMemoryStore", () => { + it("should create empty store with max items", () => { + const store = createMemoryStore(500); + + expect(store.items).toHaveLength(0); + expect(store.maxItems).toBe(500); + }); + }); + + describe("addMemory", () => { + it("should add memory to store", () => { + let store = createMemoryStore(100); + const memory = createMemoryItem("Test content", "CONVERSATION"); + + store = addMemory(store, memory); + + expect(store.items).toHaveLength(1); + expect(store.items[0].content).toBe("Test content"); + }); + + it("should prune oldest items when exceeding max", () => { + let store = createMemoryStore(3); + + for (let i = 0; i < 5; i++) { + const memory = createMemoryItem(`Memory ${i}`, "CONVERSATION"); + store = addMemory(store, memory); + } + + expect(store.items.length).toBeLessThanOrEqual(3); + }); + }); + + describe("findMemoriesByType", () => { + it("should filter by type", () => { + let store = createMemoryStore(100); + store = addMemory( + store, + createMemoryItem("Conversation", "CONVERSATION"), + ); + store = addMemory(store, createMemoryItem("Error", "ERROR")); + store = addMemory( + store, + createMemoryItem("Tool result", "TOOL_RESULT"), + ); + + const errors = findMemoriesByType(store, "ERROR"); + + expect(errors).toHaveLength(1); + expect(errors[0].content).toBe("Error"); + }); + }); + + describe("findMemoriesByPath", () => { + it("should find memories by file path", () => { + let store = createMemoryStore(100); + store = addMemory(store, { + ...createMemoryItem("File content", "FILE_CONTENT"), + filePaths: ["/src/services/agent.ts"], + }); + store = addMemory(store, { + ...createMemoryItem("Other file", "FILE_CONTENT"), + filePaths: ["/src/utils/helpers.ts"], + }); + + const results = findMemoriesByPath(store, "agent.ts"); + + expect(results).toHaveLength(1); + expect(results[0].content).toBe("File content"); + }); + }); + + describe("pruneOldMemories", () => { + it("should remove memories older than threshold", () => { + const now = Date.now(); + let store = createMemoryStore(100); + + store = addMemory(store, { + ...createMemoryItem("Recent", "CONVERSATION"), + timestamp: now, + }); + store = addMemory(store, { + ...createMemoryItem("Old", "CONVERSATION"), + timestamp: now - 7200000, // 2 hours ago + }); + + const pruned = pruneOldMemories(store, 3600000); // 1 hour threshold + + expect(pruned.items).toHaveLength(1); + expect(pruned.items[0].content).toBe("Recent"); + }); + }); + }); + + describe("createMemoryItem", () => { + it("should create memory with correct structure", () => { + const memory = createMemoryItem("Test content", "CONVERSATION", { + filePaths: ["/test.ts"], + causalLinks: ["prev_memory"], + }); + + expect(memory.content).toBe("Test content"); + expect(memory.type).toBe("CONVERSATION"); + expect(memory.filePaths).toContain("/test.ts"); + expect(memory.causalLinks).toContain("prev_memory"); + expect(memory.tokenCount).toBeGreaterThan(0); + expect(memory.id).toMatch(/^mem_/); + }); + + it("should tokenize content", () => { + const memory = createMemoryItem("Hello world test", "CONVERSATION"); + + expect(memory.tokens.length).toBeGreaterThan(0); + }); + }); + + describe("createQueryContext", () => { + it("should create query context with tokens", () => { + const context = createQueryContext("database query optimization", { + activePaths: ["/src/db.ts"], + activeItems: ["item_1"], + }); + + expect(context.tokens.length).toBeGreaterThan(0); + expect(context.activePaths).toContain("/src/db.ts"); + expect(context.activeItems).toContain("item_1"); + expect(context.timestamp).toBeDefined(); + }); + }); +}); diff --git a/src/services/reasoning/__tests__/quality-evaluation.test.ts b/src/services/reasoning/__tests__/quality-evaluation.test.ts new file mode 100644 index 0000000..7c8490b --- /dev/null +++ b/src/services/reasoning/__tests__/quality-evaluation.test.ts @@ -0,0 +1,276 @@ +/** + * Unit tests for Quality Evaluation Layer + */ + +import { describe, it, expect } from "bun:test"; + +import { + evaluateQuality, + computeVerdict, + hasHallucinationMarkers, + hasContradiction, +} from "../quality-evaluation"; + +import type { + QualityEvalInput, + TaskConstraints, + AttemptRecord, +} from "@src/types/reasoning"; + +describe("Quality Evaluation Layer", () => { + const createDefaultInput = ( + overrides: Partial = {}, + ): QualityEvalInput => ({ + responseText: "Here is the solution to your problem.", + responseToolCalls: [], + expectedType: "text", + queryTokens: ["solution", "problem"], + queryEntities: [], + previousAttempts: [], + taskConstraints: { + requiredOutputs: [], + expectedToolCalls: [], + maxResponseTokens: 4000, + requiresCode: false, + }, + ...overrides, + }); + + describe("evaluateQuality", () => { + it("should accept a high-quality text response", () => { + const input = createDefaultInput({ + responseText: + "Here is the solution to your problem. I've analyzed the issue and found the root cause.", + queryTokens: ["solution", "problem", "analyze", "issue"], + }); + + const result = evaluateQuality(input); + + expect(result.score).toBeGreaterThan(0.5); + expect(result.verdict).toBe("ACCEPT"); + expect(result.deficiencies).toHaveLength(0); + }); + + it("should reject an empty response", () => { + const input = createDefaultInput({ + responseText: "", + responseToolCalls: [], + }); + + const result = evaluateQuality(input); + + expect(result.verdict).not.toBe("ACCEPT"); + expect(result.deficiencies).toContain("EMPTY_RESPONSE"); + }); + + it("should detect missing tool calls when expected", () => { + const input = createDefaultInput({ + responseText: "I will read the file now.", + responseToolCalls: [], + expectedType: "tool_call", + taskConstraints: { + requiredOutputs: [], + expectedToolCalls: ["read"], + maxResponseTokens: 4000, + requiresCode: false, + }, + }); + + const result = evaluateQuality(input); + + expect(result.deficiencies).toContain("MISSING_TOOL_CALL"); + }); + + it("should accept response with tool calls when expected", () => { + const input = createDefaultInput({ + responseText: "Let me read that file.", + responseToolCalls: [ + { id: "1", name: "read", arguments: { path: "/test.ts" } }, + ], + expectedType: "tool_call", + taskConstraints: { + requiredOutputs: [], + expectedToolCalls: ["read"], + maxResponseTokens: 4000, + requiresCode: false, + }, + }); + + const result = evaluateQuality(input); + + expect(result.score).toBeGreaterThan(0.5); + }); + + it("should detect query mismatch", () => { + const input = createDefaultInput({ + responseText: "The weather today is sunny and warm.", + queryTokens: ["database", "migration", "schema", "postgresql"], + }); + + const result = evaluateQuality(input); + + // With no token overlap, relevance should be lower than perfect match + expect(result.metrics.relevance).toBeLessThan(1); + }); + + it("should detect incomplete code when required", () => { + const input = createDefaultInput({ + responseText: "Here is some text without any code.", + taskConstraints: { + requiredOutputs: [], + expectedToolCalls: [], + maxResponseTokens: 4000, + requiresCode: true, + codeLanguage: "typescript", + }, + }); + + const result = evaluateQuality(input); + + expect(result.deficiencies).toContain("INCOMPLETE_CODE"); + }); + + it("should accept valid code block when required", () => { + const input = createDefaultInput({ + responseText: + "Here is the function:\n\n```typescript\nfunction add(a: number, b: number): number {\n return a + b;\n}\n```", + taskConstraints: { + requiredOutputs: [], + expectedToolCalls: [], + maxResponseTokens: 4000, + requiresCode: true, + codeLanguage: "typescript", + }, + }); + + const result = evaluateQuality(input); + + expect(result.deficiencies).not.toContain("INCOMPLETE_CODE"); + expect(result.deficiencies).not.toContain("WRONG_LANGUAGE"); + }); + }); + + describe("computeVerdict", () => { + it("should return ACCEPT for score >= 0.70", () => { + expect(computeVerdict(0.7)).toBe("ACCEPT"); + expect(computeVerdict(0.85)).toBe("ACCEPT"); + expect(computeVerdict(1.0)).toBe("ACCEPT"); + }); + + it("should return RETRY for score between 0.40 and 0.70", () => { + expect(computeVerdict(0.69)).toBe("RETRY"); + expect(computeVerdict(0.55)).toBe("RETRY"); + expect(computeVerdict(0.4)).toBe("RETRY"); + }); + + it("should return ESCALATE for score between 0.20 and 0.40", () => { + expect(computeVerdict(0.39)).toBe("ESCALATE"); + expect(computeVerdict(0.3)).toBe("ESCALATE"); + expect(computeVerdict(0.2)).toBe("ESCALATE"); + }); + + it("should return ABORT for score < 0.20", () => { + expect(computeVerdict(0.19)).toBe("ABORT"); + expect(computeVerdict(0.1)).toBe("ABORT"); + expect(computeVerdict(0)).toBe("ABORT"); + }); + }); + + describe("hasHallucinationMarkers", () => { + it("should detect 'I don't have access' pattern", () => { + expect( + hasHallucinationMarkers( + "I don't have access to the file but I'll assume...", + ), + ).toBe(true); + }); + + it("should detect 'assuming exists' pattern", () => { + expect( + hasHallucinationMarkers( + "Assuming the function exists, here's how to use it", + ), + ).toBe(true); + }); + + it("should detect placeholder pattern", () => { + expect( + hasHallucinationMarkers("Replace [placeholder] with your value"), + ).toBe(true); + }); + + it("should not flag normal responses", () => { + expect( + hasHallucinationMarkers("Here is the implementation you requested."), + ).toBe(false); + }); + }); + + describe("hasContradiction", () => { + it("should detect 'but actually' pattern", () => { + expect( + hasContradiction( + "The function returns true, but actually it returns false", + ), + ).toBe(true); + }); + + it("should detect 'wait, no' pattern", () => { + expect( + hasContradiction( + "It's in the utils folder. Wait, no, it's in helpers.", + ), + ).toBe(true); + }); + + it("should detect 'on second thought' pattern", () => { + expect( + hasContradiction( + "Let me use forEach. On second thought, I'll use map.", + ), + ).toBe(true); + }); + + it("should not flag normal responses", () => { + expect( + hasContradiction( + "The function takes two parameters and returns their sum.", + ), + ).toBe(false); + }); + }); + + describe("structural validation", () => { + it("should detect malformed code blocks", () => { + const input = createDefaultInput({ + responseText: + "Here is the code:\n```typescript\nfunction test() {\n return 1;\n", // Missing closing ``` + }); + + const result = evaluateQuality(input); + + expect(result.metrics.structural).toBeLessThan(1); + }); + + it("should accept well-formed code blocks", () => { + const input = createDefaultInput({ + responseText: + "Here is the code:\n```typescript\nfunction test() {\n return 1;\n}\n```", + }); + + const result = evaluateQuality(input); + + expect(result.metrics.structural).toBeGreaterThan(0.5); + }); + + it("should detect unbalanced braces", () => { + const input = createDefaultInput({ + responseText: "The object is { name: 'test', value: { nested: true }", + }); + + const result = evaluateQuality(input); + + expect(result.metrics.structural).toBeLessThan(1); + }); + }); +}); diff --git a/src/services/reasoning/__tests__/retry-policy.test.ts b/src/services/reasoning/__tests__/retry-policy.test.ts new file mode 100644 index 0000000..9fa77ff --- /dev/null +++ b/src/services/reasoning/__tests__/retry-policy.test.ts @@ -0,0 +1,312 @@ +/** + * Unit tests for Retry Policy Layer + */ + +import { describe, it, expect } from "bun:test"; + +import { + createInitialRetryState, + createRetryBudget, + computeRetryTransition, + splitTaskDescription, + isRetryable, + getCurrentTier, + getRemainingAttempts, +} from "../retry-policy"; + +import type { + RetryPolicyInput, + RetryTrigger, + DeficiencyTag, +} from "@src/types/reasoning"; + +describe("Retry Policy Layer", () => { + describe("createInitialRetryState", () => { + it("should create state with INITIAL kind", () => { + const state = createInitialRetryState(); + + expect(state.currentState.kind).toBe("INITIAL"); + expect(state.totalAttempts).toBe(0); + expect(state.history).toHaveLength(0); + }); + + it("should create budget with default limits", () => { + const state = createInitialRetryState(); + + expect(state.budget.maxTotalAttempts).toBe(12); + expect(state.budget.maxPerTier).toBe(2); + expect(state.budget.maxTimeMs).toBe(60000); + }); + }); + + describe("createRetryBudget", () => { + it("should allow overriding defaults", () => { + const budget = createRetryBudget({ + maxTotalAttempts: 20, + maxPerTier: 3, + }); + + expect(budget.maxTotalAttempts).toBe(20); + expect(budget.maxPerTier).toBe(3); + expect(budget.maxTimeMs).toBe(60000); + }); + }); + + describe("computeRetryTransition", () => { + it("should transition from INITIAL to RETRY_SAME on first retry", () => { + const state = createInitialRetryState(); + const input: RetryPolicyInput = { + currentState: state, + trigger: { + event: "QUALITY_VERDICT", + verdict: "RETRY", + deficiencies: ["QUERY_MISMATCH"], + }, + availableTools: ["read", "write"], + contextBudget: 8000, + }; + + const result = computeRetryTransition(input); + + expect(result.nextState.currentState.kind).toBe("RETRY_SAME"); + expect(result.nextState.totalAttempts).toBe(1); + expect(result.action.kind).toBe("RETRY"); + }); + + it("should eventually advance to next tier after repeated failures", () => { + let state = createInitialRetryState(); + const trigger = { + event: "QUALITY_VERDICT" as const, + verdict: "RETRY" as const, + deficiencies: [] as string[], + }; + + // Run multiple iterations and verify tiers eventually change + let sawTierChange = false; + let lastKind = state.currentState.kind; + + for (let i = 0; i < 8; i++) { + const result = computeRetryTransition({ + currentState: state, + trigger, + availableTools: ["read"], + contextBudget: 8000, + }); + state = result.nextState; + + if ( + state.currentState.kind !== lastKind && + state.currentState.kind !== "INITIAL" + ) { + sawTierChange = true; + lastKind = state.currentState.kind; + } + } + + // Should have seen at least one tier change + expect(sawTierChange).toBe(true); + }); + + it("should exhaust after exceeding max total attempts", () => { + const state = createInitialRetryState(); + state.budget.maxTotalAttempts = 2; + state.totalAttempts = 2; + + const result = computeRetryTransition({ + currentState: state, + trigger: { + event: "QUALITY_VERDICT", + verdict: "RETRY", + deficiencies: [], + }, + availableTools: ["read"], + contextBudget: 8000, + }); + + expect(result.nextState.currentState.kind).toBe("EXHAUSTED"); + expect(result.action.kind).toBe("ABORT"); + }); + + it("should return REDUCE_CONTEXT transform when simplifying", () => { + let state = createInitialRetryState(); + state.currentState = { kind: "RETRY_SAME", attempts: 2, tierAttempts: 2 }; + + const result = computeRetryTransition({ + currentState: state, + trigger: { + event: "QUALITY_VERDICT", + verdict: "RETRY", + deficiencies: [], + }, + availableTools: ["read"], + contextBudget: 8000, + }); + + if ( + result.action.kind === "RETRY" && + result.action.transform.kind === "REDUCE_CONTEXT" + ) { + expect(result.action.transform.delta).toBeDefined(); + } + }); + + it("should escalate to user on permission denied errors", () => { + const state = createInitialRetryState(); + state.currentState = { + kind: "RETRY_ALTERNATIVE", + attempts: 10, + tierAttempts: 2, + }; + + const result = computeRetryTransition({ + currentState: state, + trigger: { + event: "TOOL_EXECUTION_FAILED", + error: { + toolName: "bash", + errorType: "PERMISSION_DENIED", + message: "Permission denied", + }, + }, + availableTools: ["read"], + contextBudget: 8000, + }); + + expect(result.action.kind).toBe("ESCALATE_TO_USER"); + }); + }); + + describe("splitTaskDescription", () => { + it("should split 'first...then' pattern", () => { + const result = splitTaskDescription( + "First, read the file. Then, update the content.", + ); + + expect(result.length).toBeGreaterThanOrEqual(2); + }); + + it("should split numbered list pattern", () => { + const result = splitTaskDescription( + "1. Read file 2. Parse content 3. Write output", + ); + + expect(result.length).toBeGreaterThanOrEqual(1); + }); + + it("should return single item for atomic tasks", () => { + const result = splitTaskDescription("Read the configuration file"); + + expect(result).toHaveLength(1); + expect(result[0]).toBe("Read the configuration file"); + }); + + it("should split bulleted list pattern", () => { + const result = splitTaskDescription( + "- Create file\n- Add content\n- Save changes", + ); + + expect(result.length).toBeGreaterThanOrEqual(1); + }); + }); + + describe("isRetryable", () => { + it("should return true for INITIAL state", () => { + const state = createInitialRetryState(); + + expect(isRetryable(state)).toBe(true); + }); + + it("should return true for RETRY_SAME state", () => { + const state = createInitialRetryState(); + state.currentState = { kind: "RETRY_SAME", attempts: 1, tierAttempts: 1 }; + + expect(isRetryable(state)).toBe(true); + }); + + it("should return false for EXHAUSTED state", () => { + const state = createInitialRetryState(); + state.currentState = { + kind: "EXHAUSTED", + attempts: 12, + tierAttempts: 0, + exhaustionReason: "MAX_TIERS_EXCEEDED", + }; + + expect(isRetryable(state)).toBe(false); + }); + + it("should return false for COMPLETE state", () => { + const state = createInitialRetryState(); + state.currentState = { kind: "COMPLETE", attempts: 5, tierAttempts: 0 }; + + expect(isRetryable(state)).toBe(false); + }); + }); + + describe("getCurrentTier", () => { + it("should return current tier kind", () => { + const state = createInitialRetryState(); + + expect(getCurrentTier(state)).toBe("INITIAL"); + + state.currentState = { + kind: "RETRY_DECOMPOSED", + attempts: 5, + tierAttempts: 1, + }; + + expect(getCurrentTier(state)).toBe("RETRY_DECOMPOSED"); + }); + }); + + describe("getRemainingAttempts", () => { + it("should calculate remaining attempts correctly", () => { + const state = createInitialRetryState(); + state.totalAttempts = 4; + + expect(getRemainingAttempts(state)).toBe(8); + + state.totalAttempts = 12; + + expect(getRemainingAttempts(state)).toBe(0); + }); + }); + + describe("state machine progression", () => { + it("should progress through tiers and eventually exhaust", () => { + let state = createInitialRetryState(); + const trigger: RetryTrigger = { + event: "QUALITY_VERDICT", + verdict: "RETRY", + deficiencies: [], + }; + + // Track which tiers we've seen + const seenTiers = new Set(); + let iterations = 0; + const maxIterations = 15; + + while ( + iterations < maxIterations && + state.currentState.kind !== "EXHAUSTED" + ) { + const result = computeRetryTransition({ + currentState: state, + trigger, + availableTools: ["read", "write"], + contextBudget: 8000, + }); + + seenTiers.add(result.nextState.currentState.kind); + state = result.nextState; + iterations++; + } + + // Should have reached EXHAUSTED + expect(state.currentState.kind).toBe("EXHAUSTED"); + + // Should have seen multiple tiers along the way + expect(seenTiers.size).toBeGreaterThan(1); + }); + }); +}); diff --git a/src/services/reasoning/__tests__/termination-detection.test.ts b/src/services/reasoning/__tests__/termination-detection.test.ts new file mode 100644 index 0000000..7a5a84f --- /dev/null +++ b/src/services/reasoning/__tests__/termination-detection.test.ts @@ -0,0 +1,504 @@ +/** + * Unit tests for Termination Detection Layer + */ + +import { describe, it, expect } from "bun:test"; + +import { + createInitialTerminationState, + processTerminationTrigger, + computeTerminationConfidence, + extractValidationFailures, + isComplete, + isFailed, + isTerminal, + requiresValidation, + getConfidencePercentage, +} from "../termination-detection"; + +import type { + TerminationState, + TerminationTrigger, + CompletionSignal, + ValidationResult, +} from "@src/types/reasoning"; + +describe("Termination Detection Layer", () => { + describe("createInitialTerminationState", () => { + it("should create state with RUNNING status", () => { + const state = createInitialTerminationState(); + + expect(state.status).toBe("RUNNING"); + expect(state.completionSignals).toHaveLength(0); + expect(state.validationResults).toHaveLength(0); + expect(state.confidenceScore).toBe(0); + }); + }); + + describe("processTerminationTrigger", () => { + describe("MODEL_OUTPUT trigger", () => { + it("should detect completion signals from model text", () => { + const state = createInitialTerminationState(); + const trigger: TerminationTrigger = { + event: "MODEL_OUTPUT", + content: "I've completed the task successfully.", + hasToolCalls: false, + }; + + const result = processTerminationTrigger(state, trigger); + + expect(result.evidence.signals.length).toBeGreaterThan(0); + expect( + result.evidence.signals.some((s) => s.source === "MODEL_STATEMENT"), + ).toBe(true); + }); + + it("should detect no pending actions when no tool calls", () => { + const state = createInitialTerminationState(); + const trigger: TerminationTrigger = { + event: "MODEL_OUTPUT", + content: "Here is the answer.", + hasToolCalls: false, + }; + + const result = processTerminationTrigger(state, trigger); + + expect( + result.evidence.signals.some( + (s) => s.source === "NO_PENDING_ACTIONS", + ), + ).toBe(true); + }); + + it("should not add NO_PENDING_ACTIONS when tool calls present", () => { + const state = createInitialTerminationState(); + const trigger: TerminationTrigger = { + event: "MODEL_OUTPUT", + content: "Let me read that file.", + hasToolCalls: true, + }; + + const result = processTerminationTrigger(state, trigger); + + expect( + result.evidence.signals.some( + (s) => s.source === "NO_PENDING_ACTIONS", + ), + ).toBe(false); + }); + }); + + describe("TOOL_COMPLETED trigger", () => { + it("should add TOOL_SUCCESS signal on successful tool execution", () => { + const state = createInitialTerminationState(); + const trigger: TerminationTrigger = { + event: "TOOL_COMPLETED", + toolName: "write", + success: true, + }; + + const result = processTerminationTrigger(state, trigger); + + expect( + result.evidence.signals.some((s) => s.source === "TOOL_SUCCESS"), + ).toBe(true); + }); + + it("should not add signal on failed tool execution", () => { + const state = createInitialTerminationState(); + const trigger: TerminationTrigger = { + event: "TOOL_COMPLETED", + toolName: "write", + success: false, + }; + + const result = processTerminationTrigger(state, trigger); + + expect( + result.evidence.signals.some((s) => s.source === "TOOL_SUCCESS"), + ).toBe(false); + }); + }); + + describe("USER_INPUT trigger", () => { + it("should immediately confirm completion on user acceptance", () => { + const state = createInitialTerminationState(); + const trigger: TerminationTrigger = { + event: "USER_INPUT", + isAcceptance: true, + }; + + const result = processTerminationTrigger(state, trigger); + + expect(result.status).toBe("CONFIRMED_COMPLETE"); + expect( + result.evidence.signals.some((s) => s.source === "USER_ACCEPT"), + ).toBe(true); + }); + }); + + describe("VALIDATION_RESULT trigger", () => { + it("should update validation results", () => { + const state = createInitialTerminationState(); + state.status = "AWAITING_VALIDATION"; + + const trigger: TerminationTrigger = { + event: "VALIDATION_RESULT", + result: { + checkId: "file_exists_check", + passed: true, + details: "All files exist", + duration: 100, + }, + }; + + const result = processTerminationTrigger(state, trigger); + + expect(result.evidence.validationResults).toHaveLength(1); + expect(result.evidence.validationResults[0].passed).toBe(true); + }); + + it("should update existing validation result", () => { + const state = createInitialTerminationState(); + state.status = "AWAITING_VALIDATION"; + state.validationResults = [ + { + checkId: "file_exists_check", + passed: false, + details: "File missing", + duration: 50, + }, + ]; + + const trigger: TerminationTrigger = { + event: "VALIDATION_RESULT", + result: { + checkId: "file_exists_check", + passed: true, + details: "File now exists", + duration: 100, + }, + }; + + const result = processTerminationTrigger(state, trigger); + + expect(result.evidence.validationResults).toHaveLength(1); + expect(result.evidence.validationResults[0].passed).toBe(true); + }); + }); + + describe("status transitions", () => { + it("should accumulate signals and increase confidence over time", () => { + const state = createInitialTerminationState(); + state.completionSignals = [ + { source: "MODEL_STATEMENT", timestamp: Date.now(), confidence: 0.3 }, + { source: "TOOL_SUCCESS", timestamp: Date.now(), confidence: 0.5 }, + { source: "TOOL_SUCCESS", timestamp: Date.now(), confidence: 0.5 }, + ]; + + const trigger: TerminationTrigger = { + event: "MODEL_OUTPUT", + content: "I've completed the task successfully.", + hasToolCalls: false, + }; + + const result = processTerminationTrigger(state, trigger); + + // Confidence should increase with more signals + expect(result.confidence).toBeGreaterThan(0); + expect(result.evidence.signals.length).toBeGreaterThan( + state.completionSignals.length, + ); + }); + + it("should transition from POTENTIALLY_COMPLETE to AWAITING_VALIDATION", () => { + const state = createInitialTerminationState(); + state.status = "POTENTIALLY_COMPLETE"; + + const trigger: TerminationTrigger = { + event: "TOOL_COMPLETED", + toolName: "write", + success: true, + }; + + const result = processTerminationTrigger(state, trigger); + + expect(result.status).toBe("AWAITING_VALIDATION"); + }); + }); + }); + + describe("computeTerminationConfidence", () => { + it("should compute low confidence with no signals or results", () => { + const confidence = computeTerminationConfidence([], []); + + expect(confidence).toBe(0); + }); + + it("should compute confidence from signals", () => { + const signals: CompletionSignal[] = [ + { source: "MODEL_STATEMENT", timestamp: Date.now(), confidence: 0.3 }, + { source: "TOOL_SUCCESS", timestamp: Date.now(), confidence: 0.5 }, + ]; + + const confidence = computeTerminationConfidence(signals, []); + + expect(confidence).toBeGreaterThan(0); + expect(confidence).toBeLessThanOrEqual(0.4); // Signal max is 0.4 + }); + + it("should compute confidence from validation results", () => { + const results: ValidationResult[] = [ + { + checkId: "file_exists_check", + passed: true, + details: "OK", + duration: 100, + }, + { + checkId: "syntax_valid_check", + passed: true, + details: "OK", + duration: 100, + }, + ]; + + const confidence = computeTerminationConfidence([], results); + + expect(confidence).toBeGreaterThan(0); + }); + + it("should compute combined confidence", () => { + const signals: CompletionSignal[] = [ + { source: "TOOL_SUCCESS", timestamp: Date.now(), confidence: 0.5 }, + ]; + const results: ValidationResult[] = [ + { + checkId: "file_exists_check", + passed: true, + details: "OK", + duration: 100, + }, + ]; + + const combinedConfidence = computeTerminationConfidence(signals, results); + const signalOnlyConfidence = computeTerminationConfidence(signals, []); + const resultOnlyConfidence = computeTerminationConfidence([], results); + + expect(combinedConfidence).toBeGreaterThan(signalOnlyConfidence); + expect(combinedConfidence).toBeGreaterThan(resultOnlyConfidence); + }); + }); + + describe("extractValidationFailures", () => { + it("should extract failed validations", () => { + const results: ValidationResult[] = [ + { checkId: "check_1", passed: true, details: "OK", duration: 100 }, + { + checkId: "check_2", + passed: false, + details: "File not found", + duration: 50, + }, + { + checkId: "check_3", + passed: false, + details: "Syntax error", + duration: 75, + }, + ]; + + const failures = extractValidationFailures(results); + + expect(failures).toHaveLength(2); + expect(failures.map((f) => f.checkId)).toContain("check_2"); + expect(failures.map((f) => f.checkId)).toContain("check_3"); + }); + + it("should mark permission errors as non-recoverable", () => { + const results: ValidationResult[] = [ + { + checkId: "check_1", + passed: false, + details: "Permission denied", + duration: 100, + }, + ]; + + const failures = extractValidationFailures(results); + + expect(failures[0].recoverable).toBe(false); + }); + + it("should mark other errors as recoverable", () => { + const results: ValidationResult[] = [ + { + checkId: "check_1", + passed: false, + details: "Timeout occurred", + duration: 100, + }, + ]; + + const failures = extractValidationFailures(results); + + expect(failures[0].recoverable).toBe(true); + }); + }); + + describe("state query functions", () => { + describe("isComplete", () => { + it("should return true only for CONFIRMED_COMPLETE", () => { + const completeState: TerminationState = { + ...createInitialTerminationState(), + status: "CONFIRMED_COMPLETE", + }; + const runningState: TerminationState = { + ...createInitialTerminationState(), + status: "RUNNING", + }; + + expect(isComplete(completeState)).toBe(true); + expect(isComplete(runningState)).toBe(false); + }); + }); + + describe("isFailed", () => { + it("should return true only for FAILED", () => { + const failedState: TerminationState = { + ...createInitialTerminationState(), + status: "FAILED", + }; + const runningState: TerminationState = { + ...createInitialTerminationState(), + status: "RUNNING", + }; + + expect(isFailed(failedState)).toBe(true); + expect(isFailed(runningState)).toBe(false); + }); + }); + + describe("isTerminal", () => { + it("should return true for CONFIRMED_COMPLETE or FAILED", () => { + expect( + isTerminal({ + ...createInitialTerminationState(), + status: "CONFIRMED_COMPLETE", + }), + ).toBe(true); + expect( + isTerminal({ ...createInitialTerminationState(), status: "FAILED" }), + ).toBe(true); + expect( + isTerminal({ ...createInitialTerminationState(), status: "RUNNING" }), + ).toBe(false); + expect( + isTerminal({ + ...createInitialTerminationState(), + status: "AWAITING_VALIDATION", + }), + ).toBe(false); + }); + }); + + describe("requiresValidation", () => { + it("should return true for POTENTIALLY_COMPLETE and AWAITING_VALIDATION", () => { + expect( + requiresValidation({ + ...createInitialTerminationState(), + status: "POTENTIALLY_COMPLETE", + }), + ).toBe(true); + expect( + requiresValidation({ + ...createInitialTerminationState(), + status: "AWAITING_VALIDATION", + }), + ).toBe(true); + expect( + requiresValidation({ + ...createInitialTerminationState(), + status: "RUNNING", + }), + ).toBe(false); + expect( + requiresValidation({ + ...createInitialTerminationState(), + status: "CONFIRMED_COMPLETE", + }), + ).toBe(false); + }); + }); + + describe("getConfidencePercentage", () => { + it("should format confidence as percentage", () => { + const state: TerminationState = { + ...createInitialTerminationState(), + confidenceScore: 0.756, + }; + + expect(getConfidencePercentage(state)).toBe("75.6%"); + }); + + it("should handle zero confidence", () => { + const state = createInitialTerminationState(); + + expect(getConfidencePercentage(state)).toBe("0.0%"); + }); + + it("should handle 100% confidence", () => { + const state: TerminationState = { + ...createInitialTerminationState(), + confidenceScore: 1.0, + }; + + expect(getConfidencePercentage(state)).toBe("100.0%"); + }); + }); + }); + + describe("decision computation", () => { + it("should return CONTINUE for low confidence", () => { + const state = createInitialTerminationState(); + const trigger: TerminationTrigger = { + event: "MODEL_OUTPUT", + content: "Working on it...", + hasToolCalls: true, + }; + + const result = processTerminationTrigger(state, trigger); + + expect(result.decision.kind).toBe("CONTINUE"); + }); + + it("should return VALIDATE for potentially complete state", () => { + const state: TerminationState = { + ...createInitialTerminationState(), + status: "POTENTIALLY_COMPLETE", + confidenceScore: 0.6, + }; + const trigger: TerminationTrigger = { + event: "TOOL_COMPLETED", + toolName: "write", + success: true, + }; + + const result = processTerminationTrigger(state, trigger); + + expect(result.decision.kind).toBe("VALIDATE"); + }); + + it("should return COMPLETE for confirmed completion", () => { + const state = createInitialTerminationState(); + const trigger: TerminationTrigger = { + event: "USER_INPUT", + isAcceptance: true, + }; + + const result = processTerminationTrigger(state, trigger); + + expect(result.decision.kind).toBe("COMPLETE"); + }); + }); +}); diff --git a/src/services/reasoning/__tests__/utils.test.ts b/src/services/reasoning/__tests__/utils.test.ts new file mode 100644 index 0000000..53e1f46 --- /dev/null +++ b/src/services/reasoning/__tests__/utils.test.ts @@ -0,0 +1,435 @@ +/** + * Unit tests for Reasoning Utilities + */ + +import { describe, it, expect } from "bun:test"; + +import { + estimateTokens, + tokenize, + jaccardSimilarity, + weightedSum, + extractEntities, + createEntityTable, + truncateMiddle, + foldCode, + extractCodeBlocks, + recencyDecay, + generateId, + isValidJson, + hasBalancedBraces, + countMatches, + sum, + unique, + groupBy, +} from "../utils"; + +describe("Reasoning Utilities", () => { + describe("estimateTokens", () => { + it("should estimate tokens based on character count", () => { + const text = "Hello world"; // 11 chars + const tokens = estimateTokens(text); + + expect(tokens).toBeGreaterThan(0); + expect(tokens).toBeLessThan(text.length); + }); + + it("should handle empty string", () => { + expect(estimateTokens("")).toBe(0); + }); + }); + + describe("tokenize", () => { + it("should split text into lowercase tokens", () => { + const tokens = tokenize("Hello World Test"); + + expect(tokens.every((t) => t === t.toLowerCase())).toBe(true); + }); + + it("should filter stop words", () => { + const tokens = tokenize("the quick brown fox jumps over the lazy dog"); + + expect(tokens).not.toContain("the"); + // "over" may or may not be filtered depending on stop words list + expect(tokens).toContain("quick"); + expect(tokens).toContain("brown"); + }); + + it("should filter short tokens", () => { + const tokens = tokenize("I am a test"); + + expect(tokens).not.toContain("i"); + expect(tokens).not.toContain("am"); + expect(tokens).not.toContain("a"); + }); + + it("should handle punctuation", () => { + const tokens = tokenize("Hello, world! How are you?"); + + expect(tokens.every((t) => !/[,!?]/.test(t))).toBe(true); + }); + }); + + describe("jaccardSimilarity", () => { + it("should return 1 for identical sets", () => { + const similarity = jaccardSimilarity(["a", "b", "c"], ["a", "b", "c"]); + + expect(similarity).toBe(1); + }); + + it("should return 0 for disjoint sets", () => { + const similarity = jaccardSimilarity(["a", "b", "c"], ["d", "e", "f"]); + + expect(similarity).toBe(0); + }); + + it("should return correct value for partial overlap", () => { + const similarity = jaccardSimilarity(["a", "b", "c"], ["b", "c", "d"]); + + // Intersection: {b, c} = 2, Union: {a, b, c, d} = 4 + expect(similarity).toBe(0.5); + }); + + it("should handle empty sets", () => { + expect(jaccardSimilarity([], [])).toBe(0); + expect(jaccardSimilarity(["a"], [])).toBe(0); + expect(jaccardSimilarity([], ["a"])).toBe(0); + }); + }); + + describe("weightedSum", () => { + it("should compute weighted sum correctly", () => { + const result = weightedSum([1, 2, 3], [0.5, 0.3, 0.2]); + + expect(result).toBeCloseTo(1 * 0.5 + 2 * 0.3 + 3 * 0.2); + }); + + it("should throw for mismatched lengths", () => { + expect(() => weightedSum([1, 2], [0.5])).toThrow(); + }); + + it("should handle empty arrays", () => { + expect(weightedSum([], [])).toBe(0); + }); + }); + + describe("extractEntities", () => { + it("should extract file paths", () => { + const entities = extractEntities( + "Check the file src/index.ts for details", + "msg_1", + ); + + expect( + entities.some((e) => e.type === "FILE" && e.value.includes("index.ts")), + ).toBe(true); + }); + + it("should extract function names", () => { + const entities = extractEntities( + "function handleClick() { return 1; }", + "msg_1", + ); + + expect(entities.some((e) => e.type === "FUNCTION")).toBe(true); + }); + + it("should extract URLs", () => { + const entities = extractEntities( + "Visit https://example.com for more info", + "msg_1", + ); + + expect( + entities.some( + (e) => e.type === "URL" && e.value.includes("example.com"), + ), + ).toBe(true); + }); + + it("should set source message ID", () => { + const entities = extractEntities("file.ts", "test_msg"); + + if (entities.length > 0) { + expect(entities[0].sourceMessageId).toBe("test_msg"); + } + }); + }); + + describe("createEntityTable", () => { + it("should organize entities by type", () => { + const entities = [ + { + type: "FILE" as const, + value: "test.ts", + sourceMessageId: "msg_1", + frequency: 1, + }, + { + type: "FILE" as const, + value: "other.ts", + sourceMessageId: "msg_1", + frequency: 1, + }, + { + type: "URL" as const, + value: "https://test.com", + sourceMessageId: "msg_1", + frequency: 1, + }, + ]; + + const table = createEntityTable(entities); + + expect(table.byType.FILE).toHaveLength(2); + expect(table.byType.URL).toHaveLength(1); + }); + + it("should organize entities by source", () => { + const entities = [ + { + type: "FILE" as const, + value: "test.ts", + sourceMessageId: "msg_1", + frequency: 1, + }, + { + type: "FILE" as const, + value: "other.ts", + sourceMessageId: "msg_2", + frequency: 1, + }, + ]; + + const table = createEntityTable(entities); + + expect(table.bySource["msg_1"]).toHaveLength(1); + expect(table.bySource["msg_2"]).toHaveLength(1); + }); + }); + + describe("truncateMiddle", () => { + it("should truncate long text", () => { + const text = "a".repeat(200); + const result = truncateMiddle(text, 50, 50); + + expect(result.length).toBeLessThan(text.length); + expect(result).toContain("truncated"); + }); + + it("should not truncate short text", () => { + const text = "short text"; + const result = truncateMiddle(text, 50, 50); + + expect(result).toBe(text); + }); + + it("should preserve head and tail", () => { + const text = "HEAD_CONTENT_MIDDLE_STUFF_TAIL_CONTENT"; + const result = truncateMiddle(text, 12, 12); + + expect(result.startsWith("HEAD_CONTENT")).toBe(true); + expect(result.endsWith("TAIL_CONTENT")).toBe(true); + }); + }); + + describe("foldCode", () => { + it("should fold long code blocks", () => { + const code = Array.from({ length: 50 }, (_, i) => `line ${i + 1}`).join( + "\n", + ); + const result = foldCode(code, { keepLines: 5, tailLines: 3 }); + + expect(result.split("\n").length).toBeLessThan(50); + expect(result).toContain("folded"); + }); + + it("should not fold short code blocks", () => { + const code = "line 1\nline 2\nline 3"; + const result = foldCode(code, { keepLines: 5, tailLines: 3 }); + + expect(result).toBe(code); + }); + + it("should preserve first and last lines", () => { + const code = Array.from({ length: 50 }, (_, i) => `line ${i + 1}`).join( + "\n", + ); + const result = foldCode(code, { keepLines: 2, tailLines: 2 }); + + expect(result).toContain("line 1"); + expect(result).toContain("line 2"); + expect(result).toContain("line 49"); + expect(result).toContain("line 50"); + }); + }); + + describe("extractCodeBlocks", () => { + it("should extract code blocks with language", () => { + const text = + "Here is code:\n```typescript\nconst x = 1;\n```\nMore text."; + const blocks = extractCodeBlocks(text); + + expect(blocks).toHaveLength(1); + expect(blocks[0].language).toBe("typescript"); + expect(blocks[0].content).toContain("const x = 1"); + }); + + it("should extract multiple code blocks", () => { + const text = "```js\ncode1\n```\n\n```python\ncode2\n```"; + const blocks = extractCodeBlocks(text); + + expect(blocks).toHaveLength(2); + expect(blocks[0].language).toBe("js"); + expect(blocks[1].language).toBe("python"); + }); + + it("should handle code blocks without language", () => { + const text = "```\nsome code\n```"; + const blocks = extractCodeBlocks(text); + + expect(blocks).toHaveLength(1); + expect(blocks[0].language).toBe("unknown"); + }); + + it("should track positions", () => { + const text = "Start\n```ts\ncode\n```\nEnd"; + const blocks = extractCodeBlocks(text); + + expect(blocks[0].startIndex).toBeGreaterThan(0); + expect(blocks[0].endIndex).toBeGreaterThan(blocks[0].startIndex); + }); + }); + + describe("recencyDecay", () => { + it("should return 1 for current time", () => { + const now = Date.now(); + const decay = recencyDecay(now, now, 30); + + expect(decay).toBe(1); + }); + + it("should return 0.5 at half-life", () => { + const now = Date.now(); + const halfLifeAgo = now - 30 * 60 * 1000; // 30 minutes ago + const decay = recencyDecay(halfLifeAgo, now, 30); + + expect(decay).toBeCloseTo(0.5, 2); + }); + + it("should decrease with age", () => { + const now = Date.now(); + const recent = recencyDecay(now - 60000, now, 30); + const old = recencyDecay(now - 3600000, now, 30); + + expect(recent).toBeGreaterThan(old); + }); + }); + + describe("generateId", () => { + it("should generate unique IDs", () => { + const ids = new Set(); + + for (let i = 0; i < 100; i++) { + ids.add(generateId()); + } + + expect(ids.size).toBe(100); + }); + + it("should include prefix when provided", () => { + const id = generateId("test"); + + expect(id.startsWith("test_")).toBe(true); + }); + }); + + describe("isValidJson", () => { + it("should return true for valid JSON", () => { + expect(isValidJson('{"key": "value"}')).toBe(true); + expect(isValidJson("[1, 2, 3]")).toBe(true); + expect(isValidJson('"string"')).toBe(true); + }); + + it("should return false for invalid JSON", () => { + expect(isValidJson("{key: value}")).toBe(false); + expect(isValidJson("not json")).toBe(false); + expect(isValidJson("{incomplete")).toBe(false); + }); + }); + + describe("hasBalancedBraces", () => { + it("should return true for balanced braces", () => { + expect(hasBalancedBraces("{ foo: { bar: [] } }")).toBe(true); + expect(hasBalancedBraces("function() { return (a + b); }")).toBe(true); + }); + + it("should return false for unbalanced braces", () => { + expect(hasBalancedBraces("{ foo: { bar }")).toBe(false); + expect(hasBalancedBraces("function() { return (a + b); ")).toBe(false); + expect(hasBalancedBraces("{ ] }")).toBe(false); + }); + + it("should handle empty string", () => { + expect(hasBalancedBraces("")).toBe(true); + }); + }); + + describe("countMatches", () => { + it("should count pattern matches", () => { + expect(countMatches("aaa", /a/g)).toBe(3); + expect(countMatches("hello world", /o/g)).toBe(2); + }); + + it("should handle no matches", () => { + expect(countMatches("hello", /z/g)).toBe(0); + }); + + it("should handle case-insensitive patterns", () => { + expect(countMatches("Hello HELLO hello", /hello/gi)).toBe(3); + }); + }); + + describe("sum", () => { + it("should sum numbers", () => { + expect(sum([1, 2, 3])).toBe(6); + expect(sum([0.1, 0.2, 0.3])).toBeCloseTo(0.6); + }); + + it("should return 0 for empty array", () => { + expect(sum([])).toBe(0); + }); + }); + + describe("unique", () => { + it("should remove duplicates", () => { + expect(unique([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]); + expect(unique(["a", "b", "a"])).toEqual(["a", "b"]); + }); + + it("should handle empty array", () => { + expect(unique([])).toEqual([]); + }); + }); + + describe("groupBy", () => { + it("should group by key function", () => { + const items = [ + { type: "a", value: 1 }, + { type: "b", value: 2 }, + { type: "a", value: 3 }, + ]; + + const grouped = groupBy(items, (item) => item.type); + + expect(grouped.a).toHaveLength(2); + expect(grouped.b).toHaveLength(1); + }); + + it("should handle empty array", () => { + const grouped = groupBy([], (x: string) => x); + + expect(Object.keys(grouped)).toHaveLength(0); + }); + }); +}); diff --git a/src/services/reasoning/context-compression.ts b/src/services/reasoning/context-compression.ts new file mode 100644 index 0000000..8fa072f --- /dev/null +++ b/src/services/reasoning/context-compression.ts @@ -0,0 +1,573 @@ +/** + * Context Compression Layer + * Reduces context window usage through deterministic transformations + */ + +import type { + CompressionLevel, + CompressionTrigger, + CompressionInput, + CompressionOutput, + CompressibleMessage, + CompressibleToolResult, + CodeBlock, + Entity, + MessageId, +} from "@/types/reasoning"; + +import { + COMPRESSION_THRESHOLDS, + COMPRESSION_LIMITS, + PRESERVATION_PRIORITIES, +} from "@constants/reasoning"; + +import { + estimateTokens, + truncateMiddle, + foldCode, + extractCodeBlocks, + extractEntities, + createEntityTable, + generateId, +} from "@services/reasoning/utils"; + +// ============================================================================= +// COMPRESSION STATE MACHINE +// ============================================================================= + +export function determineCompressionLevel( + currentTokenCount: number, + tokenLimit: number, +): CompressionLevel { + const usage = currentTokenCount / tokenLimit; + + if (usage >= COMPRESSION_THRESHOLDS.MINIMAL_AT) { + return "MINIMAL"; + } + + if (usage >= COMPRESSION_THRESHOLDS.COMPRESS_AT) { + return "COMPRESSED"; + } + + return "FULL"; +} + +export function shouldCompress(trigger: CompressionTrigger): boolean { + if (trigger.event === "EXPLICIT_COMPRESSION_REQUEST") { + return true; + } + + if (trigger.event === "RETRY_POLICY_REQUEST") { + return true; + } + + if (trigger.event === "TOKEN_THRESHOLD_EXCEEDED") { + const usage = trigger.usage / trigger.limit; + return usage >= COMPRESSION_THRESHOLDS.COMPRESS_AT; + } + + return false; +} + +// ============================================================================= +// MAIN COMPRESSION FUNCTION +// ============================================================================= + +export function compressContext(input: CompressionInput): CompressionOutput { + const targetLevel = determineCompressionLevel( + input.currentTokenCount, + input.tokenLimit, + ); + + if (targetLevel === "FULL") { + return createNoCompressionOutput(input); + } + + const rules = selectCompressionRules(targetLevel); + const result = applyCompressionRules(input, rules); + + return result; +} + +function createNoCompressionOutput(input: CompressionInput): CompressionOutput { + return { + compressedMessages: input.messages, + entityTable: input.entities, + tokensSaved: 0, + compressionRatio: 1, + appliedRules: [], + }; +} + +// ============================================================================= +// COMPRESSION RULES +// ============================================================================= + +interface CompressionRule { + id: string; + priority: number; + applicableTo: "MESSAGE" | "TOOL_RESULT" | "CODE_BLOCK"; + condition: ( + item: CompressibleMessage | CompressibleToolResult | CodeBlock, + ) => boolean; + transform: ( + item: CompressibleMessage | CompressibleToolResult | CodeBlock, + ) => CompressibleMessage | null; + estimatedReduction: number; +} + +function selectCompressionRules(level: CompressionLevel): CompressionRule[] { + const allRules: CompressionRule[] = [ + createTruncateLargeToolResultsRule(), + createFoldCodeBlocksRule(), + createRemoveRedundantMessagesRule(), + createExtractEntitiesFromOldMessagesRule(), + createCollapseFailedAttemptsRule(), + ]; + + if (level === "MINIMAL") { + return allRules; + } + + return allRules.filter((r) => r.priority <= 3); +} + +function createTruncateLargeToolResultsRule(): CompressionRule { + return { + id: "TRUNCATE_LARGE_TOOL_RESULTS", + priority: 1, + applicableTo: "TOOL_RESULT", + condition: (item) => { + const toolResult = item as CompressibleToolResult; + return toolResult.tokenCount > COMPRESSION_LIMITS.maxToolResultTokens; + }, + transform: (item) => { + const toolResult = item as CompressibleToolResult; + const truncated = truncateMiddle( + toolResult.content, + COMPRESSION_LIMITS.truncateHeadTokens * 4, + COMPRESSION_LIMITS.truncateTailTokens * 4, + ); + + return { + id: toolResult.id, + role: "tool" as const, + content: truncated, + tokenCount: estimateTokens(truncated), + age: 0, + isPreserved: false, + metadata: { toolCallId: toolResult.id }, + }; + }, + estimatedReduction: 0.6, + }; +} + +function createFoldCodeBlocksRule(): CompressionRule { + return { + id: "FOLD_CODE_BLOCKS", + priority: 2, + applicableTo: "CODE_BLOCK", + condition: (item) => { + const codeBlock = item as CodeBlock; + return codeBlock.lineCount > COMPRESSION_LIMITS.maxCodeBlockLines; + }, + transform: (item) => { + const codeBlock = item as CodeBlock; + const folded = foldCode(codeBlock.content, { + keepLines: COMPRESSION_LIMITS.keepCodeHeadLines, + tailLines: COMPRESSION_LIMITS.keepCodeTailLines, + }); + + return { + id: codeBlock.sourceMessageId, + role: "assistant" as const, + content: `\`\`\`${codeBlock.language}\n${folded}\n\`\`\``, + tokenCount: estimateTokens(folded), + age: 0, + isPreserved: false, + metadata: { containsCode: true }, + }; + }, + estimatedReduction: 0.5, + }; +} + +function createRemoveRedundantMessagesRule(): CompressionRule { + return { + id: "REMOVE_REDUNDANT_MESSAGES", + priority: 3, + applicableTo: "MESSAGE", + condition: (item) => { + const message = item as CompressibleMessage; + return ( + message.role === "assistant" && message.metadata?.isSuperseded === true + ); + }, + transform: () => null, + estimatedReduction: 1.0, + }; +} + +function createExtractEntitiesFromOldMessagesRule(): CompressionRule { + return { + id: "EXTRACT_ENTITIES_FROM_OLD_MESSAGES", + priority: 4, + applicableTo: "MESSAGE", + condition: (item) => { + const message = item as CompressibleMessage; + return ( + message.age > COMPRESSION_LIMITS.maxMessageAge && !message.isPreserved + ); + }, + transform: (item) => { + const message = item as CompressibleMessage; + const entities = extractEntities(message.content, message.id); + const entitySummary = entities + .map((e) => `${e.type}:${e.value}`) + .join(", "); + + return { + id: message.id, + role: message.role, + content: `[Message ${message.id}: mentioned ${entities.length} entities: ${entitySummary}]`, + tokenCount: estimateTokens(entitySummary) + 20, + age: message.age, + isPreserved: false, + metadata: { ...message.metadata }, + }; + }, + estimatedReduction: 0.8, + }; +} + +function createCollapseFailedAttemptsRule(): CompressionRule { + return { + id: "COLLAPSE_FAILED_ATTEMPTS", + priority: 5, + applicableTo: "MESSAGE", + condition: (item) => { + const message = item as CompressibleMessage; + return message.metadata?.attemptFailed === true; + }, + transform: (item) => { + const message = item as CompressibleMessage; + const reason = message.metadata?.failureReason || "unknown"; + + return { + id: message.id, + role: message.role, + content: `[Failed attempt: ${reason}]`, + tokenCount: estimateTokens(reason) + 20, + age: message.age, + isPreserved: false, + metadata: { ...message.metadata }, + }; + }, + estimatedReduction: 0.9, + }; +} + +// ============================================================================= +// RULE APPLICATION +// ============================================================================= + +function applyCompressionRules( + input: CompressionInput, + rules: CompressionRule[], +): CompressionOutput { + let compressedMessages = [...input.messages]; + let totalTokensSaved = 0; + const appliedRules: string[] = []; + let extractedEntities: Entity[] = [...input.entities.entities]; + + const sortedRules = [...rules].sort((a, b) => a.priority - b.priority); + + for (const rule of sortedRules) { + if (rule.applicableTo === "MESSAGE") { + const result = applyMessageRule( + compressedMessages, + rule, + input.preserveList, + ); + compressedMessages = result.messages; + totalTokensSaved += result.tokensSaved; + extractedEntities = [...extractedEntities, ...result.extractedEntities]; + + if (result.applied) { + appliedRules.push(rule.id); + } + } + + if (rule.applicableTo === "TOOL_RESULT") { + const result = applyToolResultRule(input.toolResults, rule); + const toolResultMessages = result.messages; + + for (const trMsg of toolResultMessages) { + const existingIdx = compressedMessages.findIndex( + (m) => m.metadata?.toolCallId === trMsg.metadata?.toolCallId, + ); + if (existingIdx >= 0) { + const oldTokens = compressedMessages[existingIdx].tokenCount; + compressedMessages[existingIdx] = trMsg; + totalTokensSaved += oldTokens - trMsg.tokenCount; + } + } + + if (result.applied) { + appliedRules.push(rule.id); + } + } + + if (rule.applicableTo === "CODE_BLOCK") { + const result = applyCodeBlockRule(compressedMessages, rule); + compressedMessages = result.messages; + totalTokensSaved += result.tokensSaved; + + if (result.applied) { + appliedRules.push(rule.id); + } + } + } + + const originalTokenCount = input.currentTokenCount; + const newTokenCount = originalTokenCount - totalTokensSaved; + const compressionRatio = newTokenCount / originalTokenCount; + + return { + compressedMessages, + entityTable: createEntityTable(extractedEntities), + tokensSaved: totalTokensSaved, + compressionRatio, + appliedRules, + }; +} + +interface MessageRuleResult { + messages: CompressibleMessage[]; + tokensSaved: number; + extractedEntities: Entity[]; + applied: boolean; +} + +function applyMessageRule( + messages: CompressibleMessage[], + rule: CompressionRule, + preserveList: MessageId[], +): MessageRuleResult { + const result: CompressibleMessage[] = []; + let tokensSaved = 0; + const extractedEntities: Entity[] = []; + let applied = false; + + for (const message of messages) { + if (preserveList.includes(message.id)) { + result.push(message); + continue; + } + + if (rule.condition(message)) { + applied = true; + const transformed = rule.transform(message); + + if (transformed === null) { + tokensSaved += message.tokenCount; + } else { + const saved = message.tokenCount - transformed.tokenCount; + tokensSaved += Math.max(0, saved); + result.push(transformed); + + const entities = extractEntities(message.content, message.id); + extractedEntities.push(...entities); + } + } else { + result.push(message); + } + } + + return { messages: result, tokensSaved, extractedEntities, applied }; +} + +interface ToolResultRuleResult { + messages: CompressibleMessage[]; + applied: boolean; +} + +function applyToolResultRule( + toolResults: CompressibleToolResult[], + rule: CompressionRule, +): ToolResultRuleResult { + const messages: CompressibleMessage[] = []; + let applied = false; + + for (const toolResult of toolResults) { + if (rule.condition(toolResult)) { + applied = true; + const transformed = rule.transform(toolResult); + if (transformed) { + messages.push(transformed); + } + } + } + + return { messages, applied }; +} + +interface CodeBlockRuleResult { + messages: CompressibleMessage[]; + tokensSaved: number; + applied: boolean; +} + +function applyCodeBlockRule( + messages: CompressibleMessage[], + rule: CompressionRule, +): CodeBlockRuleResult { + const result: CompressibleMessage[] = []; + let tokensSaved = 0; + let applied = false; + + for (const message of messages) { + const codeBlocks = extractCodeBlocks(message.content); + + if (codeBlocks.length === 0) { + result.push(message); + continue; + } + + let modifiedContent = message.content; + let contentTokensSaved = 0; + + for (const block of codeBlocks) { + const codeBlockObj: CodeBlock = { + id: generateId("codeblock"), + language: block.language, + content: block.content, + lineCount: block.content.split("\n").length, + sourceMessageId: message.id, + }; + + if (rule.condition(codeBlockObj)) { + applied = true; + const folded = foldCode(block.content, { + keepLines: COMPRESSION_LIMITS.keepCodeHeadLines, + tailLines: COMPRESSION_LIMITS.keepCodeTailLines, + }); + + const originalBlock = `\`\`\`${block.language}\n${block.content}\`\`\``; + const foldedBlock = `\`\`\`${block.language}\n${folded}\`\`\``; + + modifiedContent = modifiedContent.replace(originalBlock, foldedBlock); + contentTokensSaved += + estimateTokens(block.content) - estimateTokens(folded); + } + } + + result.push({ + ...message, + content: modifiedContent, + tokenCount: message.tokenCount - contentTokensSaved, + }); + + tokensSaved += contentTokensSaved; + } + + return { messages: result, tokensSaved, applied }; +} + +// ============================================================================= +// INCREMENTAL COMPRESSION +// ============================================================================= + +export function compressIncrementally( + messages: CompressibleMessage[], + targetTokenReduction: number, +): { messages: CompressibleMessage[]; actualReduction: number } { + let currentMessages = [...messages]; + let totalReduction = 0; + + const rules = selectCompressionRules("MINIMAL"); + + for (const rule of rules) { + if (totalReduction >= targetTokenReduction) { + break; + } + + if (rule.applicableTo === "MESSAGE") { + const result = applyMessageRule(currentMessages, rule, []); + currentMessages = result.messages; + totalReduction += result.tokensSaved; + } + } + + return { + messages: currentMessages, + actualReduction: totalReduction, + }; +} + +// ============================================================================= +// UTILITY FUNCTIONS +// ============================================================================= + +export function calculateMessageAge( + _messageTimestamp: number, + currentTurnNumber: number, + messageTurnNumber: number, +): number { + return currentTurnNumber - messageTurnNumber; +} + +export function markMessagesWithAge( + messages: CompressibleMessage[], + currentTurnNumber: number, +): CompressibleMessage[] { + return messages.map((msg, idx) => ({ + ...msg, + age: currentTurnNumber - idx, + })); +} + +export function getPreservationCandidates( + messages: CompressibleMessage[], +): MessageId[] { + const contextFiles = messages + .filter((m) => m.isContextFile === true) + .map((m) => m.id); + + const images = messages.filter((m) => m.isImage === true).map((m) => m.id); + + const recent = messages + .slice(-COMPRESSION_LIMITS.preserveRecentMessages) + .map((m) => m.id); + + const withErrors = messages + .filter((m) => m.content.toLowerCase().includes("error")) + .map((m) => m.id); + + return [...new Set([...contextFiles, ...images, ...recent, ...withErrors])]; +} + +export function getMessagePriority(message: CompressibleMessage): number { + if (message.isContextFile) { + return PRESERVATION_PRIORITIES.CONTEXT_FILE; + } + if (message.isImage) { + return PRESERVATION_PRIORITIES.IMAGE; + } + if (message.content.toLowerCase().includes("error")) { + return PRESERVATION_PRIORITIES.ERROR; + } + if (message.age <= COMPRESSION_LIMITS.preserveRecentMessages) { + return PRESERVATION_PRIORITIES.RECENT_MESSAGE; + } + if (message.role === "tool") { + return PRESERVATION_PRIORITIES.TOOL_RESULT; + } + return PRESERVATION_PRIORITIES.OLD_MESSAGE; +} + +export function shouldPreserveMessage(message: CompressibleMessage): boolean { + const priority = getMessagePriority(message); + return priority >= PRESERVATION_PRIORITIES.RECENT_MESSAGE; +} diff --git a/src/services/reasoning/index.ts b/src/services/reasoning/index.ts new file mode 100644 index 0000000..b6879f2 --- /dev/null +++ b/src/services/reasoning/index.ts @@ -0,0 +1,234 @@ +/** + * Reasoning Control Layer - Public API + * + * This module provides cognitive control layers that create intelligence + * through control flow, not prompt engineering. + * + * Five cognitive functions: + * 1. Quality Evaluation - Assess response acceptability + * 2. Retry Policy - Control retry behavior through state machine + * 3. Context Compression - Reduce context while preserving information + * 4. Memory Selection - Select relevant memories for context + * 5. Termination Detection - Detect task completion through validation + */ + +// ============================================================================= +// QUALITY EVALUATION +// ============================================================================= + +export { + evaluateQuality, + computeQualityMetrics, + computeStructuralScore, + computeRelevanceScore, + computeCompletenessScore, + computeCoherenceScore, + computeVerdict, + detectDeficiencies, + hasHallucinationMarkers, + hasContradiction, +} from "@services/reasoning/quality-evaluation"; + +// ============================================================================= +// RETRY POLICY +// ============================================================================= + +export { + createInitialRetryState, + createRetryBudget, + computeRetryTransition, + splitTaskDescription, + isRetryable, + getCurrentTier, + getRemainingAttempts, + getElapsedTime, + getRemainingTime, +} from "@services/reasoning/retry-policy"; + +// ============================================================================= +// CONTEXT COMPRESSION +// ============================================================================= + +export { + compressContext, + determineCompressionLevel, + shouldCompress, + compressIncrementally, + calculateMessageAge, + markMessagesWithAge, + getPreservationCandidates, +} from "@services/reasoning/context-compression"; + +// ============================================================================= +// MEMORY SELECTION +// ============================================================================= + +export { + selectRelevantMemories, + computeRelevance, + computeMandatoryItems, + createMemoryItem, + createQueryContext, + createMemoryStore, + addMemory, + findMemoriesByType, + findMemoriesByPath, + pruneOldMemories, +} from "@services/reasoning/memory-selection"; + +export { MemoryStore } from "@interfaces/memory"; + +// ============================================================================= +// TERMINATION DETECTION +// ============================================================================= + +export { + createInitialTerminationState, + processTerminationTrigger, + computeTerminationConfidence, + runValidationCheck, + extractValidationFailures, + isComplete, + isFailed, + isTerminal, + requiresValidation, + getConfidencePercentage, +} from "@services/reasoning/termination-detection"; + +export type { ValidationContext } from "@services/reasoning/termination-detection"; + +// ============================================================================= +// ORCHESTRATOR +// ============================================================================= + +export { + createOrchestratorConfig, + createInitialState, + prepareContext, + evaluateResponseQuality, + decideRetry, + checkTermination, + executeReasoningCycle, +} from "@services/reasoning/orchestrator"; + +export type { + OrchestratorConfig, + ContextPreparationInput, + ContextPreparationOutput, + QualityEvaluationInput, + RetryDecisionInput, + RetryDecisionOutput, + TerminationCheckInput, + TerminationCheckOutput, + ExecutionCycleInput, + ExecutionCycleOutput, +} from "@services/reasoning/orchestrator"; + +// ============================================================================= +// UTILITIES +// ============================================================================= + +export { + estimateTokens, + estimateTokensForObject, + tokenize, + jaccardSimilarity, + weightedSum, + extractEntities, + createEntityTable, + mergeEntityTables, + truncateMiddle, + foldCode, + extractCodeBlocks, + recencyDecay, + createTimestamp, + generateId, + isValidJson, + hasBalancedBraces, + countMatches, + sum, + unique, + groupBy, +} from "@services/reasoning/utils"; + +// ============================================================================= +// RE-EXPORT TYPES +// ============================================================================= + +export type { + // Common + MessageId, + ToolId, + MemoryId, + TaskId, + Entity, + EntityType, + EntityTable, + + // Quality Evaluation + QualityVerdict, + DeficiencyTag, + QualityMetrics, + QualityEvalInput, + QualityEvalOutput, + ResponseType, + ToolCallInfo, + TaskConstraints, + AttemptRecord, + + // Retry Policy + RetryStateKind, + ExhaustionReason, + RetryState, + RetryBudget, + RetryPolicyState, + ContextDelta, + SubTask, + RetryTrigger, + ToolExecutionError, + RetryTransform, + RetryAction, + EscalationContext, + RetryPolicyInput, + RetryPolicyOutput, + + // Context Compression + CompressionLevel, + CompressibleMessage, + MessageMetadata, + CompressibleToolResult, + CodeBlock, + CompressionTrigger, + CompressionInput, + CompressionOutput, + + // Memory Selection + MemoryItemType, + MemoryItem, + QueryContext, + RelevanceScore, + RelevanceBreakdown, + SelectionInput, + ExclusionReason, + SelectionOutput, + + // Termination Detection + TerminationStatus, + CompletionSignalSource, + CompletionSignal, + ValidationCheckType, + ValidationCheck, + ValidationResult, + ValidationFailure, + TerminationState, + TerminationTrigger, + TerminationDecision, + TerminationOutput, + TerminationEvidence, + + // Orchestrator + ReasoningControlState, + ExecutionPhase, + ExecutionMetrics, + ReasoningTaskResult, +} from "@/types/reasoning"; diff --git a/src/services/reasoning/memory-selection.ts b/src/services/reasoning/memory-selection.ts new file mode 100644 index 0000000..a4ca424 --- /dev/null +++ b/src/services/reasoning/memory-selection.ts @@ -0,0 +1,448 @@ +/** + * Memory Relevance Selection Layer + * Selects which stored memories to include based on computable relevance signals + */ + +import type { + MemoryItem, + MemoryItemType, + MemoryId, + QueryContext, + RelevanceScore, + RelevanceBreakdown, + SelectionInput, + SelectionOutput, + ExclusionReason, + Entity, +} from "@/types/reasoning"; + +import { + MEMORY_WEIGHTS, + RECENCY_HALF_LIFE_MINUTES, + RELEVANCE_THRESHOLD, + MEMORY_TYPE_BONUSES, + MANDATORY_MEMORY_AGE_THRESHOLD, + ERROR_MEMORY_AGE_THRESHOLD, +} from "@constants/reasoning"; + +import { + jaccardSimilarity, + recencyDecay, + estimateTokens, + tokenize, + createTimestamp, +} from "@services/reasoning/utils"; + +// ============================================================================= +// MAIN SELECTION FUNCTION +// ============================================================================= + +export function selectRelevantMemories(input: SelectionInput): SelectionOutput { + const { memories, query, tokenBudget, mandatoryItems } = input; + + const scored = scoreAllMemories(memories, query); + const sortedByRelevance = sortByRelevance(scored); + const deduped = deduplicateMemories(sortedByRelevance); + + const selection = buildSelection(deduped, mandatoryItems, tokenBudget); + + return selection; +} + +// ============================================================================= +// SCORING FUNCTIONS +// ============================================================================= + +interface ScoredMemory { + item: MemoryItem; + score: RelevanceScore; +} + +function scoreAllMemories( + memories: MemoryItem[], + query: QueryContext, +): ScoredMemory[] { + return memories.map((item) => ({ + item, + score: computeRelevance(item, query), + })); +} + +export function computeRelevance( + item: MemoryItem, + query: QueryContext, +): RelevanceScore { + const breakdown = computeRelevanceBreakdown(item, query); + const total = computeTotalScore(breakdown); + + return { total, breakdown }; +} + +function computeRelevanceBreakdown( + item: MemoryItem, + query: QueryContext, +): RelevanceBreakdown { + return { + keywordOverlap: computeKeywordOverlap(item.tokens, query.tokens), + entityOverlap: computeEntityOverlap(item.entities, query.entities), + recency: computeRecencyScore(item.timestamp, query.timestamp), + causalLink: computeCausalLinkScore(item, query.activeItems), + pathOverlap: computePathOverlap(item.filePaths, query.activePaths), + typeBonus: computeTypeBonus(item.type), + }; +} + +function computeTotalScore(breakdown: RelevanceBreakdown): number { + return ( + breakdown.keywordOverlap * MEMORY_WEIGHTS.keywordOverlap + + breakdown.entityOverlap * MEMORY_WEIGHTS.entityOverlap + + breakdown.recency * MEMORY_WEIGHTS.recency + + breakdown.causalLink * MEMORY_WEIGHTS.causalLink + + breakdown.pathOverlap * MEMORY_WEIGHTS.pathOverlap + + breakdown.typeBonus * MEMORY_WEIGHTS.typeBonus + ); +} + +// ============================================================================= +// INDIVIDUAL SCORE COMPONENTS +// ============================================================================= + +function computeKeywordOverlap( + itemTokens: string[], + queryTokens: string[], +): number { + return jaccardSimilarity(itemTokens, queryTokens); +} + +function computeEntityOverlap( + itemEntities: Entity[], + queryEntities: Entity[], +): number { + if (itemEntities.length === 0 || queryEntities.length === 0) { + return 0; + } + + const itemSet = new Set( + itemEntities.map((e) => `${e.type}:${e.value.toLowerCase()}`), + ); + const querySet = new Set( + queryEntities.map((e) => `${e.type}:${e.value.toLowerCase()}`), + ); + + const intersection = [...itemSet].filter((e) => querySet.has(e)).length; + const union = new Set([...itemSet, ...querySet]).size; + + return union > 0 ? intersection / union : 0; +} + +function computeRecencyScore(itemTime: number, queryTime: number): number { + return recencyDecay(itemTime, queryTime, RECENCY_HALF_LIFE_MINUTES); +} + +function computeCausalLinkScore( + item: MemoryItem, + activeItems: MemoryId[], +): number { + if (item.causalLinks.length === 0) { + return 0; + } + + const hasLink = item.causalLinks.some((link) => activeItems.includes(link)); + return hasLink ? 1.0 : 0.0; +} + +function computePathOverlap( + itemPaths: string[] | undefined, + activePaths: string[], +): number { + if (!itemPaths || itemPaths.length === 0 || activePaths.length === 0) { + return 0; + } + + const normalizedItemPaths = itemPaths.map(normalizePath); + const normalizedActivePaths = activePaths.map(normalizePath); + + let matchCount = 0; + + for (const itemPath of normalizedItemPaths) { + for (const activePath of normalizedActivePaths) { + if (pathsMatch(itemPath, activePath)) { + matchCount++; + break; + } + } + } + + return matchCount / itemPaths.length; +} + +function normalizePath(path: string): string { + return path.toLowerCase().replace(/\\/g, "/"); +} + +function pathsMatch(pathA: string, pathB: string): boolean { + if (pathA === pathB) return true; + if (pathA.includes(pathB) || pathB.includes(pathA)) return true; + + const partsA = pathA.split("/"); + const partsB = pathB.split("/"); + const fileA = partsA[partsA.length - 1]; + const fileB = partsB[partsB.length - 1]; + + return fileA === fileB; +} + +function computeTypeBonus(type: MemoryItemType): number { + return MEMORY_TYPE_BONUSES[type] || 0; +} + +// ============================================================================= +// SORTING AND DEDUPLICATION +// ============================================================================= + +function sortByRelevance(scored: ScoredMemory[]): ScoredMemory[] { + return [...scored].sort((a, b) => b.score.total - a.score.total); +} + +function deduplicateMemories(scored: ScoredMemory[]): ScoredMemory[] { + const seen = new Set(); + const result: ScoredMemory[] = []; + + for (const item of scored) { + const contentHash = hashContent(item.item.content); + + if (!seen.has(contentHash)) { + seen.add(contentHash); + result.push(item); + } + } + + return result; +} + +function hashContent(content: string): string { + const normalized = content + .toLowerCase() + .replace(/\s+/g, " ") + .trim() + .slice(0, 200); + + let hash = 0; + for (let i = 0; i < normalized.length; i++) { + const char = normalized.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + + return hash.toString(36); +} + +// ============================================================================= +// SELECTION BUILDING +// ============================================================================= + +function buildSelection( + scored: ScoredMemory[], + mandatoryItems: MemoryId[], + tokenBudget: number, +): SelectionOutput { + const selected: MemoryItem[] = []; + const scores = new Map(); + const excluded: Array<{ id: MemoryId; reason: ExclusionReason }> = []; + let tokenUsage = 0; + + const mandatorySet = new Set(mandatoryItems); + + for (const { item, score } of scored) { + if (mandatorySet.has(item.id)) { + selected.push(item); + scores.set(item.id, score); + tokenUsage += item.tokenCount; + mandatorySet.delete(item.id); + } + } + + for (const { item, score } of scored) { + if (selected.some((s) => s.id === item.id)) { + continue; + } + + if (score.total < RELEVANCE_THRESHOLD) { + excluded.push({ id: item.id, reason: "LOW_RELEVANCE" }); + continue; + } + + if (tokenUsage + item.tokenCount > tokenBudget) { + excluded.push({ id: item.id, reason: "TOKEN_BUDGET_EXCEEDED" }); + continue; + } + + selected.push(item); + scores.set(item.id, score); + tokenUsage += item.tokenCount; + } + + return { + selected, + scores, + tokenUsage, + excluded, + }; +} + +// ============================================================================= +// MANDATORY INCLUSION RULES +// ============================================================================= + +export function computeMandatoryItems( + memories: MemoryItem[], + currentTimestamp: number, +): MemoryId[] { + const mandatory: MemoryId[] = []; + + const recentMemories = memories + .filter((m) => { + const ageMinutes = (currentTimestamp - m.timestamp) / 60000; + return ageMinutes <= MANDATORY_MEMORY_AGE_THRESHOLD; + }) + .map((m) => m.id); + + mandatory.push(...recentMemories); + + const recentErrors = memories + .filter((m) => { + const ageMinutes = (currentTimestamp - m.timestamp) / 60000; + return m.type === "ERROR" && ageMinutes <= ERROR_MEMORY_AGE_THRESHOLD; + }) + .map((m) => m.id); + + mandatory.push(...recentErrors); + + const decisions = memories + .filter((m) => m.type === "DECISION") + .slice(-3) + .map((m) => m.id); + + mandatory.push(...decisions); + + return [...new Set(mandatory)]; +} + +// ============================================================================= +// MEMORY ITEM CREATION +// ============================================================================= + +export function createMemoryItem( + content: string, + type: MemoryItemType, + options: { + filePaths?: string[]; + causalLinks?: MemoryId[]; + } = {}, +): MemoryItem { + const tokens = tokenize(content); + const entities: Entity[] = []; + + const id = `mem_${createTimestamp()}_${Math.random().toString(36).slice(2, 8)}`; + + return { + id, + content, + tokens, + entities, + timestamp: createTimestamp(), + type, + causalLinks: options.causalLinks || [], + tokenCount: estimateTokens(content), + filePaths: options.filePaths, + }; +} + +export function createQueryContext( + queryText: string, + options: { + activeItems?: MemoryId[]; + activePaths?: string[]; + } = {}, +): QueryContext { + const tokens = tokenize(queryText); + const entities: Entity[] = []; + + return { + tokens, + entities, + timestamp: createTimestamp(), + activeItems: options.activeItems || [], + activePaths: options.activePaths || [], + }; +} + +// ============================================================================= +// MEMORY STORE OPERATIONS +// ============================================================================= + +export interface MemoryStore { + items: MemoryItem[]; + maxItems: number; +} + +export function createMemoryStore(maxItems: number = 1000): MemoryStore { + return { + items: [], + maxItems, + }; +} + +export function addMemory(store: MemoryStore, item: MemoryItem): MemoryStore { + const newItems = [...store.items, item]; + + if (newItems.length > store.maxItems) { + const sorted = [...newItems].sort((a, b) => { + const aScore = + MEMORY_TYPE_BONUSES[a.type] + (Date.now() - a.timestamp) / -3600000; + const bScore = + MEMORY_TYPE_BONUSES[b.type] + (Date.now() - b.timestamp) / -3600000; + return bScore - aScore; + }); + + return { + ...store, + items: sorted.slice(0, store.maxItems), + }; + } + + return { + ...store, + items: newItems, + }; +} + +export function findMemoriesByType( + store: MemoryStore, + type: MemoryItemType, +): MemoryItem[] { + return store.items.filter((m) => m.type === type); +} + +export function findMemoriesByPath( + store: MemoryStore, + path: string, +): MemoryItem[] { + const normalizedPath = normalizePath(path); + + return store.items.filter((m) => + m.filePaths?.some((p) => normalizePath(p).includes(normalizedPath)), + ); +} + +export function pruneOldMemories( + store: MemoryStore, + maxAgeMs: number, +): MemoryStore { + const cutoff = createTimestamp() - maxAgeMs; + + return { + ...store, + items: store.items.filter((m) => m.timestamp >= cutoff), + }; +} diff --git a/src/services/reasoning/orchestrator.ts b/src/services/reasoning/orchestrator.ts new file mode 100644 index 0000000..d99f19c --- /dev/null +++ b/src/services/reasoning/orchestrator.ts @@ -0,0 +1,667 @@ +/** + * Reasoning Control Orchestrator + * Coordinates all cognitive control layers during task execution + */ + +import type { + ReasoningControlState, + ReasoningTaskResult, + ExecutionPhase, + ExecutionMetrics, + QualityEvalInput, + QualityEvalOutput, + RetryPolicyInput, + RetryAction, + CompressionInput, + SelectionInput, + TerminationState, + TerminationTrigger, + TerminationOutput, + CompressibleMessage, + MemoryItem, + TaskConstraints, + AttemptRecord, + EntityTable, +} from "@/types/reasoning"; + +import { DEFAULT_TOKEN_BUDGET } from "@constants/reasoning"; + +import { evaluateQuality } from "@services/reasoning/quality-evaluation"; +import { + createInitialRetryState, + computeRetryTransition, + isRetryable, +} from "@services/reasoning/retry-policy"; +import { + compressContext, + markMessagesWithAge, + getPreservationCandidates, +} from "@services/reasoning/context-compression"; +import { + selectRelevantMemories, + computeMandatoryItems, + createQueryContext, + createMemoryItem, + createMemoryStore, + addMemory, + MemoryStore, +} from "@services/reasoning/memory-selection"; +import { + createInitialTerminationState, + processTerminationTrigger, + isTerminal, + requiresValidation, +} from "@services/reasoning/termination-detection"; +import { + createTimestamp, + estimateTokens, + tokenize, + extractEntities, + createEntityTable, +} from "@services/reasoning/utils"; + +// ============================================================================= +// ORCHESTRATOR STATE +// ============================================================================= + +export interface OrchestratorConfig { + tokenBudget: number; + availableTools: string[]; + autoValidate: boolean; + maxIterations: number; +} + +const DEFAULT_CONFIG: OrchestratorConfig = { + tokenBudget: DEFAULT_TOKEN_BUDGET, + availableTools: ["read", "write", "edit", "bash", "glob", "grep"], + autoValidate: true, + maxIterations: 20, +}; + +export function createOrchestratorConfig( + overrides: Partial = {}, +): OrchestratorConfig { + return { ...DEFAULT_CONFIG, ...overrides }; +} + +// ============================================================================= +// STATE INITIALIZATION +// ============================================================================= + +export function createInitialState(): ReasoningControlState { + return { + retryPolicy: createInitialRetryState(), + termination: createInitialTerminationState(), + compressionLevel: "FULL", + entityTable: createEntityTable([]), + currentPhase: "CONTEXT_PREPARATION", + metrics: createInitialMetrics(), + }; +} + +function createInitialMetrics(): ExecutionMetrics { + return { + totalLLMCalls: 0, + totalToolExecutions: 0, + totalRetries: 0, + totalTokensUsed: 0, + startTime: createTimestamp(), + phaseTimings: { + CONTEXT_PREPARATION: 0, + LLM_INTERACTION: 0, + QUALITY_EVALUATION: 0, + RETRY_DECISION: 0, + EXECUTION: 0, + TERMINATION_CHECK: 0, + VALIDATION: 0, + COMPLETE: 0, + FAILED: 0, + }, + }; +} + +// ============================================================================= +// PHASE 1: CONTEXT PREPARATION +// ============================================================================= + +export interface ContextPreparationInput { + query: string; + memoryStore: MemoryStore; + existingContext: CompressibleMessage[]; + tokenBudget: number; + activePaths: string[]; +} + +export interface ContextPreparationOutput { + selectedMemories: MemoryItem[]; + compressedContext: CompressibleMessage[]; + entityTable: EntityTable; + tokenUsage: number; +} + +export function prepareContext( + input: ContextPreparationInput, + state: ReasoningControlState, +): ContextPreparationOutput { + const queryContext = createQueryContext(input.query, { + activePaths: input.activePaths, + }); + + const mandatoryItems = computeMandatoryItems( + input.memoryStore.items, + createTimestamp(), + ); + + const selectionInput: SelectionInput = { + memories: input.memoryStore.items, + query: queryContext, + tokenBudget: Math.floor(input.tokenBudget * 0.6), + mandatoryItems, + }; + + const selectionOutput = selectRelevantMemories(selectionInput); + + const agedMessages = markMessagesWithAge( + input.existingContext, + input.existingContext.length, + ); + + const preserveList = getPreservationCandidates(agedMessages); + + const currentTokenCount = estimateTokens( + agedMessages.map((m) => m.content).join("\n"), + ); + + const compressionInput: CompressionInput = { + messages: agedMessages, + toolResults: [], + entities: state.entityTable, + currentTokenCount, + tokenLimit: input.tokenBudget, + preserveList, + }; + + const compressionOutput = compressContext(compressionInput); + + return { + selectedMemories: selectionOutput.selected, + compressedContext: compressionOutput.compressedMessages, + entityTable: compressionOutput.entityTable, + tokenUsage: + selectionOutput.tokenUsage + + (currentTokenCount - compressionOutput.tokensSaved), + }; +} + +// ============================================================================= +// PHASE 2: QUALITY EVALUATION +// ============================================================================= + +export interface QualityEvaluationInput { + responseText: string; + responseToolCalls: Array<{ + id: string; + name: string; + arguments: Record; + }>; + originalQuery: string; + taskConstraints: TaskConstraints; + previousAttempts: AttemptRecord[]; +} + +export function evaluateResponseQuality( + input: QualityEvaluationInput, +): QualityEvalOutput { + const queryTokens = tokenize(input.originalQuery); + const queryEntities = extractEntities(input.originalQuery, "query"); + + const expectedType = inferExpectedType( + input.taskConstraints, + input.responseToolCalls, + ); + + const evalInput: QualityEvalInput = { + responseText: input.responseText, + responseToolCalls: input.responseToolCalls, + expectedType, + queryTokens, + queryEntities, + previousAttempts: input.previousAttempts, + taskConstraints: input.taskConstraints, + }; + + return evaluateQuality(evalInput); +} + +function inferExpectedType( + constraints: TaskConstraints, + toolCalls: Array<{ + id: string; + name: string; + arguments: Record; + }>, +): "tool_call" | "text" | "code" | "mixed" { + if (constraints.expectedToolCalls.length > 0) { + return "tool_call"; + } + + if (constraints.requiresCode) { + return "code"; + } + + if (toolCalls.length > 0) { + return "mixed"; + } + + return "text"; +} + +// ============================================================================= +// PHASE 3: RETRY DECISION +// ============================================================================= + +export interface RetryDecisionInput { + qualityOutput: QualityEvalOutput; + state: ReasoningControlState; + availableTools: string[]; + contextBudget: number; +} + +export interface RetryDecisionOutput { + shouldRetry: boolean; + action: RetryAction; + updatedState: ReasoningControlState; +} + +export function decideRetry(input: RetryDecisionInput): RetryDecisionOutput { + const { qualityOutput, state, availableTools, contextBudget } = input; + + if (qualityOutput.verdict === "ACCEPT") { + return { + shouldRetry: false, + action: { kind: "RETRY", transform: { kind: "NONE" } }, + updatedState: state, + }; + } + + const retryInput: RetryPolicyInput = { + currentState: state.retryPolicy, + trigger: { + event: "QUALITY_VERDICT", + verdict: qualityOutput.verdict, + deficiencies: qualityOutput.deficiencies, + }, + availableTools, + contextBudget, + }; + + const retryOutput = computeRetryTransition(retryInput); + + const updatedState: ReasoningControlState = { + ...state, + retryPolicy: retryOutput.nextState, + metrics: { + ...state.metrics, + totalRetries: state.metrics.totalRetries + 1, + }, + }; + + const shouldRetry = + isRetryable(retryOutput.nextState) && + retryOutput.action.kind !== "ABORT" && + retryOutput.action.kind !== "ESCALATE_TO_USER"; + + return { + shouldRetry, + action: retryOutput.action, + updatedState, + }; +} + +// ============================================================================= +// PHASE 4: TERMINATION CHECK +// ============================================================================= + +export interface TerminationCheckInput { + responseText: string; + hasToolCalls: boolean; + toolResults: Array<{ name: string; success: boolean }>; + state: ReasoningControlState; +} + +export interface TerminationCheckOutput { + isTerminal: boolean; + requiresValidation: boolean; + terminationState: TerminationState; + decision: TerminationOutput["decision"]; +} + +export function checkTermination( + input: TerminationCheckInput, +): TerminationCheckOutput { + let currentState = input.state.termination; + + const modelTrigger: TerminationTrigger = { + event: "MODEL_OUTPUT", + content: input.responseText, + hasToolCalls: input.hasToolCalls, + }; + + let output = processTerminationTrigger(currentState, modelTrigger); + currentState = { + ...currentState, + status: output.status, + completionSignals: output.evidence.signals, + confidenceScore: output.confidence, + }; + + for (const toolResult of input.toolResults) { + const toolTrigger: TerminationTrigger = { + event: "TOOL_COMPLETED", + toolName: toolResult.name, + success: toolResult.success, + }; + + output = processTerminationTrigger(currentState, toolTrigger); + currentState = { + ...currentState, + status: output.status, + completionSignals: output.evidence.signals, + confidenceScore: output.confidence, + }; + } + + return { + isTerminal: isTerminal(currentState), + requiresValidation: requiresValidation(currentState), + terminationState: currentState, + decision: output.decision, + }; +} + +// ============================================================================= +// FULL EXECUTION CYCLE +// ============================================================================= + +export interface ExecutionCycleInput { + query: string; + memoryStore: MemoryStore; + existingMessages: CompressibleMessage[]; + taskConstraints: TaskConstraints; + config: OrchestratorConfig; + callLLM: (context: CompressibleMessage[]) => Promise<{ + text: string; + toolCalls: Array<{ + id: string; + name: string; + arguments: Record; + }>; + }>; + executeTool: ( + name: string, + args: Record, + ) => Promise<{ + success: boolean; + output: string; + }>; +} + +export interface ExecutionCycleOutput { + result: ReasoningTaskResult; + updatedMemoryStore: MemoryStore; + finalState: ReasoningControlState; +} + +export async function executeReasoningCycle( + input: ExecutionCycleInput, +): Promise { + let state = createInitialState(); + let memoryStore = input.memoryStore; + let messages = input.existingMessages; + let iteration = 0; + + const phaseStart = (phase: ExecutionPhase) => { + state = { ...state, currentPhase: phase }; + return createTimestamp(); + }; + + const phaseEnd = (phase: ExecutionPhase, startTime: number) => { + state = { + ...state, + metrics: { + ...state.metrics, + phaseTimings: { + ...state.metrics.phaseTimings, + [phase]: + state.metrics.phaseTimings[phase] + (createTimestamp() - startTime), + }, + }, + }; + }; + + while (iteration < input.config.maxIterations) { + iteration++; + + const prepStart = phaseStart("CONTEXT_PREPARATION"); + const contextOutput = prepareContext( + { + query: input.query, + memoryStore, + existingContext: messages, + tokenBudget: input.config.tokenBudget, + activePaths: [], + }, + state, + ); + state = { ...state, entityTable: contextOutput.entityTable }; + phaseEnd("CONTEXT_PREPARATION", prepStart); + + const llmStart = phaseStart("LLM_INTERACTION"); + const llmResponse = await input.callLLM(contextOutput.compressedContext); + state = { + ...state, + metrics: { + ...state.metrics, + totalLLMCalls: state.metrics.totalLLMCalls + 1, + totalTokensUsed: + state.metrics.totalTokensUsed + estimateTokens(llmResponse.text), + }, + }; + phaseEnd("LLM_INTERACTION", llmStart); + + const evalStart = phaseStart("QUALITY_EVALUATION"); + const qualityOutput = evaluateResponseQuality({ + responseText: llmResponse.text, + responseToolCalls: llmResponse.toolCalls, + originalQuery: input.query, + taskConstraints: input.taskConstraints, + previousAttempts: state.retryPolicy.history, + }); + phaseEnd("QUALITY_EVALUATION", evalStart); + + if (qualityOutput.verdict !== "ACCEPT") { + const retryStart = phaseStart("RETRY_DECISION"); + const retryOutput = decideRetry({ + qualityOutput, + state, + availableTools: input.config.availableTools, + contextBudget: input.config.tokenBudget, + }); + state = retryOutput.updatedState; + phaseEnd("RETRY_DECISION", retryStart); + + if (!retryOutput.shouldRetry) { + return createFailedResult(state, memoryStore, retryOutput.action); + } + + continue; + } + + const execStart = phaseStart("EXECUTION"); + const toolResults: Array<{ + name: string; + success: boolean; + output: string; + }> = []; + + for (const toolCall of llmResponse.toolCalls) { + const result = await input.executeTool(toolCall.name, toolCall.arguments); + toolResults.push({ + name: toolCall.name, + success: result.success, + output: result.output, + }); + state = { + ...state, + metrics: { + ...state.metrics, + totalToolExecutions: state.metrics.totalToolExecutions + 1, + }, + }; + } + phaseEnd("EXECUTION", execStart); + + memoryStore = addMemory( + memoryStore, + createMemoryItem(llmResponse.text, "CONVERSATION", { + filePaths: extractFilePaths(llmResponse.text), + }), + ); + + for (const result of toolResults) { + memoryStore = addMemory( + memoryStore, + createMemoryItem(result.output, "TOOL_RESULT"), + ); + } + + const termStart = phaseStart("TERMINATION_CHECK"); + const termOutput = checkTermination({ + responseText: llmResponse.text, + hasToolCalls: llmResponse.toolCalls.length > 0, + toolResults: toolResults.map((r) => ({ + name: r.name, + success: r.success, + })), + state, + }); + state = { ...state, termination: termOutput.terminationState }; + phaseEnd("TERMINATION_CHECK", termStart); + + if (termOutput.isTerminal) { + return createSuccessResult(state, memoryStore, llmResponse.text); + } + + if (!termOutput.requiresValidation && llmResponse.toolCalls.length === 0) { + return createSuccessResult(state, memoryStore, llmResponse.text); + } + + messages = updateMessages(messages, llmResponse, toolResults); + } + + return createFailedResult(state, memoryStore, { + kind: "ABORT", + reason: "MAX_ITERATIONS_EXCEEDED", + }); +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +function extractFilePaths(text: string): string[] { + const pathPattern = + /(?:^|\s|["'`])([\w\-./]+\.[a-z]{1,4})(?:\s|$|:|[()\[\]"'`])/gm; + const matches: string[] = []; + let match: RegExpExecArray | null; + + while ((match = pathPattern.exec(text)) !== null) { + matches.push(match[1]); + } + + return [...new Set(matches)]; +} + +function updateMessages( + messages: CompressibleMessage[], + llmResponse: { + text: string; + toolCalls: Array<{ + id: string; + name: string; + arguments: Record; + }>; + }, + toolResults: Array<{ name: string; success: boolean; output: string }>, +): CompressibleMessage[] { + const updated = [...messages]; + + updated.push({ + id: `msg_${createTimestamp()}`, + role: "assistant", + content: llmResponse.text, + tokenCount: estimateTokens(llmResponse.text), + age: 0, + isPreserved: false, + }); + + for (const result of toolResults) { + updated.push({ + id: `tool_${createTimestamp()}`, + role: "tool", + content: result.output, + tokenCount: estimateTokens(result.output), + age: 0, + isPreserved: false, + metadata: { toolCallId: result.name }, + }); + } + + return updated; +} + +function createSuccessResult( + state: ReasoningControlState, + memoryStore: MemoryStore, + finalResponse: string, +): ExecutionCycleOutput { + return { + result: { + status: "COMPLETE", + confidence: state.termination.confidenceScore, + outputs: [], + finalResponse, + metrics: state.metrics, + history: state.retryPolicy.history, + }, + updatedMemoryStore: memoryStore, + finalState: { ...state, currentPhase: "COMPLETE" }, + }; +} + +function createFailedResult( + state: ReasoningControlState, + memoryStore: MemoryStore, + action: RetryAction, +): ExecutionCycleOutput { + const status = action.kind === "ESCALATE_TO_USER" ? "ESCALATED" : "FAILED"; + + return { + result: { + status, + confidence: state.termination.confidenceScore, + outputs: [], + finalResponse: action.kind === "ABORT" ? action.reason : "", + metrics: state.metrics, + history: state.retryPolicy.history, + }, + updatedMemoryStore: memoryStore, + finalState: { ...state, currentPhase: "FAILED" }, + }; +} + +// ============================================================================= +// EXPORTS +// ============================================================================= + +export { createMemoryStore, addMemory, createMemoryItem, createQueryContext }; diff --git a/src/services/reasoning/quality-evaluation.ts b/src/services/reasoning/quality-evaluation.ts new file mode 100644 index 0000000..b7fbd01 --- /dev/null +++ b/src/services/reasoning/quality-evaluation.ts @@ -0,0 +1,397 @@ +/** + * Quality Evaluation Layer + * Assesses LLM response acceptability using structural and lexical signals + */ + +import type { + QualityEvalInput, + QualityEvalOutput, + QualityMetrics, + QualityVerdict, + DeficiencyTag, + Entity, + ResponseType, +} from "@/types/reasoning"; + +import { + QUALITY_THRESHOLDS, + QUALITY_WEIGHTS, + STRUCTURAL_CHECK_WEIGHTS, + HALLUCINATION_PATTERNS, + CONTRADICTION_PATTERNS, + INCOMPLETE_STATEMENT_PATTERNS, +} from "@constants/reasoning"; + +import { + tokenize, + jaccardSimilarity, + weightedSum, + hasBalancedBraces, + extractCodeBlocks, + countMatches, +} from "@services/reasoning/utils"; + +// ============================================================================= +// MAIN EVALUATION FUNCTION +// ============================================================================= + +export function evaluateQuality(input: QualityEvalInput): QualityEvalOutput { + const metrics = computeQualityMetrics(input); + const score = computeFinalScore(metrics); + const verdict = computeVerdict(score); + const deficiencies = detectDeficiencies(input, metrics); + + return { + score, + verdict, + deficiencies, + metrics, + }; +} + +// ============================================================================= +// METRICS COMPUTATION +// ============================================================================= + +function computeQualityMetrics(input: QualityEvalInput): QualityMetrics { + return { + structural: computeStructuralScore(input), + relevance: computeRelevanceScore(input), + completeness: computeCompletenessScore(input), + coherence: computeCoherenceScore(input), + }; +} + +function computeFinalScore(metrics: QualityMetrics): number { + const values = [ + metrics.structural, + metrics.relevance, + metrics.completeness, + metrics.coherence, + ]; + const weights = [ + QUALITY_WEIGHTS.structural, + QUALITY_WEIGHTS.relevance, + QUALITY_WEIGHTS.completeness, + QUALITY_WEIGHTS.coherence, + ]; + return weightedSum(values, weights); +} + +// ============================================================================= +// STRUCTURAL SCORE +// ============================================================================= + +function computeStructuralScore(input: QualityEvalInput): number { + const checks = [ + parseSucceeds(input), + hasExpectedFormat(input), + withinLengthBounds(input), + noMalformedBlocks(input), + ]; + + const weights = [ + STRUCTURAL_CHECK_WEIGHTS.parseSucceeds, + STRUCTURAL_CHECK_WEIGHTS.hasExpectedFormat, + STRUCTURAL_CHECK_WEIGHTS.withinLengthBounds, + STRUCTURAL_CHECK_WEIGHTS.noMalformedBlocks, + ]; + + return weightedSum( + checks.map((b) => (b ? 1 : 0)), + weights, + ); +} + +function parseSucceeds(input: QualityEvalInput): boolean { + const { responseText, responseToolCalls } = input; + + if (responseText.length === 0 && responseToolCalls.length === 0) { + return false; + } + + for (const toolCall of responseToolCalls) { + if (!toolCall.name || typeof toolCall.arguments !== "object") { + return false; + } + } + + return true; +} + +function hasExpectedFormat(input: QualityEvalInput): boolean { + const { responseText, responseToolCalls, expectedType } = input; + + const formatChecks: Record boolean> = { + tool_call: () => responseToolCalls.length > 0, + text: () => responseText.length > 0 && responseToolCalls.length === 0, + code: () => extractCodeBlocks(responseText).length > 0, + mixed: () => true, + }; + + return formatChecks[expectedType](); +} + +function withinLengthBounds(input: QualityEvalInput): boolean { + const { responseText, taskConstraints } = input; + const maxTokens = taskConstraints.maxResponseTokens || 4000; + const estimatedTokens = responseText.length * 0.25; + return estimatedTokens <= maxTokens; +} + +function noMalformedBlocks(input: QualityEvalInput): boolean { + const { responseText } = input; + + if (!hasBalancedBraces(responseText)) { + return false; + } + + const codeBlockCount = countMatches(responseText, /```/g); + if (codeBlockCount % 2 !== 0) { + return false; + } + + return true; +} + +// ============================================================================= +// RELEVANCE SCORE +// ============================================================================= + +function computeRelevanceScore(input: QualityEvalInput): number { + const { responseText, queryTokens, queryEntities } = input; + + const responseTokens = tokenize(responseText); + const tokenOverlap = jaccardSimilarity(queryTokens, responseTokens); + + const entityHits = countEntityMentions(responseText, queryEntities); + const entityRatio = + queryEntities.length > 0 ? entityHits / queryEntities.length : 1; + + return tokenOverlap * 0.5 + entityRatio * 0.5; +} + +function countEntityMentions(text: string, entities: Entity[]): number { + const lowerText = text.toLowerCase(); + let hits = 0; + + for (const entity of entities) { + if (lowerText.includes(entity.value.toLowerCase())) { + hits++; + } + } + + return hits; +} + +// ============================================================================= +// COMPLETENESS SCORE +// ============================================================================= + +function computeCompletenessScore(input: QualityEvalInput): number { + const { responseText, responseToolCalls, taskConstraints } = input; + const { requiredOutputs, expectedToolCalls, requiresCode, codeLanguage } = + taskConstraints; + + const scores: number[] = []; + + if (requiredOutputs.length > 0) { + const outputsPresent = requiredOutputs.filter((output) => + responseText.toLowerCase().includes(output.toLowerCase()), + ).length; + scores.push(outputsPresent / requiredOutputs.length); + } + + if (expectedToolCalls.length > 0) { + const toolCallsPresent = expectedToolCalls.filter((toolName) => + responseToolCalls.some((tc) => tc.name === toolName), + ).length; + scores.push(toolCallsPresent / expectedToolCalls.length); + } + + if (requiresCode) { + const codeBlocks = extractCodeBlocks(responseText); + const hasCode = codeBlocks.length > 0; + const hasCorrectLanguage = + !codeLanguage || + codeBlocks.some( + (b) => b.language.toLowerCase() === codeLanguage.toLowerCase(), + ); + scores.push(hasCode && hasCorrectLanguage ? 1 : 0); + } + + if (scores.length === 0) { + return responseText.length > 50 ? 1 : 0.5; + } + + return scores.reduce((a, b) => a + b, 0) / scores.length; +} + +// ============================================================================= +// COHERENCE SCORE +// ============================================================================= + +function computeCoherenceScore(input: QualityEvalInput): number { + const { responseText } = input; + + const penalties: number[] = []; + + if (hasHallucinationMarkers(responseText)) { + penalties.push(0.4); + } + + if (hasContradiction(responseText)) { + penalties.push(0.3); + } + + if (hasIncompleteStatement(responseText)) { + penalties.push(0.2); + } + + if (hasBrokenReference(responseText)) { + penalties.push(0.1); + } + + const totalPenalty = penalties.reduce((a, b) => a + b, 0); + return Math.max(0, 1 - totalPenalty); +} + +function hasHallucinationMarkers(text: string): boolean { + return HALLUCINATION_PATTERNS.some((pattern) => pattern.test(text)); +} + +function hasContradiction(text: string): boolean { + return CONTRADICTION_PATTERNS.some((pattern) => pattern.test(text)); +} + +function hasIncompleteStatement(text: string): boolean { + const trimmed = text.trim(); + return INCOMPLETE_STATEMENT_PATTERNS.some((pattern) => pattern.test(trimmed)); +} + +function hasBrokenReference(text: string): boolean { + const referencePatterns = [ + /\[\d+\]/g, + /see above/i, + /as mentioned/i, + /previously/i, + ]; + + const hasReference = referencePatterns.some((p) => p.test(text)); + if (!hasReference) return false; + + const bracketRefs = text.match(/\[(\d+)\]/g); + if (bracketRefs) { + const maxRef = Math.max( + ...bracketRefs.map((r) => parseInt(r.slice(1, -1))), + ); + const actualRefs = text.match(/^\[\d+\]:/gm); + if (actualRefs && maxRef > actualRefs.length) { + return true; + } + } + + return false; +} + +// ============================================================================= +// VERDICT COMPUTATION +// ============================================================================= + +function computeVerdict(score: number): QualityVerdict { + if (score >= QUALITY_THRESHOLDS.ACCEPT) return "ACCEPT"; + if (score >= QUALITY_THRESHOLDS.RETRY) return "RETRY"; + if (score >= QUALITY_THRESHOLDS.ESCALATE) return "ESCALATE"; + return "ABORT"; +} + +// ============================================================================= +// DEFICIENCY DETECTION +// ============================================================================= + +function detectDeficiencies( + input: QualityEvalInput, + metrics: QualityMetrics, +): DeficiencyTag[] { + const deficiencies: DeficiencyTag[] = []; + const { responseText, responseToolCalls, taskConstraints, expectedType } = + input; + + if (!parseSucceeds(input)) { + deficiencies.push("PARSE_FAILURE"); + } + + if (responseText.length === 0 && responseToolCalls.length === 0) { + deficiencies.push("EMPTY_RESPONSE"); + } + + if (expectedType === "tool_call" && responseToolCalls.length === 0) { + deficiencies.push("MISSING_TOOL_CALL"); + } + + if (metrics.relevance < 0.3) { + deficiencies.push("QUERY_MISMATCH"); + } + + if (!withinLengthBounds(input)) { + deficiencies.push("TRUNCATED"); + } + + if (hasHallucinationMarkers(responseText)) { + deficiencies.push("HALLUCINATION_MARKER"); + } + + if (hasContradiction(responseText)) { + deficiencies.push("SELF_CONTRADICTION"); + } + + if (taskConstraints.requiresCode) { + const codeBlocks = extractCodeBlocks(responseText); + if (codeBlocks.length === 0) { + deficiencies.push("INCOMPLETE_CODE"); + } else if (taskConstraints.codeLanguage) { + const hasCorrectLang = codeBlocks.some( + (b) => + b.language.toLowerCase() === + taskConstraints.codeLanguage!.toLowerCase(), + ); + if (!hasCorrectLang) { + deficiencies.push("WRONG_LANGUAGE"); + } + } + } + + if (taskConstraints.requiredOutputs.length > 0) { + const missing = taskConstraints.requiredOutputs.filter( + (o) => !responseText.toLowerCase().includes(o.toLowerCase()), + ); + if (missing.length > 0) { + deficiencies.push("MISSING_REQUIRED_OUTPUT"); + } + } + + for (const toolCall of responseToolCalls) { + if (!toolCall.id || !toolCall.name) { + deficiencies.push("MALFORMED_TOOL_CALL"); + break; + } + } + + return [...new Set(deficiencies)]; +} + +// ============================================================================= +// EXPORTS +// ============================================================================= + +export { + computeQualityMetrics, + computeStructuralScore, + computeRelevanceScore, + computeCompletenessScore, + computeCoherenceScore, + computeVerdict, + detectDeficiencies, + hasHallucinationMarkers, + hasContradiction, +}; diff --git a/src/services/reasoning/retry-policy.ts b/src/services/reasoning/retry-policy.ts new file mode 100644 index 0000000..bced41d --- /dev/null +++ b/src/services/reasoning/retry-policy.ts @@ -0,0 +1,548 @@ +/** + * Retry and Reframing Policy Layer + * Controls retry behavior through structured state transitions + */ + +import type { + RetryState, + RetryStateKind, + RetryPolicyState, + RetryBudget, + RetryTrigger, + RetryPolicyInput, + RetryPolicyOutput, + RetryAction, + RetryTransform, + ContextDelta, + SubTask, + AttemptRecord, + ExhaustionReason, + EscalationContext, + DeficiencyTag, +} from "@/types/reasoning"; + +import { + RETRY_LIMITS, + RETRY_TIER_ORDER, + TASK_SEGMENT_PATTERNS, +} from "@constants/reasoning"; + +import { generateId, createTimestamp } from "@services/reasoning/utils"; + +// ============================================================================= +// STATE MACHINE INITIALIZATION +// ============================================================================= + +export function createInitialRetryState(): RetryPolicyState { + return { + currentState: { kind: "INITIAL", attempts: 0, tierAttempts: 0 }, + totalAttempts: 0, + history: [], + budget: createRetryBudget(), + }; +} + +export function createRetryBudget( + overrides?: Partial, +): RetryBudget { + return { + maxTotalAttempts: RETRY_LIMITS.maxTotalAttempts, + maxPerTier: RETRY_LIMITS.maxPerTier, + maxTimeMs: RETRY_LIMITS.maxTimeMs, + startTime: createTimestamp(), + ...overrides, + }; +} + +// ============================================================================= +// STATE TRANSITION LOGIC +// ============================================================================= + +export function computeRetryTransition( + input: RetryPolicyInput, +): RetryPolicyOutput { + const { currentState, trigger, availableTools, contextBudget } = input; + const { budget, history } = currentState; + + if (isExhausted(currentState, budget)) { + return createExhaustedOutput( + currentState, + getExhaustionReason(currentState, budget), + ); + } + + const attemptRecord = createAttemptRecord( + trigger, + currentState.totalAttempts + 1, + ); + const newHistory = [...history, attemptRecord]; + + const nextState = computeNextState( + currentState.currentState, + trigger, + budget, + availableTools, + contextBudget, + ); + + const action = computeAction(nextState, currentState.currentState, trigger); + + return { + nextState: { + currentState: nextState, + totalAttempts: currentState.totalAttempts + 1, + history: newHistory, + budget, + }, + action, + }; +} + +function isExhausted(state: RetryPolicyState, budget: RetryBudget): boolean { + if (state.totalAttempts >= budget.maxTotalAttempts) { + return true; + } + + const elapsed = createTimestamp() - budget.startTime; + if (elapsed >= budget.maxTimeMs) { + return true; + } + + return state.currentState.kind === "EXHAUSTED"; +} + +function getExhaustionReason( + state: RetryPolicyState, + budget: RetryBudget, +): ExhaustionReason { + if (state.totalAttempts >= budget.maxTotalAttempts) { + return "MAX_ATTEMPTS_EXCEEDED"; + } + + const elapsed = createTimestamp() - budget.startTime; + if (elapsed >= budget.maxTimeMs) { + return "TIME_BUDGET_EXCEEDED"; + } + + return "MAX_TIERS_EXCEEDED"; +} + +// ============================================================================= +// NEXT STATE COMPUTATION +// ============================================================================= + +function computeNextState( + current: RetryState, + trigger: RetryTrigger, + budget: RetryBudget, + availableTools: string[], + _contextBudget: number, +): RetryState { + // INITIAL is a pre-retry state - first retry always advances to RETRY_SAME + if (current.kind === "INITIAL") { + return { + kind: "RETRY_SAME", + attempts: 1, + tierAttempts: 1, + }; + } + + const tierAttempts = current.tierAttempts + 1; + + if (tierAttempts >= budget.maxPerTier) { + return advanceToNextTier(current, trigger, availableTools); + } + + return incrementCurrentTier(current); +} + +function incrementCurrentTier(current: RetryState): RetryState { + return { + ...current, + attempts: current.attempts + 1, + tierAttempts: current.tierAttempts + 1, + }; +} + +function advanceToNextTier( + current: RetryState, + trigger: RetryTrigger, + availableTools: string[], +): RetryState { + type TierKind = (typeof RETRY_TIER_ORDER)[number]; + const currentTierIndex = RETRY_TIER_ORDER.indexOf(current.kind as TierKind); + const nextTierKind = RETRY_TIER_ORDER[currentTierIndex + 1]; + + if (!nextTierKind || nextTierKind === "EXHAUSTED") { + return { + kind: "EXHAUSTED", + attempts: current.attempts + 1, + tierAttempts: 0, + exhaustionReason: "MAX_TIERS_EXCEEDED", + }; + } + + const baseState: RetryState = { + kind: nextTierKind, + attempts: current.attempts + 1, + tierAttempts: 0, + }; + + const tierStateMap: Partial RetryState>> = { + RETRY_SIMPLIFIED: () => ({ + ...baseState, + removedContext: createContextDelta(trigger), + }), + RETRY_DECOMPOSED: () => ({ + ...baseState, + subTasks: extractSubTasks(trigger), + }), + RETRY_ALTERNATIVE: () => ({ + ...baseState, + alternativeTool: selectAlternativeTool(trigger, availableTools), + }), + }; + + const stateCreator = tierStateMap[nextTierKind]; + return stateCreator ? stateCreator() : baseState; +} + +// ============================================================================= +// CONTEXT DELTA CREATION +// ============================================================================= + +function createContextDelta(_trigger: RetryTrigger): ContextDelta { + return { + removedMessageIds: [], + truncatedResults: [], + collapsedAttempts: 1, + }; +} + +// ============================================================================= +// TASK DECOMPOSITION +// ============================================================================= + +function extractSubTasks(trigger: RetryTrigger): SubTask[] { + if (trigger.event === "QUALITY_VERDICT") { + return createDefaultSubTasks(); + } + + if (trigger.event === "TOOL_EXECUTION_FAILED") { + return [ + { + id: generateId("subtask"), + description: `Investigate tool failure: ${trigger.error.message}`, + dependencies: [], + status: "PENDING", + }, + { + id: generateId("subtask"), + description: "Retry original task with alternative approach", + dependencies: [], + status: "PENDING", + }, + ]; + } + + return createDefaultSubTasks(); +} + +function createDefaultSubTasks(): SubTask[] { + return [ + { + id: generateId("subtask"), + description: "Understand the current state", + dependencies: [], + status: "PENDING", + }, + { + id: generateId("subtask"), + description: "Make the required change", + dependencies: [], + status: "PENDING", + }, + { + id: generateId("subtask"), + description: "Verify the change", + dependencies: [], + status: "PENDING", + }, + ]; +} + +export function splitTaskDescription(description: string): string[] { + for (const pattern of TASK_SEGMENT_PATTERNS) { + const regex = new RegExp(pattern.source, pattern.flags); + const matches = description.match(regex); + + if (matches && matches.length > 1) { + return matches + .map((m) => m.replace(/^[-•\d.)\s]+/, "").trim()) + .filter((s) => s.length > 0); + } + } + + return [description]; +} + +// ============================================================================= +// ALTERNATIVE TOOL SELECTION +// ============================================================================= + +function selectAlternativeTool( + trigger: RetryTrigger, + availableTools: string[], +): string | undefined { + if (trigger.event !== "TOOL_EXECUTION_FAILED") { + return undefined; + } + + const failedTool = trigger.error.toolName; + const alternatives = availableTools.filter((t) => t !== failedTool); + + const toolAlternatives: Record = { + write: ["edit"], + edit: ["write"], + bash: [], + read: ["glob", "grep"], + glob: ["grep", "bash"], + grep: ["glob", "bash"], + }; + + const preferredAlternatives = toolAlternatives[failedTool] || []; + const available = preferredAlternatives.filter((t) => + alternatives.includes(t), + ); + + return available[0] || alternatives[0]; +} + +// ============================================================================= +// ACTION COMPUTATION +// ============================================================================= + +function computeAction( + nextState: RetryState, + previousState: RetryState, + trigger: RetryTrigger, +): RetryAction { + if (nextState.kind === "EXHAUSTED") { + const shouldEscalate = shouldEscalateToUser(trigger); + + if (shouldEscalate) { + return { + kind: "ESCALATE_TO_USER", + context: createEscalationContext(trigger, previousState), + }; + } + + return { + kind: "ABORT", + reason: nextState.exhaustionReason || "MAX_TIERS_EXCEEDED", + }; + } + + if (nextState.kind === "RETRY_DECOMPOSED" && nextState.subTasks) { + return { + kind: "DECOMPOSE", + subTasks: nextState.subTasks, + }; + } + + if (nextState.kind === "RETRY_ALTERNATIVE" && nextState.alternativeTool) { + return { + kind: "SWITCH_TOOL", + newTool: nextState.alternativeTool, + }; + } + + const transform = computeTransform(nextState, previousState); + + return { + kind: "RETRY", + transform, + }; +} + +function computeTransform( + nextState: RetryState, + previousState: RetryState, +): RetryTransform { + if (nextState.kind === previousState.kind) { + return { kind: "NONE" }; + } + + if (nextState.kind === "RETRY_SIMPLIFIED" && nextState.removedContext) { + return { + kind: "REDUCE_CONTEXT", + delta: nextState.removedContext, + }; + } + + if (nextState.kind === "RETRY_DECOMPOSED" && nextState.subTasks) { + return { + kind: "SPLIT_TASK", + subTasks: nextState.subTasks, + }; + } + + if (nextState.kind === "RETRY_ALTERNATIVE" && nextState.alternativeTool) { + return { + kind: "SELECT_ALTERNATIVE", + tool: nextState.alternativeTool, + }; + } + + return { kind: "NONE" }; +} + +// ============================================================================= +// ESCALATION LOGIC +// ============================================================================= + +function shouldEscalateToUser(trigger: RetryTrigger): boolean { + if (trigger.event === "QUALITY_VERDICT") { + const criticalDeficiencies: DeficiencyTag[] = [ + "QUERY_MISMATCH", + "HALLUCINATION_MARKER", + ]; + return trigger.deficiencies.some((d) => criticalDeficiencies.includes(d)); + } + + if (trigger.event === "TOOL_EXECUTION_FAILED") { + return trigger.error.errorType === "PERMISSION_DENIED"; + } + + return false; +} + +function createEscalationContext( + trigger: RetryTrigger, + _state: RetryState, +): EscalationContext { + const reason = + trigger.event === "QUALITY_VERDICT" + ? `Response quality issues: ${trigger.deficiencies.join(", ")}` + : trigger.event === "TOOL_EXECUTION_FAILED" + ? `Tool execution failed: ${trigger.error.message}` + : "Validation failed"; + + return { + reason, + attempts: [], + suggestedActions: computeSuggestedActions(trigger), + }; +} + +function computeSuggestedActions(trigger: RetryTrigger): string[] { + if (trigger.event === "QUALITY_VERDICT") { + return [ + "Provide more specific requirements", + "Break the task into smaller steps", + "Specify expected output format", + ]; + } + + if (trigger.event === "TOOL_EXECUTION_FAILED") { + const actionsByError: Record = { + PERMISSION_DENIED: [ + "Grant the required permission", + "Use a different approach that doesn't require this permission", + ], + TIMEOUT: ["Increase the timeout", "Simplify the operation"], + INVALID_ARGS: [ + "Review the command arguments", + "Check file paths and names", + ], + EXECUTION_ERROR: ["Check the system state", "Try an alternative command"], + }; + + return ( + actionsByError[trigger.error.errorType] || ["Review the error details"] + ); + } + + return ["Review the validation failures", "Adjust the approach"]; +} + +// ============================================================================= +// ATTEMPT RECORD CREATION +// ============================================================================= + +function createAttemptRecord( + trigger: RetryTrigger, + attemptNumber: number, +): AttemptRecord { + const baseRecord = { + attemptNumber, + timestamp: createTimestamp(), + score: 0, + }; + + if (trigger.event === "QUALITY_VERDICT") { + return { + ...baseRecord, + verdict: trigger.verdict, + deficiencies: trigger.deficiencies, + }; + } + + return { + ...baseRecord, + verdict: "RETRY", + deficiencies: [], + }; +} + +// ============================================================================= +// EXHAUSTED OUTPUT CREATION +// ============================================================================= + +function createExhaustedOutput( + state: RetryPolicyState, + reason: ExhaustionReason, +): RetryPolicyOutput { + return { + nextState: { + ...state, + currentState: { + kind: "EXHAUSTED", + attempts: state.totalAttempts, + tierAttempts: 0, + exhaustionReason: reason, + }, + }, + action: { + kind: "ABORT", + reason, + }, + }; +} + +// ============================================================================= +// STATE QUERIES +// ============================================================================= + +export function isRetryable(state: RetryPolicyState): boolean { + return ( + state.currentState.kind !== "EXHAUSTED" && + state.currentState.kind !== "COMPLETE" + ); +} + +export function getCurrentTier(state: RetryPolicyState): RetryStateKind { + return state.currentState.kind; +} + +export function getRemainingAttempts(state: RetryPolicyState): number { + return state.budget.maxTotalAttempts - state.totalAttempts; +} + +export function getElapsedTime(state: RetryPolicyState): number { + return createTimestamp() - state.budget.startTime; +} + +export function getRemainingTime(state: RetryPolicyState): number { + return Math.max(0, state.budget.maxTimeMs - getElapsedTime(state)); +} diff --git a/src/services/reasoning/termination-detection.ts b/src/services/reasoning/termination-detection.ts new file mode 100644 index 0000000..d981d07 --- /dev/null +++ b/src/services/reasoning/termination-detection.ts @@ -0,0 +1,498 @@ +/** + * Termination Confidence Detection Layer + * Determines task completion through observable validation signals + */ + +import type { + TerminationStatus, + TerminationState, + TerminationTrigger, + TerminationOutput, + TerminationDecision, + TerminationEvidence, + CompletionSignal, + ValidationCheck, + ValidationCheckType, + ValidationResult, + ValidationFailure, +} from "@/types/reasoning"; + +import { + CONFIDENCE_THRESHOLDS, + COMPLETION_SIGNAL_PATTERNS, + TOOL_SUCCESS_CONFIDENCE, + NO_PENDING_ACTIONS_CONFIDENCE, + VALIDATION_CHECK_CONFIGS, +} from "@constants/reasoning"; + +import { createTimestamp } from "@services/reasoning/utils"; + +// ============================================================================= +// STATE INITIALIZATION +// ============================================================================= + +export function createInitialTerminationState(): TerminationState { + return { + status: "RUNNING", + validationResults: [], + completionSignals: [], + confidenceScore: 0, + pendingChecks: [], + }; +} + +// ============================================================================= +// STATE MACHINE TRANSITIONS +// ============================================================================= + +export function processTerminationTrigger( + state: TerminationState, + trigger: TerminationTrigger, +): TerminationOutput { + const updatedSignals = collectSignals(state.completionSignals, trigger); + const updatedResults = updateValidationResults( + state.validationResults, + trigger, + ); + + const confidence = computeTerminationConfidence( + updatedSignals, + updatedResults, + ); + const nextStatus = computeNextStatus(state.status, confidence, trigger); + + const decision = computeDecision(nextStatus, confidence, updatedResults); + + const evidence: TerminationEvidence = { + signals: updatedSignals, + validationResults: updatedResults, + pendingItems: computePendingItems(nextStatus, state.pendingChecks), + }; + + return { + status: nextStatus, + confidence, + decision, + evidence, + }; +} + +// ============================================================================= +// SIGNAL COLLECTION +// ============================================================================= + +function collectSignals( + existing: CompletionSignal[], + trigger: TerminationTrigger, +): CompletionSignal[] { + const newSignals = extractSignalsFromTrigger(trigger); + return [...existing, ...newSignals]; +} + +function extractSignalsFromTrigger( + trigger: TerminationTrigger, +): CompletionSignal[] { + const signals: CompletionSignal[] = []; + const timestamp = createTimestamp(); + + if (trigger.event === "MODEL_OUTPUT") { + const modelSignals = detectModelCompletionSignals(trigger.content); + signals.push(...modelSignals); + + if (!trigger.hasToolCalls) { + signals.push({ + source: "NO_PENDING_ACTIONS", + timestamp, + confidence: NO_PENDING_ACTIONS_CONFIDENCE, + evidence: "No tool calls in response", + }); + } + } + + if (trigger.event === "TOOL_COMPLETED" && trigger.success) { + signals.push({ + source: "TOOL_SUCCESS", + timestamp, + confidence: TOOL_SUCCESS_CONFIDENCE, + evidence: `Tool ${trigger.toolName} completed successfully`, + }); + } + + if (trigger.event === "USER_INPUT" && trigger.isAcceptance) { + signals.push({ + source: "USER_ACCEPT", + timestamp, + confidence: 1.0, + evidence: "User accepted the result", + }); + } + + return signals; +} + +function detectModelCompletionSignals(content: string): CompletionSignal[] { + const signals: CompletionSignal[] = []; + const timestamp = createTimestamp(); + + for (const signalConfig of COMPLETION_SIGNAL_PATTERNS) { + for (const pattern of signalConfig.patterns) { + if (pattern.test(content)) { + signals.push({ + source: signalConfig.type, + timestamp, + confidence: signalConfig.confidence, + evidence: `Matched pattern: ${pattern.source}`, + }); + break; + } + } + } + + return signals; +} + +// ============================================================================= +// VALIDATION RESULT HANDLING +// ============================================================================= + +function updateValidationResults( + existing: ValidationResult[], + trigger: TerminationTrigger, +): ValidationResult[] { + if (trigger.event === "VALIDATION_RESULT") { + const existingIdx = existing.findIndex( + (r) => r.checkId === trigger.result.checkId, + ); + + if (existingIdx >= 0) { + const updated = [...existing]; + updated[existingIdx] = trigger.result; + return updated; + } + + return [...existing, trigger.result]; + } + + return existing; +} + +// ============================================================================= +// CONFIDENCE COMPUTATION +// ============================================================================= + +export function computeTerminationConfidence( + signals: CompletionSignal[], + validationResults: ValidationResult[], +): number { + const signalScore = computeSignalScore(signals); + const validationScore = computeValidationScore(validationResults); + + return signalScore + validationScore; +} + +function computeSignalScore(signals: CompletionSignal[]): number { + const maxSignalScore = 0.4; + let score = 0; + + const signalContribution = 0.15; + + for (const signal of signals) { + score += signal.confidence * signalContribution; + } + + return Math.min(maxSignalScore, score); +} + +function computeValidationScore(results: ValidationResult[]): number { + if (results.length === 0) { + return 0; + } + + const requiredResults = results.filter((r) => { + const config = Object.entries(VALIDATION_CHECK_CONFIGS).find(([type]) => + r.checkId.includes(type.toLowerCase()), + ); + return config?.[1].required ?? true; + }); + + const optionalResults = results.filter((r) => !requiredResults.includes(r)); + + const requiredPassRate = + requiredResults.length > 0 + ? requiredResults.filter((r) => r.passed).length / requiredResults.length + : 1; + + const optionalPassRate = + optionalResults.length > 0 + ? optionalResults.filter((r) => r.passed).length / optionalResults.length + : 1; + + return requiredPassRate * 0.5 + optionalPassRate * 0.1; +} + +// ============================================================================= +// STATUS TRANSITIONS +// ============================================================================= + +function computeNextStatus( + currentStatus: TerminationStatus, + confidence: number, + trigger: TerminationTrigger, +): TerminationStatus { + if (trigger.event === "USER_INPUT" && trigger.isAcceptance) { + return "CONFIRMED_COMPLETE"; + } + + const statusTransitions: Record< + TerminationStatus, + (confidence: number) => TerminationStatus + > = { + RUNNING: (conf) => { + if (conf >= CONFIDENCE_THRESHOLDS.POTENTIALLY_COMPLETE) { + return "POTENTIALLY_COMPLETE"; + } + return "RUNNING"; + }, + POTENTIALLY_COMPLETE: () => "AWAITING_VALIDATION", + AWAITING_VALIDATION: (conf) => { + if (conf >= CONFIDENCE_THRESHOLDS.CONFIRMED_COMPLETE) { + return "CONFIRMED_COMPLETE"; + } + if (conf < CONFIDENCE_THRESHOLDS.POTENTIALLY_COMPLETE) { + return "RUNNING"; + } + return "AWAITING_VALIDATION"; + }, + CONFIRMED_COMPLETE: () => "CONFIRMED_COMPLETE", + FAILED: () => "FAILED", + }; + + const transition = statusTransitions[currentStatus]; + return transition(confidence); +} + +// ============================================================================= +// DECISION COMPUTATION +// ============================================================================= + +function computeDecision( + status: TerminationStatus, + confidence: number, + validationResults: ValidationResult[], +): TerminationDecision { + const decisionMap: Record TerminationDecision> = { + RUNNING: () => ({ + kind: "CONTINUE", + reason: `Confidence ${(confidence * 100).toFixed(1)}% below threshold`, + }), + POTENTIALLY_COMPLETE: () => ({ + kind: "VALIDATE", + checks: selectValidationChecks(validationResults), + }), + AWAITING_VALIDATION: () => { + const failedChecks = validationResults.filter((r) => !r.passed); + + if (failedChecks.length > 0) { + const recoverable = failedChecks.every((c) => isRecoverableFailure(c)); + return { + kind: "FAIL", + reason: `Validation failed: ${failedChecks.map((c) => c.details).join("; ")}`, + recoverable, + }; + } + + return { + kind: "VALIDATE", + checks: selectValidationChecks(validationResults), + }; + }, + CONFIRMED_COMPLETE: () => ({ + kind: "COMPLETE", + summary: `Task completed with ${(confidence * 100).toFixed(1)}% confidence`, + }), + FAILED: () => ({ + kind: "FAIL", + reason: "Task failed after exhausting retries", + recoverable: false, + }), + }; + + const decisionFn = decisionMap[status]; + return decisionFn(); +} + +function selectValidationChecks( + existingResults: ValidationResult[], +): ValidationCheck[] { + const completedCheckIds = new Set(existingResults.map((r) => r.checkId)); + + const allChecks: ValidationCheck[] = Object.entries( + VALIDATION_CHECK_CONFIGS, + ).map(([type, config]) => ({ + id: `${type.toLowerCase()}_check`, + type: type as ValidationCheckType, + required: config.required, + timeout: config.timeout, + })); + + return allChecks.filter((check) => !completedCheckIds.has(check.id)); +} + +function isRecoverableFailure(result: ValidationResult): boolean { + const nonRecoverablePatterns = [ + /permission denied/i, + /access denied/i, + /not found/i, + /does not exist/i, + ]; + + return !nonRecoverablePatterns.some((p) => p.test(result.details)); +} + +// ============================================================================= +// PENDING ITEMS COMPUTATION +// ============================================================================= + +function computePendingItems( + status: TerminationStatus, + pendingChecks: ValidationCheck[], +): string[] { + if (status === "CONFIRMED_COMPLETE" || status === "FAILED") { + return []; + } + + return pendingChecks.map((c) => `Validation: ${c.type}`); +} + +// ============================================================================= +// VALIDATION CHECK EXECUTION +// ============================================================================= + +export interface ValidationContext { + expectedOutputs: string[]; + modifiedFiles: string[]; + taskType: string; + hasTests: boolean; + testCommand?: string; +} + +export async function runValidationCheck( + check: ValidationCheck, + context: ValidationContext, + fileExists: (path: string) => Promise, + validateSyntax: (path: string) => Promise<{ valid: boolean; error?: string }>, + runCommand: ( + cmd: string, + timeout: number, + ) => Promise<{ exitCode: number; output: string }>, +): Promise { + const startTime = createTimestamp(); + + const checkExecutors: Record< + ValidationCheckType, + () => Promise<{ passed: boolean; details: string }> + > = { + FILE_EXISTS: async () => { + const results = await Promise.all( + context.expectedOutputs.map((f) => fileExists(f)), + ); + const allExist = results.every((r) => r); + return { + passed: allExist, + details: `${results.filter((r) => r).length}/${results.length} files exist`, + }; + }, + SYNTAX_VALID: async () => { + const results = await Promise.all( + context.modifiedFiles.map((f) => validateSyntax(f)), + ); + const allValid = results.every((r) => r.valid); + const errors = results.filter((r) => !r.valid).map((r) => r.error); + return { + passed: allValid, + details: allValid ? "All files have valid syntax" : errors.join("; "), + }; + }, + DIFF_NONEMPTY: async () => { + if (context.taskType !== "EDIT") { + return { passed: true, details: "N/A for non-edit tasks" }; + } + return { + passed: context.modifiedFiles.length > 0, + details: `${context.modifiedFiles.length} files modified`, + }; + }, + TESTS_PASS: async () => { + if (!context.hasTests || !context.testCommand) { + return { passed: true, details: "No tests configured" }; + } + const result = await runCommand(context.testCommand, check.timeout); + return { + passed: result.exitCode === 0, + details: + result.exitCode === 0 + ? "Tests passed" + : `Tests failed: ${result.output.slice(0, 200)}`, + }; + }, + SCHEMA_VALID: async () => { + return { passed: true, details: "Schema validation not implemented" }; + }, + NO_REGRESSIONS: async () => { + return { passed: true, details: "Regression check not implemented" }; + }, + }; + + const executor = checkExecutors[check.type]; + const result = await executor(); + + return { + checkId: check.id, + passed: result.passed, + details: result.details, + duration: createTimestamp() - startTime, + }; +} + +// ============================================================================= +// VALIDATION FAILURE EXTRACTION +// ============================================================================= + +export function extractValidationFailures( + results: ValidationResult[], +): ValidationFailure[] { + return results + .filter((r) => !r.passed) + .map((r) => ({ + checkId: r.checkId, + reason: r.details, + recoverable: isRecoverableFailure(r), + })); +} + +// ============================================================================= +// COMPLETION DETECTION HELPERS +// ============================================================================= + +export function isComplete(state: TerminationState): boolean { + return state.status === "CONFIRMED_COMPLETE"; +} + +export function isFailed(state: TerminationState): boolean { + return state.status === "FAILED"; +} + +export function isTerminal(state: TerminationState): boolean { + return isComplete(state) || isFailed(state); +} + +export function requiresValidation(state: TerminationState): boolean { + return ( + state.status === "POTENTIALLY_COMPLETE" || + state.status === "AWAITING_VALIDATION" + ); +} + +export function getConfidencePercentage(state: TerminationState): string { + return `${(state.confidenceScore * 100).toFixed(1)}%`; +} diff --git a/src/services/reasoning/utils.ts b/src/services/reasoning/utils.ts new file mode 100644 index 0000000..6b9e34e --- /dev/null +++ b/src/services/reasoning/utils.ts @@ -0,0 +1,409 @@ +/** + * Utility functions for the Reasoning Control Layer + * Pure functions for common operations + */ + +import type { Entity, EntityType, EntityTable } from "@/types/reasoning"; +import { + ENTITY_PATTERNS, + TOKENS_PER_CHAR_ESTIMATE, +} from "@constants/reasoning"; + +// ============================================================================= +// TOKEN ESTIMATION +// ============================================================================= + +export function estimateTokens(text: string): number { + return Math.ceil(text.length * TOKENS_PER_CHAR_ESTIMATE); +} + +export function estimateTokensForObject(obj: unknown): number { + return estimateTokens(JSON.stringify(obj)); +} + +// ============================================================================= +// TOKENIZATION +// ============================================================================= + +const STOP_WORDS = new Set([ + "the", + "a", + "an", + "is", + "are", + "was", + "were", + "be", + "been", + "being", + "have", + "has", + "had", + "do", + "does", + "did", + "will", + "would", + "could", + "should", + "may", + "might", + "must", + "shall", + "can", + "need", + "dare", + "to", + "of", + "in", + "for", + "on", + "with", + "at", + "by", + "from", + "as", + "into", + "through", + "during", + "before", + "after", + "above", + "below", + "between", + "under", + "again", + "further", + "then", + "once", + "here", + "there", + "when", + "where", + "why", + "how", + "all", + "each", + "few", + "more", + "most", + "other", + "some", + "such", + "no", + "nor", + "not", + "only", + "own", + "same", + "so", + "than", + "too", + "very", + "just", + "and", + "but", + "if", + "or", + "because", + "until", + "while", + "this", + "that", + "these", + "those", + "i", + "me", + "my", + "myself", + "we", + "our", + "ours", + "ourselves", + "you", + "your", + "yours", + "yourself", + "yourselves", + "he", + "him", + "his", + "himself", + "she", + "her", + "hers", + "herself", + "it", + "its", + "itself", + "they", + "them", + "their", + "theirs", + "themselves", + "what", + "which", + "who", + "whom", + "whose", + "this", + "that", + "am", + "been", + "being", +]); + +export function tokenize(text: string): string[] { + return text + .toLowerCase() + .replace(/[^\w\s]/g, " ") + .split(/\s+/) + .filter((token) => token.length > 2 && !STOP_WORDS.has(token)); +} + +// ============================================================================= +// SIMILARITY FUNCTIONS +// ============================================================================= + +export function jaccardSimilarity(setA: string[], setB: string[]): number { + const a = new Set(setA); + const b = new Set(setB); + const intersection = [...a].filter((x) => b.has(x)).length; + const union = new Set([...a, ...b]).size; + return union > 0 ? intersection / union : 0; +} + +export function weightedSum(values: number[], weights: number[]): number { + if (values.length !== weights.length) { + throw new Error("Values and weights must have same length"); + } + return values.reduce((sum, val, i) => sum + val * weights[i], 0); +} + +// ============================================================================= +// ENTITY EXTRACTION +// ============================================================================= + +export function extractEntities( + text: string, + sourceMessageId: string, +): Entity[] { + const entities: Entity[] = []; + const seen = new Set(); + + for (const [type, pattern] of Object.entries(ENTITY_PATTERNS)) { + const regex = new RegExp(pattern.source, pattern.flags); + let match: RegExpExecArray | null; + + while ((match = regex.exec(text)) !== null) { + const value = match[1] || match[0]; + const key = `${type}:${value}`; + + if (!seen.has(key)) { + seen.add(key); + entities.push({ + type: type as EntityType, + value: value.trim(), + sourceMessageId, + frequency: 1, + }); + } + } + } + + return entities; +} + +export function createEntityTable(entities: Entity[]): EntityTable { + const byType: Record = { + FILE: [], + FUNCTION: [], + VARIABLE: [], + CLASS: [], + URL: [], + ERROR_CODE: [], + }; + + const bySource: Record = {}; + + for (const entity of entities) { + byType[entity.type].push(entity); + + if (!bySource[entity.sourceMessageId]) { + bySource[entity.sourceMessageId] = []; + } + bySource[entity.sourceMessageId].push(entity); + } + + return { entities, byType, bySource }; +} + +export function mergeEntityTables(a: EntityTable, b: EntityTable): EntityTable { + const merged = [...a.entities, ...b.entities]; + return createEntityTable(merged); +} + +// ============================================================================= +// TEXT PROCESSING +// ============================================================================= + +export function truncateMiddle( + text: string, + keepHead: number, + keepTail: number, +): string { + const totalKeep = keepHead + keepTail; + if (text.length <= totalKeep) { + return text; + } + + const head = text.slice(0, keepHead); + const tail = text.slice(-keepTail); + const removed = text.length - totalKeep; + + return `${head}\n\n... [${removed} characters truncated] ...\n\n${tail}`; +} + +export function foldCode( + code: string, + options: { keepLines: number; tailLines: number }, +): string { + const lines = code.split("\n"); + const totalLines = lines.length; + + if (totalLines <= options.keepLines + options.tailLines) { + return code; + } + + const head = lines.slice(0, options.keepLines); + const tail = lines.slice(-options.tailLines); + const folded = totalLines - options.keepLines - options.tailLines; + + return [...head, `// ... [${folded} lines folded] ...`, ...tail].join("\n"); +} + +export function extractCodeBlocks(text: string): Array<{ + language: string; + content: string; + startIndex: number; + endIndex: number; +}> { + const blocks: Array<{ + language: string; + content: string; + startIndex: number; + endIndex: number; + }> = []; + + const regex = /```(\w*)\n([\s\S]*?)```/g; + let match: RegExpExecArray | null; + + while ((match = regex.exec(text)) !== null) { + blocks.push({ + language: match[1] || "unknown", + content: match[2], + startIndex: match.index, + endIndex: match.index + match[0].length, + }); + } + + return blocks; +} + +// ============================================================================= +// TIME UTILITIES +// ============================================================================= + +export function recencyDecay( + itemTime: number, + queryTime: number, + halfLifeMinutes: number, +): number { + const ageMs = queryTime - itemTime; + const ageMinutes = ageMs / 60000; + return Math.pow(0.5, ageMinutes / halfLifeMinutes); +} + +export function createTimestamp(): number { + return Date.now(); +} + +// ============================================================================= +// ID GENERATION +// ============================================================================= + +export function generateId(prefix: string = ""): string { + const timestamp = Date.now().toString(36); + const random = Math.random().toString(36).substring(2, 8); + return prefix ? `${prefix}_${timestamp}_${random}` : `${timestamp}_${random}`; +} + +// ============================================================================= +// VALIDATION HELPERS +// ============================================================================= + +export function isValidJson(text: string): boolean { + try { + JSON.parse(text); + return true; + } catch { + return false; + } +} + +export function hasBalancedBraces(text: string): boolean { + const stack: string[] = []; + const pairs: Record = { + "(": ")", + "[": "]", + "{": "}", + }; + + for (const char of text) { + if (char in pairs) { + stack.push(pairs[char]); + } else if (Object.values(pairs).includes(char)) { + if (stack.pop() !== char) { + return false; + } + } + } + + return stack.length === 0; +} + +export function countMatches(text: string, pattern: RegExp): number { + const matches = text.match( + new RegExp(pattern.source, "g" + (pattern.flags.includes("i") ? "i" : "")), + ); + return matches ? matches.length : 0; +} + +// ============================================================================= +// ARRAY UTILITIES +// ============================================================================= + +export function sum(values: number[]): number { + return values.reduce((acc, val) => acc + val, 0); +} + +export function unique(arr: T[]): T[] { + return [...new Set(arr)]; +} + +export function groupBy( + arr: T[], + keyFn: (item: T) => K, +): Record { + return arr.reduce( + (acc, item) => { + const key = keyFn(item); + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(item); + return acc; + }, + {} as Record, + ); +} diff --git a/src/services/refactoring-service.ts b/src/services/refactoring-service.ts new file mode 100644 index 0000000..68f0647 --- /dev/null +++ b/src/services/refactoring-service.ts @@ -0,0 +1,271 @@ +/** + * Refactoring Detection Service + * + * Detects refactoring requests and provides refactoring context. + */ + +import { + REFACTORING_SYSTEM_PROMPT, + REFACTORING_CONTEXT_TEMPLATE, +} from "@prompts/system/refactoring"; + +export interface RefactoringContext { + isRefactoring: boolean; + refactoringType: RefactoringType; + target?: string; + goal?: RefactoringGoal; +} + +export type RefactoringType = + | "extract" + | "inline" + | "rename" + | "move" + | "simplify" + | "decompose" + | "general" + | "none"; + +export type RefactoringGoal = + | "readability" + | "performance" + | "maintainability" + | "testability" + | "duplication" + | "complexity" + | "general"; + +const REFACTORING_KEYWORDS: Record< + Exclude, + string[] +> = { + extract: [ + "extract function", + "extract method", + "extract variable", + "extract constant", + "extract class", + "extract interface", + "pull out", + "break out", + "separate into", + ], + inline: [ + "inline", + "inline function", + "inline variable", + "merge into", + "combine into", + ], + rename: ["rename", "better name", "naming", "change name", "call it"], + move: [ + "move to", + "relocate", + "move function", + "move method", + "move class", + "reorganize", + ], + simplify: [ + "simplify", + "make simpler", + "reduce complexity", + "flatten", + "clean up", + "cleanup", + "tidy", + ], + decompose: [ + "decompose", + "break down", + "split up", + "divide into", + "modularize", + ], + general: [ + "refactor", + "refactoring", + "restructure", + "rewrite", + "improve", + "better way", + "cleaner", + "more readable", + "more maintainable", + ], +}; + +const GOAL_KEYWORDS: Record = { + readability: [ + "readable", + "readability", + "understand", + "clear", + "clarity", + "easier to read", + ], + performance: ["faster", "performance", "efficient", "optimize", "speed"], + maintainability: [ + "maintainable", + "maintainability", + "easier to change", + "flexible", + ], + testability: [ + "testable", + "testability", + "easier to test", + "unit test", + "mockable", + ], + duplication: [ + "duplication", + "duplicate", + "dry", + "repeated", + "copy paste", + "same code", + ], + complexity: ["complexity", "complex", "complicated", "nested", "cyclomatic"], + general: [], +}; + +const detectRefactoringType = (input: string): RefactoringType => { + const lowerInput = input.toLowerCase(); + + // Check specific types first (more specific before general) + const typeOrder: Exclude[] = [ + "extract", + "inline", + "rename", + "move", + "simplify", + "decompose", + "general", + ]; + + for (const type of typeOrder) { + const keywords = REFACTORING_KEYWORDS[type]; + for (const keyword of keywords) { + if (lowerInput.includes(keyword)) { + return type; + } + } + } + + return "none"; +}; + +const detectGoal = (input: string): RefactoringGoal | undefined => { + const lowerInput = input.toLowerCase(); + + for (const [goal, keywords] of Object.entries(GOAL_KEYWORDS)) { + if (goal === "general") continue; + for (const keyword of keywords) { + if (lowerInput.includes(keyword)) { + return goal as RefactoringGoal; + } + } + } + + return undefined; +}; + +const extractTarget = (input: string): string | undefined => { + const patterns = [ + // Function/method names + /(?:refactor|extract|inline|rename|move|simplify)\s+(?:the\s+)?(?:function|method|class|variable|constant)?\s*[`"']?(\w+)[`"']?/i, + // File references + /(?:in|from|at)\s+[`"']?([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)[`"']?/i, + // This/the X pattern + /(?:this|the)\s+(\w+)\s+(?:function|method|class|code)/i, + ]; + + for (const pattern of patterns) { + const match = input.match(pattern); + if (match?.[1]) { + return match[1]; + } + } + + return undefined; +}; + +export const detectRefactoringRequest = (input: string): RefactoringContext => { + const refactoringType = detectRefactoringType(input); + + if (refactoringType === "none") { + return { + isRefactoring: false, + refactoringType: "none", + }; + } + + return { + isRefactoring: true, + refactoringType, + target: extractTarget(input), + goal: detectGoal(input), + }; +}; + +export const buildRefactoringContext = ( + context: RefactoringContext, +): string => { + if (!context.isRefactoring) { + return ""; + } + + const typeLabels: Record = { + extract: "Extract (function/variable/class)", + inline: "Inline (function/variable)", + rename: "Rename", + move: "Move/Relocate", + simplify: "Simplify/Clean up", + decompose: "Decompose/Break down", + general: "General Refactoring", + none: "None", + }; + + const goalLabels: Record = { + readability: "Improve Readability", + performance: "Improve Performance", + maintainability: "Improve Maintainability", + testability: "Improve Testability", + duplication: "Remove Duplication", + complexity: "Reduce Complexity", + general: "General Improvement", + }; + + return REFACTORING_CONTEXT_TEMPLATE.replace( + "{{refactoringType}}", + typeLabels[context.refactoringType], + ) + .replace("{{target}}", context.target || "Not specified") + .replace( + "{{goal}}", + context.goal ? goalLabels[context.goal] : "General Improvement", + ); +}; + +export const getRefactoringPrompt = (): string => { + return REFACTORING_SYSTEM_PROMPT; +}; + +export const enhancePromptForRefactoring = ( + basePrompt: string, + userInput: string, +): { prompt: string; context: RefactoringContext } => { + const context = detectRefactoringRequest(userInput); + + if (!context.isRefactoring) { + return { prompt: basePrompt, context }; + } + + const refactoringPrompt = getRefactoringPrompt(); + const refactoringContext = buildRefactoringContext(context); + + const enhancedPrompt = `${basePrompt}\n\n${refactoringPrompt}\n${refactoringContext}`; + + return { prompt: enhancedPrompt, context }; +}; diff --git a/src/services/rules-service.ts b/src/services/rules-service.ts new file mode 100644 index 0000000..c8c3a76 --- /dev/null +++ b/src/services/rules-service.ts @@ -0,0 +1,32 @@ +/** + * Rules loader service for project-specific agent behavior + * + * Loads rules from the project directory to customize how the agent + * should behave. Supports: + * - General rules (rules.md) + * - MCP-specific rules (rules/figma.md, rules/browser.md, etc.) + * - Tool-specific rules (rules/bash.md, rules/edit.md, etc.) + * - GitHub files (.github/copilot-instructions.md, .github/*.md) + */ + +export { loadRuleFile, loadRulesDirectory } from "@services/rules/load"; +export { + categorizeRule, + addRuleToProject, + RULE_TYPE_HANDLERS, +} from "@services/rules/categorize"; +export { formatRulesSection, formatRulesByType } from "@services/rules/format"; +export { + loadProjectRules, + buildSystemPromptWithRules, + getRulesForCategory, +} from "@services/rules/prompt"; +export type { + RuleFile, + RuleCategory, + RuleType, + ProjectRules, + RulesResult, + MCPCategory, + ToolCategory, +} from "@/types/rules"; diff --git a/src/services/rules/categorize.ts b/src/services/rules/categorize.ts new file mode 100644 index 0000000..2efae5c --- /dev/null +++ b/src/services/rules/categorize.ts @@ -0,0 +1,38 @@ +/** + * Rule categorization utilities + */ + +import { MCP_CATEGORIES, TOOL_CATEGORIES } from "@constants/rules"; +import type { RuleFile, RuleType, ProjectRules } from "@/types/rules"; + +const isMCPCategory = (category: string): boolean => + (MCP_CATEGORIES as readonly string[]).includes(category); + +const isToolCategory = (category: string): boolean => + (TOOL_CATEGORIES as readonly string[]).includes(category); + +export const categorizeRule = (rule: RuleFile): RuleType => { + if (isMCPCategory(rule.category)) { + return "mcp"; + } + if (isToolCategory(rule.category)) { + return "tool"; + } + return "custom"; +}; + +export const RULE_TYPE_HANDLERS: Record< + RuleType, + (rules: ProjectRules, rule: RuleFile) => void +> = { + mcp: (rules, rule) => rules.mcp.push(rule), + tool: (rules, rule) => rules.tools.push(rule), + custom: (rules, rule) => rules.custom.push(rule), +}; + +export const addRuleToProject = (rules: ProjectRules, rule: RuleFile): void => { + const type = categorizeRule(rule); + const handler = RULE_TYPE_HANDLERS[type]; + handler(rules, rule); + rules.allPaths.push(rule.path); +}; diff --git a/src/services/rules/format.ts b/src/services/rules/format.ts new file mode 100644 index 0000000..0b0079e --- /dev/null +++ b/src/services/rules/format.ts @@ -0,0 +1,35 @@ +/** + * Rule formatting utilities + */ + +import { RULES_SECTION_TITLES } from "@constants/rules"; +import type { RuleFile, RuleType } from "@/types/rules"; + +const capitalizeWord = (word: string): string => + word.charAt(0).toUpperCase() + word.slice(1); + +const formatCategoryTitle = (category: string): string => + category.split("-").map(capitalizeWord).join(" "); + +const formatSingleRule = (rule: RuleFile): string => { + const categoryTitle = formatCategoryTitle(rule.category); + return `### ${categoryTitle}\n\n${rule.content}`; +}; + +export const formatRulesSection = ( + title: string, + rules: RuleFile[], +): string => { + if (rules.length === 0) return ""; + + const sections = rules.map(formatSingleRule); + return `## ${title}\n\n${sections.join("\n\n")}`; +}; + +export const formatRulesByType = ( + type: RuleType, + rules: RuleFile[], +): string => { + const title = RULES_SECTION_TITLES[type]; + return formatRulesSection(title, rules); +}; diff --git a/src/services/rules/load.ts b/src/services/rules/load.ts new file mode 100644 index 0000000..9fe349d --- /dev/null +++ b/src/services/rules/load.ts @@ -0,0 +1,70 @@ +/** + * Rule file loading utilities + */ + +import { readFile, readdir } from "fs/promises"; +import { existsSync } from "fs"; +import { join, basename, extname } from "path"; + +import type { RuleFile, RuleCategory } from "@/types/rules"; + +export const loadRuleFile = async ( + filePath: string, + category: RuleCategory, +): Promise => { + if (!existsSync(filePath)) { + return null; + } + + try { + const content = await readFile(filePath, "utf-8"); + const trimmedContent = content.trim(); + + if (trimmedContent) { + return { + category, + content: trimmedContent, + path: filePath, + }; + } + } catch { + // Ignore read errors + } + + return null; +}; + +const isMarkdownFile = (filename: string): boolean => filename.endsWith(".md"); + +const getCategoryFromFilename = (filename: string): string => + basename(filename, extname(filename)).toLowerCase(); + +export const loadRulesDirectory = async ( + dirPath: string, +): Promise => { + if (!existsSync(dirPath)) { + return []; + } + + const rules: RuleFile[] = []; + + try { + const files = await readdir(dirPath); + + for (const file of files) { + if (!isMarkdownFile(file)) continue; + + const category = getCategoryFromFilename(file); + const filePath = join(dirPath, file); + const rule = await loadRuleFile(filePath, category); + + if (rule) { + rules.push(rule); + } + } + } catch { + // Ignore directory read errors + } + + return rules; +}; diff --git a/src/services/rules/prompt.ts b/src/services/rules/prompt.ts new file mode 100644 index 0000000..6c202d9 --- /dev/null +++ b/src/services/rules/prompt.ts @@ -0,0 +1,145 @@ +/** + * Rules prompt building utilities + */ + +import { join } from "path"; + +import { + GENERAL_RULES_PATHS, + RULES_DIRECTORIES, + RULES_PROMPT_TEMPLATES, +} from "@constants/rules"; +import { loadRuleFile, loadRulesDirectory } from "@services/rules/load"; +import { addRuleToProject } from "@services/rules/categorize"; +import { formatRulesByType } from "@services/rules/format"; +import type { + RuleFile, + RuleCategory, + ProjectRules, + RulesResult, +} from "@/types/rules"; + +const createEmptyProjectRules = (): ProjectRules => ({ + general: null, + mcp: [], + tools: [], + custom: [], + allPaths: [], +}); + +const loadGeneralRules = async ( + workingDir: string, + result: ProjectRules, +): Promise => { + for (const filename of GENERAL_RULES_PATHS) { + const rulePath = join(workingDir, filename); + const rule = await loadRuleFile(rulePath, "general"); + + if (rule) { + result.general = rule; + result.allPaths.push(rule.path); + break; + } + } +}; + +const processDirectoryRule = (rule: RuleFile, result: ProjectRules): void => { + if (rule.category === "general") { + if (!result.general) { + result.general = rule; + result.allPaths.push(rule.path); + } + return; + } + + addRuleToProject(result, rule); +}; + +const loadCategorizedRules = async ( + workingDir: string, + result: ProjectRules, +): Promise => { + for (const dirName of RULES_DIRECTORIES) { + const dirPath = join(workingDir, dirName); + const rules = await loadRulesDirectory(dirPath); + + for (const rule of rules) { + processDirectoryRule(rule, result); + } + } +}; + +export const loadProjectRules = async ( + workingDir: string = process.cwd(), +): Promise => { + const result = createEmptyProjectRules(); + + await loadGeneralRules(workingDir, result); + await loadCategorizedRules(workingDir, result); + + return result; +}; + +const hasNoRules = (rules: ProjectRules): boolean => + !rules.general && + rules.mcp.length === 0 && + rules.tools.length === 0 && + rules.custom.length === 0; + +const buildRulesSections = (rules: ProjectRules): string[] => { + const sections: string[] = []; + + if (rules.general) { + sections.push( + `${RULES_PROMPT_TEMPLATES.PROJECT_RULES_HEADER}${rules.general.content}`, + ); + } + + if (rules.mcp.length > 0) { + sections.push(formatRulesByType("mcp", rules.mcp)); + } + + if (rules.tools.length > 0) { + sections.push(formatRulesByType("tool", rules.tools)); + } + + if (rules.custom.length > 0) { + sections.push(formatRulesByType("custom", rules.custom)); + } + + return sections; +}; + +export const buildSystemPromptWithRules = async ( + basePrompt: string, + workingDir: string = process.cwd(), +): Promise => { + const rules = await loadProjectRules(workingDir); + + if (hasNoRules(rules)) { + return { prompt: basePrompt, rulesPath: null, rulesPaths: [] }; + } + + const sections = buildRulesSections(rules); + const promptWithRules = `${basePrompt}\n\n${sections.join("\n\n")}`; + + return { + prompt: promptWithRules, + rulesPath: rules.general?.path ?? rules.allPaths[0] ?? null, + rulesPaths: rules.allPaths, + }; +}; + +export const getRulesForCategory = async ( + category: RuleCategory, + workingDir: string = process.cwd(), +): Promise => { + const rules = await loadProjectRules(workingDir); + + if (category === "general") { + return rules.general; + } + + const allCategorized = [...rules.mcp, ...rules.tools, ...rules.custom]; + return allCategorized.find((r) => r.category === category) ?? null; +}; diff --git a/src/services/session.ts b/src/services/session.ts new file mode 100644 index 0000000..f9d344b --- /dev/null +++ b/src/services/session.ts @@ -0,0 +1,260 @@ +/** + * Session management for persisting chat history + */ + +import fs from "fs/promises"; +import path from "path"; +import type { ChatSession, ChatMessage, AgentType } from "@/types/index"; +import type { SessionInfo } from "@/types/session"; +import { DIRS } from "@constants/paths"; + +/** + * Current session state + */ +let currentSession: ChatSession | null = null; + +/** + * Generate unique session ID + */ +const generateId = (): string => + `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; + +/** + * Create a new session + */ +export const createSession = async (agent: AgentType): Promise => { + const session: ChatSession = { + id: generateId(), + agent, + messages: [], + contextFiles: [], + createdAt: Date.now(), + updatedAt: Date.now(), + }; + + currentSession = session; + await saveSession(session); + return session; +}; + +/** + * Load session by ID + */ +export const loadSession = async (id: string): Promise => { + try { + const sessionFile = path.join(DIRS.sessions, `${id}.json`); + const data = await fs.readFile(sessionFile, "utf-8"); + currentSession = JSON.parse(data); + return currentSession; + } catch { + return null; + } +}; + +/** + * Save session + */ +export const saveSession = async (session?: ChatSession): Promise => { + const sessionToSave = session ?? currentSession; + if (!sessionToSave) return; + + try { + await fs.mkdir(DIRS.sessions, { recursive: true }); + const sessionFile = path.join(DIRS.sessions, `${sessionToSave.id}.json`); + sessionToSave.updatedAt = Date.now(); + await fs.writeFile( + sessionFile, + JSON.stringify(sessionToSave, null, 2), + "utf-8", + ); + } catch (error) { + throw new Error(`Failed to save session: ${error}`); + } +}; + +/** + * Add message to current session + */ +export const addMessage = async ( + role: "user" | "assistant" | "system", + content: string, +): Promise => { + if (!currentSession) { + throw new Error("No active session"); + } + + const message: ChatMessage = { + role, + content, + timestamp: Date.now(), + }; + + currentSession.messages.push(message); + await saveSession(); +}; + +/** + * Add context file to current session + */ +export const addContextFile = async (filePath: string): Promise => { + if (!currentSession) { + throw new Error("No active session"); + } + + if (!currentSession.contextFiles.includes(filePath)) { + currentSession.contextFiles.push(filePath); + await saveSession(); + } +}; + +/** + * Remove context file from current session + */ +export const removeContextFile = async (filePath: string): Promise => { + if (!currentSession) { + throw new Error("No active session"); + } + + currentSession.contextFiles = currentSession.contextFiles.filter( + (f) => f !== filePath, + ); + await saveSession(); +}; + +/** + * Get current session + */ +export const getCurrentSession = (): ChatSession | null => currentSession; + +/** + * List all sessions + */ +export const listSessions = async (): Promise => { + try { + await fs.mkdir(DIRS.sessions, { recursive: true }); + const files = await fs.readdir(DIRS.sessions); + const sessions: ChatSession[] = []; + + for (const file of files) { + if (file.endsWith(".json")) { + const data = await fs.readFile(path.join(DIRS.sessions, file), "utf-8"); + sessions.push(JSON.parse(data)); + } + } + + return sessions.sort((a, b) => b.updatedAt - a.updatedAt); + } catch { + return []; + } +}; + +/** + * Delete session by ID + */ +export const deleteSession = async (id: string): Promise => { + try { + const sessionFile = path.join(DIRS.sessions, `${id}.json`); + await fs.unlink(sessionFile); + if (currentSession?.id === id) { + currentSession = null; + } + } catch (error) { + throw new Error(`Failed to delete session: ${error}`); + } +}; + +/** + * Clear all messages in current session + */ +export const clearMessages = async (): Promise => { + if (!currentSession) { + throw new Error("No active session"); + } + + currentSession.messages = []; + await saveSession(); +}; + +/** + * Get most recent session + */ +export const getMostRecentSession = async ( + workingDir?: string, +): Promise => { + const sessions = await listSessions(); + + if (sessions.length === 0) return null; + + if (workingDir) { + const filtered = sessions.filter( + (s) => + (s as ChatSession & { workingDirectory?: string }).workingDirectory === + workingDir, + ); + return filtered[0] ?? null; + } + + return sessions[0]; +}; + +/** + * Get session summaries for listing + */ +export const getSessionSummaries = async (): Promise => { + const sessions = await listSessions(); + + return sessions.map((session) => { + const lastUserMessage = [...session.messages] + .reverse() + .find((m) => m.role === "user"); + + return { + id: session.id, + agent: session.agent, + messageCount: session.messages.length, + lastMessage: lastUserMessage?.content?.slice(0, 100), + workingDirectory: (session as ChatSession & { workingDirectory?: string }) + .workingDirectory, + createdAt: session.createdAt, + updatedAt: session.updatedAt, + }; + }); +}; + +/** + * Find session by ID prefix or exact match + */ +export const findSession = async ( + idOrPrefix: string, +): Promise => { + const sessions = await listSessions(); + + // Exact match + let session = sessions.find((s) => s.id === idOrPrefix); + if (session) return session; + + // Prefix match + const matches = sessions.filter((s) => s.id.startsWith(idOrPrefix)); + if (matches.length === 1) return matches[0]; + if (matches.length > 1) { + throw new Error( + `Ambiguous session ID: ${matches.length} sessions match "${idOrPrefix}"`, + ); + } + + return null; +}; + +/** + * Set working directory for current session + */ +export const setWorkingDirectory = async (dir: string): Promise => { + if (!currentSession) return; + ( + currentSession as ChatSession & { workingDirectory?: string } + ).workingDirectory = dir; + await saveSession(); +}; + +// Re-export types +export type { SessionInfo } from "@/types/session"; diff --git a/src/services/upgrade.ts b/src/services/upgrade.ts new file mode 100644 index 0000000..8f558d5 --- /dev/null +++ b/src/services/upgrade.ts @@ -0,0 +1,323 @@ +/** + * Upgrade Service + * + * Handles self-update functionality for the CLI + */ + +import { exec } from "child_process"; +import { promisify } from "util"; +import chalk from "chalk"; +import appVersion from "@/version.json"; + +const execAsync = promisify(exec); + +const REPO_URL = "git@github.com:CarGDev/codetyper.cli.git"; +const REPO_API_URL = "https://api.github.com/repos/CarGDev/codetyper.cli"; + +interface VersionInfo { + current: string; + latest: string; + hasUpdate: boolean; +} + +interface UpgradeOptions { + check?: boolean; + version?: string; +} + +interface UpgradeResult { + success: boolean; + previousVersion: string; + newVersion: string; + message: string; +} + +/** + * Parse semantic version string + */ +const parseVersion = ( + version: string, +): { major: number; minor: number; patch: number } => { + const cleaned = version.replace(/^v/, ""); + const [major = 0, minor = 0, patch = 0] = cleaned.split(".").map(Number); + return { major, minor, patch }; +}; + +/** + * Compare two semantic versions + * Returns: 1 if a > b, -1 if a < b, 0 if equal + */ +const compareVersions = (a: string, b: string): number => { + const vA = parseVersion(a); + const vB = parseVersion(b); + + if (vA.major !== vB.major) return vA.major > vB.major ? 1 : -1; + if (vA.minor !== vB.minor) return vA.minor > vB.minor ? 1 : -1; + if (vA.patch !== vB.patch) return vA.patch > vB.patch ? 1 : -1; + return 0; +}; + +/** + * Get the current installed version + */ +export const getCurrentVersion = (): string => { + return appVersion.version; +}; + +interface GitHubRelease { + tag_name?: string; +} + +interface GitHubTag { + name?: string; +} + +/** + * Get the latest version from GitHub + */ +export const getLatestVersion = async (): Promise => { + try { + // Try to get latest release from GitHub API + const response = await fetch(`${REPO_API_URL}/releases/latest`, { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "codetyper-cli", + }, + }); + + if (response.ok) { + const data = (await response.json()) as GitHubRelease; + return data.tag_name?.replace(/^v/, "") || getCurrentVersion(); + } + + // Fallback: get latest tag + const tagsResponse = await fetch(`${REPO_API_URL}/tags`, { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "codetyper-cli", + }, + }); + + if (tagsResponse.ok) { + const tags = (await tagsResponse.json()) as GitHubTag[]; + if (tags.length > 0) { + return tags[0].name?.replace(/^v/, "") || getCurrentVersion(); + } + } + + // Fallback: get latest commit info + const commitsResponse = await fetch(`${REPO_API_URL}/commits/master`, { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "codetyper-cli", + }, + }); + + if (commitsResponse.ok) { + // Can't determine version from commits, return current + return getCurrentVersion(); + } + + return getCurrentVersion(); + } catch { + // If we can't reach GitHub, return current version + return getCurrentVersion(); + } +}; + +/** + * Check for available updates + */ +export const checkForUpdates = async (): Promise => { + const current = getCurrentVersion(); + const latest = await getLatestVersion(); + const hasUpdate = compareVersions(latest, current) > 0; + + return { current, latest, hasUpdate }; +}; + +/** + * Display spinner animation + */ +const createSpinner = ( + message: string, +): { stop: (success: boolean, finalMessage?: string) => void } => { + const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + let frameIndex = 0; + let isRunning = true; + + const interval = setInterval(() => { + if (isRunning) { + process.stdout.write(`\r${chalk.cyan(frames[frameIndex])} ${message}`); + frameIndex = (frameIndex + 1) % frames.length; + } + }, 80); + + return { + stop: (success: boolean, finalMessage?: string) => { + isRunning = false; + clearInterval(interval); + const icon = success ? chalk.green("✓") : chalk.red("✗"); + const msg = finalMessage || message; + process.stdout.write(`\r${icon} ${msg}\n`); + }, + }; +}; + +/** + * Backup current installation info for rollback + */ +const backupCurrentVersion = async (): Promise => { + return getCurrentVersion(); +}; + +/** + * Perform the upgrade + */ +export const performUpgrade = async ( + options: UpgradeOptions = {}, +): Promise => { + const previousVersion = getCurrentVersion(); + + // Check only mode + if (options.check) { + const versionInfo = await checkForUpdates(); + + if (versionInfo.hasUpdate) { + console.log( + chalk.yellow( + `\nUpdate available: ${versionInfo.current} → ${versionInfo.latest}`, + ), + ); + console.log(chalk.gray(`Run 'codetyper upgrade' to update\n`)); + } else { + console.log( + chalk.green( + `\nYou're on the latest version (${versionInfo.current})\n`, + ), + ); + } + + return { + success: true, + previousVersion, + newVersion: versionInfo.latest, + message: versionInfo.hasUpdate + ? "Update available" + : "Already up to date", + }; + } + + // Display current version + console.log(chalk.cyan(`\nCurrent version: ${previousVersion}`)); + + // Check for updates first + const spinner1 = createSpinner("Checking for updates..."); + const versionInfo = await checkForUpdates(); + spinner1.stop(true, "Checked for updates"); + + if (!options.version && !versionInfo.hasUpdate) { + console.log( + chalk.green(`\nAlready on the latest version (${versionInfo.current})\n`), + ); + return { + success: true, + previousVersion, + newVersion: versionInfo.current, + message: "Already up to date", + }; + } + + const targetVersion = options.version || versionInfo.latest; + console.log(chalk.cyan(`Target version: ${targetVersion}\n`)); + + // Backup current version info + const backupVersion = await backupCurrentVersion(); + + // Perform upgrade + const spinner2 = createSpinner("Downloading and installing update..."); + + try { + const installCommand = options.version + ? `npm install -g ${REPO_URL}#v${options.version}` + : `npm install -g ${REPO_URL}`; + + await execAsync(installCommand, { + timeout: 120000, // 2 minute timeout + }); + + spinner2.stop(true, "Update installed successfully"); + + // Verify new version + const newVersion = options.version || targetVersion; + + console.log( + chalk.green( + `\n✓ Successfully upgraded from ${previousVersion} to ${newVersion}`, + ), + ); + console.log( + chalk.gray( + "Restart your terminal or run 'codetyper --version' to verify\n", + ), + ); + + return { + success: true, + previousVersion, + newVersion, + message: `Upgraded from ${previousVersion} to ${newVersion}`, + }; + } catch (error) { + spinner2.stop(false, "Update failed"); + + // Attempt rollback + console.log(chalk.yellow("\nAttempting rollback...")); + const rollbackSpinner = createSpinner( + `Rolling back to ${backupVersion}...`, + ); + + try { + await execAsync(`npm install -g ${REPO_URL}#v${backupVersion}`, { + timeout: 120000, + }); + rollbackSpinner.stop(true, `Rolled back to ${backupVersion}`); + console.log( + chalk.yellow("Rollback successful. Previous version restored.\n"), + ); + } catch { + rollbackSpinner.stop(false, "Rollback failed"); + console.log( + chalk.red("Rollback failed. Manual intervention may be required."), + ); + console.log(chalk.gray(`Run: npm install -g ${REPO_URL}`)); + } + + const errorMessage = error instanceof Error ? error.message : String(error); + return { + success: false, + previousVersion, + newVersion: previousVersion, + message: `Upgrade failed: ${errorMessage}`, + }; + } +}; + +/** + * Display upgrade help + */ +export const displayUpgradeHelp = (): void => { + console.log(chalk.bold("\nCodeTyper Upgrade\n")); + console.log("Usage:"); + console.log( + chalk.gray(" codetyper upgrade # Update to latest version"), + ); + console.log( + chalk.gray(" codetyper upgrade --check # Check for updates only"), + ); + console.log( + chalk.gray( + " codetyper upgrade --version 1.0.0 # Install specific version\n", + ), + ); +}; diff --git a/src/stores/theme-store.ts b/src/stores/theme-store.ts new file mode 100644 index 0000000..2412faa --- /dev/null +++ b/src/stores/theme-store.ts @@ -0,0 +1,50 @@ +/** + * Theme Store + * + * Manages the current theme state for the TUI + */ + +import { createStore } from "zustand/vanilla"; +import type { Theme, ThemeColors } from "@/types/theme"; +import { THEMES, DEFAULT_THEME, getTheme } from "@constants/themes"; + +interface ThemeState { + currentTheme: string; + theme: Theme; + colors: ThemeColors; +} + +const store = createStore(() => ({ + currentTheme: DEFAULT_THEME, + theme: getTheme(DEFAULT_THEME), + colors: getTheme(DEFAULT_THEME).colors, +})); + +export const themeActions = { + setTheme: (themeName: string): void => { + const theme = getTheme(themeName); + store.setState({ + currentTheme: theme.name, + theme, + colors: theme.colors, + }); + }, + + getCurrentTheme: (): string => { + return store.getState().currentTheme; + }, + + getTheme: (): Theme => { + return store.getState().theme; + }, + + getColors: (): ThemeColors => { + return store.getState().colors; + }, + + getAvailableThemes: (): string[] => { + return Object.keys(THEMES); + }, + + subscribe: store.subscribe, +}; diff --git a/src/stores/todo-store.ts b/src/stores/todo-store.ts new file mode 100644 index 0000000..2922f2d --- /dev/null +++ b/src/stores/todo-store.ts @@ -0,0 +1,218 @@ +/** + * Todo Store - Manages agent-generated task plans + */ + +import { createStore } from "zustand/vanilla"; +import { v4 as uuidv4 } from "uuid"; +import type { TodoItem, TodoPlan, TodoStatus } from "@/types/todo"; + +interface TodoState { + currentPlan: TodoPlan | null; + history: TodoPlan[]; +} + +const store = createStore(() => ({ + currentPlan: null, + history: [], +})); + +const createPlan = ( + title: string, + items: Array<{ title: string; description?: string }>, +): string => { + const planId = uuidv4(); + const now = Date.now(); + + const todoItems: TodoItem[] = items.map((item, index) => ({ + id: uuidv4(), + title: item.title, + description: item.description, + status: index === 0 ? "in_progress" : "pending", + createdAt: now, + updatedAt: now, + })); + + const plan: TodoPlan = { + id: planId, + title, + items: todoItems, + createdAt: now, + updatedAt: now, + completed: false, + }; + + store.setState({ currentPlan: plan }); + return planId; +}; + +const clearPlan = (): void => { + const { currentPlan, history } = store.getState(); + if (currentPlan) { + store.setState({ + currentPlan: null, + history: [...history, { ...currentPlan, completed: false }], + }); + } +}; + +const completePlan = (): void => { + const { currentPlan, history } = store.getState(); + if (currentPlan) { + const completedPlan = { + ...currentPlan, + completed: true, + updatedAt: Date.now(), + }; + store.setState({ + currentPlan: null, + history: [...history, completedPlan], + }); + } +}; + +const addItem = (title: string, description?: string): string | null => { + const { currentPlan } = store.getState(); + if (!currentPlan) return null; + + const itemId = uuidv4(); + const now = Date.now(); + + const newItem: TodoItem = { + id: itemId, + title, + description, + status: "pending", + createdAt: now, + updatedAt: now, + }; + + store.setState({ + currentPlan: { + ...currentPlan, + items: [...currentPlan.items, newItem], + updatedAt: now, + }, + }); + + return itemId; +}; + +const updateItemStatus = (itemId: string, status: TodoStatus): void => { + const { currentPlan } = store.getState(); + if (!currentPlan) return; + + const now = Date.now(); + const updatedItems = currentPlan.items.map((item) => { + if (item.id === itemId) { + return { ...item, status, updatedAt: now }; + } + return item; + }); + + // If completing an item, start the next pending one + if (status === "completed") { + const nextPending = updatedItems.find((item) => item.status === "pending"); + if (nextPending) { + const idx = updatedItems.findIndex((item) => item.id === nextPending.id); + if (idx !== -1) { + updatedItems[idx] = { + ...updatedItems[idx], + status: "in_progress", + updatedAt: now, + }; + } + } + } + + store.setState({ + currentPlan: { + ...currentPlan, + items: updatedItems, + updatedAt: now, + }, + }); + + // Check if all items are completed + const allCompleted = updatedItems.every( + (item) => item.status === "completed" || item.status === "failed", + ); + if (allCompleted) { + completePlan(); + } +}; + +const updateItemTitle = (itemId: string, title: string): void => { + const { currentPlan } = store.getState(); + if (!currentPlan) return; + + const now = Date.now(); + store.setState({ + currentPlan: { + ...currentPlan, + items: currentPlan.items.map((item) => + item.id === itemId ? { ...item, title, updatedAt: now } : item, + ), + updatedAt: now, + }, + }); +}; + +const removeItem = (itemId: string): void => { + const { currentPlan } = store.getState(); + if (!currentPlan) return; + + store.setState({ + currentPlan: { + ...currentPlan, + items: currentPlan.items.filter((item) => item.id !== itemId), + updatedAt: Date.now(), + }, + }); +}; + +const hasPlan = (): boolean => { + return store.getState().currentPlan !== null; +}; + +const getCurrentItem = (): TodoItem | null => { + const { currentPlan } = store.getState(); + if (!currentPlan) return null; + return ( + currentPlan.items.find((item) => item.status === "in_progress") || null + ); +}; + +const getProgress = (): { + completed: number; + total: number; + percentage: number; +} => { + const { currentPlan } = store.getState(); + if (!currentPlan || currentPlan.items.length === 0) { + return { completed: 0, total: 0, percentage: 0 }; + } + + const completed = currentPlan.items.filter( + (item) => item.status === "completed", + ).length; + const total = currentPlan.items.length; + const percentage = Math.round((completed / total) * 100); + + return { completed, total, percentage }; +}; + +export const todoStore = { + createPlan, + clearPlan, + completePlan, + addItem, + updateItemStatus, + updateItemTitle, + removeItem, + hasPlan, + getCurrentItem, + getProgress, + getPlan: () => store.getState().currentPlan, + getHistory: () => store.getState().history, + subscribe: store.subscribe, +}; diff --git a/src/stores/usage-store.ts b/src/stores/usage-store.ts new file mode 100644 index 0000000..b3c193e --- /dev/null +++ b/src/stores/usage-store.ts @@ -0,0 +1,64 @@ +/** + * Usage tracking store + */ + +import { createStore } from "zustand/vanilla"; +import type { UsageStats, UsageEntry } from "@/types/usage"; + +interface UsageState extends UsageStats { + history: UsageEntry[]; +} + +const createInitialStats = (): UsageStats => ({ + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + requestCount: 0, + sessionStartTime: Date.now(), +}); + +const store = createStore(() => ({ + ...createInitialStats(), + history: [], +})); + +export const usageStore = { + addUsage: (entry: Omit) => { + const newEntry: UsageEntry = { + ...entry, + timestamp: Date.now(), + }; + + store.setState((state) => ({ + promptTokens: state.promptTokens + entry.promptTokens, + completionTokens: state.completionTokens + entry.completionTokens, + totalTokens: state.totalTokens + entry.totalTokens, + requestCount: state.requestCount + 1, + history: [...state.history, newEntry], + })); + }, + + reset: () => { + store.setState({ + ...createInitialStats(), + history: [], + }); + }, + + getStats: (): UsageStats => { + const state = store.getState(); + return { + promptTokens: state.promptTokens, + completionTokens: state.completionTokens, + totalTokens: state.totalTokens, + requestCount: state.requestCount, + sessionStartTime: state.sessionStartTime, + }; + }, + + getHistory: (): UsageEntry[] => { + return store.getState().history; + }, + + subscribe: store.subscribe, +}; diff --git a/src/tools/bash.ts b/src/tools/bash.ts new file mode 100644 index 0000000..98cb2d8 --- /dev/null +++ b/src/tools/bash.ts @@ -0,0 +1,21 @@ +/** + * Bash tool for executing shell commands + * + * Output is captured and returned in the result - NOT streamed to stdout. + * This allows the TUI to remain interactive during command execution. + */ + +export { bashParams, type BashParamsSchema } from "@tools/bash/params"; +export { + truncateOutput, + createOutputHandler, + updateRunningStatus, +} from "@tools/bash/output"; +export { + killProcess, + createTimeoutHandler, + createAbortHandler, + setupAbortListener, + removeAbortListener, +} from "@tools/bash/process"; +export { executeBash, bashTool } from "@tools/bash/execute"; diff --git a/src/tools/bash/execute.ts b/src/tools/bash/execute.ts new file mode 100644 index 0000000..4c4b380 --- /dev/null +++ b/src/tools/bash/execute.ts @@ -0,0 +1,188 @@ +/** + * Bash command execution + */ + +import { spawn } from "child_process"; + +import { + BASH_DEFAULTS, + BASH_MESSAGES, + BASH_DESCRIPTION, +} from "@constants/bash"; +import { promptPermission } from "@services/permissions"; +import { bashParams } from "@tools/bash/params"; +import { + truncateOutput, + createOutputHandler, + updateRunningStatus, +} from "@tools/bash/output"; +import { + createTimeoutHandler, + createAbortHandler, + setupAbortListener, + removeAbortListener, +} from "@tools/bash/process"; +import type { + ToolDefinition, + ToolContext, + ToolResult, + BashParams, +} from "@/types/tools"; + +const createDeniedResult = (description: string): ToolResult => ({ + success: false, + title: description, + output: "", + error: BASH_MESSAGES.PERMISSION_DENIED, +}); + +const createTimeoutResult = ( + description: string, + output: string, + timeout: number, + code: number | null, +): ToolResult => ({ + success: false, + title: description, + output: truncateOutput(output), + error: BASH_MESSAGES.TIMED_OUT(timeout), + metadata: { + exitCode: code, + timedOut: true, + }, +}); + +const createAbortedResult = ( + description: string, + output: string, + code: number | null, +): ToolResult => ({ + success: false, + title: description, + output: truncateOutput(output), + error: BASH_MESSAGES.ABORTED, + metadata: { + exitCode: code, + aborted: true, + }, +}); + +const createCompletedResult = ( + description: string, + output: string, + code: number | null, +): ToolResult => ({ + success: code === 0, + title: description, + output: truncateOutput(output), + error: code !== 0 ? BASH_MESSAGES.EXIT_CODE(code ?? -1) : undefined, + metadata: { + exitCode: code, + }, +}); + +const createErrorResult = ( + description: string, + output: string, + error: Error, +): ToolResult => ({ + success: false, + title: description, + output, + error: error.message, +}); + +const checkPermission = async ( + command: string, + description: string, + autoApprove: boolean, +): Promise => { + if (autoApprove) { + return true; + } + + const result = await promptPermission(command, description); + return result.allowed; +}; + +const executeCommand = ( + args: BashParams, + ctx: ToolContext, +): Promise => { + const { + command, + description, + workdir, + timeout = BASH_DEFAULTS.TIMEOUT, + } = args; + const cwd = workdir ?? ctx.workingDir; + + updateRunningStatus(ctx, description); + + return new Promise((resolve) => { + const proc = spawn(command, { + shell: process.env.SHELL ?? "/bin/bash", + cwd, + env: { ...process.env }, + stdio: ["ignore", "pipe", "pipe"], + }); + + const outputRef = { value: "" }; + const timedOutRef = { value: false }; + + const appendOutput = createOutputHandler(ctx, description, outputRef); + proc.stdout?.on("data", appendOutput); + proc.stderr?.on("data", appendOutput); + + const timeoutId = createTimeoutHandler(proc, timeout, timedOutRef); + const abortHandler = createAbortHandler(proc); + setupAbortListener(ctx, abortHandler); + + proc.on("close", (code) => { + clearTimeout(timeoutId); + removeAbortListener(ctx, abortHandler); + + if (timedOutRef.value) { + resolve( + createTimeoutResult(description, outputRef.value, timeout, code), + ); + } else if (ctx.abort.signal.aborted) { + resolve(createAbortedResult(description, outputRef.value, code)); + } else { + resolve(createCompletedResult(description, outputRef.value, code)); + } + }); + + proc.on("error", (error) => { + clearTimeout(timeoutId); + removeAbortListener(ctx, abortHandler); + resolve(createErrorResult(description, outputRef.value, error)); + }); + }); +}; + +export const executeBash = async ( + args: BashParams, + ctx: ToolContext, +): Promise => { + const { command, description } = args; + + const allowed = await checkPermission( + command, + description, + ctx.autoApprove ?? false, + ); + + if (!allowed) { + return createDeniedResult(description); + } + + return executeCommand(args, ctx); +}; + +export const bashTool: ToolDefinition = { + name: "bash", + description: BASH_DESCRIPTION, + parameters: bashParams, + execute: executeBash, +}; diff --git a/src/tools/bash/output.ts b/src/tools/bash/output.ts new file mode 100644 index 0000000..5075eac --- /dev/null +++ b/src/tools/bash/output.ts @@ -0,0 +1,39 @@ +/** + * Bash output handling utilities + */ + +import { BASH_DEFAULTS, BASH_MESSAGES } from "@constants/bash"; +import type { ToolContext } from "@/types/tools"; + +export const truncateOutput = (output: string): string => + output.length > BASH_DEFAULTS.MAX_OUTPUT_LENGTH + ? output.slice(0, BASH_DEFAULTS.MAX_OUTPUT_LENGTH) + BASH_MESSAGES.TRUNCATED + : output; + +export const createOutputHandler = ( + ctx: ToolContext, + description: string, + outputRef: { value: string }, +) => { + return (data: Buffer): void => { + const chunk = data.toString(); + outputRef.value += chunk; + + ctx.onMetadata?.({ + title: description, + status: "running", + output: truncateOutput(outputRef.value), + }); + }; +}; + +export const updateRunningStatus = ( + ctx: ToolContext, + description: string, +): void => { + ctx.onMetadata?.({ + title: description, + status: "running", + output: "", + }); +}; diff --git a/src/tools/bash/params.ts b/src/tools/bash/params.ts new file mode 100644 index 0000000..7e83a7a --- /dev/null +++ b/src/tools/bash/params.ts @@ -0,0 +1,24 @@ +/** + * Bash tool parameter schema + */ + +import { z } from "zod"; + +export const bashParams = z.object({ + command: z.string().describe("The bash command to execute"), + description: z + .string() + .describe("A brief description of what this command does"), + workdir: z + .string() + .optional() + .describe( + "Working directory for the command (defaults to current directory)", + ), + timeout: z + .number() + .optional() + .describe("Timeout in milliseconds (default: 120000)"), +}); + +export type BashParamsSchema = typeof bashParams; diff --git a/src/tools/bash/process.ts b/src/tools/bash/process.ts new file mode 100644 index 0000000..1f71e77 --- /dev/null +++ b/src/tools/bash/process.ts @@ -0,0 +1,40 @@ +/** + * Bash process management utilities + */ + +import type { ChildProcess } from "child_process"; + +import { BASH_DEFAULTS, BASH_SIGNALS } from "@constants/bash"; + +export const killProcess = (proc: ChildProcess): void => { + proc.kill(BASH_SIGNALS.TERMINATE); + setTimeout(() => proc.kill(BASH_SIGNALS.KILL), BASH_DEFAULTS.KILL_DELAY); +}; + +export const createTimeoutHandler = ( + proc: ChildProcess, + timeout: number, + timedOutRef: { value: boolean }, +): NodeJS.Timeout => + setTimeout(() => { + timedOutRef.value = true; + killProcess(proc); + }, timeout); + +export const createAbortHandler = (proc: ChildProcess) => (): void => { + killProcess(proc); +}; + +export const setupAbortListener = ( + ctx: { abort: AbortController }, + handler: () => void, +): void => { + ctx.abort.signal.addEventListener("abort", handler, { once: true }); +}; + +export const removeAbortListener = ( + ctx: { abort: AbortController }, + handler: () => void, +): void => { + ctx.abort.signal.removeEventListener("abort", handler); +}; diff --git a/src/tools/edit.ts b/src/tools/edit.ts new file mode 100644 index 0000000..36b4647 --- /dev/null +++ b/src/tools/edit.ts @@ -0,0 +1,11 @@ +/** + * Edit tool for modifying files + */ + +export { editParams, type EditParamsSchema } from "@tools/edit/params"; +export { + validateTextExists, + validateUniqueness, + countOccurrences, +} from "@tools/edit/validate"; +export { executeEdit, editTool } from "@tools/edit/execute"; diff --git a/src/tools/edit/execute.ts b/src/tools/edit/execute.ts new file mode 100644 index 0000000..f40a871 --- /dev/null +++ b/src/tools/edit/execute.ts @@ -0,0 +1,154 @@ +/** + * Edit tool execution + */ + +import fs from "fs/promises"; +import path from "path"; + +import { EDIT_MESSAGES, EDIT_TITLES, EDIT_DESCRIPTION } from "@constants/edit"; +import { isFileOpAllowed, promptFilePermission } from "@services/permissions"; +import { formatDiff, generateDiff } from "@utils/diff"; +import { editParams } from "@tools/edit/params"; +import { + validateTextExists, + validateUniqueness, + countOccurrences, +} from "@tools/edit/validate"; +import type { + ToolDefinition, + ToolContext, + ToolResult, + EditParams, +} from "@/types/tools"; + +const createDeniedResult = (relativePath: string): ToolResult => ({ + success: false, + title: EDIT_TITLES.CANCELLED(relativePath), + output: "", + error: EDIT_MESSAGES.PERMISSION_DENIED, +}); + +const createErrorResult = (relativePath: string, error: Error): ToolResult => ({ + success: false, + title: EDIT_TITLES.FAILED(relativePath), + output: "", + error: error.message, +}); + +const createSuccessResult = ( + relativePath: string, + fullPath: string, + diffOutput: string, + replacements: number, + additions: number, + deletions: number, +): ToolResult => ({ + success: true, + title: EDIT_TITLES.SUCCESS(relativePath), + output: diffOutput, + metadata: { + filepath: fullPath, + replacements, + additions, + deletions, + }, +}); + +const resolvePath = ( + filePath: string, + workingDir: string, +): { fullPath: string; relativePath: string } => { + const fullPath = path.isAbsolute(filePath) + ? filePath + : path.join(workingDir, filePath); + const relativePath = path.relative(workingDir, fullPath); + return { fullPath, relativePath }; +}; + +const checkPermission = async ( + fullPath: string, + relativePath: string, + autoApprove: boolean, +): Promise => { + if (autoApprove || isFileOpAllowed("Edit", fullPath)) { + return true; + } + + const { allowed } = await promptFilePermission( + "Edit", + fullPath, + `Edit file: ${relativePath}`, + ); + return allowed; +}; + +const applyEdit = ( + content: string, + oldString: string, + newString: string, + replaceAll: boolean, +): string => + replaceAll + ? content.split(oldString).join(newString) + : content.replace(oldString, newString); + +export const executeEdit = async ( + args: EditParams, + ctx: ToolContext, +): Promise => { + const { filePath, oldString, newString, replaceAll = false } = args; + const { fullPath, relativePath } = resolvePath(filePath, ctx.workingDir); + + try { + const content = await fs.readFile(fullPath, "utf-8"); + + const existsError = validateTextExists(content, oldString, relativePath); + if (existsError) return existsError; + + const uniqueError = validateUniqueness( + content, + oldString, + replaceAll, + relativePath, + ); + if (uniqueError) return uniqueError; + + const allowed = await checkPermission( + fullPath, + relativePath, + ctx.autoApprove ?? false, + ); + if (!allowed) return createDeniedResult(relativePath); + + ctx.onMetadata?.({ + title: EDIT_TITLES.EDITING(path.basename(filePath)), + status: "running", + }); + + const newContent = applyEdit(content, oldString, newString, replaceAll); + const diff = generateDiff(content, newContent); + const diffOutput = formatDiff(diff, relativePath); + + await fs.writeFile(fullPath, newContent, "utf-8"); + + const replacements = replaceAll ? countOccurrences(content, oldString) : 1; + + return createSuccessResult( + relativePath, + fullPath, + diffOutput, + replacements, + diff.additions, + diff.deletions, + ); + } catch (error) { + return createErrorResult(relativePath, error as Error); + } +}; + +export const editTool: ToolDefinition = { + name: "edit", + description: EDIT_DESCRIPTION, + parameters: editParams, + execute: executeEdit, +}; diff --git a/src/tools/edit/params.ts b/src/tools/edit/params.ts new file mode 100644 index 0000000..c7803a4 --- /dev/null +++ b/src/tools/edit/params.ts @@ -0,0 +1,17 @@ +/** + * Edit tool parameter schema + */ + +import { z } from "zod"; + +export const editParams = z.object({ + filePath: z.string().describe("The absolute path to the file to edit"), + oldString: z.string().describe("The exact text to find and replace"), + newString: z.string().describe("The text to replace it with"), + replaceAll: z + .boolean() + .optional() + .describe("Replace all occurrences (default: false)"), +}); + +export type EditParamsSchema = typeof editParams; diff --git a/src/tools/edit/validate.ts b/src/tools/edit/validate.ts new file mode 100644 index 0000000..dd31d79 --- /dev/null +++ b/src/tools/edit/validate.ts @@ -0,0 +1,47 @@ +/** + * Edit tool validation utilities + */ + +import { EDIT_MESSAGES, EDIT_TITLES } from "@constants/edit"; +import type { ToolResult } from "@/types/tools"; + +export const validateTextExists = ( + content: string, + oldString: string, + relativePath: string, +): ToolResult | null => { + if (!content.includes(oldString)) { + return { + success: false, + title: EDIT_TITLES.FAILED(relativePath), + output: "", + error: EDIT_MESSAGES.NOT_FOUND, + }; + } + return null; +}; + +export const validateUniqueness = ( + content: string, + oldString: string, + replaceAll: boolean, + relativePath: string, +): ToolResult | null => { + if (replaceAll) { + return null; + } + + const occurrences = content.split(oldString).length - 1; + if (occurrences > 1) { + return { + success: false, + title: EDIT_TITLES.FAILED(relativePath), + output: "", + error: EDIT_MESSAGES.MULTIPLE_OCCURRENCES(occurrences), + }; + } + return null; +}; + +export const countOccurrences = (content: string, search: string): number => + content.split(search).length - 1; diff --git a/src/tools/glob.ts b/src/tools/glob.ts new file mode 100644 index 0000000..22d339f --- /dev/null +++ b/src/tools/glob.ts @@ -0,0 +1,30 @@ +/** + * Glob tool - list files matching patterns (functional) + */ + +export { + executeGlob, + listFiles, + findByExtension, + findByName, + listDirectories, +} from "@tools/glob/execute"; + +import { + executeGlob, + listFiles, + findByExtension, + findByName, + listDirectories, +} from "@tools/glob/execute"; + +/** + * Glob tool object for backward compatibility + */ +export const globTool = { + execute: executeGlob, + list: listFiles, + findByExtension, + findByName, + listDirectories, +}; diff --git a/src/tools/glob/definition.ts b/src/tools/glob/definition.ts new file mode 100644 index 0000000..6d1db3e --- /dev/null +++ b/src/tools/glob/definition.ts @@ -0,0 +1,44 @@ +/** + * Glob Tool Definition - File pattern matching + */ + +import { z } from "zod"; +import { executeGlob } from "@tools/glob/execute"; +import type { ToolDefinition, ToolContext, ToolResult } from "@/types/tools"; + +export const globParams = z.object({ + pattern: z.string().describe("The glob pattern to match files against (e.g., '**/*.ts', 'src/**/*.tsx')"), + path: z + .string() + .optional() + .describe("The directory to search in. Defaults to current working directory."), +}); + +type GlobParams = z.infer; + +const executeGlobTool = async ( + args: GlobParams, + ctx: ToolContext, +): Promise => { + const result = await executeGlob(args.pattern, { + cwd: args.path || ctx.workingDir, + }); + + return { + success: result.success, + title: result.title, + output: result.output, + error: result.error, + }; +}; + +export const globToolDefinition: ToolDefinition = { + name: "glob", + description: `Fast file pattern matching tool that works with any codebase size. +- Supports glob patterns like "**/*.js" or "src/**/*.ts" +- Returns matching file paths sorted by modification time +- Use this when you need to find files by name patterns +- For searching file contents, use grep instead`, + parameters: globParams, + execute: executeGlobTool, +}; diff --git a/src/tools/glob/execute.ts b/src/tools/glob/execute.ts new file mode 100644 index 0000000..42cce37 --- /dev/null +++ b/src/tools/glob/execute.ts @@ -0,0 +1,107 @@ +/** + * Glob tool execution (functional) + */ + +import fg from "fast-glob"; + +import { + GLOB_DEFAULTS, + GLOB_IGNORE_PATTERNS, + GLOB_MESSAGES, +} from "@constants/glob"; +import type { GlobOptions, GlobResult } from "@/types/tools"; + +const createSuccessResult = (files: string[]): GlobResult => ({ + success: true, + title: "Glob", + output: files.join("\n"), + files, +}); + +const createErrorResult = (error: unknown): GlobResult => ({ + success: false, + title: "Glob failed", + output: "", + error: GLOB_MESSAGES.FAILED(error), +}); + +export const executeGlob = async ( + patterns: string | string[], + options?: GlobOptions, +): Promise => { + try { + const files = await fg(patterns, { + cwd: options?.cwd ?? process.cwd(), + ignore: [...GLOB_IGNORE_PATTERNS, ...(options?.ignore ?? [])], + onlyFiles: options?.onlyFiles ?? GLOB_DEFAULTS.ONLY_FILES, + onlyDirectories: + options?.onlyDirectories ?? GLOB_DEFAULTS.ONLY_DIRECTORIES, + dot: GLOB_DEFAULTS.DOT, + }); + + return createSuccessResult(files); + } catch (error) { + return createErrorResult(error); + } +}; + +export const listFiles = async ( + directory: string = ".", + options?: { + recursive?: boolean; + extensions?: string[]; + }, +): Promise => { + try { + const pattern = options?.recursive ? "**/*" : "*"; + const patterns = options?.extensions + ? options.extensions.map((ext) => `${pattern}.${ext}`) + : [pattern]; + + return executeGlob(patterns, { + cwd: directory, + onlyFiles: true, + }); + } catch (error) { + return { + success: false, + title: "List failed", + output: "", + error: GLOB_MESSAGES.LIST_FAILED(error), + }; + } +}; + +export const findByExtension = async ( + extension: string, + directory: string = ".", +): Promise => { + const result = await executeGlob(`**/*.${extension}`, { + cwd: directory, + onlyFiles: true, + }); + + return result.files ?? []; +}; + +export const findByName = async ( + name: string, + directory: string = ".", +): Promise => { + const result = await executeGlob(`**/${name}`, { + cwd: directory, + }); + + return result.files ?? []; +}; + +export const listDirectories = async ( + directory: string = ".", +): Promise => { + const result = await executeGlob("*", { + cwd: directory, + onlyDirectories: true, + }); + + return result.files ?? []; +}; diff --git a/src/tools/grep.ts b/src/tools/grep.ts new file mode 100644 index 0000000..471064f --- /dev/null +++ b/src/tools/grep.ts @@ -0,0 +1,17 @@ +/** + * Grep tool - search files for patterns (functional) + */ + +export { searchLines, formatMatches } from "@tools/grep/search"; +export { executeGrep, searchInFile, executeRipgrep } from "@tools/grep/execute"; + +import { executeGrep, searchInFile, executeRipgrep } from "@tools/grep/execute"; + +/** + * Grep tool object for backward compatibility + */ +export const grepTool = { + execute: executeGrep, + searchInFile, + ripgrep: executeRipgrep, +}; diff --git a/src/tools/grep/definition.ts b/src/tools/grep/definition.ts new file mode 100644 index 0000000..af64fd5 --- /dev/null +++ b/src/tools/grep/definition.ts @@ -0,0 +1,58 @@ +/** + * Grep Tool Definition - Search file contents + */ + +import { z } from "zod"; +import { executeRipgrep } from "@tools/grep/execute"; +import type { ToolDefinition, ToolContext, ToolResult } from "@/types/tools"; + +export const grepParams = z.object({ + pattern: z + .string() + .describe("The regular expression pattern to search for in file contents"), + path: z + .string() + .optional() + .describe("File or directory to search in. Defaults to current working directory."), + glob: z + .string() + .optional() + .describe("Glob pattern to filter files (e.g., '*.ts', '**/*.tsx')"), + case_insensitive: z + .boolean() + .optional() + .describe("Case insensitive search"), + context_lines: z + .number() + .optional() + .describe("Number of context lines to show before and after each match"), +}); + +type GrepParams = z.infer; + +const executeGrepTool = async ( + args: GrepParams, + ctx: ToolContext, +): Promise => { + // Execute ripgrep search + const directory = args.path || ctx.workingDir; + const result = await executeRipgrep(args.pattern, directory); + + return { + success: result.success, + title: result.title, + output: result.output, + error: result.error, + }; +}; + +export const grepToolDefinition: ToolDefinition = { + name: "grep", + description: `A powerful search tool built on ripgrep for searching file contents. +- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+") +- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") +- Use this when you need to find code patterns, function definitions, or specific text +- For finding files by name, use glob instead`, + parameters: grepParams, + execute: executeGrepTool, +}; diff --git a/src/tools/grep/execute.ts b/src/tools/grep/execute.ts new file mode 100644 index 0000000..8ba4413 --- /dev/null +++ b/src/tools/grep/execute.ts @@ -0,0 +1,117 @@ +/** + * Grep tool execution (functional) + */ + +import { exec } from "child_process"; +import { promisify } from "util"; +import fg from "fast-glob"; +import fs from "fs/promises"; + +import { + GREP_DEFAULTS, + GREP_IGNORE_PATTERNS, + GREP_MESSAGES, + GREP_COMMANDS, +} from "@constants/grep"; +import { searchLines, formatMatches } from "@tools/grep/search"; +import type { GrepMatch, GrepOptions, GrepResult } from "@/types/tools"; + +const execAsync = promisify(exec); + +const createSuccessResult = (output: string, files: string[]): GrepResult => ({ + success: true, + title: "Grep", + output: output || GREP_MESSAGES.NO_MATCHES, + files, +}); + +const createErrorResult = (error: unknown): GrepResult => ({ + success: false, + title: "Grep failed", + output: "", + error: GREP_MESSAGES.SEARCH_FAILED(error), +}); + +const searchFile = async ( + file: string, + pattern: string, + options?: GrepOptions, +): Promise => { + try { + const content = await fs.readFile(file, "utf-8"); + const lines = content.split("\n"); + return searchLines(lines, pattern, file, options); + } catch { + return []; + } +}; + +export const executeGrep = async ( + pattern: string, + files: string[] = [GREP_DEFAULTS.DEFAULT_PATTERN], + options?: GrepOptions, +): Promise => { + try { + const fileList = await fg(files, { + ignore: [...GREP_IGNORE_PATTERNS], + }); + + const matches: GrepMatch[] = []; + const maxResults = options?.maxResults ?? GREP_DEFAULTS.MAX_RESULTS; + + for (const file of fileList) { + if (matches.length >= maxResults) break; + + const fileMatches = await searchFile(file, pattern, options); + const remaining = maxResults - matches.length; + matches.push(...fileMatches.slice(0, remaining)); + } + + const output = formatMatches(matches); + const uniqueFiles = [...new Set(matches.map((m) => m.file))]; + + return createSuccessResult(output, uniqueFiles); + } catch (error) { + return createErrorResult(error); + } +}; + +export const searchInFile = async ( + filePath: string, + pattern: string, + options?: GrepOptions, +): Promise => searchFile(filePath, pattern, options); + +export const executeRipgrep = async ( + pattern: string, + directory: string = ".", +): Promise => { + try { + const { stdout } = await execAsync( + GREP_COMMANDS.RIPGREP(pattern, directory), + ); + + return { + success: true, + title: "Ripgrep", + output: stdout || GREP_MESSAGES.NO_MATCHES, + }; + } catch (error: unknown) { + const execError = error as { code?: number; message?: string }; + + if (execError.code === GREP_DEFAULTS.NO_MATCHES_EXIT_CODE) { + return { + success: true, + title: "Ripgrep", + output: GREP_MESSAGES.NO_MATCHES, + }; + } + + return { + success: false, + title: "Ripgrep failed", + output: "", + error: GREP_MESSAGES.RIPGREP_FAILED(execError.message ?? "Unknown error"), + }; + } +}; diff --git a/src/tools/grep/search.ts b/src/tools/grep/search.ts new file mode 100644 index 0000000..9211891 --- /dev/null +++ b/src/tools/grep/search.ts @@ -0,0 +1,52 @@ +/** + * Grep search utilities + */ + +import type { GrepMatch, GrepOptions } from "@/types/tools"; + +const normalizeForSearch = (text: string, ignoreCase: boolean): string => + ignoreCase ? text.toLowerCase() : text; + +const matchesPattern = ( + line: string, + pattern: string, + options?: GrepOptions, +): boolean => { + if (options?.regex) { + const regex = new RegExp(pattern, options.ignoreCase ? "i" : ""); + return regex.test(line); + } + + const searchPattern = normalizeForSearch( + pattern, + options?.ignoreCase ?? false, + ); + const searchLine = normalizeForSearch(line, options?.ignoreCase ?? false); + return searchLine.includes(searchPattern); +}; + +export const searchLines = ( + lines: string[], + pattern: string, + file: string, + options?: GrepOptions, +): GrepMatch[] => { + const matches: GrepMatch[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (matchesPattern(line, pattern, options)) { + matches.push({ + file, + line: i + 1, + content: line.trim(), + }); + } + } + + return matches; +}; + +export const formatMatches = (matches: GrepMatch[]): string => + matches.map((m) => `${m.file}:${m.line}: ${m.content}`).join("\n"); diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..53d4d8a --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,183 @@ +/** + * Tool registry - exports all available tools + */ + +export * from "@tools/types"; +export { bashTool } from "@tools/bash"; +export { readTool } from "@tools/read"; +export { writeTool } from "@tools/write"; +export { editTool } from "@tools/edit"; +export { todoWriteTool } from "@tools/todo-write"; +export { todoReadTool } from "@tools/todo-read"; +export { globToolDefinition } from "@tools/glob/definition"; +export { grepToolDefinition } from "@tools/grep/definition"; + +import type { ToolDefinition, FunctionDefinition } from "@tools/types"; +import { toolToFunction } from "@tools/types"; +import { bashTool } from "@tools/bash"; +import { readTool } from "@tools/read"; +import { writeTool } from "@tools/write"; +import { editTool } from "@tools/edit"; +import { todoWriteTool } from "@tools/todo-write"; +import { todoReadTool } from "@tools/todo-read"; +import { globToolDefinition } from "@tools/glob/definition"; +import { grepToolDefinition } from "@tools/grep/definition"; +import { + isMCPTool, + executeMCPTool, + getMCPToolsForApi, +} from "@services/mcp/tools"; +import { z } from "zod"; + +// All available tools +export const tools: ToolDefinition[] = [ + bashTool, + readTool, + writeTool, + editTool, + globToolDefinition, + grepToolDefinition, + todoWriteTool, + todoReadTool, +]; + +// Tools that are read-only (allowed in chat mode) +const READ_ONLY_TOOLS = new Set([ + "read", + "glob", + "grep", + "todo_read", +]); + +// Map of tools by name +export const toolMap: Map = new Map( + tools.map((t) => [t.name, t]), +); + +// Cached MCP tools +let mcpToolsCache: Awaited> | null = null; + +/** + * Get tool by name (including MCP tools) + */ +export function getTool(name: string): ToolDefinition | undefined { + // Check built-in tools first + const builtInTool = toolMap.get(name); + if (builtInTool) { + return builtInTool; + } + + // Check if it's an MCP tool + if (isMCPTool(name)) { + // Return a wrapper tool definition for MCP tools + return { + name, + description: `MCP tool: ${name}`, + parameters: z.object({}).passthrough(), + execute: async (args) => { + const result = await executeMCPTool( + name, + args as Record, + ); + return { + success: result.success, + title: name, + output: result.output, + error: result.error, + }; + }, + }; + } + + return undefined; +} + +// Get all tools as OpenAI function definitions +export function getToolFunctions(): FunctionDefinition[] { + return tools.map(toolToFunction); +} + +/** + * Filter tools based on chat mode (read-only vs full access) + */ +const filterToolsForMode = ( + toolList: ToolDefinition[], + chatMode: boolean, +): ToolDefinition[] => { + if (!chatMode) return toolList; + return toolList.filter((t) => READ_ONLY_TOOLS.has(t.name)); +}; + +/** + * Get tools as format expected by Copilot/OpenAI API + * This includes both built-in tools and MCP tools + * @param chatMode - If true, only return read-only tools (no file modifications) + */ +export async function getToolsForApiAsync( + chatMode = false, +): Promise< + { + type: "function"; + function: FunctionDefinition; + }[] +> { + const filteredTools = filterToolsForMode(tools, chatMode); + const builtInTools = filteredTools.map((t) => ({ + type: "function" as const, + function: toolToFunction(t), + })); + + // In chat mode, don't include MCP tools (they might modify files) + if (chatMode) { + return builtInTools; + } + + // Get MCP tools (uses cache if available) + try { + mcpToolsCache = await getMCPToolsForApi(); + return [...builtInTools, ...mcpToolsCache]; + } catch { + // If MCP tools fail to load, just return built-in tools + return builtInTools; + } +} + +/** + * Get tools synchronously (uses cached MCP tools if available) + * @param chatMode - If true, only return read-only tools (no file modifications) + */ +export function getToolsForApi( + chatMode = false, +): { + type: "function"; + function: FunctionDefinition; +}[] { + const filteredTools = filterToolsForMode(tools, chatMode); + const builtInTools = filteredTools.map((t) => ({ + type: "function" as const, + function: toolToFunction(t), + })); + + // In chat mode, don't include MCP tools + if (chatMode) { + return builtInTools; + } + + // Include cached MCP tools if available + if (mcpToolsCache) { + return [...builtInTools, ...mcpToolsCache]; + } + + return builtInTools; +} + +/** + * Refresh MCP tools cache + */ +export async function refreshMCPTools(): Promise { + try { + mcpToolsCache = await getMCPToolsForApi(); + } catch { + mcpToolsCache = null; + } +} diff --git a/src/tools/read.ts b/src/tools/read.ts new file mode 100644 index 0000000..1204cb0 --- /dev/null +++ b/src/tools/read.ts @@ -0,0 +1,12 @@ +/** + * Read tool for reading files + */ + +export { readParams, type ReadParamsSchema } from "@tools/read/params"; +export { + truncateLine, + formatLineWithNumber, + calculateLineSize, + processLines, +} from "@tools/read/format"; +export { executeRead, readTool } from "@tools/read/execute"; diff --git a/src/tools/read/execute.ts b/src/tools/read/execute.ts new file mode 100644 index 0000000..5fc74e3 --- /dev/null +++ b/src/tools/read/execute.ts @@ -0,0 +1,139 @@ +/** + * Read tool execution + */ + +import fs from "fs/promises"; +import path from "path"; + +import { + READ_DEFAULTS, + READ_MESSAGES, + READ_TITLES, + READ_DESCRIPTION, +} from "@constants/read"; +import { isFileOpAllowed, promptFilePermission } from "@services/permissions"; +import { readParams } from "@tools/read/params"; +import { processLines } from "@tools/read/format"; +import type { + ToolDefinition, + ToolContext, + ToolResult, + ReadParams, +} from "@/types/tools"; + +const createDeniedResult = (filePath: string): ToolResult => ({ + success: false, + title: READ_TITLES.DENIED(filePath), + output: "", + error: READ_MESSAGES.PERMISSION_DENIED, +}); + +const createErrorResult = (filePath: string, error: Error): ToolResult => ({ + success: false, + title: READ_TITLES.FAILED(filePath), + output: "", + error: error.message, +}); + +const createDirectoryResult = ( + filePath: string, + files: string[], +): ToolResult => ({ + success: true, + title: READ_TITLES.DIRECTORY(filePath), + output: "Directory contents:\n" + files.join("\n"), + metadata: { + isDirectory: true, + fileCount: files.length, + }, +}); + +const createFileResult = ( + filePath: string, + fullPath: string, + output: string, + totalLines: number, + linesRead: number, + truncated: boolean, + offset: number, +): ToolResult => ({ + success: true, + title: path.basename(filePath), + output, + metadata: { + filepath: fullPath, + totalLines, + linesRead, + truncated, + offset, + }, +}); + +const resolvePath = (filePath: string, workingDir: string): string => + path.isAbsolute(filePath) ? filePath : path.join(workingDir, filePath); + +const checkPermission = async ( + fullPath: string, + autoApprove: boolean, +): Promise => { + if (autoApprove || isFileOpAllowed("Read", fullPath)) { + return true; + } + + const { allowed } = await promptFilePermission("Read", fullPath); + return allowed; +}; + +const readDirectory = async (fullPath: string): Promise => + fs.readdir(fullPath); + +const readFileContent = async (fullPath: string): Promise => + fs.readFile(fullPath, "utf-8"); + +export const executeRead = async ( + args: ReadParams, + ctx: ToolContext, +): Promise => { + const { filePath, offset = 0, limit = READ_DEFAULTS.MAX_LINES } = args; + const fullPath = resolvePath(filePath, ctx.workingDir); + + const allowed = await checkPermission(fullPath, ctx.autoApprove ?? false); + if (!allowed) return createDeniedResult(filePath); + + ctx.onMetadata?.({ + title: READ_TITLES.READING(path.basename(filePath)), + status: "running", + }); + + try { + const stat = await fs.stat(fullPath); + + if (stat.isDirectory()) { + const files = await readDirectory(fullPath); + return createDirectoryResult(filePath, files); + } + + const content = await readFileContent(fullPath); + const lines = content.split("\n"); + const { output, truncated } = processLines(lines, offset, limit); + + return createFileResult( + filePath, + fullPath, + output.join("\n"), + lines.length, + output.length, + truncated, + offset, + ); + } catch (error) { + return createErrorResult(filePath, error as Error); + } +}; + +export const readTool: ToolDefinition = { + name: "read", + description: READ_DESCRIPTION, + parameters: readParams, + execute: executeRead, +}; diff --git a/src/tools/read/format.ts b/src/tools/read/format.ts new file mode 100644 index 0000000..14d2501 --- /dev/null +++ b/src/tools/read/format.ts @@ -0,0 +1,45 @@ +/** + * Read tool formatting utilities + */ + +import { READ_DEFAULTS } from "@constants/read"; + +export const truncateLine = (line: string): string => + line.length > READ_DEFAULTS.MAX_LINE_LENGTH + ? line.substring(0, READ_DEFAULTS.MAX_LINE_LENGTH) + "..." + : line; + +export const formatLineWithNumber = ( + line: string, + lineNumber: number, +): string => + String(lineNumber).padStart(READ_DEFAULTS.LINE_NUMBER_PAD, " ") + "\t" + line; + +export const calculateLineSize = (line: string): number => + Buffer.byteLength(line, "utf-8") + 1; + +export const processLines = ( + lines: string[], + offset: number, + limit: number, +): { output: string[]; truncated: boolean } => { + const result: string[] = []; + let bytes = 0; + const maxLine = Math.min(lines.length, offset + limit); + + for (let i = offset; i < maxLine; i++) { + const truncatedLine = truncateLine(lines[i]); + const lineWithNumber = formatLineWithNumber(truncatedLine, i + 1); + const size = calculateLineSize(lineWithNumber); + + if (bytes + size > READ_DEFAULTS.MAX_BYTES) break; + + result.push(lineWithNumber); + bytes += size; + } + + return { + output: result, + truncated: result.length < lines.length - offset, + }; +}; diff --git a/src/tools/read/params.ts b/src/tools/read/params.ts new file mode 100644 index 0000000..bc3683a --- /dev/null +++ b/src/tools/read/params.ts @@ -0,0 +1,23 @@ +/** + * Read tool parameter schema + */ + +import { z } from "zod"; + +import { READ_DEFAULTS } from "@constants/read"; + +export const readParams = z.object({ + filePath: z.string().describe("The absolute path to the file to read"), + offset: z + .number() + .optional() + .describe("Line number to start reading from (0-indexed)"), + limit: z + .number() + .optional() + .describe( + `Maximum number of lines to read (default: ${READ_DEFAULTS.MAX_LINES})`, + ), +}); + +export type ReadParamsSchema = typeof readParams; diff --git a/src/tools/schema/clean.ts b/src/tools/schema/clean.ts new file mode 100644 index 0000000..f40d6ea --- /dev/null +++ b/src/tools/schema/clean.ts @@ -0,0 +1,42 @@ +/** + * JSON Schema cleaning utilities for OpenAI/Copilot API compatibility + */ + +import { + SCHEMA_SKIP_KEYS, + SCHEMA_SKIP_VALUES, + type SchemaSkipKey, +} from "@constants/tools"; + +const shouldSkipKey = (key: string): boolean => + SCHEMA_SKIP_KEYS.includes(key as SchemaSkipKey); + +const shouldSkipValue = (key: string, value: unknown): boolean => + SCHEMA_SKIP_VALUES[key] === value; + +const isNestedObject = (value: unknown): value is Record => + value !== null && typeof value === "object" && !Array.isArray(value); + +export const cleanJsonSchema = ( + schema: Record, +): Record => { + const result: Record = {}; + + for (const [key, value] of Object.entries(schema)) { + if (shouldSkipKey(key)) { + continue; + } + + if (shouldSkipValue(key, value)) { + continue; + } + + if (isNestedObject(value)) { + result[key] = cleanJsonSchema(value); + } else { + result[key] = value; + } + } + + return result; +}; diff --git a/src/tools/schema/convert.ts b/src/tools/schema/convert.ts new file mode 100644 index 0000000..d4698c7 --- /dev/null +++ b/src/tools/schema/convert.ts @@ -0,0 +1,22 @@ +/** + * Tool to function conversion utilities + */ + +import { cleanJsonSchema } from "@tools/schema/clean"; +import type { ToolDefinition, FunctionDefinition } from "@/types/tools"; + +export const toolToFunction = (tool: ToolDefinition): FunctionDefinition => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const jsonSchema = (tool.parameters as any).toJSONSchema() as Record< + string, + unknown + >; + + const cleanSchema = cleanJsonSchema(jsonSchema); + + return { + name: tool.name, + description: tool.description, + parameters: cleanSchema as FunctionDefinition["parameters"], + }; +}; diff --git a/src/tools/todo-read.ts b/src/tools/todo-read.ts new file mode 100644 index 0000000..084f808 --- /dev/null +++ b/src/tools/todo-read.ts @@ -0,0 +1,64 @@ +/** + * TodoRead Tool - Allows agent to read current task list + * + * The agent calls this tool to see current progress and pending tasks. + */ + +import { z } from "zod"; +import { todoStore } from "@stores/todo-store"; +import type { ToolDefinition } from "@tools/types"; + +const parametersSchema = z.object({}); + +export const todoReadTool: ToolDefinition = { + name: "todoread", + description: `Read the current todo list to see task progress. + +Use this tool to: +- Check what tasks are pending +- See which task is currently in progress +- Review completed tasks +- Plan next steps based on remaining work + +Returns the complete todo list with status for each item.`, + parameters: parametersSchema, + execute: async () => { + const plan = todoStore.getPlan(); + + if (!plan || plan.items.length === 0) { + return { + success: true, + title: "No todos", + output: "No tasks in the todo list. Use todowrite to create tasks.", + }; + } + + const progress = todoStore.getProgress(); + + const items = plan.items.map((item) => ({ + id: item.id, + title: item.title, + status: item.status, + })); + + const summary = items + .map((item) => { + const icon = + item.status === "completed" + ? "✓" + : item.status === "in_progress" + ? "→" + : item.status === "failed" + ? "✗" + : "○"; + return `${icon} [${item.id}] ${item.title} (${item.status})`; + }) + .join("\n"); + + return { + success: true, + title: `Todos: ${progress.completed}/${progress.total}`, + output: `Progress: ${progress.completed}/${progress.total} (${progress.percentage}%)\n\n${summary}\n\nTodos JSON:\n${JSON.stringify(items, null, 2)}`, + }; + }, +}; diff --git a/src/tools/todo-write.ts b/src/tools/todo-write.ts new file mode 100644 index 0000000..0ecf0a3 --- /dev/null +++ b/src/tools/todo-write.ts @@ -0,0 +1,134 @@ +/** + * TodoWrite Tool - Allows agent to create and update task lists + * + * The agent calls this tool to track progress through multi-step tasks. + */ + +import { z } from "zod"; +import { todoStore } from "@stores/todo-store"; +import type { ToolDefinition } from "@tools/types"; +import type { TodoStatus } from "@/types/todo"; + +const TodoItemSchema = z.object({ + id: z.string().describe("Unique identifier for the todo item"), + title: z.string().describe("Brief description of the task"), + status: z + .enum(["pending", "in_progress", "completed", "failed"]) + .describe("Current status of the task"), +}); + +const parametersSchema = z.object({ + todos: z + .array(TodoItemSchema) + .describe( + "Complete list of todo items. Include all items, not just changes.", + ), +}); + +type TodoWriteParams = z.infer; + +export const todoWriteTool: ToolDefinition = { + name: "todowrite", + description: `Update the todo list to track progress through multi-step tasks. + +Use this tool to: +- Create a task list when starting complex work +- Update task status as you complete each step +- Add new tasks discovered during work +- Mark tasks as completed or failed + +Always include the COMPLETE todo list, not just changes. The list will replace the current todos. + +Example: +{ + "todos": [ + { "id": "1", "title": "Read the source file", "status": "completed" }, + { "id": "2", "title": "Identify the bug", "status": "in_progress" }, + { "id": "3", "title": "Apply the fix", "status": "pending" }, + { "id": "4", "title": "Verify the build", "status": "pending" } + ] +}`, + parameters: parametersSchema, + execute: async (args: TodoWriteParams) => { + const { todos } = args; + + // Check if we have an existing plan or need to create one + const existingPlan = todoStore.getPlan(); + + if (!existingPlan) { + // Create new plan from todos + const tasks = todos.map((t) => ({ + title: t.title, + description: undefined, + })); + + if (tasks.length > 0) { + todoStore.createPlan("Task Plan", tasks); + + // Update statuses after creation + const plan = todoStore.getPlan(); + if (plan) { + todos.forEach((todo, index) => { + if (plan.items[index]) { + todoStore.updateItemStatus( + plan.items[index].id, + todo.status as TodoStatus, + ); + } + }); + } + } + } else { + // Update existing plan - sync with provided todos + const currentItems = existingPlan.items; + + // Update existing items + todos.forEach((todo) => { + const existing = currentItems.find( + (item) => item.id === todo.id || item.title === todo.title, + ); + if (existing) { + todoStore.updateItemStatus(existing.id, todo.status as TodoStatus); + } else { + // Add new item + const newId = todoStore.addItem(todo.title); + if (newId) { + todoStore.updateItemStatus(newId, todo.status as TodoStatus); + } + } + }); + } + + // Get final state + const plan = todoStore.getPlan(); + const progress = todoStore.getProgress(); + + if (!plan) { + return { + success: true, + title: "Todos cleared", + output: "Todo list is now empty.", + }; + } + + const summary = plan.items + .map((item) => { + const icon = + item.status === "completed" + ? "✓" + : item.status === "in_progress" + ? "→" + : item.status === "failed" + ? "✗" + : "○"; + return `${icon} ${item.title}`; + }) + .join("\n"); + + return { + success: true, + title: "Todos updated", + output: `Progress: ${progress.completed}/${progress.total} (${progress.percentage}%)\n\n${summary}`, + }; + }, +}; diff --git a/src/tools/types.ts b/src/tools/types.ts new file mode 100644 index 0000000..7b511af --- /dev/null +++ b/src/tools/types.ts @@ -0,0 +1,32 @@ +/** + * Tool system types and utilities + */ + +export type { + ToolContext, + ToolMetadata, + ToolResult, + ToolDefinition, + ToolCall, + ToolCallResult, + FunctionDefinition, + ToolStatus, + BashParams, + BashResultMetadata, + EditParams, + EditResultMetadata, + GlobOptions, + GlobResult, + GrepMatch, + GrepOptions, + GrepResult, + ReadParams, + ReadResultMetadata, + WriteParams, + WriteResultMetadata, + ViewResult, + FileStat, +} from "@/types/tools"; + +export { cleanJsonSchema } from "@tools/schema/clean"; +export { toolToFunction } from "@tools/schema/convert"; diff --git a/src/tools/view.ts b/src/tools/view.ts new file mode 100644 index 0000000..27385c2 --- /dev/null +++ b/src/tools/view.ts @@ -0,0 +1,16 @@ +/** + * File viewing tool - read file contents (functional) + */ + +export { executeView, fileExists, getFileStat } from "@tools/view/execute"; + +import { executeView, fileExists, getFileStat } from "@tools/view/execute"; + +/** + * View tool object for backward compatibility + */ +export const viewTool = { + execute: executeView, + exists: fileExists, + stat: getFileStat, +}; diff --git a/src/tools/view/execute.ts b/src/tools/view/execute.ts new file mode 100644 index 0000000..68f79a1 --- /dev/null +++ b/src/tools/view/execute.ts @@ -0,0 +1,76 @@ +/** + * View tool execution (functional) + */ + +import fs from "fs/promises"; +import path from "path"; + +import { VIEW_MESSAGES, VIEW_DEFAULTS } from "@constants/view"; +import type { ViewResult, FileStat } from "@/types/tools"; + +const createSuccessResult = (output: string): ViewResult => ({ + success: true, + title: "View", + output, +}); + +const createErrorResult = (error: unknown): ViewResult => ({ + success: false, + title: "View failed", + output: "", + error: VIEW_MESSAGES.FAILED(error), +}); + +const extractLines = ( + content: string, + startLine?: number, + endLine?: number, +): string => { + if (startLine === undefined && endLine === undefined) { + return content; + } + + const lines = content.split("\n"); + const start = (startLine ?? VIEW_DEFAULTS.START_LINE) - 1; + const end = endLine ?? lines.length; + return lines.slice(start, end).join("\n"); +}; + +export const executeView = async ( + filePath: string, + startLine?: number, + endLine?: number, +): Promise => { + try { + const absolutePath = path.resolve(filePath); + const content = await fs.readFile(absolutePath, "utf-8"); + const output = extractLines(content, startLine, endLine); + + return createSuccessResult(output); + } catch (error) { + return createErrorResult(error); + } +}; + +export const fileExists = async (filePath: string): Promise => { + try { + await fs.access(path.resolve(filePath)); + return true; + } catch { + return false; + } +}; + +export const getFileStat = async ( + filePath: string, +): Promise => { + try { + const stats = await fs.stat(path.resolve(filePath)); + return { + size: stats.size, + modified: stats.mtime, + }; + } catch { + return null; + } +}; diff --git a/src/tools/write.ts b/src/tools/write.ts new file mode 100644 index 0000000..23aabb8 --- /dev/null +++ b/src/tools/write.ts @@ -0,0 +1,6 @@ +/** + * Write tool for creating/overwriting files + */ + +export { writeParams, type WriteParamsSchema } from "@tools/write/params"; +export { executeWrite, writeTool } from "@tools/write/execute"; diff --git a/src/tools/write/execute.ts b/src/tools/write/execute.ts new file mode 100644 index 0000000..538912b --- /dev/null +++ b/src/tools/write/execute.ts @@ -0,0 +1,170 @@ +/** + * Write tool execution + */ + +import fs from "fs/promises"; +import path from "path"; + +import { + WRITE_MESSAGES, + WRITE_TITLES, + WRITE_DESCRIPTION, +} from "@constants/write"; +import { isFileOpAllowed, promptFilePermission } from "@services/permissions"; +import { formatDiff, generateDiff } from "@utils/diff"; +import { writeParams } from "@tools/write/params"; +import type { + ToolDefinition, + ToolContext, + ToolResult, + WriteParams, +} from "@/types/tools"; + +const createDeniedResult = (relativePath: string): ToolResult => ({ + success: false, + title: WRITE_TITLES.CANCELLED(relativePath), + output: "", + error: WRITE_MESSAGES.PERMISSION_DENIED, +}); + +const createErrorResult = (relativePath: string, error: Error): ToolResult => ({ + success: false, + title: WRITE_TITLES.FAILED(relativePath), + output: "", + error: error.message, +}); + +const createSuccessResult = ( + relativePath: string, + fullPath: string, + diffOutput: string, + exists: boolean, + content: string, + additions: number, + deletions: number, +): ToolResult => ({ + success: true, + title: exists + ? WRITE_TITLES.OVERWROTE(relativePath) + : WRITE_TITLES.CREATED(relativePath), + output: diffOutput, + metadata: { + filepath: fullPath, + exists, + bytes: Buffer.byteLength(content, "utf-8"), + lines: content.split("\n").length, + additions, + deletions, + }, +}); + +const resolvePaths = ( + filePath: string, + workingDir: string, +): { fullPath: string; relativePath: string } => { + const fullPath = path.isAbsolute(filePath) + ? filePath + : path.join(workingDir, filePath); + const relativePath = path.relative(workingDir, fullPath); + return { fullPath, relativePath }; +}; + +const readExistingContent = async ( + fullPath: string, +): Promise<{ exists: boolean; content: string }> => { + try { + const content = await fs.readFile(fullPath, "utf-8"); + return { exists: true, content }; + } catch { + return { exists: false, content: "" }; + } +}; + +const getPermissionDescription = ( + exists: boolean, + relativePath: string, +): string => + exists + ? WRITE_TITLES.OVERWRITE_DESC(relativePath) + : WRITE_TITLES.CREATE_DESC(relativePath); + +const checkPermission = async ( + fullPath: string, + relativePath: string, + exists: boolean, + autoApprove: boolean, +): Promise => { + if (autoApprove || isFileOpAllowed("Write", fullPath)) { + return true; + } + + const description = getPermissionDescription(exists, relativePath); + const { allowed } = await promptFilePermission( + "Write", + fullPath, + description, + ); + return allowed; +}; + +const ensureDirectory = async (fullPath: string): Promise => { + await fs.mkdir(path.dirname(fullPath), { recursive: true }); +}; + +const writeContent = async ( + fullPath: string, + content: string, +): Promise => { + await fs.writeFile(fullPath, content, "utf-8"); +}; + +export const executeWrite = async ( + args: WriteParams, + ctx: ToolContext, +): Promise => { + const { filePath, content } = args; + const { fullPath, relativePath } = resolvePaths(filePath, ctx.workingDir); + + const { exists, content: oldContent } = await readExistingContent(fullPath); + + const allowed = await checkPermission( + fullPath, + relativePath, + exists, + ctx.autoApprove ?? false, + ); + if (!allowed) return createDeniedResult(relativePath); + + ctx.onMetadata?.({ + title: WRITE_TITLES.WRITING(path.basename(filePath)), + status: "running", + }); + + try { + await ensureDirectory(fullPath); + + const diff = generateDiff(oldContent, content); + const diffOutput = formatDiff(diff, relativePath); + + await writeContent(fullPath, content); + + return createSuccessResult( + relativePath, + fullPath, + diffOutput, + exists, + content, + diff.additions, + diff.deletions, + ); + } catch (error) { + return createErrorResult(relativePath, error as Error); + } +}; + +export const writeTool: ToolDefinition = { + name: "write", + description: WRITE_DESCRIPTION, + parameters: writeParams, + execute: executeWrite, +}; diff --git a/src/tools/write/params.ts b/src/tools/write/params.ts new file mode 100644 index 0000000..5cc0a3a --- /dev/null +++ b/src/tools/write/params.ts @@ -0,0 +1,12 @@ +/** + * Write tool parameter schema + */ + +import { z } from "zod"; + +export const writeParams = z.object({ + filePath: z.string().describe("The absolute path to the file to write"), + content: z.string().describe("The content to write to the file"), +}); + +export type WriteParamsSchema = typeof writeParams; diff --git a/src/tui-solid/app.tsx b/src/tui-solid/app.tsx new file mode 100644 index 0000000..eeea8fd --- /dev/null +++ b/src/tui-solid/app.tsx @@ -0,0 +1,439 @@ +import { render, useKeyboard } from "@opentui/solid"; +import { TextAttributes } from "@opentui/core"; +import { + ErrorBoundary, + Match, + Switch, + createSignal, + createEffect, +} from "solid-js"; +import { batch } from "solid-js"; +import { getFiles } from "@services/file-picker/files"; +import versionData from "@/version.json"; +import { + ExitProvider, + useExit, + RouteProvider, + useRoute, + AppStoreProvider, + useAppStore, + setAppStoreRef, + ThemeProvider, + useTheme, + KeybindProvider, + DialogProvider, +} from "@tui-solid/context"; +import { ToastProvider, Toast, useToast } from "@tui-solid/ui/toast"; +import { Home } from "@tui-solid/routes/home"; +import { Session } from "@tui-solid/routes/session"; +import type { TuiInput, TuiOutput } from "@tui-solid/types"; +import type { PermissionScope, LearningScope } from "@/types/tui"; +import type { MCPAddFormData } from "@/types/mcp"; + +interface AgentOption { + id: string; + name: string; + description?: string; +} + +interface MCPServer { + id: string; + name: string; + status: "connected" | "disconnected" | "error"; + description?: string; +} + +interface AppProps extends TuiInput { + onExit: (output: TuiOutput) => void; + onSubmit: (input: string) => Promise; + onCommand: (command: string) => Promise; + onModelSelect: (model: string) => Promise; + onThemeSelect: (theme: string) => void; + onAgentSelect?: (agentId: string) => Promise; + onMCPSelect?: (serverId: string) => Promise; + onMCPAdd?: (data: MCPAddFormData) => Promise; + onFileSelect?: (file: string) => void; + onProviderSelect?: (providerId: string) => Promise; + onCascadeToggle?: (enabled: boolean) => Promise; + onPermissionResponse: (allowed: boolean, scope?: PermissionScope) => void; + onLearningResponse: ( + save: boolean, + scope?: LearningScope, + editedContent?: string, + ) => void; + plan?: { + id: string; + title: string; + items: Array<{ id: string; text: string; completed: boolean }>; + } | null; + agents?: AgentOption[]; + currentAgent?: string; + mcpServers?: MCPServer[]; + files?: string[]; +} + +function ErrorFallback(props: { error: Error }) { + const theme = useTheme(); + + return ( + + + Application Error + + + {props.error.message} + + + Press Ctrl+C to exit + + + ); +} + +function AppContent(props: AppProps) { + const route = useRoute(); + const app = useAppStore(); + const exit = useExit(); + const toast = useToast(); + const theme = useTheme(); + const [fileList, setFileList] = createSignal([]); + + setAppStoreRef(app); + + // Load files when file_picker mode is activated + createEffect(() => { + if (app.mode() === "file_picker") { + const cwd = process.cwd(); + const entries = getFiles(cwd, cwd); + const paths = entries.map((e) => e.relativePath); + setFileList(paths); + } + }); + + // Initialize version from version.json + app.setVersion(versionData.version); + + // Initialize theme from props (from config) + if (props.theme) { + theme.setTheme(props.theme); + } + + // Initialize provider and model from props (from config) + if (props.provider) { + app.setSessionInfo( + props.sessionId ?? "", + props.provider, + props.model ?? "", + ); + } + + // Initialize cascade setting from props (from config) + if (props.cascadeEnabled !== undefined) { + app.setCascadeEnabled(props.cascadeEnabled); + } + + // Navigate to session if resuming + if (props.sessionId) { + route.goToSession(props.sessionId); + } + + if (props.availableModels && props.availableModels.length > 0) { + app.setAvailableModels(props.availableModels); + } + + // Handle initial prompt after store is initialized + if (props.initialPrompt && props.initialPrompt.trim()) { + setTimeout(async () => { + app.addLog({ type: "user", content: props.initialPrompt! }); + app.setMode("thinking"); + await props.onSubmit(props.initialPrompt!); + }, 100); + } + + useKeyboard((evt) => { + if (evt.ctrl && evt.name === "c") { + if (app.interruptPending()) { + exit.exit(0); + } else { + app.setInterruptPending(true); + toast.warning("Press Ctrl+C again to exit"); + setTimeout(() => { + app.setInterruptPending(false); + }, 2000); + } + evt.preventDefault(); + return; + } + + if (evt.name === "/" && app.mode() === "idle" && !app.inputBuffer()) { + app.openCommandMenu(); + evt.preventDefault(); + return; + } + }); + + const handleSubmit = async (input: string): Promise => { + if (!input.trim()) return; + + if (route.isHome()) { + const sessionId = `session-${Date.now()}`; + batch(() => { + app.setSessionInfo(sessionId, app.provider(), app.model()); + route.goToSession(sessionId); + }); + } + + app.addLog({ type: "user", content: input }); + app.clearInput(); + app.setMode("thinking"); + + try { + await props.onSubmit(input); + } finally { + app.setMode("idle"); + } + }; + + const handleCommand = async (command: string): Promise => { + // Start a session if on home page for commands that produce output + if (route.isHome()) { + const sessionId = `session-${Date.now()}`; + batch(() => { + app.setSessionInfo(sessionId, app.provider(), app.model()); + route.goToSession(sessionId); + }); + } + + try { + await props.onCommand(command); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : String(err)); + } + }; + + const handleModelSelect = async (model: string): Promise => { + // Start a session if on home page + if (route.isHome()) { + const sessionId = `session-${Date.now()}`; + batch(() => { + app.setSessionInfo(sessionId, app.provider(), app.model()); + route.goToSession(sessionId); + }); + } + + app.setMode("idle"); + try { + await props.onModelSelect(model); + app.setModel(model); + toast.success(`Model changed to ${model}`); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : String(err)); + } + }; + + const handleThemeSelect = (themeName: string): void => { + // Start a session if on home page + if (route.isHome()) { + const sessionId = `session-${Date.now()}`; + batch(() => { + app.setSessionInfo(sessionId, app.provider(), app.model()); + route.goToSession(sessionId); + }); + } + + app.setMode("idle"); + props.onThemeSelect(themeName); + toast.success(`Theme changed to ${themeName}`); + }; + + const handlePermissionResponse = ( + allowed: boolean, + scope?: PermissionScope, + ): void => { + app.setMode("idle"); + props.onPermissionResponse(allowed, scope); + }; + + const handleLearningResponse = ( + save: boolean, + scope?: LearningScope, + editedContent?: string, + ): void => { + app.setMode("idle"); + props.onLearningResponse(save, scope, editedContent); + }; + + const handleAgentSelect = async (agentId: string): Promise => { + app.setMode("idle"); + try { + await props.onAgentSelect?.(agentId); + toast.success(`Agent changed to ${agentId}`); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : String(err)); + } + }; + + const handleMCPSelect = async (serverId: string): Promise => { + app.setMode("idle"); + try { + await props.onMCPSelect?.(serverId); + toast.success(`MCP server selected: ${serverId}`); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : String(err)); + } + }; + + const handleMCPAdd = async (data: MCPAddFormData): Promise => { + app.setMode("idle"); + try { + await props.onMCPAdd?.(data); + toast.success(`MCP server added: ${data.name}`); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : String(err)); + } + }; + + const handleFileSelect = (file: string): void => { + app.setMode("idle"); + // Insert the file reference into the textarea as @path + const fileRef = `@${file} `; + app.insertText(fileRef); + props.onFileSelect?.(file); + }; + + const handleProviderSelect = async (providerId: string): Promise => { + app.setMode("idle"); + try { + await props.onProviderSelect?.(providerId); + app.setProvider(providerId); + toast.success(`Provider changed to ${providerId}`); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : String(err)); + } + }; + + const handleCascadeToggle = async (): Promise => { + const newValue = !app.cascadeEnabled(); + app.setCascadeEnabled(newValue); + try { + await props.onCascadeToggle?.(newValue); + toast.success(`Cascade mode ${newValue ? "enabled" : "disabled"}`); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : String(err)); + } + }; + + return ( + + + + + + + + + + + + ); +} + +function App(props: AppProps) { + return ( + }> + props.onExit({ exitCode: 0, sessionId: props.sessionId })} + > + + + + + + + + + + + + + + + + ); +} + +export interface TuiRenderOptions extends TuiInput { + onSubmit: (input: string) => Promise; + onCommand: (command: string) => Promise; + onModelSelect: (model: string) => Promise; + onThemeSelect: (theme: string) => void; + onAgentSelect?: (agentId: string) => Promise; + onMCPSelect?: (serverId: string) => Promise; + onMCPAdd?: (data: MCPAddFormData) => Promise; + onFileSelect?: (file: string) => void; + onProviderSelect?: (providerId: string) => Promise; + onCascadeToggle?: (enabled: boolean) => Promise; + onPermissionResponse: (allowed: boolean, scope?: PermissionScope) => void; + onLearningResponse: ( + save: boolean, + scope?: LearningScope, + editedContent?: string, + ) => void; + plan?: { + id: string; + title: string; + items: Array<{ id: string; text: string; completed: boolean }>; + } | null; + agents?: AgentOption[]; + currentAgent?: string; + mcpServers?: MCPServer[]; + files?: string[]; +} + +export function tui(options: TuiRenderOptions): Promise { + return new Promise((resolve) => { + render(() => , { + targetFps: 60, + exitOnCtrlC: false, + useKittyKeyboard: {}, + useMouse: false, + }); + }); +} + +export { appStore } from "@tui-solid/context/app"; diff --git a/src/tui-solid/components/agent-select.tsx b/src/tui-solid/components/agent-select.tsx new file mode 100644 index 0000000..35407e2 --- /dev/null +++ b/src/tui-solid/components/agent-select.tsx @@ -0,0 +1,122 @@ +import { createSignal, For, Show } from "solid-js"; +import { useKeyboard } from "@opentui/solid"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; + +interface AgentOption { + id: string; + name: string; + description?: string; +} + +interface AgentSelectProps { + agents: AgentOption[]; + currentAgent: string; + onSelect: (agentId: string) => void; + onClose: () => void; + isActive?: boolean; +} + +export function AgentSelect(props: AgentSelectProps) { + const theme = useTheme(); + const isActive = () => props.isActive ?? true; + const [selectedIndex, setSelectedIndex] = createSignal(0); + + useKeyboard((evt) => { + if (!isActive()) return; + + if (evt.name === "escape") { + props.onClose(); + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (evt.name === "return") { + const agent = props.agents[selectedIndex()]; + if (agent) { + props.onSelect(agent.id); + props.onClose(); + } + evt.preventDefault(); + return; + } + + if (evt.name === "up") { + setSelectedIndex((prev) => + prev > 0 ? prev - 1 : props.agents.length - 1, + ); + evt.preventDefault(); + return; + } + + if (evt.name === "down") { + setSelectedIndex((prev) => + prev < props.agents.length - 1 ? prev + 1 : 0, + ); + evt.preventDefault(); + } + }); + + return ( + + + + Select Agent + + + + + Current: + {props.currentAgent} + + + + {(agent, index) => { + const isSelected = () => index() === selectedIndex(); + const isCurrent = () => agent.id === props.currentAgent; + + return ( + + + {isSelected() ? "> " : " "} + + + {agent.name} + + + (current) + + + - {agent.description} + + + ); + }} + + + + + ↑↓ navigate | Enter select | Esc close + + + + ); +} diff --git a/src/tui-solid/components/bouncing-loader.tsx b/src/tui-solid/components/bouncing-loader.tsx new file mode 100644 index 0000000..94e1730 --- /dev/null +++ b/src/tui-solid/components/bouncing-loader.tsx @@ -0,0 +1,94 @@ +import { createSignal, onCleanup, onMount, For } from "solid-js"; +import { TextAttributes } from "@opentui/core"; + +const LOADER_COLORS = [ + "#ff00ff", + "#ff33ff", + "#cc66ff", + "#9966ff", + "#6699ff", + "#33ccff", + "#00ffff", + "#33ffcc", +] as const; + +const LOADER_CONFIG = { + dotCount: 8, + frameInterval: 100, + dotChar: "●", + emptyChar: "○", +} as const; + +interface DotInfo { + char: string; + color: string; + dim: boolean; +} + +export function BouncingLoader() { + const [position, setPosition] = createSignal(0); + const [direction, setDirection] = createSignal(1); + + let intervalId: ReturnType | null = null; + + onMount(() => { + intervalId = setInterval(() => { + const dir = direction(); + const prev = position(); + const next = prev + dir; + + if (next >= LOADER_CONFIG.dotCount - 1) { + setDirection(-1); + setPosition(LOADER_CONFIG.dotCount - 1); + return; + } + + if (next <= 0) { + setDirection(1); + setPosition(0); + return; + } + + setPosition(next); + }, LOADER_CONFIG.frameInterval); + }); + + onCleanup(() => { + if (intervalId) { + clearInterval(intervalId); + } + }); + + const dots = (): DotInfo[] => { + const pos = position(); + return Array.from({ length: LOADER_CONFIG.dotCount }, (_, i) => { + const distance = Math.abs(i - pos); + const isActive = distance === 0; + const isTrail = distance <= 2; + const colorIndex = i % LOADER_COLORS.length; + const color = LOADER_COLORS[colorIndex]; + + return { + char: + isActive || isTrail ? LOADER_CONFIG.dotChar : LOADER_CONFIG.emptyChar, + color, + dim: !isActive && !isTrail, + }; + }); + }; + + return ( + + + {(dot) => ( + + {dot.char} + + )} + + + ); +} diff --git a/src/tui-solid/components/command-menu.tsx b/src/tui-solid/components/command-menu.tsx new file mode 100644 index 0000000..a60a9a1 --- /dev/null +++ b/src/tui-solid/components/command-menu.tsx @@ -0,0 +1,230 @@ +import { createMemo, For, Show } from "solid-js"; +import { useKeyboard } from "@opentui/solid"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import { useAppStore } from "@tui-solid/context/app"; +import type { SlashCommand, CommandCategory } from "@/types/tui"; +import { SLASH_COMMANDS, COMMAND_CATEGORIES } from "@constants/tui-components"; + +export { SLASH_COMMANDS } from "@constants/tui-components"; + +interface CommandMenuProps { + onSelect: (command: string) => void; + onCancel?: () => void; + isActive?: boolean; +} + +interface CommandWithIndex extends SlashCommand { + flatIndex: number; +} + +const filterCommands = ( + commands: readonly SlashCommand[], + filter: string, +): SlashCommand[] => { + if (!filter) return [...commands]; + const query = filter.toLowerCase(); + return commands.filter( + (cmd) => + cmd.name.toLowerCase().includes(query) || + cmd.description.toLowerCase().includes(query), + ); +}; + +const groupCommandsByCategory = ( + commands: SlashCommand[], +): Array<{ category: CommandCategory; commands: SlashCommand[] }> => { + return COMMAND_CATEGORIES.map((cat) => ({ + category: cat, + commands: commands.filter((cmd) => cmd.category === cat), + })).filter((group) => group.commands.length > 0); +}; + +const capitalizeCategory = (category: string): string => + category.charAt(0).toUpperCase() + category.slice(1); + +export function CommandMenu(props: CommandMenuProps) { + const theme = useTheme(); + const app = useAppStore(); + const isActive = () => props.isActive ?? true; + + const filteredCommands = createMemo(() => + filterCommands(SLASH_COMMANDS, app.commandMenu().filter), + ); + + const groupedCommands = createMemo(() => + groupCommandsByCategory(filteredCommands()), + ); + + const commandsWithIndex = createMemo((): CommandWithIndex[] => { + let flatIndex = 0; + return groupedCommands().flatMap((group) => + group.commands.map((cmd) => ({ + ...cmd, + flatIndex: flatIndex++, + })), + ); + }); + + useKeyboard((evt) => { + if (!isActive() || !app.commandMenu().isOpen) return; + + if (evt.name === "escape") { + app.closeCommandMenu(); + props.onCancel?.(); + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (evt.name === "return") { + const commands = filteredCommands(); + if (commands.length > 0) { + const selected = commands[app.commandMenu().selectedIndex]; + if (selected) { + props.onSelect(selected.name); + } + } + evt.preventDefault(); + return; + } + + if (evt.name === "up") { + const newIndex = + app.commandMenu().selectedIndex > 0 + ? app.commandMenu().selectedIndex - 1 + : filteredCommands().length - 1; + app.setCommandSelectedIndex(newIndex); + evt.preventDefault(); + return; + } + + if (evt.name === "down") { + const newIndex = + app.commandMenu().selectedIndex < filteredCommands().length - 1 + ? app.commandMenu().selectedIndex + 1 + : 0; + app.setCommandSelectedIndex(newIndex); + evt.preventDefault(); + return; + } + + if (evt.name === "tab") { + const commands = filteredCommands(); + if (commands.length > 0) { + const selected = commands[app.commandMenu().selectedIndex]; + if (selected) { + props.onSelect(selected.name); + } + } + evt.preventDefault(); + return; + } + + if (evt.name === "backspace" || evt.name === "delete") { + if (app.commandMenu().filter.length > 0) { + app.setCommandFilter(app.commandMenu().filter.slice(0, -1)); + } else { + app.closeCommandMenu(); + props.onCancel?.(); + } + evt.preventDefault(); + return; + } + + if (evt.name.length === 1 && !evt.ctrl && !evt.meta) { + app.setCommandFilter(app.commandMenu().filter + evt.name); + evt.preventDefault(); + } + }); + + return ( + + + + + Commands + + + - filtering: + {app.commandMenu().filter} + + + + 0} + fallback={ + + No commands match "{app.commandMenu().filter}" + + } + > + + + {(group) => ( + + + {capitalizeCategory(group.category)} + + + {(cmd) => { + const cmdWithIndex = () => + commandsWithIndex().find((c) => c.name === cmd.name); + const isSelected = () => + cmdWithIndex()?.flatIndex === + app.commandMenu().selectedIndex; + + return ( + + + {isSelected() ? "> " : " "} + + + /{cmd.name} + + + {" "} + - {cmd.description} + + + ); + }} + + + )} + + + + + + + Esc to close | Enter/Tab to select | Type to filter + + + + + ); +} diff --git a/src/tui-solid/components/diff-view.tsx b/src/tui-solid/components/diff-view.tsx new file mode 100644 index 0000000..395a0b1 --- /dev/null +++ b/src/tui-solid/components/diff-view.tsx @@ -0,0 +1,115 @@ +import { For, Show } from "solid-js"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import type { DiffLineData } from "@/types/tui"; + +interface DiffViewProps { + lines: DiffLineData[]; + filePath?: string; + additions?: number; + deletions?: number; + compact?: boolean; +} + +export function DiffView(props: DiffViewProps) { + const theme = useTheme(); + const compact = () => props.compact ?? false; + + return ( + + + + + {props.filePath} + + 0 || (props.deletions ?? 0) > 0}> + ( + 0}> + +{props.additions} + + 0 && (props.deletions ?? 0) > 0} + > + / + + 0}> + -{props.deletions} + + ) + + + + + + {(line) => } + + + ); +} + +interface DiffLineProps { + line: DiffLineData; + compact: boolean; +} + +function DiffLine(props: DiffLineProps) { + const theme = useTheme(); + + const lineColor = (): string => { + // Use white text for add/remove lines since they have colored backgrounds + if (props.line.type === "add" || props.line.type === "remove") { + return theme.colors.text; + } + const colorMap: Record = { + context: theme.colors.diffContext, + header: theme.colors.diffHeader, + hunk: theme.colors.diffHunk, + summary: theme.colors.textDim, + }; + return colorMap[props.line.type] ?? theme.colors.text; + }; + + const prefix = (): string => { + const prefixMap: Record = { + add: "+", + remove: "-", + context: " ", + header: "", + hunk: "", + summary: "", + }; + return prefixMap[props.line.type] ?? " "; + }; + + const bgColor = (): string | undefined => { + if (props.line.type === "add") return theme.colors.bgAdded; + if (props.line.type === "remove") return theme.colors.bgRemoved; + return undefined; + }; + + return ( + + + + {props.line.oldLineNum?.toString().padStart(3) ?? " "} + + + {props.line.newLineNum?.toString().padStart(3) ?? " "} + + + + {prefix()} + {props.line.content} + + + ); +} + +export { parseDiffOutput, isDiffContent } from "@/utils/diff"; diff --git a/src/tui-solid/components/file-picker.tsx b/src/tui-solid/components/file-picker.tsx new file mode 100644 index 0000000..93f40ca --- /dev/null +++ b/src/tui-solid/components/file-picker.tsx @@ -0,0 +1,176 @@ +import { createSignal, createMemo, For, Show } from "solid-js"; +import { useKeyboard } from "@opentui/solid"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; + +interface FilePickerProps { + files: string[]; + onSelect: (file: string) => void; + onClose: () => void; + title?: string; + isActive?: boolean; +} + +const MAX_VISIBLE = 15; + +export function FilePicker(props: FilePickerProps) { + const theme = useTheme(); + const isActive = () => props.isActive ?? true; + const [selectedIndex, setSelectedIndex] = createSignal(0); + const [scrollOffset, setScrollOffset] = createSignal(0); + const [filter, setFilter] = createSignal(""); + + const filteredFiles = createMemo(() => { + const query = filter().toLowerCase(); + if (!query) return props.files; + return props.files.filter((f) => f.toLowerCase().includes(query)); + }); + + useKeyboard((evt) => { + if (!isActive()) return; + + if (evt.name === "escape") { + props.onClose(); + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (evt.name === "return") { + const file = filteredFiles()[selectedIndex()]; + if (file) { + props.onSelect(file); + props.onClose(); + } + evt.preventDefault(); + return; + } + + if (evt.name === "up") { + const files = filteredFiles(); + setSelectedIndex((prev) => { + const newIndex = prev > 0 ? prev - 1 : files.length - 1; + if (newIndex < scrollOffset()) { + setScrollOffset(newIndex); + } + if (prev === 0 && newIndex === files.length - 1) { + setScrollOffset(Math.max(0, files.length - MAX_VISIBLE)); + } + return newIndex; + }); + evt.preventDefault(); + return; + } + + if (evt.name === "down") { + const files = filteredFiles(); + setSelectedIndex((prev) => { + const newIndex = prev < files.length - 1 ? prev + 1 : 0; + if (newIndex >= scrollOffset() + MAX_VISIBLE) { + setScrollOffset(newIndex - MAX_VISIBLE + 1); + } + if (prev === files.length - 1 && newIndex === 0) { + setScrollOffset(0); + } + return newIndex; + }); + evt.preventDefault(); + return; + } + + if (evt.name === "backspace" || evt.name === "delete") { + if (filter().length > 0) { + setFilter(filter().slice(0, -1)); + setSelectedIndex(0); + setScrollOffset(0); + } + evt.preventDefault(); + return; + } + + if (evt.name.length === 1 && !evt.ctrl && !evt.meta) { + setFilter(filter() + evt.name); + setSelectedIndex(0); + setScrollOffset(0); + evt.preventDefault(); + } + }); + + const visibleFiles = createMemo(() => + filteredFiles().slice(scrollOffset(), scrollOffset() + MAX_VISIBLE), + ); + + return ( + + + + {props.title ?? "Select File"} + + + - filtering: + {filter()} + + + + 0} + fallback={ + No files match "{filter()}" + } + > + + 0}> + + {" "} + ↑ {scrollOffset()} more above + + + + + {(file, visibleIndex) => { + const actualIndex = () => scrollOffset() + visibleIndex(); + const isSelected = () => actualIndex() === selectedIndex(); + + return ( + + + {isSelected() ? "> " : " "} + + + {file} + + + ); + }} + + + + + {" "} + ↓ {filteredFiles().length - scrollOffset() - MAX_VISIBLE} more + below + + + + + + + + ↑↓ navigate | Enter select | Type to filter | Esc close + + + + ); +} diff --git a/src/tui-solid/components/header.tsx b/src/tui-solid/components/header.tsx new file mode 100644 index 0000000..498c143 --- /dev/null +++ b/src/tui-solid/components/header.tsx @@ -0,0 +1,92 @@ +import { Show, createMemo } from "solid-js"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import { useAppStore } from "@tui-solid/context/app"; + +interface HeaderProps { + showBanner?: boolean; +} + +const MODE_LABELS = { + agent: "Agent", + ask: "Ask", + "code-review": "Code Review", +} as const; + +const MODE_DESCRIPTIONS = { + agent: "Full access - can modify files", + ask: "Read-only - answers questions", + "code-review": "Review PRs and diffs", +} as const; + +const MODE_COLORS = { + agent: "warning", + ask: "info", + "code-review": "success", +} as const; + +export function Header(props: HeaderProps) { + const theme = useTheme(); + const app = useAppStore(); + const showBanner = () => props.showBanner ?? true; + + const modeColor = createMemo(() => { + const colorKey = MODE_COLORS[app.interactionMode()]; + return theme.colors[colorKey]; + }); + + return ( + + + + + CodeTyper + + + v{app.version()} + | + + + [{MODE_LABELS[app.interactionMode()]}] + + + + {" "} + @{app.currentAgent()} + + + + {" "} + - {MODE_DESCRIPTIONS[app.interactionMode()]} + + + + + + + Provider: + {app.provider()} + + + Model: + {app.model() || "auto"} + + + + Session: + + {app.sessionId()?.replace("session-", "").slice(-5)} + + + + + + ); +} diff --git a/src/tui-solid/components/index.ts b/src/tui-solid/components/index.ts new file mode 100644 index 0000000..f25baa7 --- /dev/null +++ b/src/tui-solid/components/index.ts @@ -0,0 +1,25 @@ +export { StatusBar } from "./status-bar"; +export { Logo } from "./logo"; +export { ThinkingIndicator } from "./thinking-indicator"; +export { BouncingLoader } from "./bouncing-loader"; +export { LogPanel } from "./log-panel"; +export { LogEntryDisplay } from "./log-entry"; +export { StreamingMessage } from "./streaming-message"; +export { InputArea } from "./input-area"; +export { Header } from "./header"; +export { CommandMenu, SLASH_COMMANDS } from "./command-menu"; +export { ModelSelect } from "./model-select"; +export { AgentSelect } from "./agent-select"; +export { ThemeSelect } from "./theme-select"; +export { MCPSelect } from "./mcp-select"; +export { MCPAddForm } from "./mcp-add-form"; +export { ModeSelect } from "./mode-select"; +export { ProviderSelect } from "./provider-select"; +export { FilePicker } from "./file-picker"; +export { SelectMenu } from "./select-menu"; +export type { SelectOption } from "./select-menu"; +export { PermissionModal } from "./permission-modal"; +export { LearningModal } from "./learning-modal"; +export { TodoPanel } from "./todo-panel"; +export type { TodoItem, Plan } from "./todo-panel"; +export { DiffView, parseDiffOutput, isDiffContent } from "./diff-view"; diff --git a/src/tui-solid/components/input-area.tsx b/src/tui-solid/components/input-area.tsx new file mode 100644 index 0000000..70848bd --- /dev/null +++ b/src/tui-solid/components/input-area.tsx @@ -0,0 +1,248 @@ +import { createMemo, Show, onMount, onCleanup } from "solid-js"; +import { useKeyboard } from "@opentui/solid"; +import { TextareaRenderable, type PasteEvent } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import { useAppStore } from "@tui-solid/context/app"; + +/** Minimum lines to trigger paste summary */ +const MIN_PASTE_LINES = 3; +/** Minimum characters to trigger paste summary */ +const MIN_PASTE_CHARS = 150; + +interface InputAreaProps { + onSubmit: (input: string) => void; + placeholder?: string; +} + +/** Stores pasted content that was summarized */ +type PastedBlock = { + id: number; + content: string; + placeholder: string; +}; + +export function InputArea(props: InputAreaProps) { + let inputRef: TextareaRenderable; + let pasteCounter = 0; + let pastedBlocks: Map = new Map(); + + const theme = useTheme(); + const app = useAppStore(); + + const isLocked = createMemo(() => app.isInputLocked()); + + /** + * Insert pasted text as virtual placeholder + */ + const pasteText = (text: string, virtualText: string): void => { + if (!inputRef) return; + + pasteCounter++; + const id = pasteCounter; + + // Insert the placeholder text + inputRef.insertText(virtualText + " "); + + // Store the actual content - use placeholder as key for simple lookup + const block: PastedBlock = { + id, + content: text, + placeholder: virtualText, + }; + pastedBlocks.set(virtualText, block); + + app.setInputBuffer(inputRef.plainText); + }; + + /** + * Expand all pasted blocks back to their original content + */ + const expandPastedContent = (inputText: string): string => { + if (pastedBlocks.size === 0) return inputText; + + let result = inputText; + + // Simple string replacement - replace each placeholder with actual content + for (const block of pastedBlocks.values()) { + // Replace placeholder (with or without trailing space) + result = result.replace(block.placeholder + " ", block.content); + result = result.replace(block.placeholder, block.content); + } + + return result; + }; + + /** + * Clear all pasted blocks + */ + const clearPastedBlocks = (): void => { + pastedBlocks.clear(); + pasteCounter = 0; + }; + const isMenuOpen = createMemo(() => { + const mode = app.mode(); + return ( + app.commandMenu().isOpen || + mode === "command_menu" || + mode === "model_select" || + mode === "theme_select" || + mode === "agent_select" || + mode === "mode_select" || + mode === "mcp_select" || + mode === "mcp_add" || + mode === "file_picker" || + mode === "permission_prompt" || + mode === "learning_prompt" + ); + }); + const placeholder = () => + props.placeholder ?? "Ask anything... (@ for files, / for commands)"; + + const borderColor = createMemo(() => { + if (isLocked()) return theme.colors.borderWarning; + if (app.inputBuffer()) return theme.colors.borderFocus; + return theme.colors.border; + }); + + // Handle "/" to open command menu when input is empty + // Handle Enter to submit (backup in case onSubmit doesn't fire) + // Handle Ctrl+Tab to toggle interaction mode + useKeyboard((evt) => { + // Ctrl+Tab works even when locked or menus are open + if (evt.ctrl && evt.name === "tab") { + app.toggleInteractionMode(); + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (isLocked()) return; + // Don't capture keys when any menu/modal is open + if (isMenuOpen()) return; + + if (evt.name === "/" && !app.inputBuffer()) { + app.openCommandMenu(); + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (evt.name === "@") { + app.setMode("file_picker"); + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (evt.name === "return" && !evt.shift && !evt.ctrl && !evt.meta) { + handleSubmit(); + evt.preventDefault(); + evt.stopPropagation(); + } + }); + + const handleSubmit = (): void => { + // Get value from app store (synced via onContentChange) or directly from ref + let value = (app.inputBuffer() || inputRef?.plainText || "").trim(); + if (value && !isLocked()) { + // Expand pasted content placeholders back to actual content + value = expandPastedContent(value); + props.onSubmit(value); + if (inputRef) inputRef.clear(); + app.setInputBuffer(""); + clearPastedBlocks(); + } + }; + + /** + * Handle paste events - summarize large pastes + */ + const handlePaste = (event: PasteEvent): void => { + if (isLocked()) { + event.preventDefault(); + return; + } + + // Normalize line endings (Windows ConPTY sends CR-only newlines) + const normalizedText = event.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + const pastedContent = normalizedText.trim(); + + if (!pastedContent) return; + + // Check if paste should be summarized + const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1; + if (lineCount >= MIN_PASTE_LINES || pastedContent.length > MIN_PASTE_CHARS) { + event.preventDefault(); + pasteText(pastedContent, `[Pasted ~${lineCount} lines]`); + } + // Otherwise let default paste behavior handle it + }; + + // Register insert function so external code can insert text + onMount(() => { + app.setInputInsertFn((text: string) => { + if (inputRef) { + inputRef.insertText(text); + app.setInputBuffer(inputRef.plainText); + } + }); + }); + + onCleanup(() => { + app.setInputInsertFn(null); + }); + + return ( + + + + Input locked while processing... + + + } + > +