This is page 30 of 52. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .claude │ ├── agents │ │ ├── task-checker.md │ │ ├── task-executor.md │ │ └── task-orchestrator.md │ ├── commands │ │ ├── dedupe.md │ │ └── tm │ │ ├── add-dependency │ │ │ └── add-dependency.md │ │ ├── add-subtask │ │ │ ├── add-subtask.md │ │ │ └── convert-task-to-subtask.md │ │ ├── add-task │ │ │ └── add-task.md │ │ ├── analyze-complexity │ │ │ └── analyze-complexity.md │ │ ├── complexity-report │ │ │ └── complexity-report.md │ │ ├── expand │ │ │ ├── expand-all-tasks.md │ │ │ └── expand-task.md │ │ ├── fix-dependencies │ │ │ └── fix-dependencies.md │ │ ├── generate │ │ │ └── generate-tasks.md │ │ ├── help.md │ │ ├── init │ │ │ ├── init-project-quick.md │ │ │ └── init-project.md │ │ ├── learn.md │ │ ├── list │ │ │ ├── list-tasks-by-status.md │ │ │ ├── list-tasks-with-subtasks.md │ │ │ └── list-tasks.md │ │ ├── models │ │ │ ├── setup-models.md │ │ │ └── view-models.md │ │ ├── next │ │ │ └── next-task.md │ │ ├── parse-prd │ │ │ ├── parse-prd-with-research.md │ │ │ └── parse-prd.md │ │ ├── remove-dependency │ │ │ └── remove-dependency.md │ │ ├── remove-subtask │ │ │ └── remove-subtask.md │ │ ├── remove-subtasks │ │ │ ├── remove-all-subtasks.md │ │ │ └── remove-subtasks.md │ │ ├── remove-task │ │ │ └── remove-task.md │ │ ├── set-status │ │ │ ├── to-cancelled.md │ │ │ ├── to-deferred.md │ │ │ ├── to-done.md │ │ │ ├── to-in-progress.md │ │ │ ├── to-pending.md │ │ │ └── to-review.md │ │ ├── setup │ │ │ ├── install-taskmaster.md │ │ │ └── quick-install-taskmaster.md │ │ ├── show │ │ │ └── show-task.md │ │ ├── status │ │ │ └── project-status.md │ │ ├── sync-readme │ │ │ └── sync-readme.md │ │ ├── tm-main.md │ │ ├── update │ │ │ ├── update-single-task.md │ │ │ ├── update-task.md │ │ │ └── update-tasks-from-id.md │ │ ├── utils │ │ │ └── analyze-project.md │ │ ├── validate-dependencies │ │ │ └── validate-dependencies.md │ │ └── workflows │ │ ├── auto-implement-tasks.md │ │ ├── command-pipeline.md │ │ └── smart-workflow.md │ └── TM_COMMANDS_GUIDE.md ├── .coderabbit.yaml ├── .cursor │ ├── mcp.json │ └── rules │ ├── ai_providers.mdc │ ├── ai_services.mdc │ ├── architecture.mdc │ ├── changeset.mdc │ ├── commands.mdc │ ├── context_gathering.mdc │ ├── cursor_rules.mdc │ ├── dependencies.mdc │ ├── dev_workflow.mdc │ ├── git_workflow.mdc │ ├── glossary.mdc │ ├── mcp.mdc │ ├── new_features.mdc │ ├── self_improve.mdc │ ├── tags.mdc │ ├── taskmaster.mdc │ ├── tasks.mdc │ ├── telemetry.mdc │ ├── test_workflow.mdc │ ├── tests.mdc │ ├── ui.mdc │ └── utilities.mdc ├── .cursorignore ├── .env.example ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── enhancements---feature-requests.md │ │ └── feedback.md │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bugfix.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── integration.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── scripts │ │ ├── auto-close-duplicates.mjs │ │ ├── backfill-duplicate-comments.mjs │ │ ├── check-pre-release-mode.mjs │ │ ├── parse-metrics.mjs │ │ ├── release.mjs │ │ ├── tag-extension.mjs │ │ └── utils.mjs │ └── workflows │ ├── auto-close-duplicates.yml │ ├── backfill-duplicate-comments.yml │ ├── ci.yml │ ├── claude-dedupe-issues.yml │ ├── claude-docs-trigger.yml │ ├── claude-docs-updater.yml │ ├── claude-issue-triage.yml │ ├── claude.yml │ ├── extension-ci.yml │ ├── extension-release.yml │ ├── log-issue-events.yml │ ├── pre-release.yml │ ├── release-check.yml │ ├── release.yml │ ├── update-models-md.yml │ └── weekly-metrics-discord.yml ├── .gitignore ├── .kiro │ ├── hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── settings │ │ └── mcp.json │ └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ ├── taskmaster_hooks_workflow.md │ └── taskmaster.md ├── .manypkg.json ├── .mcp.json ├── .npmignore ├── .nvmrc ├── .taskmaster │ ├── CLAUDE.md │ ├── config.json │ ├── docs │ │ ├── MIGRATION-ROADMAP.md │ │ ├── prd-tm-start.txt │ │ ├── prd.txt │ │ ├── README.md │ │ ├── research │ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md │ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md │ │ │ ├── 2025-06-14_test-save-functionality.md │ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md │ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md │ │ ├── task-template-importing-prd.txt │ │ ├── test-prd.txt │ │ └── tm-core-phase-1.txt │ ├── reports │ │ ├── task-complexity-report_cc-kiro-hooks.json │ │ ├── task-complexity-report_test-prd-tag.json │ │ ├── task-complexity-report_tm-core-phase-1.json │ │ ├── task-complexity-report.json │ │ └── tm-core-complexity.json │ ├── state.json │ ├── tasks │ │ ├── task_001_tm-start.txt │ │ ├── task_002_tm-start.txt │ │ ├── task_003_tm-start.txt │ │ ├── task_004_tm-start.txt │ │ ├── task_007_tm-start.txt │ │ └── tasks.json │ └── templates │ └── example_prd.txt ├── .vscode │ ├── extensions.json │ └── settings.json ├── apps │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── auth.command.ts │ │ │ │ ├── context.command.ts │ │ │ │ ├── list.command.ts │ │ │ │ ├── set-status.command.ts │ │ │ │ ├── show.command.ts │ │ │ │ └── start.command.ts │ │ │ ├── index.ts │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── header.component.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── next-task.component.ts │ │ │ │ │ ├── suggested-steps.component.ts │ │ │ │ │ └── task-detail.component.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ ├── auto-update.ts │ │ │ └── ui.ts │ │ └── tsconfig.json │ ├── docs │ │ ├── archive │ │ │ ├── ai-client-utils-example.mdx │ │ │ ├── ai-development-workflow.mdx │ │ │ ├── command-reference.mdx │ │ │ ├── configuration.mdx │ │ │ ├── cursor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── Installation.mdx │ │ ├── best-practices │ │ │ ├── advanced-tasks.mdx │ │ │ ├── configuration-advanced.mdx │ │ │ └── index.mdx │ │ ├── capabilities │ │ │ ├── cli-root-commands.mdx │ │ │ ├── index.mdx │ │ │ ├── mcp.mdx │ │ │ └── task-structure.mdx │ │ ├── CHANGELOG.md │ │ ├── docs.json │ │ ├── favicon.svg │ │ ├── getting-started │ │ │ ├── contribute.mdx │ │ │ ├── faq.mdx │ │ │ └── quick-start │ │ │ ├── configuration-quick.mdx │ │ │ ├── execute-quick.mdx │ │ │ ├── installation.mdx │ │ │ ├── moving-forward.mdx │ │ │ ├── prd-quick.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── requirements.mdx │ │ │ ├── rules-quick.mdx │ │ │ └── tasks-quick.mdx │ │ ├── introduction.mdx │ │ ├── licensing.md │ │ ├── logo │ │ │ ├── dark.svg │ │ │ ├── light.svg │ │ │ └── task-master-logo.png │ │ ├── package.json │ │ ├── README.md │ │ ├── style.css │ │ ├── vercel.json │ │ └── whats-new.mdx │ └── extension │ ├── .vscodeignore │ ├── assets │ │ ├── banner.png │ │ ├── icon-dark.svg │ │ ├── icon-light.svg │ │ ├── icon.png │ │ ├── screenshots │ │ │ ├── kanban-board.png │ │ │ └── task-details.png │ │ └── sidebar-icon.svg │ ├── CHANGELOG.md │ ├── components.json │ ├── docs │ │ ├── extension-CI-setup.md │ │ └── extension-development-guide.md │ ├── esbuild.js │ ├── LICENSE │ ├── package.json │ ├── package.mjs │ ├── package.publish.json │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── ConfigView.tsx │ │ │ ├── constants.ts │ │ │ ├── TaskDetails │ │ │ │ ├── AIActionsSection.tsx │ │ │ │ ├── DetailsSection.tsx │ │ │ │ ├── PriorityBadge.tsx │ │ │ │ ├── SubtasksSection.tsx │ │ │ │ ├── TaskMetadataSidebar.tsx │ │ │ │ └── useTaskDetails.ts │ │ │ ├── TaskDetailsView.tsx │ │ │ ├── TaskMasterLogo.tsx │ │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── CollapsibleSection.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── label.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── shadcn-io │ │ │ │ └── kanban │ │ │ │ └── index.tsx │ │ │ └── textarea.tsx │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── services │ │ │ ├── config-service.ts │ │ │ ├── error-handler.ts │ │ │ ├── notification-preferences.ts │ │ │ ├── polling-service.ts │ │ │ ├── polling-strategies.ts │ │ │ ├── sidebar-webview-manager.ts │ │ │ ├── task-repository.ts │ │ │ ├── terminal-manager.ts │ │ │ └── webview-manager.ts │ │ ├── test │ │ │ └── extension.test.ts │ │ ├── utils │ │ │ ├── configManager.ts │ │ │ ├── connectionManager.ts │ │ │ ├── errorHandler.ts │ │ │ ├── event-emitter.ts │ │ │ ├── logger.ts │ │ │ ├── mcpClient.ts │ │ │ ├── notificationPreferences.ts │ │ │ └── task-master-api │ │ │ ├── cache │ │ │ │ └── cache-manager.ts │ │ │ ├── index.ts │ │ │ ├── mcp-client.ts │ │ │ ├── transformers │ │ │ │ └── task-transformer.ts │ │ │ └── types │ │ │ └── index.ts │ │ └── webview │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppContent.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── PollingStatus.tsx │ │ │ ├── PriorityBadge.tsx │ │ │ ├── SidebarView.tsx │ │ │ ├── TagDropdown.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskEditModal.tsx │ │ │ ├── TaskMasterKanban.tsx │ │ │ ├── ToastContainer.tsx │ │ │ └── ToastNotification.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── VSCodeContext.tsx │ │ ├── hooks │ │ │ ├── useTaskQueries.ts │ │ │ ├── useVSCodeMessages.ts │ │ │ └── useWebviewHeight.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── providers │ │ │ └── QueryProvider.tsx │ │ ├── reducers │ │ │ └── appReducer.ts │ │ ├── sidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ ├── logger.ts │ │ └── toast.ts │ └── tsconfig.json ├── assets │ ├── .windsurfrules │ ├── AGENTS.md │ ├── claude │ │ ├── agents │ │ │ ├── task-checker.md │ │ │ ├── task-executor.md │ │ │ └── task-orchestrator.md │ │ ├── commands │ │ │ └── tm │ │ │ ├── add-dependency │ │ │ │ └── add-dependency.md │ │ │ ├── add-subtask │ │ │ │ ├── add-subtask.md │ │ │ │ └── convert-task-to-subtask.md │ │ │ ├── add-task │ │ │ │ └── add-task.md │ │ │ ├── analyze-complexity │ │ │ │ └── analyze-complexity.md │ │ │ ├── clear-subtasks │ │ │ │ ├── clear-all-subtasks.md │ │ │ │ └── clear-subtasks.md │ │ │ ├── complexity-report │ │ │ │ └── complexity-report.md │ │ │ ├── expand │ │ │ │ ├── expand-all-tasks.md │ │ │ │ └── expand-task.md │ │ │ ├── fix-dependencies │ │ │ │ └── fix-dependencies.md │ │ │ ├── generate │ │ │ │ └── generate-tasks.md │ │ │ ├── help.md │ │ │ ├── init │ │ │ │ ├── init-project-quick.md │ │ │ │ └── init-project.md │ │ │ ├── learn.md │ │ │ ├── list │ │ │ │ ├── list-tasks-by-status.md │ │ │ │ ├── list-tasks-with-subtasks.md │ │ │ │ └── list-tasks.md │ │ │ ├── models │ │ │ │ ├── setup-models.md │ │ │ │ └── view-models.md │ │ │ ├── next │ │ │ │ └── next-task.md │ │ │ ├── parse-prd │ │ │ │ ├── parse-prd-with-research.md │ │ │ │ └── parse-prd.md │ │ │ ├── remove-dependency │ │ │ │ └── remove-dependency.md │ │ │ ├── remove-subtask │ │ │ │ └── remove-subtask.md │ │ │ ├── remove-subtasks │ │ │ │ ├── remove-all-subtasks.md │ │ │ │ └── remove-subtasks.md │ │ │ ├── remove-task │ │ │ │ └── remove-task.md │ │ │ ├── set-status │ │ │ │ ├── to-cancelled.md │ │ │ │ ├── to-deferred.md │ │ │ │ ├── to-done.md │ │ │ │ ├── to-in-progress.md │ │ │ │ ├── to-pending.md │ │ │ │ └── to-review.md │ │ │ ├── setup │ │ │ │ ├── install-taskmaster.md │ │ │ │ └── quick-install-taskmaster.md │ │ │ ├── show │ │ │ │ └── show-task.md │ │ │ ├── status │ │ │ │ └── project-status.md │ │ │ ├── sync-readme │ │ │ │ └── sync-readme.md │ │ │ ├── tm-main.md │ │ │ ├── update │ │ │ │ ├── update-single-task.md │ │ │ │ ├── update-task.md │ │ │ │ └── update-tasks-from-id.md │ │ │ ├── utils │ │ │ │ └── analyze-project.md │ │ │ ├── validate-dependencies │ │ │ │ └── validate-dependencies.md │ │ │ └── workflows │ │ │ ├── auto-implement-tasks.md │ │ │ ├── command-pipeline.md │ │ │ └── smart-workflow.md │ │ └── TM_COMMANDS_GUIDE.md │ ├── config.json │ ├── env.example │ ├── example_prd.txt │ ├── gitignore │ ├── kiro-hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── roocode │ │ ├── .roo │ │ │ ├── rules-architect │ │ │ │ └── architect-rules │ │ │ ├── rules-ask │ │ │ │ └── ask-rules │ │ │ ├── rules-code │ │ │ │ └── code-rules │ │ │ ├── rules-debug │ │ │ │ └── debug-rules │ │ │ ├── rules-orchestrator │ │ │ │ └── orchestrator-rules │ │ │ └── rules-test │ │ │ └── test-rules │ │ └── .roomodes │ ├── rules │ │ ├── cursor_rules.mdc │ │ ├── dev_workflow.mdc │ │ ├── self_improve.mdc │ │ ├── taskmaster_hooks_workflow.mdc │ │ └── taskmaster.mdc │ └── scripts_README.md ├── bin │ └── task-master.js ├── biome.json ├── CHANGELOG.md ├── CLAUDE.md ├── context │ ├── chats │ │ ├── add-task-dependencies-1.md │ │ └── max-min-tokens.txt.md │ ├── fastmcp-core.txt │ ├── fastmcp-docs.txt │ ├── MCP_INTEGRATION.md │ ├── mcp-js-sdk-docs.txt │ ├── mcp-protocol-repo.txt │ ├── mcp-protocol-schema-03262025.json │ └── mcp-protocol-spec.txt ├── CONTRIBUTING.md ├── docs │ ├── CLI-COMMANDER-PATTERN.md │ ├── command-reference.md │ ├── configuration.md │ ├── contributor-docs │ │ └── testing-roo-integration.md │ ├── cross-tag-task-movement.md │ ├── examples │ │ └── claude-code-usage.md │ ├── examples.md │ ├── licensing.md │ ├── mcp-provider-guide.md │ ├── mcp-provider.md │ ├── migration-guide.md │ ├── models.md │ ├── providers │ │ └── gemini-cli.md │ ├── README.md │ ├── scripts │ │ └── models-json-to-markdown.js │ ├── task-structure.md │ └── tutorial.md ├── images │ └── logo.png ├── index.js ├── jest.config.js ├── jest.resolver.cjs ├── LICENSE ├── llms-install.md ├── mcp-server │ ├── server.js │ └── src │ ├── core │ │ ├── __tests__ │ │ │ └── context-manager.test.js │ │ ├── context-manager.js │ │ ├── direct-functions │ │ │ ├── add-dependency.js │ │ │ ├── add-subtask.js │ │ │ ├── add-tag.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── cache-stats.js │ │ │ ├── clear-subtasks.js │ │ │ ├── complexity-report.js │ │ │ ├── copy-tag.js │ │ │ ├── create-tag-from-branch.js │ │ │ ├── delete-tag.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── fix-dependencies.js │ │ │ ├── generate-task-files.js │ │ │ ├── initialize-project.js │ │ │ ├── list-tags.js │ │ │ ├── list-tasks.js │ │ │ ├── models.js │ │ │ ├── move-task-cross-tag.js │ │ │ ├── move-task.js │ │ │ ├── next-task.js │ │ │ ├── parse-prd.js │ │ │ ├── remove-dependency.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── rename-tag.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── rules.js │ │ │ ├── scope-down.js │ │ │ ├── scope-up.js │ │ │ ├── set-task-status.js │ │ │ ├── show-task.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ ├── update-tasks.js │ │ │ ├── use-tag.js │ │ │ └── validate-dependencies.js │ │ ├── task-master-core.js │ │ └── utils │ │ ├── env-utils.js │ │ └── path-utils.js │ ├── custom-sdk │ │ ├── errors.js │ │ ├── index.js │ │ ├── json-extractor.js │ │ ├── language-model.js │ │ ├── message-converter.js │ │ └── schema-converter.js │ ├── index.js │ ├── logger.js │ ├── providers │ │ └── mcp-provider.js │ └── tools │ ├── add-dependency.js │ ├── add-subtask.js │ ├── add-tag.js │ ├── add-task.js │ ├── analyze.js │ ├── clear-subtasks.js │ ├── complexity-report.js │ ├── copy-tag.js │ ├── delete-tag.js │ ├── expand-all.js │ ├── expand-task.js │ ├── fix-dependencies.js │ ├── generate.js │ ├── get-operation-status.js │ ├── get-task.js │ ├── get-tasks.js │ ├── index.js │ ├── initialize-project.js │ ├── list-tags.js │ ├── models.js │ ├── move-task.js │ ├── next-task.js │ ├── parse-prd.js │ ├── remove-dependency.js │ ├── remove-subtask.js │ ├── remove-task.js │ ├── rename-tag.js │ ├── research.js │ ├── response-language.js │ ├── rules.js │ ├── scope-down.js │ ├── scope-up.js │ ├── set-task-status.js │ ├── update-subtask.js │ ├── update-task.js │ ├── update.js │ ├── use-tag.js │ ├── utils.js │ └── validate-dependencies.js ├── mcp-test.js ├── output.json ├── package-lock.json ├── package.json ├── packages │ ├── build-config │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ └── tsdown.base.ts │ │ └── tsconfig.json │ └── tm-core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── docs │ │ └── listTasks-architecture.md │ ├── package.json │ ├── POC-STATUS.md │ ├── README.md │ ├── src │ │ ├── auth │ │ │ ├── auth-manager.test.ts │ │ │ ├── auth-manager.ts │ │ │ ├── config.ts │ │ │ ├── credential-store.test.ts │ │ │ ├── credential-store.ts │ │ │ ├── index.ts │ │ │ ├── oauth-service.ts │ │ │ ├── supabase-session-storage.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── index.ts │ │ │ └── supabase-client.ts │ │ ├── config │ │ │ ├── config-manager.spec.ts │ │ │ ├── config-manager.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── config-loader.service.spec.ts │ │ │ ├── config-loader.service.ts │ │ │ ├── config-merger.service.spec.ts │ │ │ ├── config-merger.service.ts │ │ │ ├── config-persistence.service.spec.ts │ │ │ ├── config-persistence.service.ts │ │ │ ├── environment-config-provider.service.spec.ts │ │ │ ├── environment-config-provider.service.ts │ │ │ ├── index.ts │ │ │ ├── runtime-state-manager.service.spec.ts │ │ │ └── runtime-state-manager.service.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── entities │ │ │ └── task.entity.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── task-master-error.ts │ │ ├── executors │ │ │ ├── base-executor.ts │ │ │ ├── claude-executor.ts │ │ │ ├── executor-factory.ts │ │ │ ├── executor-service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── ai-provider.interface.ts │ │ │ ├── configuration.interface.ts │ │ │ ├── index.ts │ │ │ └── storage.interface.ts │ │ ├── logger │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── mappers │ │ │ └── TaskMapper.ts │ │ ├── parser │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ai │ │ │ │ ├── base-provider.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── supabase-task-repository.ts │ │ │ └── task-repository.interface.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── organization.service.ts │ │ │ ├── task-execution-service.ts │ │ │ └── task-service.ts │ │ ├── storage │ │ │ ├── api-storage.ts │ │ │ ├── file-storage │ │ │ │ ├── file-operations.ts │ │ │ │ ├── file-storage.ts │ │ │ │ ├── format-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── path-resolver.ts │ │ │ ├── index.ts │ │ │ └── storage-factory.ts │ │ ├── subpath-exports.test.ts │ │ ├── task-master-core.ts │ │ ├── types │ │ │ ├── database.types.ts │ │ │ ├── index.ts │ │ │ └── legacy.ts │ │ └── utils │ │ ├── id-generator.ts │ │ └── index.ts │ ├── tests │ │ ├── integration │ │ │ └── list-tasks.test.ts │ │ ├── mocks │ │ │ └── mock-provider.ts │ │ ├── setup.ts │ │ └── unit │ │ ├── base-provider.test.ts │ │ ├── executor.test.ts │ │ └── smoke.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── README-task-master.md ├── README.md ├── scripts │ ├── dev.js │ ├── init.js │ ├── modules │ │ ├── ai-services-unified.js │ │ ├── commands.js │ │ ├── config-manager.js │ │ ├── dependency-manager.js │ │ ├── index.js │ │ ├── prompt-manager.js │ │ ├── supported-models.json │ │ ├── sync-readme.js │ │ ├── task-manager │ │ │ ├── add-subtask.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── clear-subtasks.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── find-next-task.js │ │ │ ├── generate-task-files.js │ │ │ ├── is-task-dependent.js │ │ │ ├── list-tasks.js │ │ │ ├── migrate.js │ │ │ ├── models.js │ │ │ ├── move-task.js │ │ │ ├── parse-prd │ │ │ │ ├── index.js │ │ │ │ ├── parse-prd-config.js │ │ │ │ ├── parse-prd-helpers.js │ │ │ │ ├── parse-prd-non-streaming.js │ │ │ │ ├── parse-prd-streaming.js │ │ │ │ └── parse-prd.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── scope-adjustment.js │ │ │ ├── set-task-status.js │ │ │ ├── tag-management.js │ │ │ ├── task-exists.js │ │ │ ├── update-single-task-status.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ └── update-tasks.js │ │ ├── task-manager.js │ │ ├── ui.js │ │ ├── update-config-tokens.js │ │ ├── utils │ │ │ ├── contextGatherer.js │ │ │ ├── fuzzyTaskSearch.js │ │ │ └── git-utils.js │ │ └── utils.js │ ├── task-complexity-report.json │ ├── test-claude-errors.js │ └── test-claude.js ├── src │ ├── ai-providers │ │ ├── anthropic.js │ │ ├── azure.js │ │ ├── base-provider.js │ │ ├── bedrock.js │ │ ├── claude-code.js │ │ ├── custom-sdk │ │ │ ├── claude-code │ │ │ │ ├── errors.js │ │ │ │ ├── index.js │ │ │ │ ├── json-extractor.js │ │ │ │ ├── language-model.js │ │ │ │ ├── message-converter.js │ │ │ │ └── types.js │ │ │ └── grok-cli │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── json-extractor.js │ │ │ ├── language-model.js │ │ │ ├── message-converter.js │ │ │ └── types.js │ │ ├── gemini-cli.js │ │ ├── google-vertex.js │ │ ├── google.js │ │ ├── grok-cli.js │ │ ├── groq.js │ │ ├── index.js │ │ ├── ollama.js │ │ ├── openai.js │ │ ├── openrouter.js │ │ ├── perplexity.js │ │ └── xai.js │ ├── constants │ │ ├── commands.js │ │ ├── paths.js │ │ ├── profiles.js │ │ ├── providers.js │ │ ├── rules-actions.js │ │ ├── task-priority.js │ │ └── task-status.js │ ├── profiles │ │ ├── amp.js │ │ ├── base-profile.js │ │ ├── claude.js │ │ ├── cline.js │ │ ├── codex.js │ │ ├── cursor.js │ │ ├── gemini.js │ │ ├── index.js │ │ ├── kilo.js │ │ ├── kiro.js │ │ ├── opencode.js │ │ ├── roo.js │ │ ├── trae.js │ │ ├── vscode.js │ │ ├── windsurf.js │ │ └── zed.js │ ├── progress │ │ ├── base-progress-tracker.js │ │ ├── cli-progress-factory.js │ │ ├── parse-prd-tracker.js │ │ ├── progress-tracker-builder.js │ │ └── tracker-ui.js │ ├── prompts │ │ ├── add-task.json │ │ ├── analyze-complexity.json │ │ ├── expand-task.json │ │ ├── parse-prd.json │ │ ├── README.md │ │ ├── research.json │ │ ├── schemas │ │ │ ├── parameter.schema.json │ │ │ ├── prompt-template.schema.json │ │ │ ├── README.md │ │ │ └── variant.schema.json │ │ ├── update-subtask.json │ │ ├── update-task.json │ │ └── update-tasks.json │ ├── provider-registry │ │ └── index.js │ ├── task-master.js │ ├── ui │ │ ├── confirm.js │ │ ├── indicators.js │ │ └── parse-prd.js │ └── utils │ ├── asset-resolver.js │ ├── create-mcp-config.js │ ├── format.js │ ├── getVersion.js │ ├── logger-utils.js │ ├── manage-gitignore.js │ ├── path-utils.js │ ├── profiles.js │ ├── rule-transformer.js │ ├── stream-parser.js │ └── timeout-manager.js ├── test-clean-tags.js ├── test-config-manager.js ├── test-prd.txt ├── test-tag-functions.js ├── test-version-check-full.js ├── test-version-check.js ├── tests │ ├── e2e │ │ ├── e2e_helpers.sh │ │ ├── parse_llm_output.cjs │ │ ├── run_e2e.sh │ │ ├── run_fallback_verification.sh │ │ └── test_llm_analysis.sh │ ├── fixture │ │ └── test-tasks.json │ ├── fixtures │ │ ├── .taskmasterconfig │ │ ├── sample-claude-response.js │ │ ├── sample-prd.txt │ │ └── sample-tasks.js │ ├── integration │ │ ├── claude-code-optional.test.js │ │ ├── cli │ │ │ ├── commands.test.js │ │ │ ├── complex-cross-tag-scenarios.test.js │ │ │ └── move-cross-tag.test.js │ │ ├── manage-gitignore.test.js │ │ ├── mcp-server │ │ │ └── direct-functions.test.js │ │ ├── move-task-cross-tag.integration.test.js │ │ ├── move-task-simple.integration.test.js │ │ └── profiles │ │ ├── amp-init-functionality.test.js │ │ ├── claude-init-functionality.test.js │ │ ├── cline-init-functionality.test.js │ │ ├── codex-init-functionality.test.js │ │ ├── cursor-init-functionality.test.js │ │ ├── gemini-init-functionality.test.js │ │ ├── opencode-init-functionality.test.js │ │ ├── roo-files-inclusion.test.js │ │ ├── roo-init-functionality.test.js │ │ ├── rules-files-inclusion.test.js │ │ ├── trae-init-functionality.test.js │ │ ├── vscode-init-functionality.test.js │ │ └── windsurf-init-functionality.test.js │ ├── manual │ │ ├── progress │ │ │ ├── parse-prd-analysis.js │ │ │ ├── test-parse-prd.js │ │ │ └── TESTING_GUIDE.md │ │ └── prompts │ │ ├── prompt-test.js │ │ └── README.md │ ├── README.md │ ├── setup.js │ └── unit │ ├── ai-providers │ │ ├── claude-code.test.js │ │ ├── custom-sdk │ │ │ └── claude-code │ │ │ └── language-model.test.js │ │ ├── gemini-cli.test.js │ │ ├── mcp-components.test.js │ │ └── openai.test.js │ ├── ai-services-unified.test.js │ ├── commands.test.js │ ├── config-manager.test.js │ ├── config-manager.test.mjs │ ├── dependency-manager.test.js │ ├── init.test.js │ ├── initialize-project.test.js │ ├── kebab-case-validation.test.js │ ├── manage-gitignore.test.js │ ├── mcp │ │ └── tools │ │ ├── __mocks__ │ │ │ └── move-task.js │ │ ├── add-task.test.js │ │ ├── analyze-complexity.test.js │ │ ├── expand-all.test.js │ │ ├── get-tasks.test.js │ │ ├── initialize-project.test.js │ │ ├── move-task-cross-tag-options.test.js │ │ ├── move-task-cross-tag.test.js │ │ └── remove-task.test.js │ ├── mcp-providers │ │ ├── mcp-components.test.js │ │ └── mcp-provider.test.js │ ├── parse-prd.test.js │ ├── profiles │ │ ├── amp-integration.test.js │ │ ├── claude-integration.test.js │ │ ├── cline-integration.test.js │ │ ├── codex-integration.test.js │ │ ├── cursor-integration.test.js │ │ ├── gemini-integration.test.js │ │ ├── kilo-integration.test.js │ │ ├── kiro-integration.test.js │ │ ├── mcp-config-validation.test.js │ │ ├── opencode-integration.test.js │ │ ├── profile-safety-check.test.js │ │ ├── roo-integration.test.js │ │ ├── rule-transformer-cline.test.js │ │ ├── rule-transformer-cursor.test.js │ │ ├── rule-transformer-gemini.test.js │ │ ├── rule-transformer-kilo.test.js │ │ ├── rule-transformer-kiro.test.js │ │ ├── rule-transformer-opencode.test.js │ │ ├── rule-transformer-roo.test.js │ │ ├── rule-transformer-trae.test.js │ │ ├── rule-transformer-vscode.test.js │ │ ├── rule-transformer-windsurf.test.js │ │ ├── rule-transformer-zed.test.js │ │ ├── rule-transformer.test.js │ │ ├── selective-profile-removal.test.js │ │ ├── subdirectory-support.test.js │ │ ├── trae-integration.test.js │ │ ├── vscode-integration.test.js │ │ ├── windsurf-integration.test.js │ │ └── zed-integration.test.js │ ├── progress │ │ └── base-progress-tracker.test.js │ ├── prompt-manager.test.js │ ├── prompts │ │ └── expand-task-prompt.test.js │ ├── providers │ │ └── provider-registry.test.js │ ├── scripts │ │ └── modules │ │ ├── commands │ │ │ ├── move-cross-tag.test.js │ │ │ └── README.md │ │ ├── dependency-manager │ │ │ ├── circular-dependencies.test.js │ │ │ ├── cross-tag-dependencies.test.js │ │ │ └── fix-dependencies-command.test.js │ │ ├── task-manager │ │ │ ├── add-subtask.test.js │ │ │ ├── add-task.test.js │ │ │ ├── analyze-task-complexity.test.js │ │ │ ├── clear-subtasks.test.js │ │ │ ├── complexity-report-tag-isolation.test.js │ │ │ ├── expand-all-tasks.test.js │ │ │ ├── expand-task.test.js │ │ │ ├── find-next-task.test.js │ │ │ ├── generate-task-files.test.js │ │ │ ├── list-tasks.test.js │ │ │ ├── move-task-cross-tag.test.js │ │ │ ├── move-task.test.js │ │ │ ├── parse-prd.test.js │ │ │ ├── remove-subtask.test.js │ │ │ ├── remove-task.test.js │ │ │ ├── research.test.js │ │ │ ├── scope-adjustment.test.js │ │ │ ├── set-task-status.test.js │ │ │ ├── setup.js │ │ │ ├── update-single-task-status.test.js │ │ │ ├── update-subtask-by-id.test.js │ │ │ ├── update-task-by-id.test.js │ │ │ └── update-tasks.test.js │ │ ├── ui │ │ │ └── cross-tag-error-display.test.js │ │ └── utils-tag-aware-paths.test.js │ ├── task-finder.test.js │ ├── task-manager │ │ ├── clear-subtasks.test.js │ │ ├── move-task.test.js │ │ ├── tag-boundary.test.js │ │ └── tag-management.test.js │ ├── task-master.test.js │ ├── ui │ │ └── indicators.test.js │ ├── ui.test.js │ ├── utils-strip-ansi.test.js │ └── utils.test.js ├── tsconfig.json ├── tsdown.config.ts └── turbo.json ``` # Files -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- ```markdown 1 | # Configuration 2 | 3 | Taskmaster uses two primary methods for configuration: 4 | 5 | 1. **`.taskmaster/config.json` File (Recommended - New Structure)** 6 | 7 | - This JSON file stores most configuration settings, including AI model selections, parameters, logging levels, and project defaults. 8 | - **Location:** This file is created in the `.taskmaster/` directory when you run the `task-master models --setup` interactive setup or initialize a new project with `task-master init`. 9 | - **Migration:** Existing projects with `.taskmasterconfig` in the root will continue to work, but should be migrated to the new structure using `task-master migrate`. 10 | - **Management:** Use the `task-master models --setup` command (or `models` MCP tool) to interactively create and manage this file. You can also set specific models directly using `task-master models --set-<role>=<model_id>`, adding `--ollama` or `--openrouter` flags for custom models. Manual editing is possible but not recommended unless you understand the structure. 11 | - **Example Structure:** 12 | ```json 13 | { 14 | "models": { 15 | "main": { 16 | "provider": "anthropic", 17 | "modelId": "claude-3-7-sonnet-20250219", 18 | "maxTokens": 64000, 19 | "temperature": 0.2, 20 | "baseURL": "https://api.anthropic.com/v1" 21 | }, 22 | "research": { 23 | "provider": "perplexity", 24 | "modelId": "sonar-pro", 25 | "maxTokens": 8700, 26 | "temperature": 0.1, 27 | "baseURL": "https://api.perplexity.ai/v1" 28 | }, 29 | "fallback": { 30 | "provider": "anthropic", 31 | "modelId": "claude-3-5-sonnet", 32 | "maxTokens": 64000, 33 | "temperature": 0.2 34 | } 35 | }, 36 | "global": { 37 | "logLevel": "info", 38 | "debug": false, 39 | "defaultNumTasks": 10, 40 | "defaultSubtasks": 5, 41 | "defaultPriority": "medium", 42 | "defaultTag": "master", 43 | "projectName": "Your Project Name", 44 | "ollamaBaseURL": "http://localhost:11434/api", 45 | "azureBaseURL": "https://your-endpoint.azure.com/openai/deployments", 46 | "vertexProjectId": "your-gcp-project-id", 47 | "vertexLocation": "us-central1", 48 | "responseLanguage": "English" 49 | } 50 | } 51 | ``` 52 | 53 | > For MCP-specific setup and troubleshooting, see [Provider-Specific Configuration](#provider-specific-configuration). 54 | 55 | 2. **Legacy `.taskmasterconfig` File (Backward Compatibility)** 56 | 57 | - For projects that haven't migrated to the new structure yet. 58 | - **Location:** Project root directory. 59 | - **Migration:** Use `task-master migrate` to move this to `.taskmaster/config.json`. 60 | - **Deprecation:** While still supported, you'll see warnings encouraging migration to the new structure. 61 | 62 | ## Environment Variables (`.env` file or MCP `env` block - For API Keys Only) 63 | 64 | - Used **exclusively** for sensitive API keys and specific endpoint URLs. 65 | - **Location:** 66 | - For CLI usage: Create a `.env` file in your project root. 67 | - For MCP/Cursor usage: Configure keys in the `env` section of your `.cursor/mcp.json` file. 68 | - **Required API Keys (Depending on configured providers):** 69 | - `ANTHROPIC_API_KEY`: Your Anthropic API key. 70 | - `PERPLEXITY_API_KEY`: Your Perplexity API key. 71 | - `OPENAI_API_KEY`: Your OpenAI API key. 72 | - `GOOGLE_API_KEY`: Your Google API key (also used for Vertex AI provider). 73 | - `MISTRAL_API_KEY`: Your Mistral API key. 74 | - `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (also requires `AZURE_OPENAI_ENDPOINT`). 75 | - `OPENROUTER_API_KEY`: Your OpenRouter API key. 76 | - `XAI_API_KEY`: Your X-AI API key. 77 | - **Optional Endpoint Overrides:** 78 | - **Per-role `baseURL` in `.taskmasterconfig`:** You can add a `baseURL` property to any model role (`main`, `research`, `fallback`) to override the default API endpoint for that provider. If omitted, the provider's standard endpoint is used. 79 | - **Environment Variable Overrides (`<PROVIDER>_BASE_URL`):** For greater flexibility, especially with third-party services, you can set an environment variable like `OPENAI_BASE_URL` or `MISTRAL_BASE_URL`. This will override any `baseURL` set in the configuration file for that provider. This is the recommended way to connect to OpenAI-compatible APIs. 80 | - `AZURE_OPENAI_ENDPOINT`: Required if using Azure OpenAI key (can also be set as `baseURL` for the Azure model role). 81 | - `OLLAMA_BASE_URL`: Override the default Ollama API URL (Default: `http://localhost:11434/api`). 82 | - `VERTEX_PROJECT_ID`: Your Google Cloud project ID for Vertex AI. Required when using the 'vertex' provider. 83 | - `VERTEX_LOCATION`: Google Cloud region for Vertex AI (e.g., 'us-central1'). Default is 'us-central1'. 84 | - `GOOGLE_APPLICATION_CREDENTIALS`: Path to service account credentials JSON file for Google Cloud auth (alternative to API key for Vertex AI). 85 | 86 | **Important:** Settings like model ID selections (`main`, `research`, `fallback`), `maxTokens`, `temperature`, `logLevel`, `defaultSubtasks`, `defaultPriority`, and `projectName` are **managed in `.taskmaster/config.json`** (or `.taskmasterconfig` for unmigrated projects), not environment variables. 87 | 88 | ## Tagged Task Lists Configuration (v0.17+) 89 | 90 | Taskmaster includes a tagged task lists system for multi-context task management. 91 | 92 | ### Global Tag Settings 93 | 94 | ```json 95 | "global": { 96 | "defaultTag": "master" 97 | } 98 | ``` 99 | 100 | - **`defaultTag`** (string): Default tag context for new operations (default: "master") 101 | 102 | ### Git Integration 103 | 104 | Task Master provides manual git integration through the `--from-branch` option: 105 | 106 | - **Manual Tag Creation**: Use `task-master add-tag --from-branch` to create a tag based on your current git branch name 107 | - **User Control**: No automatic tag switching - you control when and how tags are created 108 | - **Flexible Workflow**: Supports any git workflow without imposing rigid branch-tag mappings 109 | 110 | ## State Management File 111 | 112 | Taskmaster uses `.taskmaster/state.json` to track tagged system runtime information: 113 | 114 | ```json 115 | { 116 | "currentTag": "master", 117 | "lastSwitched": "2025-06-11T20:26:12.598Z", 118 | "migrationNoticeShown": true 119 | } 120 | ``` 121 | 122 | - **`currentTag`**: Currently active tag context 123 | - **`lastSwitched`**: Timestamp of last tag switch 124 | - **`migrationNoticeShown`**: Whether migration notice has been displayed 125 | 126 | This file is automatically created during tagged system migration and should not be manually edited. 127 | 128 | ## Example `.env` File (for API Keys) 129 | 130 | ``` 131 | # Required API keys for providers configured in .taskmaster/config.json 132 | ANTHROPIC_API_KEY=sk-ant-api03-your-key-here 133 | PERPLEXITY_API_KEY=pplx-your-key-here 134 | # OPENAI_API_KEY=sk-your-key-here 135 | # GOOGLE_API_KEY=AIzaSy... 136 | # AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here 137 | # etc. 138 | 139 | # Optional Endpoint Overrides 140 | # Use a specific provider's base URL, e.g., for an OpenAI-compatible API 141 | # OPENAI_BASE_URL=https://api.third-party.com/v1 142 | # 143 | # Azure OpenAI Configuration 144 | # AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/ or https://your-endpoint-name.cognitiveservices.azure.com/openai/deployments 145 | # OLLAMA_BASE_URL=http://custom-ollama-host:11434/api 146 | 147 | # Google Vertex AI Configuration (Required if using 'vertex' provider) 148 | # VERTEX_PROJECT_ID=your-gcp-project-id 149 | ``` 150 | 151 | ## Troubleshooting 152 | 153 | ### Configuration Errors 154 | 155 | - If Task Master reports errors about missing configuration or cannot find the config file, run `task-master models --setup` in your project root to create or repair the file. 156 | - For new projects, config will be created at `.taskmaster/config.json`. For legacy projects, you may want to use `task-master migrate` to move to the new structure. 157 | - Ensure API keys are correctly placed in your `.env` file (for CLI) or `.cursor/mcp.json` (for MCP) and are valid for the providers selected in your config file. 158 | 159 | ### If `task-master init` doesn't respond: 160 | 161 | Try running it with Node directly: 162 | 163 | ```bash 164 | node node_modules/claude-task-master/scripts/init.js 165 | ``` 166 | 167 | Or clone the repository and run: 168 | 169 | ```bash 170 | git clone https://github.com/eyaltoledano/claude-task-master.git 171 | cd claude-task-master 172 | node scripts/init.js 173 | ``` 174 | 175 | ## Provider-Specific Configuration 176 | 177 | ### MCP (Model Context Protocol) Provider 178 | 179 | 1. **Prerequisites**: 180 | - An active MCP session with sampling capability 181 | - MCP client with sampling support (e.g. VS Code) 182 | - No API keys required (uses session-based authentication) 183 | 184 | 2. **Configuration**: 185 | ```json 186 | { 187 | "models": { 188 | "main": { 189 | "provider": "mcp", 190 | "modelId": "mcp-sampling" 191 | }, 192 | "research": { 193 | "provider": "mcp", 194 | "modelId": "mcp-sampling" 195 | } 196 | } 197 | } 198 | ``` 199 | 200 | 3. **Available Model IDs**: 201 | - `mcp-sampling` - General text generation using MCP client sampling (supports all roles) 202 | - `claude-3-5-sonnet-20241022` - High-performance model for general tasks (supports all roles) 203 | - `claude-3-opus-20240229` - Enhanced reasoning model for complex tasks (supports all roles) 204 | 205 | 4. **Features**: 206 | - ✅ **Text Generation**: Standard AI text generation via MCP sampling 207 | - ✅ **Object Generation**: Full schema-driven structured output generation 208 | - ✅ **PRD Parsing**: Parse Product Requirements Documents into structured tasks 209 | - ✅ **Task Creation**: AI-powered task creation with validation 210 | - ✅ **Session Management**: Automatic session detection and context handling 211 | - ✅ **Error Recovery**: Robust error handling and fallback mechanisms 212 | 213 | 5. **Usage Requirements**: 214 | - Must be running in an MCP context (session must be available) 215 | - Session must provide `clientCapabilities.sampling` capability 216 | 217 | 6. **Best Practices**: 218 | - Always configure a non-MCP fallback provider 219 | - Use `mcp` for main/research roles when in MCP environments 220 | - Test sampling capability before production use 221 | 222 | 7. **Setup Commands**: 223 | ```bash 224 | # Set MCP provider for main role 225 | task-master models set-main --provider mcp --model claude-3-5-sonnet-20241022 226 | 227 | # Set MCP provider for research role 228 | task-master models set-research --provider mcp --model claude-3-opus-20240229 229 | 230 | # Verify configuration 231 | task-master models list 232 | ``` 233 | 234 | 8. **Troubleshooting**: 235 | - "MCP provider requires session context" → Ensure running in MCP environment 236 | - See the [MCP Provider Guide](./mcp-provider-guide.md) for detailed troubleshooting 237 | 238 | ### Google Vertex AI Configuration 239 | 240 | Google Vertex AI is Google Cloud's enterprise AI platform and requires specific configuration: 241 | 242 | 1. **Prerequisites**: 243 | - A Google Cloud account with Vertex AI API enabled 244 | - Either a Google API key with Vertex AI permissions OR a service account with appropriate roles 245 | - A Google Cloud project ID 246 | 2. **Authentication Options**: 247 | - **API Key**: Set the `GOOGLE_API_KEY` environment variable 248 | - **Service Account**: Set `GOOGLE_APPLICATION_CREDENTIALS` to point to your service account JSON file 249 | 3. **Required Configuration**: 250 | - Set `VERTEX_PROJECT_ID` to your Google Cloud project ID 251 | - Set `VERTEX_LOCATION` to your preferred Google Cloud region (default: us-central1) 252 | 4. **Example Setup**: 253 | 254 | ```bash 255 | # In .env file 256 | GOOGLE_API_KEY=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXX 257 | VERTEX_PROJECT_ID=my-gcp-project-123 258 | VERTEX_LOCATION=us-central1 259 | ``` 260 | 261 | Or using service account: 262 | 263 | ```bash 264 | # In .env file 265 | GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json 266 | VERTEX_PROJECT_ID=my-gcp-project-123 267 | VERTEX_LOCATION=us-central1 268 | ``` 269 | 270 | 5. **In .taskmaster/config.json**: 271 | ```json 272 | "global": { 273 | "vertexProjectId": "my-gcp-project-123", 274 | "vertexLocation": "us-central1" 275 | } 276 | ``` 277 | 278 | ### Azure OpenAI Configuration 279 | 280 | Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure cloud platform and requires specific configuration: 281 | 282 | 1. **Prerequisites**: 283 | - An Azure account with an active subscription 284 | - Azure OpenAI service resource created in the Azure portal 285 | - Azure OpenAI API key and endpoint URL 286 | - Deployed models (e.g., gpt-4o, gpt-4o-mini, gpt-4.1, etc) in your Azure OpenAI resource 287 | 288 | 2. **Authentication**: 289 | - Set the `AZURE_OPENAI_API_KEY` environment variable with your Azure OpenAI API key 290 | - Configure the endpoint URL using one of the methods below 291 | 292 | 3. **Configuration Options**: 293 | 294 | **Option 1: Using Global Azure Base URL (affects all Azure models)** 295 | ```json 296 | // In .taskmaster/config.json 297 | { 298 | "models": { 299 | "main": { 300 | "provider": "azure", 301 | "modelId": "gpt-4o", 302 | "maxTokens": 16000, 303 | "temperature": 0.7 304 | }, 305 | "fallback": { 306 | "provider": "azure", 307 | "modelId": "gpt-4o-mini", 308 | "maxTokens": 10000, 309 | "temperature": 0.7 310 | } 311 | }, 312 | "global": { 313 | "azureBaseURL": "https://your-resource-name.azure.com/openai/deployments" 314 | } 315 | } 316 | ``` 317 | 318 | **Option 2: Using Per-Model Base URLs (recommended for flexibility)** 319 | ```json 320 | // In .taskmaster/config.json 321 | { 322 | "models": { 323 | "main": { 324 | "provider": "azure", 325 | "modelId": "gpt-4o", 326 | "maxTokens": 16000, 327 | "temperature": 0.7, 328 | "baseURL": "https://your-resource-name.azure.com/openai/deployments" 329 | }, 330 | "research": { 331 | "provider": "perplexity", 332 | "modelId": "sonar-pro", 333 | "maxTokens": 8700, 334 | "temperature": 0.1 335 | }, 336 | "fallback": { 337 | "provider": "azure", 338 | "modelId": "gpt-4o-mini", 339 | "maxTokens": 10000, 340 | "temperature": 0.7, 341 | "baseURL": "https://your-resource-name.azure.com/openai/deployments" 342 | } 343 | } 344 | } 345 | ``` 346 | 347 | 4. **Environment Variables**: 348 | ```bash 349 | # In .env file 350 | AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here 351 | 352 | # Optional: Override endpoint for all Azure models 353 | AZURE_OPENAI_ENDPOINT=https://your-resource-name.azure.com/openai/deployments 354 | ``` 355 | 356 | 5. **Important Notes**: 357 | - **Model Deployment Names**: The `modelId` in your configuration should match the **deployment name** you created in Azure OpenAI Studio, not the underlying model name 358 | - **Base URL Priority**: Per-model `baseURL` settings override the global `azureBaseURL` setting 359 | - **Endpoint Format**: When using per-model `baseURL`, use the full path including `/openai/deployments` 360 | 361 | 6. **Troubleshooting**: 362 | 363 | **"Resource not found" errors:** 364 | - Ensure your `baseURL` includes the full path: `https://your-resource-name.openai.azure.com/openai/deployments` 365 | - Verify that your deployment name in `modelId` exactly matches what's configured in Azure OpenAI Studio 366 | - Check that your Azure OpenAI resource is in the correct region and properly deployed 367 | 368 | **Authentication errors:** 369 | - Verify your `AZURE_OPENAI_API_KEY` is correct and has not expired 370 | - Ensure your Azure OpenAI resource has the necessary permissions 371 | - Check that your subscription has not been suspended or reached quota limits 372 | 373 | **Model availability errors:** 374 | - Confirm the model is deployed in your Azure OpenAI resource 375 | - Verify the deployment name matches your configuration exactly (case-sensitive) 376 | - Ensure the model deployment is in a "Succeeded" state in Azure OpenAI Studio 377 | - Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment. 378 | ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/ui/cross-tag-error-display.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import { 3 | displayCrossTagDependencyError, 4 | displaySubtaskMoveError, 5 | displayInvalidTagCombinationError, 6 | displayDependencyValidationHints, 7 | formatTaskIdForDisplay 8 | } from '../../../../../scripts/modules/ui.js'; 9 | 10 | // Mock console.log to capture output 11 | const originalConsoleLog = console.log; 12 | const mockConsoleLog = jest.fn(); 13 | global.console.log = mockConsoleLog; 14 | 15 | // Add afterAll hook to restore 16 | afterAll(() => { 17 | global.console.log = originalConsoleLog; 18 | }); 19 | 20 | describe('Cross-Tag Error Display Functions', () => { 21 | beforeEach(() => { 22 | mockConsoleLog.mockClear(); 23 | }); 24 | 25 | describe('displayCrossTagDependencyError', () => { 26 | it('should display cross-tag dependency error with conflicts', () => { 27 | const conflicts = [ 28 | { 29 | taskId: 1, 30 | dependencyId: 2, 31 | dependencyTag: 'backlog', 32 | message: 'Task 1 depends on 2 (in backlog)' 33 | }, 34 | { 35 | taskId: 3, 36 | dependencyId: 4, 37 | dependencyTag: 'done', 38 | message: 'Task 3 depends on 4 (in done)' 39 | } 40 | ]; 41 | 42 | displayCrossTagDependencyError(conflicts, 'in-progress', 'done', '1,3'); 43 | 44 | expect(mockConsoleLog).toHaveBeenCalledWith( 45 | expect.stringContaining( 46 | '❌ Cannot move tasks from "in-progress" to "done"' 47 | ) 48 | ); 49 | expect(mockConsoleLog).toHaveBeenCalledWith( 50 | expect.stringContaining('Cross-tag dependency conflicts detected:') 51 | ); 52 | expect(mockConsoleLog).toHaveBeenCalledWith( 53 | expect.stringContaining('• Task 1 depends on 2 (in backlog)') 54 | ); 55 | expect(mockConsoleLog).toHaveBeenCalledWith( 56 | expect.stringContaining('• Task 3 depends on 4 (in done)') 57 | ); 58 | expect(mockConsoleLog).toHaveBeenCalledWith( 59 | expect.stringContaining('Resolution options:') 60 | ); 61 | expect(mockConsoleLog).toHaveBeenCalledWith( 62 | expect.stringContaining('--with-dependencies') 63 | ); 64 | expect(mockConsoleLog).toHaveBeenCalledWith( 65 | expect.stringContaining('--ignore-dependencies') 66 | ); 67 | }); 68 | 69 | it('should handle empty conflicts array', () => { 70 | displayCrossTagDependencyError([], 'backlog', 'done', '1'); 71 | 72 | expect(mockConsoleLog).toHaveBeenCalledWith( 73 | expect.stringContaining('❌ Cannot move tasks from "backlog" to "done"') 74 | ); 75 | expect(mockConsoleLog).toHaveBeenCalledWith( 76 | expect.stringContaining('Cross-tag dependency conflicts detected:') 77 | ); 78 | }); 79 | }); 80 | 81 | describe('displaySubtaskMoveError', () => { 82 | it('should display subtask movement restriction error', () => { 83 | displaySubtaskMoveError('5.2', 'backlog', 'in-progress'); 84 | 85 | expect(mockConsoleLog).toHaveBeenCalledWith( 86 | expect.stringContaining( 87 | '❌ Cannot move subtask 5.2 directly between tags' 88 | ) 89 | ); 90 | expect(mockConsoleLog).toHaveBeenCalledWith( 91 | expect.stringContaining('Subtask movement restriction:') 92 | ); 93 | expect(mockConsoleLog).toHaveBeenCalledWith( 94 | expect.stringContaining( 95 | '• Subtasks cannot be moved directly between tags' 96 | ) 97 | ); 98 | expect(mockConsoleLog).toHaveBeenCalledWith( 99 | expect.stringContaining('Resolution options:') 100 | ); 101 | expect(mockConsoleLog).toHaveBeenCalledWith( 102 | expect.stringContaining('remove-subtask --id=5.2 --convert') 103 | ); 104 | }); 105 | 106 | it('should handle nested subtask IDs (three levels)', () => { 107 | displaySubtaskMoveError('5.2.1', 'feature-auth', 'production'); 108 | 109 | expect(mockConsoleLog).toHaveBeenCalledWith( 110 | expect.stringContaining( 111 | '❌ Cannot move subtask 5.2.1 directly between tags' 112 | ) 113 | ); 114 | expect(mockConsoleLog).toHaveBeenCalledWith( 115 | expect.stringContaining('remove-subtask --id=5.2.1 --convert') 116 | ); 117 | }); 118 | 119 | it('should handle deeply nested subtask IDs (four levels)', () => { 120 | displaySubtaskMoveError('10.3.2.1', 'development', 'testing'); 121 | 122 | expect(mockConsoleLog).toHaveBeenCalledWith( 123 | expect.stringContaining( 124 | '❌ Cannot move subtask 10.3.2.1 directly between tags' 125 | ) 126 | ); 127 | expect(mockConsoleLog).toHaveBeenCalledWith( 128 | expect.stringContaining('remove-subtask --id=10.3.2.1 --convert') 129 | ); 130 | }); 131 | 132 | it('should handle single-level subtask IDs', () => { 133 | displaySubtaskMoveError('15.1', 'master', 'feature-branch'); 134 | 135 | expect(mockConsoleLog).toHaveBeenCalledWith( 136 | expect.stringContaining( 137 | '❌ Cannot move subtask 15.1 directly between tags' 138 | ) 139 | ); 140 | expect(mockConsoleLog).toHaveBeenCalledWith( 141 | expect.stringContaining('remove-subtask --id=15.1 --convert') 142 | ); 143 | }); 144 | 145 | it('should handle invalid subtask ID format gracefully', () => { 146 | displaySubtaskMoveError('invalid-id', 'tag1', 'tag2'); 147 | 148 | expect(mockConsoleLog).toHaveBeenCalledWith( 149 | expect.stringContaining( 150 | '❌ Cannot move subtask invalid-id directly between tags' 151 | ) 152 | ); 153 | expect(mockConsoleLog).toHaveBeenCalledWith( 154 | expect.stringContaining('remove-subtask --id=invalid-id --convert') 155 | ); 156 | }); 157 | 158 | it('should handle empty subtask ID', () => { 159 | displaySubtaskMoveError('', 'source', 'target'); 160 | 161 | expect(mockConsoleLog).toHaveBeenCalledWith( 162 | expect.stringContaining( 163 | `❌ Cannot move subtask ${formatTaskIdForDisplay('')} directly between tags` 164 | ) 165 | ); 166 | expect(mockConsoleLog).toHaveBeenCalledWith( 167 | expect.stringContaining( 168 | `remove-subtask --id=${formatTaskIdForDisplay('')} --convert` 169 | ) 170 | ); 171 | }); 172 | 173 | it('should handle null subtask ID', () => { 174 | displaySubtaskMoveError(null, 'source', 'target'); 175 | 176 | expect(mockConsoleLog).toHaveBeenCalledWith( 177 | expect.stringContaining( 178 | '❌ Cannot move subtask null directly between tags' 179 | ) 180 | ); 181 | expect(mockConsoleLog).toHaveBeenCalledWith( 182 | expect.stringContaining('remove-subtask --id=null --convert') 183 | ); 184 | }); 185 | 186 | it('should handle undefined subtask ID', () => { 187 | displaySubtaskMoveError(undefined, 'source', 'target'); 188 | 189 | expect(mockConsoleLog).toHaveBeenCalledWith( 190 | expect.stringContaining( 191 | '❌ Cannot move subtask undefined directly between tags' 192 | ) 193 | ); 194 | expect(mockConsoleLog).toHaveBeenCalledWith( 195 | expect.stringContaining('remove-subtask --id=undefined --convert') 196 | ); 197 | }); 198 | 199 | it('should handle special characters in subtask ID', () => { 200 | displaySubtaskMoveError('5.2@test', 'dev', 'prod'); 201 | 202 | expect(mockConsoleLog).toHaveBeenCalledWith( 203 | expect.stringContaining( 204 | '❌ Cannot move subtask 5.2@test directly between tags' 205 | ) 206 | ); 207 | expect(mockConsoleLog).toHaveBeenCalledWith( 208 | expect.stringContaining('remove-subtask --id=5.2@test --convert') 209 | ); 210 | }); 211 | 212 | it('should handle numeric subtask IDs', () => { 213 | displaySubtaskMoveError('123.456', 'alpha', 'beta'); 214 | 215 | expect(mockConsoleLog).toHaveBeenCalledWith( 216 | expect.stringContaining( 217 | '❌ Cannot move subtask 123.456 directly between tags' 218 | ) 219 | ); 220 | expect(mockConsoleLog).toHaveBeenCalledWith( 221 | expect.stringContaining('remove-subtask --id=123.456 --convert') 222 | ); 223 | }); 224 | 225 | it('should handle identical source and target tags', () => { 226 | displaySubtaskMoveError('7.3', 'same-tag', 'same-tag'); 227 | 228 | expect(mockConsoleLog).toHaveBeenCalledWith( 229 | expect.stringContaining( 230 | '❌ Cannot move subtask 7.3 directly between tags' 231 | ) 232 | ); 233 | expect(mockConsoleLog).toHaveBeenCalledWith( 234 | expect.stringContaining('• Source tag: "same-tag"') 235 | ); 236 | expect(mockConsoleLog).toHaveBeenCalledWith( 237 | expect.stringContaining('• Target tag: "same-tag"') 238 | ); 239 | }); 240 | 241 | it('should handle empty tag names', () => { 242 | displaySubtaskMoveError('9.1', '', ''); 243 | 244 | expect(mockConsoleLog).toHaveBeenCalledWith( 245 | expect.stringContaining( 246 | '❌ Cannot move subtask 9.1 directly between tags' 247 | ) 248 | ); 249 | expect(mockConsoleLog).toHaveBeenCalledWith( 250 | expect.stringContaining('• Source tag: ""') 251 | ); 252 | expect(mockConsoleLog).toHaveBeenCalledWith( 253 | expect.stringContaining('• Target tag: ""') 254 | ); 255 | }); 256 | 257 | it('should handle null tag names', () => { 258 | displaySubtaskMoveError('12.4', null, null); 259 | 260 | expect(mockConsoleLog).toHaveBeenCalledWith( 261 | expect.stringContaining( 262 | '❌ Cannot move subtask 12.4 directly between tags' 263 | ) 264 | ); 265 | expect(mockConsoleLog).toHaveBeenCalledWith( 266 | expect.stringContaining('• Source tag: "null"') 267 | ); 268 | expect(mockConsoleLog).toHaveBeenCalledWith( 269 | expect.stringContaining('• Target tag: "null"') 270 | ); 271 | }); 272 | 273 | it('should handle complex tag names with special characters', () => { 274 | displaySubtaskMoveError( 275 | '3.2.1', 276 | 'feature/[email protected]', 277 | 'production@stable' 278 | ); 279 | 280 | expect(mockConsoleLog).toHaveBeenCalledWith( 281 | expect.stringContaining( 282 | '❌ Cannot move subtask 3.2.1 directly between tags' 283 | ) 284 | ); 285 | expect(mockConsoleLog).toHaveBeenCalledWith( 286 | expect.stringContaining('• Source tag: "feature/[email protected]"') 287 | ); 288 | expect(mockConsoleLog).toHaveBeenCalledWith( 289 | expect.stringContaining('• Target tag: "production@stable"') 290 | ); 291 | }); 292 | 293 | it('should handle very long subtask IDs', () => { 294 | const longId = '1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20'; 295 | displaySubtaskMoveError(longId, 'short', 'long'); 296 | 297 | expect(mockConsoleLog).toHaveBeenCalledWith( 298 | expect.stringContaining( 299 | `❌ Cannot move subtask ${longId} directly between tags` 300 | ) 301 | ); 302 | expect(mockConsoleLog).toHaveBeenCalledWith( 303 | expect.stringContaining(`remove-subtask --id=${longId} --convert`) 304 | ); 305 | }); 306 | 307 | it('should handle whitespace in subtask ID', () => { 308 | displaySubtaskMoveError(' 5.2 ', 'clean', 'dirty'); 309 | 310 | expect(mockConsoleLog).toHaveBeenCalledWith( 311 | expect.stringContaining( 312 | '❌ Cannot move subtask 5.2 directly between tags' 313 | ) 314 | ); 315 | expect(mockConsoleLog).toHaveBeenCalledWith( 316 | expect.stringContaining('remove-subtask --id= 5.2 --convert') 317 | ); 318 | }); 319 | }); 320 | 321 | describe('displayInvalidTagCombinationError', () => { 322 | it('should display invalid tag combination error', () => { 323 | displayInvalidTagCombinationError( 324 | 'backlog', 325 | 'backlog', 326 | 'Source and target tags are identical' 327 | ); 328 | 329 | expect(mockConsoleLog).toHaveBeenCalledWith( 330 | expect.stringContaining('❌ Invalid tag combination') 331 | ); 332 | expect(mockConsoleLog).toHaveBeenCalledWith( 333 | expect.stringContaining('Error details:') 334 | ); 335 | expect(mockConsoleLog).toHaveBeenCalledWith( 336 | expect.stringContaining('• Source tag: "backlog"') 337 | ); 338 | expect(mockConsoleLog).toHaveBeenCalledWith( 339 | expect.stringContaining('• Target tag: "backlog"') 340 | ); 341 | expect(mockConsoleLog).toHaveBeenCalledWith( 342 | expect.stringContaining( 343 | '• Reason: Source and target tags are identical' 344 | ) 345 | ); 346 | expect(mockConsoleLog).toHaveBeenCalledWith( 347 | expect.stringContaining('Resolution options:') 348 | ); 349 | }); 350 | }); 351 | 352 | describe('displayDependencyValidationHints', () => { 353 | it('should display general hints by default', () => { 354 | displayDependencyValidationHints(); 355 | 356 | expect(mockConsoleLog).toHaveBeenCalledWith( 357 | expect.stringContaining('Helpful hints:') 358 | ); 359 | expect(mockConsoleLog).toHaveBeenCalledWith( 360 | expect.stringContaining('💡 Use "task-master validate-dependencies"') 361 | ); 362 | expect(mockConsoleLog).toHaveBeenCalledWith( 363 | expect.stringContaining('💡 Use "task-master fix-dependencies"') 364 | ); 365 | }); 366 | 367 | it('should display before-move hints', () => { 368 | displayDependencyValidationHints('before-move'); 369 | 370 | expect(mockConsoleLog).toHaveBeenCalledWith( 371 | expect.stringContaining('Helpful hints:') 372 | ); 373 | expect(mockConsoleLog).toHaveBeenCalledWith( 374 | expect.stringContaining( 375 | '💡 Tip: Run "task-master validate-dependencies"' 376 | ) 377 | ); 378 | expect(mockConsoleLog).toHaveBeenCalledWith( 379 | expect.stringContaining('💡 Tip: Use "task-master fix-dependencies"') 380 | ); 381 | }); 382 | 383 | it('should display after-error hints', () => { 384 | displayDependencyValidationHints('after-error'); 385 | 386 | expect(mockConsoleLog).toHaveBeenCalledWith( 387 | expect.stringContaining('Helpful hints:') 388 | ); 389 | expect(mockConsoleLog).toHaveBeenCalledWith( 390 | expect.stringContaining( 391 | '🔧 Quick fix: Run "task-master validate-dependencies"' 392 | ) 393 | ); 394 | expect(mockConsoleLog).toHaveBeenCalledWith( 395 | expect.stringContaining( 396 | '🔧 Quick fix: Use "task-master fix-dependencies"' 397 | ) 398 | ); 399 | }); 400 | 401 | it('should handle unknown context gracefully', () => { 402 | displayDependencyValidationHints('unknown-context'); 403 | 404 | expect(mockConsoleLog).toHaveBeenCalledWith( 405 | expect.stringContaining('Helpful hints:') 406 | ); 407 | // Should fall back to general hints 408 | expect(mockConsoleLog).toHaveBeenCalledWith( 409 | expect.stringContaining('💡 Use "task-master validate-dependencies"') 410 | ); 411 | }); 412 | }); 413 | }); 414 | 415 | /** 416 | * Test for ID type consistency in dependency comparisons 417 | * This test verifies that the fix for mixed string/number ID comparison issues works correctly 418 | */ 419 | 420 | describe('ID Type Consistency in Dependency Comparisons', () => { 421 | test('should handle mixed string/number ID comparisons correctly', () => { 422 | // Test the pattern that was fixed in the move-task tests 423 | const sourceTasks = [ 424 | { id: 1, title: 'Task 1' }, 425 | { id: 2, title: 'Task 2' }, 426 | { id: '3.1', title: 'Subtask 3.1' } 427 | ]; 428 | 429 | const allTasks = [ 430 | { id: 1, title: 'Task 1', dependencies: [2, '3.1'] }, 431 | { id: 2, title: 'Task 2', dependencies: ['1'] }, 432 | { 433 | id: 3, 434 | title: 'Task 3', 435 | subtasks: [{ id: 1, title: 'Subtask 3.1', dependencies: [1] }] 436 | } 437 | ]; 438 | 439 | // Test the fixed pattern: normalize source IDs and compare with string conversion 440 | const sourceIds = sourceTasks.map((t) => t.id); 441 | const normalizedSourceIds = sourceIds.map((id) => String(id)); 442 | 443 | // Test that dependencies are correctly identified regardless of type 444 | const result = []; 445 | allTasks.forEach((task) => { 446 | if (task.dependencies && Array.isArray(task.dependencies)) { 447 | const hasDependency = task.dependencies.some((depId) => 448 | normalizedSourceIds.includes(String(depId)) 449 | ); 450 | if (hasDependency) { 451 | result.push(task.id); 452 | } 453 | } 454 | }); 455 | 456 | // Verify that the comparison works correctly 457 | expect(result).toContain(1); // Task 1 has dependency on 2 and '3.1' 458 | expect(result).toContain(2); // Task 2 has dependency on '1' 459 | 460 | // Test edge cases 461 | const mixedDependencies = [ 462 | { id: 1, dependencies: [1, 2, '3.1', '4.2'] }, 463 | { id: 2, dependencies: ['1', 3, '5.1'] } 464 | ]; 465 | 466 | const testSourceIds = [1, '3.1', 4]; 467 | const normalizedTestSourceIds = testSourceIds.map((id) => String(id)); 468 | 469 | mixedDependencies.forEach((task) => { 470 | const hasMatch = task.dependencies.some((depId) => 471 | normalizedTestSourceIds.includes(String(depId)) 472 | ); 473 | expect(typeof hasMatch).toBe('boolean'); 474 | expect(hasMatch).toBe(true); // Should find matches in both tasks 475 | }); 476 | }); 477 | 478 | test('should handle edge cases in ID normalization', () => { 479 | // Test various ID formats 480 | const testCases = [ 481 | { source: 1, dependency: '1', expected: true }, 482 | { source: '1', dependency: 1, expected: true }, 483 | { source: '3.1', dependency: '3.1', expected: true }, 484 | { source: 3, dependency: '3.1', expected: false }, // Different formats 485 | { source: '3.1', dependency: 3, expected: false }, // Different formats 486 | { source: 1, dependency: 2, expected: false }, // No match 487 | { source: '1.2', dependency: '1.2', expected: true }, 488 | { source: 1, dependency: null, expected: false }, // Handle null 489 | { source: 1, dependency: undefined, expected: false } // Handle undefined 490 | ]; 491 | 492 | testCases.forEach(({ source, dependency, expected }) => { 493 | const normalizedSourceIds = [String(source)]; 494 | const hasMatch = normalizedSourceIds.includes(String(dependency)); 495 | expect(hasMatch).toBe(expected); 496 | }); 497 | }); 498 | }); 499 | ``` -------------------------------------------------------------------------------- /apps/cli/src/ui/components/dashboard.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Dashboard components for Task Master CLI 3 | * Displays project statistics and dependency information 4 | */ 5 | 6 | import chalk from 'chalk'; 7 | import boxen from 'boxen'; 8 | import type { Task, TaskPriority } from '@tm/core/types'; 9 | 10 | /** 11 | * Statistics for task collection 12 | */ 13 | export interface TaskStatistics { 14 | total: number; 15 | done: number; 16 | inProgress: number; 17 | pending: number; 18 | blocked: number; 19 | deferred: number; 20 | cancelled: number; 21 | review?: number; 22 | completionPercentage: number; 23 | } 24 | 25 | /** 26 | * Statistics for dependencies 27 | */ 28 | export interface DependencyStatistics { 29 | tasksWithNoDeps: number; 30 | tasksReadyToWork: number; 31 | tasksBlockedByDeps: number; 32 | mostDependedOnTaskId?: number; 33 | mostDependedOnCount?: number; 34 | avgDependenciesPerTask: number; 35 | } 36 | 37 | /** 38 | * Next task information 39 | */ 40 | export interface NextTaskInfo { 41 | id: string | number; 42 | title: string; 43 | priority?: TaskPriority; 44 | dependencies?: (string | number)[]; 45 | complexity?: number | string; 46 | } 47 | 48 | /** 49 | * Status breakdown for progress bars 50 | */ 51 | export interface StatusBreakdown { 52 | 'in-progress'?: number; 53 | pending?: number; 54 | blocked?: number; 55 | deferred?: number; 56 | cancelled?: number; 57 | review?: number; 58 | } 59 | 60 | /** 61 | * Create a progress bar with color-coded status segments 62 | */ 63 | function createProgressBar( 64 | completionPercentage: number, 65 | width: number = 30, 66 | statusBreakdown?: StatusBreakdown 67 | ): string { 68 | // If no breakdown provided, use simple green bar 69 | if (!statusBreakdown) { 70 | const filled = Math.round((completionPercentage / 100) * width); 71 | const empty = width - filled; 72 | return chalk.green('█').repeat(filled) + chalk.gray('░').repeat(empty); 73 | } 74 | 75 | // Build the bar with different colored sections 76 | // Order matches the status display: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked 77 | let bar = ''; 78 | let charsUsed = 0; 79 | 80 | // 1. Green filled blocks for completed tasks (done) 81 | const completedChars = Math.round((completionPercentage / 100) * width); 82 | if (completedChars > 0) { 83 | bar += chalk.green('█').repeat(completedChars); 84 | charsUsed += completedChars; 85 | } 86 | 87 | // 2. Gray filled blocks for cancelled (won't be done) 88 | if (statusBreakdown.cancelled && charsUsed < width) { 89 | const cancelledChars = Math.round( 90 | (statusBreakdown.cancelled / 100) * width 91 | ); 92 | const actualChars = Math.min(cancelledChars, width - charsUsed); 93 | if (actualChars > 0) { 94 | bar += chalk.gray('█').repeat(actualChars); 95 | charsUsed += actualChars; 96 | } 97 | } 98 | 99 | // 3. Gray filled blocks for deferred (won't be done now) 100 | if (statusBreakdown.deferred && charsUsed < width) { 101 | const deferredChars = Math.round((statusBreakdown.deferred / 100) * width); 102 | const actualChars = Math.min(deferredChars, width - charsUsed); 103 | if (actualChars > 0) { 104 | bar += chalk.gray('█').repeat(actualChars); 105 | charsUsed += actualChars; 106 | } 107 | } 108 | 109 | // 4. Blue filled blocks for in-progress (actively working) 110 | if (statusBreakdown['in-progress'] && charsUsed < width) { 111 | const inProgressChars = Math.round( 112 | (statusBreakdown['in-progress'] / 100) * width 113 | ); 114 | const actualChars = Math.min(inProgressChars, width - charsUsed); 115 | if (actualChars > 0) { 116 | bar += chalk.blue('█').repeat(actualChars); 117 | charsUsed += actualChars; 118 | } 119 | } 120 | 121 | // 5. Magenta empty blocks for review (almost done) 122 | if (statusBreakdown.review && charsUsed < width) { 123 | const reviewChars = Math.round((statusBreakdown.review / 100) * width); 124 | const actualChars = Math.min(reviewChars, width - charsUsed); 125 | if (actualChars > 0) { 126 | bar += chalk.magenta('░').repeat(actualChars); 127 | charsUsed += actualChars; 128 | } 129 | } 130 | 131 | // 6. Yellow empty blocks for pending (ready to start) 132 | if (statusBreakdown.pending && charsUsed < width) { 133 | const pendingChars = Math.round((statusBreakdown.pending / 100) * width); 134 | const actualChars = Math.min(pendingChars, width - charsUsed); 135 | if (actualChars > 0) { 136 | bar += chalk.yellow('░').repeat(actualChars); 137 | charsUsed += actualChars; 138 | } 139 | } 140 | 141 | // 7. Red empty blocks for blocked (can't start yet) 142 | if (statusBreakdown.blocked && charsUsed < width) { 143 | const blockedChars = Math.round((statusBreakdown.blocked / 100) * width); 144 | const actualChars = Math.min(blockedChars, width - charsUsed); 145 | if (actualChars > 0) { 146 | bar += chalk.red('░').repeat(actualChars); 147 | charsUsed += actualChars; 148 | } 149 | } 150 | 151 | // Fill any remaining space with gray empty yellow blocks 152 | if (charsUsed < width) { 153 | bar += chalk.yellow('░').repeat(width - charsUsed); 154 | } 155 | 156 | return bar; 157 | } 158 | 159 | /** 160 | * Calculate task statistics from a list of tasks 161 | */ 162 | export function calculateTaskStatistics(tasks: Task[]): TaskStatistics { 163 | const stats: TaskStatistics = { 164 | total: tasks.length, 165 | done: 0, 166 | inProgress: 0, 167 | pending: 0, 168 | blocked: 0, 169 | deferred: 0, 170 | cancelled: 0, 171 | review: 0, 172 | completionPercentage: 0 173 | }; 174 | 175 | tasks.forEach((task) => { 176 | switch (task.status) { 177 | case 'done': 178 | stats.done++; 179 | break; 180 | case 'in-progress': 181 | stats.inProgress++; 182 | break; 183 | case 'pending': 184 | stats.pending++; 185 | break; 186 | case 'blocked': 187 | stats.blocked++; 188 | break; 189 | case 'deferred': 190 | stats.deferred++; 191 | break; 192 | case 'cancelled': 193 | stats.cancelled++; 194 | break; 195 | case 'review': 196 | stats.review = (stats.review || 0) + 1; 197 | break; 198 | } 199 | }); 200 | 201 | stats.completionPercentage = 202 | stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0; 203 | 204 | return stats; 205 | } 206 | 207 | /** 208 | * Calculate subtask statistics from tasks 209 | */ 210 | export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics { 211 | const stats: TaskStatistics = { 212 | total: 0, 213 | done: 0, 214 | inProgress: 0, 215 | pending: 0, 216 | blocked: 0, 217 | deferred: 0, 218 | cancelled: 0, 219 | review: 0, 220 | completionPercentage: 0 221 | }; 222 | 223 | tasks.forEach((task) => { 224 | if (task.subtasks && task.subtasks.length > 0) { 225 | task.subtasks.forEach((subtask) => { 226 | stats.total++; 227 | switch (subtask.status) { 228 | case 'done': 229 | stats.done++; 230 | break; 231 | case 'in-progress': 232 | stats.inProgress++; 233 | break; 234 | case 'pending': 235 | stats.pending++; 236 | break; 237 | case 'blocked': 238 | stats.blocked++; 239 | break; 240 | case 'deferred': 241 | stats.deferred++; 242 | break; 243 | case 'cancelled': 244 | stats.cancelled++; 245 | break; 246 | case 'review': 247 | stats.review = (stats.review || 0) + 1; 248 | break; 249 | } 250 | }); 251 | } 252 | }); 253 | 254 | stats.completionPercentage = 255 | stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0; 256 | 257 | return stats; 258 | } 259 | 260 | /** 261 | * Calculate dependency statistics 262 | */ 263 | export function calculateDependencyStatistics( 264 | tasks: Task[] 265 | ): DependencyStatistics { 266 | const completedTaskIds = new Set( 267 | tasks.filter((t) => t.status === 'done').map((t) => t.id) 268 | ); 269 | 270 | const tasksWithNoDeps = tasks.filter( 271 | (t) => 272 | t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0) 273 | ).length; 274 | 275 | const tasksWithAllDepsSatisfied = tasks.filter( 276 | (t) => 277 | t.status !== 'done' && 278 | t.dependencies && 279 | t.dependencies.length > 0 && 280 | t.dependencies.every((depId) => completedTaskIds.has(depId)) 281 | ).length; 282 | 283 | const tasksBlockedByDeps = tasks.filter( 284 | (t) => 285 | t.status !== 'done' && 286 | t.dependencies && 287 | t.dependencies.length > 0 && 288 | !t.dependencies.every((depId) => completedTaskIds.has(depId)) 289 | ).length; 290 | 291 | // Calculate most depended-on task 292 | const dependencyCount: Record<string, number> = {}; 293 | tasks.forEach((task) => { 294 | if (task.dependencies && task.dependencies.length > 0) { 295 | task.dependencies.forEach((depId) => { 296 | const key = String(depId); 297 | dependencyCount[key] = (dependencyCount[key] || 0) + 1; 298 | }); 299 | } 300 | }); 301 | 302 | let mostDependedOnTaskId: number | undefined; 303 | let mostDependedOnCount = 0; 304 | 305 | for (const [taskId, count] of Object.entries(dependencyCount)) { 306 | if (count > mostDependedOnCount) { 307 | mostDependedOnCount = count; 308 | mostDependedOnTaskId = parseInt(taskId); 309 | } 310 | } 311 | 312 | // Calculate average dependencies 313 | const totalDependencies = tasks.reduce( 314 | (sum, task) => sum + (task.dependencies ? task.dependencies.length : 0), 315 | 0 316 | ); 317 | const avgDependenciesPerTask = 318 | tasks.length > 0 ? totalDependencies / tasks.length : 0; 319 | 320 | return { 321 | tasksWithNoDeps, 322 | tasksReadyToWork: tasksWithNoDeps + tasksWithAllDepsSatisfied, 323 | tasksBlockedByDeps, 324 | mostDependedOnTaskId, 325 | mostDependedOnCount, 326 | avgDependenciesPerTask 327 | }; 328 | } 329 | 330 | /** 331 | * Get priority counts 332 | */ 333 | export function getPriorityBreakdown( 334 | tasks: Task[] 335 | ): Record<TaskPriority, number> { 336 | const breakdown: Record<TaskPriority, number> = { 337 | critical: 0, 338 | high: 0, 339 | medium: 0, 340 | low: 0 341 | }; 342 | 343 | tasks.forEach((task) => { 344 | const priority = task.priority || 'medium'; 345 | breakdown[priority]++; 346 | }); 347 | 348 | return breakdown; 349 | } 350 | 351 | /** 352 | * Calculate status breakdown as percentages 353 | */ 354 | function calculateStatusBreakdown(stats: TaskStatistics): StatusBreakdown { 355 | if (stats.total === 0) return {}; 356 | 357 | return { 358 | 'in-progress': (stats.inProgress / stats.total) * 100, 359 | pending: (stats.pending / stats.total) * 100, 360 | blocked: (stats.blocked / stats.total) * 100, 361 | deferred: (stats.deferred / stats.total) * 100, 362 | cancelled: (stats.cancelled / stats.total) * 100, 363 | review: ((stats.review || 0) / stats.total) * 100 364 | }; 365 | } 366 | 367 | /** 368 | * Format status counts in the correct order with colors 369 | * @param stats - The statistics object containing counts 370 | * @param isSubtask - Whether this is for subtasks (affects "Done" vs "Completed" label) 371 | */ 372 | function formatStatusLine( 373 | stats: TaskStatistics, 374 | isSubtask: boolean = false 375 | ): string { 376 | const parts: string[] = []; 377 | 378 | // Order: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked 379 | if (isSubtask) { 380 | parts.push(`Completed: ${chalk.green(`${stats.done}/${stats.total}`)}`); 381 | } else { 382 | parts.push(`Done: ${chalk.green(stats.done)}`); 383 | } 384 | 385 | parts.push(`Cancelled: ${chalk.gray(stats.cancelled)}`); 386 | parts.push(`Deferred: ${chalk.gray(stats.deferred)}`); 387 | 388 | // Add line break for second row 389 | const firstLine = parts.join(' '); 390 | parts.length = 0; 391 | 392 | parts.push(`In Progress: ${chalk.blue(stats.inProgress)}`); 393 | parts.push(`Review: ${chalk.magenta(stats.review || 0)}`); 394 | parts.push(`Pending: ${chalk.yellow(stats.pending)}`); 395 | parts.push(`Blocked: ${chalk.red(stats.blocked)}`); 396 | 397 | const secondLine = parts.join(' '); 398 | 399 | return firstLine + '\n' + secondLine; 400 | } 401 | 402 | /** 403 | * Display the project dashboard box 404 | */ 405 | export function displayProjectDashboard( 406 | taskStats: TaskStatistics, 407 | subtaskStats: TaskStatistics, 408 | priorityBreakdown: Record<TaskPriority, number> 409 | ): string { 410 | // Calculate status breakdowns using the helper function 411 | const taskStatusBreakdown = calculateStatusBreakdown(taskStats); 412 | const subtaskStatusBreakdown = calculateStatusBreakdown(subtaskStats); 413 | 414 | // Create progress bars with the breakdowns 415 | const taskProgressBar = createProgressBar( 416 | taskStats.completionPercentage, 417 | 30, 418 | taskStatusBreakdown 419 | ); 420 | const subtaskProgressBar = createProgressBar( 421 | subtaskStats.completionPercentage, 422 | 30, 423 | subtaskStatusBreakdown 424 | ); 425 | 426 | const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}/${taskStats.total}`; 427 | const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}/${subtaskStats.total}`; 428 | 429 | const content = 430 | chalk.white.bold('Project Dashboard') + 431 | '\n' + 432 | `Tasks Progress: ${taskProgressBar} ${chalk.yellow(taskPercentage)}\n` + 433 | formatStatusLine(taskStats, false) + 434 | '\n\n' + 435 | `Subtasks Progress: ${subtaskProgressBar} ${chalk.cyan(subtaskPercentage)}\n` + 436 | formatStatusLine(subtaskStats, true) + 437 | '\n\n' + 438 | chalk.cyan.bold('Priority Breakdown:') + 439 | '\n' + 440 | `${chalk.red('•')} ${chalk.white('High priority:')} ${priorityBreakdown.high}\n` + 441 | `${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${priorityBreakdown.medium}\n` + 442 | `${chalk.green('•')} ${chalk.white('Low priority:')} ${priorityBreakdown.low}`; 443 | 444 | return content; 445 | } 446 | 447 | /** 448 | * Display the dependency dashboard box 449 | */ 450 | export function displayDependencyDashboard( 451 | depStats: DependencyStatistics, 452 | nextTask?: NextTaskInfo 453 | ): string { 454 | const content = 455 | chalk.white.bold('Dependency Status & Next Task') + 456 | '\n' + 457 | chalk.cyan.bold('Dependency Metrics:') + 458 | '\n' + 459 | `${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${depStats.tasksWithNoDeps}\n` + 460 | `${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${depStats.tasksReadyToWork}\n` + 461 | `${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${depStats.tasksBlockedByDeps}\n` + 462 | `${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${ 463 | depStats.mostDependedOnTaskId 464 | ? chalk.cyan( 465 | `#${depStats.mostDependedOnTaskId} (${depStats.mostDependedOnCount} dependents)` 466 | ) 467 | : chalk.gray('None') 468 | }\n` + 469 | `${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${depStats.avgDependenciesPerTask.toFixed(1)}\n\n` + 470 | chalk.cyan.bold('Next Task to Work On:') + 471 | '\n' + 472 | `ID: ${nextTask ? chalk.cyan(String(nextTask.id)) : chalk.gray('N/A')} - ${ 473 | nextTask 474 | ? chalk.white.bold(nextTask.title) 475 | : chalk.yellow('No task available') 476 | }\n` + 477 | `Priority: ${nextTask?.priority || chalk.gray('N/A')} Dependencies: ${ 478 | nextTask?.dependencies?.length 479 | ? chalk.cyan(nextTask.dependencies.join(', ')) 480 | : chalk.gray('None') 481 | }\n` + 482 | `Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`; 483 | 484 | return content; 485 | } 486 | 487 | /** 488 | * Display dashboard boxes side by side or stacked 489 | */ 490 | export function displayDashboards( 491 | taskStats: TaskStatistics, 492 | subtaskStats: TaskStatistics, 493 | priorityBreakdown: Record<TaskPriority, number>, 494 | depStats: DependencyStatistics, 495 | nextTask?: NextTaskInfo 496 | ): void { 497 | const projectDashboardContent = displayProjectDashboard( 498 | taskStats, 499 | subtaskStats, 500 | priorityBreakdown 501 | ); 502 | const dependencyDashboardContent = displayDependencyDashboard( 503 | depStats, 504 | nextTask 505 | ); 506 | 507 | // Get terminal width 508 | const terminalWidth = process.stdout.columns || 80; 509 | const minDashboardWidth = 50; 510 | const minDependencyWidth = 50; 511 | const totalMinWidth = minDashboardWidth + minDependencyWidth + 4; 512 | 513 | // If terminal is wide enough, show side by side 514 | if (terminalWidth >= totalMinWidth) { 515 | const halfWidth = Math.floor(terminalWidth / 2); 516 | const boxContentWidth = halfWidth - 4; 517 | 518 | const dashboardBox = boxen(projectDashboardContent, { 519 | padding: 1, 520 | borderColor: 'blue', 521 | borderStyle: 'round', 522 | width: boxContentWidth, 523 | dimBorder: false 524 | }); 525 | 526 | const dependencyBox = boxen(dependencyDashboardContent, { 527 | padding: 1, 528 | borderColor: 'magenta', 529 | borderStyle: 'round', 530 | width: boxContentWidth, 531 | dimBorder: false 532 | }); 533 | 534 | // Create side-by-side layout 535 | const dashboardLines = dashboardBox.split('\n'); 536 | const dependencyLines = dependencyBox.split('\n'); 537 | const maxHeight = Math.max(dashboardLines.length, dependencyLines.length); 538 | 539 | const combinedLines = []; 540 | for (let i = 0; i < maxHeight; i++) { 541 | const dashLine = i < dashboardLines.length ? dashboardLines[i] : ''; 542 | const depLine = i < dependencyLines.length ? dependencyLines[i] : ''; 543 | const paddedDashLine = dashLine.padEnd(halfWidth, ' '); 544 | combinedLines.push(paddedDashLine + depLine); 545 | } 546 | 547 | console.log(combinedLines.join('\n')); 548 | } else { 549 | // Show stacked vertically 550 | const dashboardBox = boxen(projectDashboardContent, { 551 | padding: 1, 552 | borderColor: 'blue', 553 | borderStyle: 'round', 554 | margin: { top: 0, bottom: 1 } 555 | }); 556 | 557 | const dependencyBox = boxen(dependencyDashboardContent, { 558 | padding: 1, 559 | borderColor: 'magenta', 560 | borderStyle: 'round', 561 | margin: { top: 0, bottom: 1 } 562 | }); 563 | 564 | console.log(dashboardBox); 565 | console.log(dependencyBox); 566 | } 567 | } 568 | ``` -------------------------------------------------------------------------------- /src/ai-providers/custom-sdk/claude-code/language-model.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * @fileoverview Claude Code Language Model implementation 3 | */ 4 | 5 | import { NoSuchModelError } from '@ai-sdk/provider'; 6 | import { generateId } from '@ai-sdk/provider-utils'; 7 | import { convertToClaudeCodeMessages } from './message-converter.js'; 8 | import { extractJson } from './json-extractor.js'; 9 | import { createAPICallError, createAuthenticationError } from './errors.js'; 10 | 11 | let query; 12 | let AbortError; 13 | 14 | async function loadClaudeCodeModule() { 15 | if (!query || !AbortError) { 16 | try { 17 | const mod = await import('@anthropic-ai/claude-code'); 18 | query = mod.query; 19 | AbortError = mod.AbortError; 20 | } catch (err) { 21 | throw new Error( 22 | "Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider." 23 | ); 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * @typedef {import('./types.js').ClaudeCodeSettings} ClaudeCodeSettings 30 | * @typedef {import('./types.js').ClaudeCodeModelId} ClaudeCodeModelId 31 | * @typedef {import('./types.js').ClaudeCodeLanguageModelOptions} ClaudeCodeLanguageModelOptions 32 | */ 33 | 34 | const modelMap = { 35 | opus: 'opus', 36 | sonnet: 'sonnet' 37 | }; 38 | 39 | export class ClaudeCodeLanguageModel { 40 | specificationVersion = 'v1'; 41 | defaultObjectGenerationMode = 'json'; 42 | supportsImageUrls = false; 43 | supportsStructuredOutputs = false; 44 | 45 | /** @type {ClaudeCodeModelId} */ 46 | modelId; 47 | 48 | /** @type {ClaudeCodeSettings} */ 49 | settings; 50 | 51 | /** @type {string|undefined} */ 52 | sessionId; 53 | 54 | /** 55 | * @param {ClaudeCodeLanguageModelOptions} options 56 | */ 57 | constructor(options) { 58 | this.modelId = options.id; 59 | this.settings = options.settings ?? {}; 60 | 61 | // Validate model ID format 62 | if ( 63 | !this.modelId || 64 | typeof this.modelId !== 'string' || 65 | this.modelId.trim() === '' 66 | ) { 67 | throw new NoSuchModelError({ 68 | modelId: this.modelId, 69 | modelType: 'languageModel' 70 | }); 71 | } 72 | } 73 | 74 | get provider() { 75 | return 'claude-code'; 76 | } 77 | 78 | /** 79 | * Get the model name for Claude Code CLI 80 | * @returns {string} 81 | */ 82 | getModel() { 83 | const mapped = modelMap[this.modelId]; 84 | return mapped ?? this.modelId; 85 | } 86 | 87 | /** 88 | * Generate unsupported parameter warnings 89 | * @param {Object} options - Generation options 90 | * @returns {Array} Warnings array 91 | */ 92 | generateUnsupportedWarnings(options) { 93 | const warnings = []; 94 | const unsupportedParams = []; 95 | 96 | // Check for unsupported parameters 97 | if (options.temperature !== undefined) 98 | unsupportedParams.push('temperature'); 99 | if (options.maxTokens !== undefined) unsupportedParams.push('maxTokens'); 100 | if (options.topP !== undefined) unsupportedParams.push('topP'); 101 | if (options.topK !== undefined) unsupportedParams.push('topK'); 102 | if (options.presencePenalty !== undefined) 103 | unsupportedParams.push('presencePenalty'); 104 | if (options.frequencyPenalty !== undefined) 105 | unsupportedParams.push('frequencyPenalty'); 106 | if (options.stopSequences !== undefined && options.stopSequences.length > 0) 107 | unsupportedParams.push('stopSequences'); 108 | if (options.seed !== undefined) unsupportedParams.push('seed'); 109 | 110 | if (unsupportedParams.length > 0) { 111 | // Add a warning for each unsupported parameter 112 | for (const param of unsupportedParams) { 113 | warnings.push({ 114 | type: 'unsupported-setting', 115 | setting: param, 116 | details: `Claude Code CLI does not support the ${param} parameter. It will be ignored.` 117 | }); 118 | } 119 | } 120 | 121 | return warnings; 122 | } 123 | 124 | /** 125 | * Generate text using Claude Code 126 | * @param {Object} options - Generation options 127 | * @returns {Promise<Object>} 128 | */ 129 | async doGenerate(options) { 130 | await loadClaudeCodeModule(); 131 | const { messagesPrompt } = convertToClaudeCodeMessages( 132 | options.prompt, 133 | options.mode 134 | ); 135 | 136 | const abortController = new AbortController(); 137 | if (options.abortSignal) { 138 | options.abortSignal.addEventListener('abort', () => 139 | abortController.abort() 140 | ); 141 | } 142 | 143 | const queryOptions = { 144 | model: this.getModel(), 145 | abortController, 146 | resume: this.sessionId, 147 | pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable, 148 | customSystemPrompt: this.settings.customSystemPrompt, 149 | appendSystemPrompt: this.settings.appendSystemPrompt, 150 | maxTurns: this.settings.maxTurns, 151 | maxThinkingTokens: this.settings.maxThinkingTokens, 152 | cwd: this.settings.cwd, 153 | executable: this.settings.executable, 154 | executableArgs: this.settings.executableArgs, 155 | permissionMode: this.settings.permissionMode, 156 | permissionPromptToolName: this.settings.permissionPromptToolName, 157 | continue: this.settings.continue, 158 | allowedTools: this.settings.allowedTools, 159 | disallowedTools: this.settings.disallowedTools, 160 | mcpServers: this.settings.mcpServers 161 | }; 162 | 163 | let text = ''; 164 | let usage = { promptTokens: 0, completionTokens: 0 }; 165 | let finishReason = 'stop'; 166 | let costUsd; 167 | let durationMs; 168 | let rawUsage; 169 | const warnings = this.generateUnsupportedWarnings(options); 170 | 171 | try { 172 | if (!query) { 173 | throw new Error( 174 | "Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider." 175 | ); 176 | } 177 | const response = query({ 178 | prompt: messagesPrompt, 179 | options: queryOptions 180 | }); 181 | 182 | for await (const message of response) { 183 | if (message.type === 'assistant') { 184 | text += message.message.content 185 | .map((c) => (c.type === 'text' ? c.text : '')) 186 | .join(''); 187 | } else if (message.type === 'result') { 188 | this.sessionId = message.session_id; 189 | costUsd = message.total_cost_usd; 190 | durationMs = message.duration_ms; 191 | 192 | if ('usage' in message) { 193 | rawUsage = message.usage; 194 | usage = { 195 | promptTokens: 196 | (message.usage.cache_creation_input_tokens ?? 0) + 197 | (message.usage.cache_read_input_tokens ?? 0) + 198 | (message.usage.input_tokens ?? 0), 199 | completionTokens: message.usage.output_tokens ?? 0 200 | }; 201 | } 202 | 203 | if (message.subtype === 'error_max_turns') { 204 | finishReason = 'length'; 205 | } else if (message.subtype === 'error_during_execution') { 206 | finishReason = 'error'; 207 | } 208 | } else if (message.type === 'system' && message.subtype === 'init') { 209 | this.sessionId = message.session_id; 210 | } 211 | } 212 | } catch (error) { 213 | // ------------------------------------------------------------- 214 | // Work-around for Claude-Code CLI/SDK JSON truncation bug (#913) 215 | // ------------------------------------------------------------- 216 | // If the SDK throws a JSON SyntaxError *but* we already hold some 217 | // buffered text, assume the response was truncated by the CLI. 218 | // We keep the accumulated text, mark the finish reason, push a 219 | // provider-warning and *skip* the normal error handling so Task 220 | // Master can continue processing. 221 | const isJsonTruncation = 222 | error instanceof SyntaxError && 223 | /JSON/i.test(error.message || '') && 224 | (error.message.includes('position') || 225 | error.message.includes('Unexpected end')); 226 | if (isJsonTruncation && text && text.length > 0) { 227 | warnings.push({ 228 | type: 'provider-warning', 229 | details: 230 | 'Claude Code SDK emitted a JSON parse error but Task Master recovered buffered text (possible CLI truncation).' 231 | }); 232 | finishReason = 'truncated'; 233 | // Skip re-throwing: fall through so the caller receives usable data 234 | } else { 235 | if (AbortError && error instanceof AbortError) { 236 | throw options.abortSignal?.aborted 237 | ? options.abortSignal.reason 238 | : error; 239 | } 240 | 241 | // Check for authentication errors 242 | if ( 243 | error.message?.includes('not logged in') || 244 | error.message?.includes('authentication') || 245 | error.exitCode === 401 246 | ) { 247 | throw createAuthenticationError({ 248 | message: 249 | error.message || 250 | 'Authentication failed. Please ensure Claude Code CLI is properly authenticated.' 251 | }); 252 | } 253 | 254 | // Wrap other errors with API call error 255 | throw createAPICallError({ 256 | message: error.message || 'Claude Code CLI error', 257 | code: error.code, 258 | exitCode: error.exitCode, 259 | stderr: error.stderr, 260 | promptExcerpt: messagesPrompt.substring(0, 200), 261 | isRetryable: error.code === 'ENOENT' || error.code === 'ECONNREFUSED' 262 | }); 263 | } 264 | } 265 | 266 | // Extract JSON if in object-json mode 267 | if (options.mode?.type === 'object-json' && text) { 268 | text = extractJson(text); 269 | } 270 | 271 | return { 272 | text: text || undefined, 273 | usage, 274 | finishReason, 275 | rawCall: { 276 | rawPrompt: messagesPrompt, 277 | rawSettings: queryOptions 278 | }, 279 | warnings: warnings.length > 0 ? warnings : undefined, 280 | response: { 281 | id: generateId(), 282 | timestamp: new Date(), 283 | modelId: this.modelId 284 | }, 285 | request: { 286 | body: messagesPrompt 287 | }, 288 | providerMetadata: { 289 | 'claude-code': { 290 | ...(this.sessionId !== undefined && { sessionId: this.sessionId }), 291 | ...(costUsd !== undefined && { costUsd }), 292 | ...(durationMs !== undefined && { durationMs }), 293 | ...(rawUsage !== undefined && { rawUsage }) 294 | } 295 | } 296 | }; 297 | } 298 | 299 | /** 300 | * Stream text using Claude Code 301 | * @param {Object} options - Stream options 302 | * @returns {Promise<Object>} 303 | */ 304 | async doStream(options) { 305 | await loadClaudeCodeModule(); 306 | const { messagesPrompt } = convertToClaudeCodeMessages( 307 | options.prompt, 308 | options.mode 309 | ); 310 | 311 | const abortController = new AbortController(); 312 | if (options.abortSignal) { 313 | options.abortSignal.addEventListener('abort', () => 314 | abortController.abort() 315 | ); 316 | } 317 | 318 | const queryOptions = { 319 | model: this.getModel(), 320 | abortController, 321 | resume: this.sessionId, 322 | pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable, 323 | customSystemPrompt: this.settings.customSystemPrompt, 324 | appendSystemPrompt: this.settings.appendSystemPrompt, 325 | maxTurns: this.settings.maxTurns, 326 | maxThinkingTokens: this.settings.maxThinkingTokens, 327 | cwd: this.settings.cwd, 328 | executable: this.settings.executable, 329 | executableArgs: this.settings.executableArgs, 330 | permissionMode: this.settings.permissionMode, 331 | permissionPromptToolName: this.settings.permissionPromptToolName, 332 | continue: this.settings.continue, 333 | allowedTools: this.settings.allowedTools, 334 | disallowedTools: this.settings.disallowedTools, 335 | mcpServers: this.settings.mcpServers 336 | }; 337 | 338 | const warnings = this.generateUnsupportedWarnings(options); 339 | 340 | const stream = new ReadableStream({ 341 | start: async (controller) => { 342 | try { 343 | if (!query) { 344 | throw new Error( 345 | "Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider." 346 | ); 347 | } 348 | const response = query({ 349 | prompt: messagesPrompt, 350 | options: queryOptions 351 | }); 352 | 353 | let usage = { promptTokens: 0, completionTokens: 0 }; 354 | let accumulatedText = ''; 355 | 356 | for await (const message of response) { 357 | if (message.type === 'assistant') { 358 | const text = message.message.content 359 | .map((c) => (c.type === 'text' ? c.text : '')) 360 | .join(''); 361 | 362 | if (text) { 363 | accumulatedText += text; 364 | 365 | // In object-json mode, we need to accumulate the full text 366 | // and extract JSON at the end, so don't stream individual deltas 367 | if (options.mode?.type !== 'object-json') { 368 | controller.enqueue({ 369 | type: 'text-delta', 370 | textDelta: text 371 | }); 372 | } 373 | } 374 | } else if (message.type === 'result') { 375 | let rawUsage; 376 | if ('usage' in message) { 377 | rawUsage = message.usage; 378 | usage = { 379 | promptTokens: 380 | (message.usage.cache_creation_input_tokens ?? 0) + 381 | (message.usage.cache_read_input_tokens ?? 0) + 382 | (message.usage.input_tokens ?? 0), 383 | completionTokens: message.usage.output_tokens ?? 0 384 | }; 385 | } 386 | 387 | let finishReason = 'stop'; 388 | if (message.subtype === 'error_max_turns') { 389 | finishReason = 'length'; 390 | } else if (message.subtype === 'error_during_execution') { 391 | finishReason = 'error'; 392 | } 393 | 394 | // Store session ID in the model instance 395 | this.sessionId = message.session_id; 396 | 397 | // In object-json mode, extract JSON and send the full text at once 398 | if (options.mode?.type === 'object-json' && accumulatedText) { 399 | const extractedJson = extractJson(accumulatedText); 400 | controller.enqueue({ 401 | type: 'text-delta', 402 | textDelta: extractedJson 403 | }); 404 | } 405 | 406 | controller.enqueue({ 407 | type: 'finish', 408 | finishReason, 409 | usage, 410 | providerMetadata: { 411 | 'claude-code': { 412 | sessionId: message.session_id, 413 | ...(message.total_cost_usd !== undefined && { 414 | costUsd: message.total_cost_usd 415 | }), 416 | ...(message.duration_ms !== undefined && { 417 | durationMs: message.duration_ms 418 | }), 419 | ...(rawUsage !== undefined && { rawUsage }) 420 | } 421 | } 422 | }); 423 | } else if ( 424 | message.type === 'system' && 425 | message.subtype === 'init' 426 | ) { 427 | // Store session ID for future use 428 | this.sessionId = message.session_id; 429 | 430 | // Emit response metadata when session is initialized 431 | controller.enqueue({ 432 | type: 'response-metadata', 433 | id: message.session_id, 434 | timestamp: new Date(), 435 | modelId: this.modelId 436 | }); 437 | } 438 | } 439 | 440 | // ------------------------------------------------------------- 441 | // Work-around for Claude-Code CLI/SDK JSON truncation bug (#913) 442 | // ------------------------------------------------------------- 443 | // If we hit the SDK JSON SyntaxError but have buffered text, finalize 444 | // the stream gracefully instead of emitting an error. 445 | const isJsonTruncation = 446 | error instanceof SyntaxError && 447 | /JSON/i.test(error.message || '') && 448 | (error.message.includes('position') || 449 | error.message.includes('Unexpected end')); 450 | 451 | if ( 452 | isJsonTruncation && 453 | accumulatedText && 454 | accumulatedText.length > 0 455 | ) { 456 | // Prepare final text payload 457 | const finalText = 458 | options.mode?.type === 'object-json' 459 | ? extractJson(accumulatedText) 460 | : accumulatedText; 461 | 462 | // Emit any remaining text 463 | controller.enqueue({ 464 | type: 'text-delta', 465 | textDelta: finalText 466 | }); 467 | 468 | // Emit finish with truncated reason and warning 469 | controller.enqueue({ 470 | type: 'finish', 471 | finishReason: 'truncated', 472 | usage, 473 | providerMetadata: { 'claude-code': { truncated: true } }, 474 | warnings: [ 475 | { 476 | type: 'provider-warning', 477 | details: 478 | 'Claude Code SDK JSON truncation detected; stream recovered.' 479 | } 480 | ] 481 | }); 482 | 483 | controller.close(); 484 | return; // Skip normal error path 485 | } 486 | 487 | controller.close(); 488 | } catch (error) { 489 | let errorToEmit; 490 | 491 | if (AbortError && error instanceof AbortError) { 492 | errorToEmit = options.abortSignal?.aborted 493 | ? options.abortSignal.reason 494 | : error; 495 | } else if ( 496 | error.message?.includes('not logged in') || 497 | error.message?.includes('authentication') || 498 | error.exitCode === 401 499 | ) { 500 | errorToEmit = createAuthenticationError({ 501 | message: 502 | error.message || 503 | 'Authentication failed. Please ensure Claude Code CLI is properly authenticated.' 504 | }); 505 | } else { 506 | errorToEmit = createAPICallError({ 507 | message: error.message || 'Claude Code CLI error', 508 | code: error.code, 509 | exitCode: error.exitCode, 510 | stderr: error.stderr, 511 | promptExcerpt: messagesPrompt.substring(0, 200), 512 | isRetryable: 513 | error.code === 'ENOENT' || error.code === 'ECONNREFUSED' 514 | }); 515 | } 516 | 517 | // Emit error as a stream part 518 | controller.enqueue({ 519 | type: 'error', 520 | error: errorToEmit 521 | }); 522 | 523 | controller.close(); 524 | } 525 | } 526 | }); 527 | 528 | return { 529 | stream, 530 | rawCall: { 531 | rawPrompt: messagesPrompt, 532 | rawSettings: queryOptions 533 | }, 534 | warnings: warnings.length > 0 ? warnings : undefined, 535 | request: { 536 | body: messagesPrompt 537 | } 538 | }; 539 | } 540 | } 541 | ``` -------------------------------------------------------------------------------- /src/utils/rule-transformer.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Rule Transformer Module 3 | * Handles conversion of Cursor rules to profile rules 4 | * 5 | * This module procedurally generates .{profile}/rules files from assets/rules files, 6 | * eliminating the need to maintain both sets of files manually. 7 | */ 8 | import fs from 'fs'; 9 | import path from 'path'; 10 | import { log } from '../../scripts/modules/utils.js'; 11 | 12 | // Import asset resolver 13 | import { assetExists, readAsset, getAssetsDir } from './asset-resolver.js'; 14 | 15 | // Import the shared MCP configuration helper 16 | import { 17 | setupMCPConfiguration, 18 | removeTaskMasterMCPConfiguration 19 | } from './create-mcp-config.js'; 20 | 21 | // Import profile constants (single source of truth) 22 | import { RULE_PROFILES } from '../constants/profiles.js'; 23 | 24 | // --- Profile Imports --- 25 | import * as profilesModule from '../profiles/index.js'; 26 | 27 | export function isValidProfile(profile) { 28 | return RULE_PROFILES.includes(profile); 29 | } 30 | 31 | /** 32 | * Get rule profile by name 33 | * @param {string} name - Profile name 34 | * @returns {Object|null} Profile object or null if not found 35 | */ 36 | export function getRulesProfile(name) { 37 | if (!isValidProfile(name)) { 38 | return null; 39 | } 40 | 41 | // Get the profile from the imported profiles module 42 | const profileKey = `${name}Profile`; 43 | const profile = profilesModule[profileKey]; 44 | 45 | if (!profile) { 46 | throw new Error( 47 | `Profile not found: static import missing for '${name}'. Valid profiles: ${RULE_PROFILES.join(', ')}` 48 | ); 49 | } 50 | 51 | return profile; 52 | } 53 | 54 | /** 55 | * Replace basic Cursor terms with profile equivalents 56 | */ 57 | function replaceBasicTerms(content, conversionConfig) { 58 | let result = content; 59 | 60 | // Apply profile term replacements 61 | conversionConfig.profileTerms.forEach((pattern) => { 62 | if (typeof pattern.to === 'function') { 63 | result = result.replace(pattern.from, pattern.to); 64 | } else { 65 | result = result.replace(pattern.from, pattern.to); 66 | } 67 | }); 68 | 69 | // Apply file extension replacements 70 | conversionConfig.fileExtensions.forEach((pattern) => { 71 | result = result.replace(pattern.from, pattern.to); 72 | }); 73 | 74 | return result; 75 | } 76 | 77 | /** 78 | * Replace Cursor tool references with profile tool equivalents 79 | */ 80 | function replaceToolReferences(content, conversionConfig) { 81 | let result = content; 82 | 83 | // Basic pattern for direct tool name replacements 84 | const toolNames = conversionConfig.toolNames; 85 | const toolReferencePattern = new RegExp( 86 | `\\b(${Object.keys(toolNames).join('|')})\\b`, 87 | 'g' 88 | ); 89 | 90 | // Apply direct tool name replacements 91 | result = result.replace(toolReferencePattern, (match, toolName) => { 92 | return toolNames[toolName] || toolName; 93 | }); 94 | 95 | // Apply contextual tool replacements 96 | conversionConfig.toolContexts.forEach((pattern) => { 97 | result = result.replace(pattern.from, pattern.to); 98 | }); 99 | 100 | // Apply tool group replacements 101 | conversionConfig.toolGroups.forEach((pattern) => { 102 | result = result.replace(pattern.from, pattern.to); 103 | }); 104 | 105 | return result; 106 | } 107 | 108 | /** 109 | * Update documentation URLs to point to profile documentation 110 | */ 111 | function updateDocReferences(content, conversionConfig) { 112 | let result = content; 113 | 114 | // Apply documentation URL replacements 115 | conversionConfig.docUrls.forEach((pattern) => { 116 | if (typeof pattern.to === 'function') { 117 | result = result.replace(pattern.from, pattern.to); 118 | } else { 119 | result = result.replace(pattern.from, pattern.to); 120 | } 121 | }); 122 | 123 | return result; 124 | } 125 | 126 | /** 127 | * Update file references in markdown links 128 | */ 129 | function updateFileReferences(content, conversionConfig) { 130 | const { pathPattern, replacement } = conversionConfig.fileReferences; 131 | return content.replace(pathPattern, replacement); 132 | } 133 | 134 | /** 135 | * Transform rule content to profile-specific rules 136 | * @param {string} content - The content to transform 137 | * @param {Object} conversionConfig - The conversion configuration 138 | * @param {Object} globalReplacements - Global text replacements 139 | * @returns {string} - The transformed content 140 | */ 141 | function transformRuleContent(content, conversionConfig, globalReplacements) { 142 | let result = content; 143 | 144 | // Apply all transformations in appropriate order 145 | result = updateFileReferences(result, conversionConfig); 146 | result = replaceBasicTerms(result, conversionConfig); 147 | result = replaceToolReferences(result, conversionConfig); 148 | result = updateDocReferences(result, conversionConfig); 149 | 150 | // Apply any global/catch-all replacements from the profile 151 | // Super aggressive failsafe pass to catch any variations we might have missed 152 | // This ensures critical transformations are applied even in contexts we didn't anticipate 153 | globalReplacements.forEach((pattern) => { 154 | if (typeof pattern.to === 'function') { 155 | result = result.replace(pattern.from, pattern.to); 156 | } else { 157 | result = result.replace(pattern.from, pattern.to); 158 | } 159 | }); 160 | 161 | return result; 162 | } 163 | 164 | /** 165 | * Convert a Cursor rule file to a profile-specific rule file 166 | * @param {string} sourcePath - Path to the source .mdc file 167 | * @param {string} targetPath - Path to the target file 168 | * @param {Object} profile - The profile configuration 169 | * @returns {boolean} - Success status 170 | */ 171 | export function convertRuleToProfileRule(sourcePath, targetPath, profile) { 172 | const { conversionConfig, globalReplacements } = profile; 173 | try { 174 | // Read source content 175 | const content = fs.readFileSync(sourcePath, 'utf8'); 176 | 177 | // Transform content 178 | const transformedContent = transformRuleContent( 179 | content, 180 | conversionConfig, 181 | globalReplacements 182 | ); 183 | 184 | // Ensure target directory exists 185 | const targetDir = path.dirname(targetPath); 186 | if (!fs.existsSync(targetDir)) { 187 | fs.mkdirSync(targetDir, { recursive: true }); 188 | } 189 | 190 | // Write transformed content 191 | fs.writeFileSync(targetPath, transformedContent); 192 | 193 | return true; 194 | } catch (error) { 195 | console.error(`Error converting rule file: ${error.message}`); 196 | return false; 197 | } 198 | } 199 | 200 | /** 201 | * Convert all Cursor rules to profile rules for a specific profile 202 | */ 203 | export function convertAllRulesToProfileRules(projectRoot, profile) { 204 | const targetDir = path.join(projectRoot, profile.rulesDir); 205 | 206 | let success = 0; 207 | let failed = 0; 208 | 209 | // 1. Call onAddRulesProfile first (for pre-processing like copying assets) 210 | if (typeof profile.onAddRulesProfile === 'function') { 211 | try { 212 | const assetsDir = getAssetsDir(); 213 | profile.onAddRulesProfile(projectRoot, assetsDir); 214 | log( 215 | 'debug', 216 | `[Rule Transformer] Called onAddRulesProfile for ${profile.profileName}` 217 | ); 218 | } catch (error) { 219 | log( 220 | 'error', 221 | `[Rule Transformer] onAddRulesProfile failed for ${profile.profileName}: ${error.message}` 222 | ); 223 | failed++; 224 | } 225 | } 226 | 227 | // 2. Handle fileMap-based rule conversion (if any) 228 | const sourceFiles = Object.keys(profile.fileMap); 229 | if (sourceFiles.length > 0) { 230 | // Only create rules directory if we have files to copy 231 | if (!fs.existsSync(targetDir)) { 232 | fs.mkdirSync(targetDir, { recursive: true }); 233 | } 234 | 235 | for (const sourceFile of sourceFiles) { 236 | // Determine if this is an asset file (not a rule file) 237 | const isAssetFile = !sourceFile.startsWith('rules/'); 238 | 239 | try { 240 | // Check if source file exists using asset resolver 241 | if (!assetExists(sourceFile)) { 242 | log( 243 | 'warn', 244 | `[Rule Transformer] Source file not found: ${sourceFile}, skipping` 245 | ); 246 | continue; 247 | } 248 | 249 | const targetFilename = profile.fileMap[sourceFile]; 250 | const targetPath = path.join(targetDir, targetFilename); 251 | 252 | // Ensure target subdirectory exists (for rules like taskmaster/dev_workflow.md) 253 | const targetFileDir = path.dirname(targetPath); 254 | if (!fs.existsSync(targetFileDir)) { 255 | fs.mkdirSync(targetFileDir, { recursive: true }); 256 | } 257 | 258 | // Read source content using asset resolver 259 | let content = readAsset(sourceFile, 'utf8'); 260 | 261 | // Apply transformations (only if this is a rule file, not an asset file) 262 | if (!isAssetFile) { 263 | content = transformRuleContent( 264 | content, 265 | profile.conversionConfig, 266 | profile.globalReplacements 267 | ); 268 | } 269 | 270 | // Write to target 271 | fs.writeFileSync(targetPath, content, 'utf8'); 272 | success++; 273 | 274 | log( 275 | 'debug', 276 | `[Rule Transformer] ${isAssetFile ? 'Copied' : 'Converted'} ${sourceFile} -> ${targetFilename} for ${profile.profileName}` 277 | ); 278 | } catch (error) { 279 | failed++; 280 | log( 281 | 'error', 282 | `[Rule Transformer] Failed to ${isAssetFile ? 'copy' : 'convert'} ${sourceFile} for ${profile.profileName}: ${error.message}` 283 | ); 284 | } 285 | } 286 | } 287 | 288 | // 3. Setup MCP configuration (if enabled) 289 | if (profile.mcpConfig !== false) { 290 | try { 291 | setupMCPConfiguration(projectRoot, profile.mcpConfigPath); 292 | log( 293 | 'debug', 294 | `[Rule Transformer] Setup MCP configuration for ${profile.profileName}` 295 | ); 296 | } catch (error) { 297 | log( 298 | 'error', 299 | `[Rule Transformer] MCP setup failed for ${profile.profileName}: ${error.message}` 300 | ); 301 | } 302 | } 303 | 304 | // 4. Call post-conversion hook (for finalization) 305 | if (typeof profile.onPostConvertRulesProfile === 'function') { 306 | try { 307 | const assetsDir = getAssetsDir(); 308 | profile.onPostConvertRulesProfile(projectRoot, assetsDir); 309 | log( 310 | 'debug', 311 | `[Rule Transformer] Called onPostConvertRulesProfile for ${profile.profileName}` 312 | ); 313 | } catch (error) { 314 | log( 315 | 'error', 316 | `[Rule Transformer] onPostConvertRulesProfile failed for ${profile.profileName}: ${error.message}` 317 | ); 318 | } 319 | } 320 | 321 | // Ensure we return at least 1 success for profiles that only use lifecycle functions 322 | return { success: Math.max(success, 1), failed }; 323 | } 324 | 325 | /** 326 | * Remove only Task Master specific files from a profile, leaving other existing rules intact 327 | * @param {string} projectRoot - Target project directory 328 | * @param {Object} profile - Profile configuration 329 | * @returns {Object} Result object 330 | */ 331 | export function removeProfileRules(projectRoot, profile) { 332 | const targetDir = path.join(projectRoot, profile.rulesDir); 333 | const profileDir = path.join(projectRoot, profile.profileDir); 334 | 335 | const result = { 336 | profileName: profile.profileName, 337 | success: false, 338 | skipped: false, 339 | error: null, 340 | filesRemoved: [], 341 | mcpResult: null, 342 | profileDirRemoved: false, 343 | notice: null 344 | }; 345 | 346 | try { 347 | // 1. Call onRemoveRulesProfile first (for custom cleanup like removing assets) 348 | if (typeof profile.onRemoveRulesProfile === 'function') { 349 | try { 350 | profile.onRemoveRulesProfile(projectRoot); 351 | log( 352 | 'debug', 353 | `[Rule Transformer] Called onRemoveRulesProfile for ${profile.profileName}` 354 | ); 355 | } catch (error) { 356 | log( 357 | 'error', 358 | `[Rule Transformer] onRemoveRulesProfile failed for ${profile.profileName}: ${error.message}` 359 | ); 360 | } 361 | } 362 | 363 | // 2. Remove fileMap-based files (if any) 364 | const sourceFiles = Object.keys(profile.fileMap); 365 | if (sourceFiles.length > 0) { 366 | // Check if profile directory exists at all (for full profiles) 367 | if (!fs.existsSync(profileDir)) { 368 | result.success = true; 369 | result.skipped = true; 370 | log( 371 | 'debug', 372 | `[Rule Transformer] Profile directory does not exist: ${profileDir}` 373 | ); 374 | return result; 375 | } 376 | 377 | let hasOtherRulesFiles = false; 378 | 379 | if (fs.existsSync(targetDir)) { 380 | // Get list of files we're responsible for 381 | const taskMasterFiles = sourceFiles.map( 382 | (sourceFile) => profile.fileMap[sourceFile] 383 | ); 384 | 385 | // Get all files in the rules directory 386 | // For root-level directories, we need to be careful to avoid circular symlinks 387 | let allFiles = []; 388 | if (targetDir === projectRoot || profile.rulesDir === '.') { 389 | // For root directory, manually read without recursion into problematic directories 390 | const items = fs.readdirSync(targetDir); 391 | for (const item of items) { 392 | // Skip directories that can cause issues or are irrelevant 393 | if (item === 'node_modules' || item === '.git' || item === 'dist') { 394 | continue; 395 | } 396 | const itemPath = path.join(targetDir, item); 397 | try { 398 | const stats = fs.lstatSync(itemPath); 399 | if (stats.isFile()) { 400 | allFiles.push(item); 401 | } else if (stats.isDirectory() && !stats.isSymbolicLink()) { 402 | // Only recurse into safe directories 403 | const subFiles = fs.readdirSync(itemPath, { recursive: true }); 404 | subFiles.forEach((subFile) => { 405 | allFiles.push(path.join(item, subFile.toString())); 406 | }); 407 | } 408 | } catch (err) { 409 | // Silently skip files we can't access 410 | } 411 | } 412 | } else { 413 | // For non-root directories, use normal recursive read 414 | allFiles = fs.readdirSync(targetDir, { recursive: true }); 415 | } 416 | 417 | const allFilePaths = allFiles 418 | .filter((file) => { 419 | const fullPath = path.join(targetDir, file); 420 | try { 421 | const stats = fs.statSync(fullPath); 422 | return stats.isFile(); 423 | } catch (err) { 424 | return false; 425 | } 426 | }) 427 | .map((file) => file.toString()); // Ensure it's a string 428 | 429 | // Remove only Task Master files 430 | for (const taskMasterFile of taskMasterFiles) { 431 | const filePath = path.join(targetDir, taskMasterFile); 432 | if (fs.existsSync(filePath)) { 433 | try { 434 | fs.rmSync(filePath, { force: true }); 435 | result.filesRemoved.push(taskMasterFile); 436 | log( 437 | 'debug', 438 | `[Rule Transformer] Removed Task Master file: ${taskMasterFile}` 439 | ); 440 | } catch (error) { 441 | log( 442 | 'error', 443 | `[Rule Transformer] Failed to remove ${taskMasterFile}: ${error.message}` 444 | ); 445 | } 446 | } 447 | } 448 | 449 | // Check for other (non-Task Master) files 450 | const remainingFiles = allFilePaths.filter( 451 | (file) => !taskMasterFiles.includes(file) 452 | ); 453 | 454 | hasOtherRulesFiles = remainingFiles.length > 0; 455 | 456 | // Remove empty directories or note preserved files 457 | if (remainingFiles.length === 0) { 458 | fs.rmSync(targetDir, { recursive: true, force: true }); 459 | log( 460 | 'debug', 461 | `[Rule Transformer] Removed empty rules directory: ${targetDir}` 462 | ); 463 | } else if (hasOtherRulesFiles) { 464 | result.notice = `Preserved ${remainingFiles.length} existing rule files in ${profile.rulesDir}`; 465 | log('info', `[Rule Transformer] ${result.notice}`); 466 | } 467 | } 468 | } 469 | 470 | // 3. Handle MCP configuration - only remove Task Master, preserve other servers 471 | if (profile.mcpConfig !== false) { 472 | try { 473 | result.mcpResult = removeTaskMasterMCPConfiguration( 474 | projectRoot, 475 | profile.mcpConfigPath 476 | ); 477 | if (result.mcpResult.hasOtherServers) { 478 | if (!result.notice) { 479 | result.notice = 'Preserved other MCP server configurations'; 480 | } else { 481 | result.notice += '; preserved other MCP server configurations'; 482 | } 483 | } 484 | log( 485 | 'debug', 486 | `[Rule Transformer] Processed MCP configuration for ${profile.profileName}` 487 | ); 488 | } catch (error) { 489 | log( 490 | 'error', 491 | `[Rule Transformer] MCP cleanup failed for ${profile.profileName}: ${error.message}` 492 | ); 493 | } 494 | } 495 | 496 | // 4. Check if we should remove the entire profile directory 497 | if (fs.existsSync(profileDir)) { 498 | const remainingContents = fs.readdirSync(profileDir); 499 | if (remainingContents.length === 0 && profile.profileDir !== '.') { 500 | // Only remove profile directory if it's empty and not root directory 501 | try { 502 | fs.rmSync(profileDir, { recursive: true, force: true }); 503 | result.profileDirRemoved = true; 504 | log( 505 | 'debug', 506 | `[Rule Transformer] Removed empty profile directory: ${profileDir}` 507 | ); 508 | } catch (error) { 509 | log( 510 | 'error', 511 | `[Rule Transformer] Failed to remove profile directory ${profileDir}: ${error.message}` 512 | ); 513 | } 514 | } else if (remainingContents.length > 0) { 515 | // Profile directory has remaining files/folders, add notice 516 | const preservedNotice = `Preserved ${remainingContents.length} existing files/folders in ${profile.profileDir}`; 517 | if (!result.notice) { 518 | result.notice = preservedNotice; 519 | } else { 520 | result.notice += `; ${preservedNotice.toLowerCase()}`; 521 | } 522 | log('info', `[Rule Transformer] ${preservedNotice}`); 523 | } 524 | } 525 | 526 | result.success = true; 527 | log( 528 | 'debug', 529 | `[Rule Transformer] Successfully removed ${profile.profileName} Task Master files from ${projectRoot}` 530 | ); 531 | } catch (error) { 532 | result.error = error.message; 533 | log( 534 | 'error', 535 | `[Rule Transformer] Failed to remove ${profile.profileName} rules: ${error.message}` 536 | ); 537 | } 538 | 539 | return result; 540 | } 541 | ``` -------------------------------------------------------------------------------- /packages/tm-core/src/storage/file-storage/file-storage.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Refactored file-based storage implementation for Task Master 3 | */ 4 | 5 | import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js'; 6 | import type { 7 | IStorage, 8 | StorageStats, 9 | UpdateStatusResult 10 | } from '../../interfaces/storage.interface.js'; 11 | import { FormatHandler } from './format-handler.js'; 12 | import { FileOperations } from './file-operations.js'; 13 | import { PathResolver } from './path-resolver.js'; 14 | 15 | /** 16 | * File-based storage implementation using a single tasks.json file with separated concerns 17 | */ 18 | export class FileStorage implements IStorage { 19 | private formatHandler: FormatHandler; 20 | private fileOps: FileOperations; 21 | private pathResolver: PathResolver; 22 | 23 | constructor(projectPath: string) { 24 | this.formatHandler = new FormatHandler(); 25 | this.fileOps = new FileOperations(); 26 | this.pathResolver = new PathResolver(projectPath); 27 | } 28 | 29 | /** 30 | * Initialize storage by creating necessary directories 31 | */ 32 | async initialize(): Promise<void> { 33 | await this.fileOps.ensureDir(this.pathResolver.getTasksDir()); 34 | } 35 | 36 | /** 37 | * Close storage and cleanup resources 38 | */ 39 | async close(): Promise<void> { 40 | await this.fileOps.cleanup(); 41 | } 42 | 43 | /** 44 | * Get statistics about the storage 45 | */ 46 | async getStats(): Promise<StorageStats> { 47 | const filePath = this.pathResolver.getTasksPath(); 48 | 49 | try { 50 | const stats = await this.fileOps.getStats(filePath); 51 | const data = await this.fileOps.readJson(filePath); 52 | const tags = this.formatHandler.extractTags(data); 53 | 54 | let totalTasks = 0; 55 | const tagStats = tags.map((tag) => { 56 | const tasks = this.formatHandler.extractTasks(data, tag); 57 | const taskCount = tasks.length; 58 | totalTasks += taskCount; 59 | 60 | return { 61 | tag, 62 | taskCount, 63 | lastModified: stats.mtime.toISOString() 64 | }; 65 | }); 66 | 67 | return { 68 | totalTasks, 69 | totalTags: tags.length, 70 | lastModified: stats.mtime.toISOString(), 71 | storageSize: 0, // Could calculate actual file sizes if needed 72 | tagStats 73 | }; 74 | } catch (error: any) { 75 | if (error.code === 'ENOENT') { 76 | return { 77 | totalTasks: 0, 78 | totalTags: 0, 79 | lastModified: new Date().toISOString(), 80 | storageSize: 0, 81 | tagStats: [] 82 | }; 83 | } 84 | throw new Error(`Failed to get storage stats: ${error.message}`); 85 | } 86 | } 87 | 88 | /** 89 | * Load tasks from the single tasks.json file for a specific tag 90 | */ 91 | async loadTasks(tag?: string): Promise<Task[]> { 92 | const filePath = this.pathResolver.getTasksPath(); 93 | const resolvedTag = tag || 'master'; 94 | 95 | try { 96 | const rawData = await this.fileOps.readJson(filePath); 97 | return this.formatHandler.extractTasks(rawData, resolvedTag); 98 | } catch (error: any) { 99 | if (error.code === 'ENOENT') { 100 | return []; // File doesn't exist, return empty array 101 | } 102 | throw new Error(`Failed to load tasks: ${error.message}`); 103 | } 104 | } 105 | 106 | /** 107 | * Load a single task by ID from the tasks.json file 108 | * Handles both regular tasks and subtasks (with dotted notation like "1.2") 109 | */ 110 | async loadTask(taskId: string, tag?: string): Promise<Task | null> { 111 | const tasks = await this.loadTasks(tag); 112 | 113 | // Check if this is a subtask (contains a dot) 114 | if (taskId.includes('.')) { 115 | const [parentId, subtaskId] = taskId.split('.'); 116 | const parentTask = tasks.find((t) => String(t.id) === parentId); 117 | 118 | if (!parentTask || !parentTask.subtasks) { 119 | return null; 120 | } 121 | 122 | const subtask = parentTask.subtasks.find( 123 | (st) => String(st.id) === subtaskId 124 | ); 125 | if (!subtask) { 126 | return null; 127 | } 128 | 129 | const toFullSubId = (maybeDotId: string | number): string => { 130 | const depId = String(maybeDotId); 131 | return depId.includes('.') ? depId : `${parentTask.id}.${depId}`; 132 | }; 133 | const resolvedDependencies = 134 | subtask.dependencies?.map((dep) => toFullSubId(dep)) ?? []; 135 | 136 | // Return a Task-like object for the subtask with the full dotted ID 137 | // Following the same pattern as findTaskById in utils.js 138 | const subtaskResult = { 139 | ...subtask, 140 | id: taskId, // Use the full dotted ID 141 | title: subtask.title || `Subtask ${subtaskId}`, 142 | description: subtask.description || '', 143 | status: subtask.status || 'pending', 144 | priority: subtask.priority || parentTask.priority || 'medium', 145 | dependencies: resolvedDependencies, 146 | details: subtask.details || '', 147 | testStrategy: subtask.testStrategy || '', 148 | subtasks: [], 149 | tags: parentTask.tags || [], 150 | assignee: subtask.assignee || parentTask.assignee, 151 | complexity: subtask.complexity || parentTask.complexity, 152 | createdAt: subtask.createdAt || parentTask.createdAt, 153 | updatedAt: subtask.updatedAt || parentTask.updatedAt, 154 | // Add reference to parent task for context (like utils.js does) 155 | parentTask: { 156 | id: parentTask.id, 157 | title: parentTask.title, 158 | status: parentTask.status 159 | }, 160 | isSubtask: true 161 | }; 162 | 163 | return subtaskResult; 164 | } 165 | 166 | // Handle regular task lookup 167 | return tasks.find((task) => String(task.id) === String(taskId)) || null; 168 | } 169 | 170 | /** 171 | * Save tasks for a specific tag in the single tasks.json file 172 | */ 173 | async saveTasks(tasks: Task[], tag?: string): Promise<void> { 174 | const filePath = this.pathResolver.getTasksPath(); 175 | const resolvedTag = tag || 'master'; 176 | 177 | // Ensure directory exists 178 | await this.fileOps.ensureDir(this.pathResolver.getTasksDir()); 179 | 180 | // Get existing data from the file 181 | let existingData: any = {}; 182 | try { 183 | existingData = await this.fileOps.readJson(filePath); 184 | } catch (error: any) { 185 | if (error.code !== 'ENOENT') { 186 | throw new Error(`Failed to read existing tasks: ${error.message}`); 187 | } 188 | // File doesn't exist, start with empty data 189 | } 190 | 191 | // Create metadata for this tag 192 | const metadata: TaskMetadata = { 193 | version: '1.0.0', 194 | lastModified: new Date().toISOString(), 195 | taskCount: tasks.length, 196 | completedCount: tasks.filter((t) => t.status === 'done').length, 197 | tags: [resolvedTag] 198 | }; 199 | 200 | // Normalize tasks 201 | const normalizedTasks = this.normalizeTaskIds(tasks); 202 | 203 | // Update the specific tag in the existing data structure 204 | if ( 205 | this.formatHandler.detectFormat(existingData) === 'legacy' || 206 | Object.keys(existingData).some( 207 | (key) => key !== 'tasks' && key !== 'metadata' 208 | ) 209 | ) { 210 | // Legacy format - update/add the tag 211 | existingData[resolvedTag] = { 212 | tasks: normalizedTasks, 213 | metadata 214 | }; 215 | } else if (resolvedTag === 'master') { 216 | // Standard format for master tag 217 | existingData = { 218 | tasks: normalizedTasks, 219 | metadata 220 | }; 221 | } else { 222 | // Convert to legacy format when adding non-master tags 223 | const masterTasks = existingData.tasks || []; 224 | const masterMetadata = existingData.metadata || metadata; 225 | 226 | existingData = { 227 | master: { 228 | tasks: masterTasks, 229 | metadata: masterMetadata 230 | }, 231 | [resolvedTag]: { 232 | tasks: normalizedTasks, 233 | metadata 234 | } 235 | }; 236 | } 237 | 238 | // Write the updated file 239 | await this.fileOps.writeJson(filePath, existingData); 240 | } 241 | 242 | /** 243 | * Normalize task IDs - keep Task IDs as strings, Subtask IDs as numbers 244 | */ 245 | private normalizeTaskIds(tasks: Task[]): Task[] { 246 | return tasks.map((task) => ({ 247 | ...task, 248 | id: String(task.id), // Task IDs are strings 249 | dependencies: task.dependencies?.map((dep) => String(dep)) || [], 250 | subtasks: 251 | task.subtasks?.map((subtask) => ({ 252 | ...subtask, 253 | id: Number(subtask.id), // Subtask IDs are numbers 254 | parentId: String(subtask.parentId) // Parent ID is string (Task ID) 255 | })) || [] 256 | })); 257 | } 258 | 259 | /** 260 | * Check if the tasks file exists 261 | */ 262 | async exists(_tag?: string): Promise<boolean> { 263 | const filePath = this.pathResolver.getTasksPath(); 264 | return this.fileOps.exists(filePath); 265 | } 266 | 267 | /** 268 | * Get all available tags from the single tasks.json file 269 | */ 270 | async getAllTags(): Promise<string[]> { 271 | try { 272 | const filePath = this.pathResolver.getTasksPath(); 273 | const data = await this.fileOps.readJson(filePath); 274 | return this.formatHandler.extractTags(data); 275 | } catch (error: any) { 276 | if (error.code === 'ENOENT') { 277 | return []; // File doesn't exist 278 | } 279 | throw new Error(`Failed to get tags: ${error.message}`); 280 | } 281 | } 282 | 283 | /** 284 | * Load metadata from the single tasks.json file for a specific tag 285 | */ 286 | async loadMetadata(tag?: string): Promise<TaskMetadata | null> { 287 | const filePath = this.pathResolver.getTasksPath(); 288 | const resolvedTag = tag || 'master'; 289 | 290 | try { 291 | const rawData = await this.fileOps.readJson(filePath); 292 | return this.formatHandler.extractMetadata(rawData, resolvedTag); 293 | } catch (error: any) { 294 | if (error.code === 'ENOENT') { 295 | return null; 296 | } 297 | throw new Error(`Failed to load metadata: ${error.message}`); 298 | } 299 | } 300 | 301 | /** 302 | * Save metadata (stored with tasks) 303 | */ 304 | async saveMetadata(_metadata: TaskMetadata, tag?: string): Promise<void> { 305 | const tasks = await this.loadTasks(tag); 306 | await this.saveTasks(tasks, tag); 307 | } 308 | 309 | /** 310 | * Append tasks to existing storage 311 | */ 312 | async appendTasks(tasks: Task[], tag?: string): Promise<void> { 313 | const existingTasks = await this.loadTasks(tag); 314 | const allTasks = [...existingTasks, ...tasks]; 315 | await this.saveTasks(allTasks, tag); 316 | } 317 | 318 | /** 319 | * Update a specific task 320 | */ 321 | async updateTask( 322 | taskId: string, 323 | updates: Partial<Task>, 324 | tag?: string 325 | ): Promise<void> { 326 | const tasks = await this.loadTasks(tag); 327 | const taskIndex = tasks.findIndex((t) => String(t.id) === String(taskId)); 328 | 329 | if (taskIndex === -1) { 330 | throw new Error(`Task ${taskId} not found`); 331 | } 332 | 333 | tasks[taskIndex] = { 334 | ...tasks[taskIndex], 335 | ...updates, 336 | id: String(taskId) // Keep consistent with normalizeTaskIds 337 | }; 338 | await this.saveTasks(tasks, tag); 339 | } 340 | 341 | /** 342 | * Update task or subtask status by ID - handles file storage logic with parent/subtask relationships 343 | */ 344 | async updateTaskStatus( 345 | taskId: string, 346 | newStatus: TaskStatus, 347 | tag?: string 348 | ): Promise<UpdateStatusResult> { 349 | const tasks = await this.loadTasks(tag); 350 | 351 | // Check if this is a subtask (contains a dot) 352 | if (taskId.includes('.')) { 353 | return this.updateSubtaskStatusInFile(tasks, taskId, newStatus, tag); 354 | } 355 | 356 | // Handle regular task update 357 | const taskIndex = tasks.findIndex((t) => String(t.id) === String(taskId)); 358 | 359 | if (taskIndex === -1) { 360 | throw new Error(`Task ${taskId} not found`); 361 | } 362 | 363 | const oldStatus = tasks[taskIndex].status; 364 | if (oldStatus === newStatus) { 365 | return { 366 | success: true, 367 | oldStatus, 368 | newStatus, 369 | taskId: String(taskId) 370 | }; 371 | } 372 | 373 | tasks[taskIndex] = { 374 | ...tasks[taskIndex], 375 | status: newStatus, 376 | updatedAt: new Date().toISOString() 377 | }; 378 | 379 | await this.saveTasks(tasks, tag); 380 | 381 | return { 382 | success: true, 383 | oldStatus, 384 | newStatus, 385 | taskId: String(taskId) 386 | }; 387 | } 388 | 389 | /** 390 | * Update subtask status within file storage - handles parent status auto-adjustment 391 | */ 392 | private async updateSubtaskStatusInFile( 393 | tasks: Task[], 394 | subtaskId: string, 395 | newStatus: TaskStatus, 396 | tag?: string 397 | ): Promise<UpdateStatusResult> { 398 | // Parse the subtask ID to get parent ID and subtask ID 399 | const parts = subtaskId.split('.'); 400 | if (parts.length !== 2) { 401 | throw new Error( 402 | `Invalid subtask ID format: ${subtaskId}. Expected format: parentId.subtaskId` 403 | ); 404 | } 405 | 406 | const [parentId, subIdRaw] = parts; 407 | const subId = subIdRaw.trim(); 408 | if (!/^\d+$/.test(subId)) { 409 | throw new Error( 410 | `Invalid subtask ID: ${subId}. Subtask ID must be a positive integer.` 411 | ); 412 | } 413 | const subtaskNumericId = Number(subId); 414 | 415 | // Find the parent task 416 | const parentTaskIndex = tasks.findIndex( 417 | (t) => String(t.id) === String(parentId) 418 | ); 419 | 420 | if (parentTaskIndex === -1) { 421 | throw new Error(`Parent task ${parentId} not found`); 422 | } 423 | 424 | const parentTask = tasks[parentTaskIndex]; 425 | 426 | // Find the subtask within the parent task 427 | const subtaskIndex = parentTask.subtasks.findIndex( 428 | (st) => st.id === subtaskNumericId || String(st.id) === subId 429 | ); 430 | 431 | if (subtaskIndex === -1) { 432 | throw new Error( 433 | `Subtask ${subtaskId} not found in parent task ${parentId}` 434 | ); 435 | } 436 | 437 | const oldStatus = parentTask.subtasks[subtaskIndex].status || 'pending'; 438 | if (oldStatus === newStatus) { 439 | return { 440 | success: true, 441 | oldStatus, 442 | newStatus, 443 | taskId: subtaskId 444 | }; 445 | } 446 | 447 | const now = new Date().toISOString(); 448 | 449 | // Update the subtask status 450 | parentTask.subtasks[subtaskIndex] = { 451 | ...parentTask.subtasks[subtaskIndex], 452 | status: newStatus, 453 | updatedAt: now 454 | }; 455 | 456 | // Auto-adjust parent status based on subtask statuses 457 | const subs = parentTask.subtasks; 458 | let parentNewStatus = parentTask.status; 459 | if (subs.length > 0) { 460 | const norm = (s: any) => s.status || 'pending'; 461 | const isDoneLike = (s: any) => { 462 | const st = norm(s); 463 | return st === 'done' || st === 'completed'; 464 | }; 465 | const allDone = subs.every(isDoneLike); 466 | const anyInProgress = subs.some((s) => norm(s) === 'in-progress'); 467 | const anyDone = subs.some(isDoneLike); 468 | if (allDone) parentNewStatus = 'done'; 469 | else if (anyInProgress || anyDone) parentNewStatus = 'in-progress'; 470 | } 471 | 472 | // Always bump updatedAt; update status only if changed 473 | tasks[parentTaskIndex] = { 474 | ...parentTask, 475 | ...(parentNewStatus !== parentTask.status 476 | ? { status: parentNewStatus } 477 | : {}), 478 | updatedAt: now 479 | }; 480 | 481 | await this.saveTasks(tasks, tag); 482 | 483 | return { 484 | success: true, 485 | oldStatus, 486 | newStatus, 487 | taskId: subtaskId 488 | }; 489 | } 490 | 491 | /** 492 | * Delete a task 493 | */ 494 | async deleteTask(taskId: string, tag?: string): Promise<void> { 495 | const tasks = await this.loadTasks(tag); 496 | const filteredTasks = tasks.filter((t) => String(t.id) !== String(taskId)); 497 | 498 | if (filteredTasks.length === tasks.length) { 499 | throw new Error(`Task ${taskId} not found`); 500 | } 501 | 502 | await this.saveTasks(filteredTasks, tag); 503 | } 504 | 505 | /** 506 | * Delete a tag from the single tasks.json file 507 | */ 508 | async deleteTag(tag: string): Promise<void> { 509 | const filePath = this.pathResolver.getTasksPath(); 510 | 511 | try { 512 | const existingData = await this.fileOps.readJson(filePath); 513 | 514 | if (this.formatHandler.detectFormat(existingData) === 'legacy') { 515 | // Legacy format - remove the tag key 516 | if (tag in existingData) { 517 | delete existingData[tag]; 518 | await this.fileOps.writeJson(filePath, existingData); 519 | } else { 520 | throw new Error(`Tag ${tag} not found`); 521 | } 522 | } else if (tag === 'master') { 523 | // Standard format - delete the entire file for master tag 524 | await this.fileOps.deleteFile(filePath); 525 | } else { 526 | throw new Error(`Tag ${tag} not found in standard format`); 527 | } 528 | } catch (error: any) { 529 | if (error.code === 'ENOENT') { 530 | throw new Error(`Tag ${tag} not found - file doesn't exist`); 531 | } 532 | throw error; 533 | } 534 | } 535 | 536 | /** 537 | * Rename a tag within the single tasks.json file 538 | */ 539 | async renameTag(oldTag: string, newTag: string): Promise<void> { 540 | const filePath = this.pathResolver.getTasksPath(); 541 | 542 | try { 543 | const existingData = await this.fileOps.readJson(filePath); 544 | 545 | if (this.formatHandler.detectFormat(existingData) === 'legacy') { 546 | // Legacy format - rename the tag key 547 | if (oldTag in existingData) { 548 | existingData[newTag] = existingData[oldTag]; 549 | delete existingData[oldTag]; 550 | 551 | // Update metadata tags array 552 | if (existingData[newTag].metadata) { 553 | existingData[newTag].metadata.tags = [newTag]; 554 | } 555 | 556 | await this.fileOps.writeJson(filePath, existingData); 557 | } else { 558 | throw new Error(`Tag ${oldTag} not found`); 559 | } 560 | } else if (oldTag === 'master') { 561 | // Convert standard format to legacy when renaming master 562 | const masterTasks = existingData.tasks || []; 563 | const masterMetadata = existingData.metadata || {}; 564 | 565 | const newData = { 566 | [newTag]: { 567 | tasks: masterTasks, 568 | metadata: { ...masterMetadata, tags: [newTag] } 569 | } 570 | }; 571 | 572 | await this.fileOps.writeJson(filePath, newData); 573 | } else { 574 | throw new Error(`Tag ${oldTag} not found in standard format`); 575 | } 576 | } catch (error: any) { 577 | if (error.code === 'ENOENT') { 578 | throw new Error(`Tag ${oldTag} not found - file doesn't exist`); 579 | } 580 | throw error; 581 | } 582 | } 583 | 584 | /** 585 | * Copy a tag within the single tasks.json file 586 | */ 587 | async copyTag(sourceTag: string, targetTag: string): Promise<void> { 588 | const tasks = await this.loadTasks(sourceTag); 589 | 590 | if (tasks.length === 0) { 591 | throw new Error(`Source tag ${sourceTag} not found or has no tasks`); 592 | } 593 | 594 | await this.saveTasks(tasks, targetTag); 595 | } 596 | } 597 | 598 | // Export as default for convenience 599 | export default FileStorage; 600 | ``` -------------------------------------------------------------------------------- /docs/mcp-provider-guide.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Provider Integration Guide 2 | 3 | ## Overview 4 | 5 | Task Master provides a **unified MCP provider** for AI operations: 6 | 7 | **MCP Provider** (`mcp`) - Modern AI SDK-compatible provider with full structured object generation support 8 | 9 | The MCP provider enables Task Master to act as an MCP client, using MCP servers as AI providers alongside traditional API-based providers. This integration follows the existing provider pattern and supports all standard AI operations including structured object generation for PRD parsing and task creation. 10 | 11 | ## MCP Provider Features 12 | 13 | The **MCP Provider** (`mcp`) provides: 14 | 15 | ✅ **Full AI SDK Compatibility** - Complete LanguageModelV1 interface implementation 16 | ✅ **Structured Object Generation** - Schema-driven outputs for PRD parsing and task creation 17 | ✅ **Enhanced Error Handling** - Robust JSON extraction and validation 18 | ✅ **Session Management** - Automatic session detection and context handling 19 | ✅ **Schema Validation** - Type-safe object generation with Zod validation 20 | 21 | ### Quick Setup 22 | 23 | ```bash 24 | # Set MCP provider for main role 25 | task-master models set-main --provider mcp --model claude-3-5-sonnet-20241022 26 | ``` 27 | 28 | For detailed information, see [MCP Provider Documentation](mcp-provider.md). 29 | 30 | ## What is MCP Provider? 31 | 32 | The MCP provider allows Task Master to: 33 | - Connect to MCP servers/tools as AI providers 34 | - Use session-based authentication instead of API keys 35 | - Map AI operations to MCP tool calls 36 | - Integrate with existing role-based provider assignment 37 | - Maintain compatibility with fallback chains 38 | - Support structured object generation for schema-driven features 39 | 40 | ## Configuration 41 | 42 | ### MCP Provider Setup 43 | 44 | Add MCP provider to your `.taskmaster/config.json`: 45 | 46 | ```json 47 | { 48 | "models": { 49 | "main": { 50 | "provider": "mcp", 51 | "modelId": "claude-3-5-sonnet-20241022", 52 | "maxTokens": 50000, 53 | "temperature": 0.2 54 | }, 55 | "research": { 56 | "provider": "mcp", 57 | "modelId": "claude-3-5-sonnet-20241022", 58 | "maxTokens": 8700, 59 | "temperature": 0.1 60 | }, 61 | "fallback": { 62 | "provider": "anthropic", 63 | "modelId": "claude-3-5-sonnet-20241022" 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ### Available Models 70 | 71 | **MCP Provider Models:** 72 | 73 | - **`claude-3-5-sonnet-20241022`** - High-performance model for general tasks 74 | - **SWE Score**: 0.49 75 | - **Features**: Text + Object generation 76 | 77 | - **`claude-3-opus-20240229`** - Enhanced reasoning model for complex tasks 78 | - **SWE Score**: 0.725 79 | - **Features**: Text + Object generation 80 | 81 | - **`mcp-sampling`** - General text generation using MCP client sampling 82 | - **SWE Score**: null 83 | - **Roles**: Supports main, research, and fallback roles 84 | - **SWE Score**: 0.49 85 | - **Cost**: $0 (session-based) 86 | - **Max Tokens**: 200,000 87 | - **Supported Roles**: main, research, fallback 88 | - **Features**: Text + Object generation 89 | 90 | - **`claude-3-opus-20240229`** - Enhanced reasoning model for complex tasks 91 | - **SWE Score**: 0.725 92 | - **Cost**: $0 (session-based) 93 | - **Max Tokens**: 200,000 94 | - **Supported Roles**: main, research, fallback 95 | - **Features**: Text + Object generation 96 | 97 | **Basic MCP Provider Models:** 98 | 99 | - **`mcp-sampling`** - General text generation using MCP client sampling 100 | - **`mcp-sampling`** - General text generation using MCP client sampling 101 | - **SWE Score**: null 102 | - **Roles**: Supports main, research, and fallback roles 103 | 104 | ### Model ID Format 105 | 106 | MCP model IDs use a simple format: 107 | 108 | - **`claude-3-5-sonnet-20241022`** - Uses Claude 3.5 Sonnet via MCP sampling 109 | - **`claude-3-opus-20240229`** - Uses Claude 3 Opus via MCP sampling 110 | - **`mcp-sampling`** - Uses MCP client's sampling capability for text generation 111 | 112 | ## Session Requirements 113 | 114 | The MCP provider requires an active MCP session with sampling capabilities: 115 | 116 | ```javascript 117 | session: { 118 | clientCapabilities: { 119 | sampling: {} // Client supports sampling requests 120 | } 121 | } 122 | ``` 123 | 124 | ## Usage Examples 125 | 126 | ### Basic Text Generation 127 | 128 | ```javascript 129 | import { generateTextService } from './scripts/modules/ai-services-unified.js'; 130 | 131 | const result = await generateTextService({ 132 | role: 'main', 133 | session: mcpSession, // Required for MCP provider 134 | prompt: 'Explain MCP integration', 135 | systemPrompt: 'You are a helpful AI assistant' 136 | }); 137 | 138 | console.log(result.text); 139 | ``` 140 | 141 | ### Structured Object Generation 142 | 143 | ```javascript 144 | import { generateObjectService } from './scripts/modules/ai-services-unified.js'; 145 | 146 | const result = await generateObjectService({ 147 | role: 'main', 148 | session: mcpSession, 149 | prompt: 'Create a task breakdown', 150 | schema: { 151 | type: 'object', 152 | properties: { 153 | tasks: { 154 | type: 'array', 155 | items: { type: 'string' } 156 | } 157 | } 158 | } 159 | }); 160 | 161 | console.log(result.object); 162 | ``` 163 | 164 | ### Research Operations 165 | 166 | ```javascript 167 | const research = await generateTextService({ 168 | role: 'research', 169 | session: mcpSession, 170 | prompt: 'Research the latest developments in AI', 171 | systemPrompt: 'You are a research assistant' 172 | }); 173 | ``` 174 | 175 | ## CLI Integration 176 | 177 | The MCP provider works seamlessly with Task Master CLI commands when running in an MCP context: 178 | 179 | ```bash 180 | # Generate tasks using MCP provider (if configured as main) 181 | task-master add-task "Implement user authentication" 182 | 183 | # Research using MCP provider (if configured as research) 184 | task-master research "OAuth 2.0 best practices" 185 | 186 | # Parse PRD using MCP provider 187 | task-master parse-prd requirements.txt 188 | ``` 189 | 190 | ## Architecture Details 191 | 192 | ### Provider Architecture 193 | **MCPProvider** (`mcp-server/src/providers/mcp-provider.js`) 194 | - Modern AI SDK-compliant provider for Task Master's MCP server 195 | - Auto-registers when MCP sessions connect to Task Master 196 | - Enables Task Master to use MCP sessions for AI operations 197 | - Supports both text generation and structured object generation 198 | 199 | ### Auto-Registration Process 200 | 201 | When running as an MCP server, Task Master automatically: 202 | 203 | ```javascript 204 | // On MCP session connect 205 | server.on("connect", (event) => { 206 | // Check session capabilities 207 | if (session.clientCapabilities?.sampling) { 208 | // Create and register MCP provider 209 | const mcpProvider = new MCPProvider(); 210 | mcpProvider.setSession(session); 211 | 212 | // Auto-register with provider registry 213 | providerRegistry.registerProvider('mcp', mcpProvider); 214 | } 215 | }); 216 | ``` 217 | 218 | This enables seamless self-referential AI operations within MCP contexts. 219 | 220 | ### Provider Pattern Integration 221 | 222 | The MCP provider follows the same pattern as other providers: 223 | 224 | ```javascript 225 | class MCPProvider extends BaseAIProvider { 226 | // Implements generateText, generateObject 227 | // Uses session context instead of API keys 228 | // Maps operations to MCP sampling requests 229 | } 230 | ``` 231 | 232 | ### Session Detection 233 | 234 | The provider automatically detects MCP sampling capability when sessions connect: 235 | 236 | ```javascript 237 | // On MCP session connect 238 | if (session.clientCapabilities?.sampling) { 239 | // Auto-register MCP provider for use 240 | const mcpProvider = new MCPProvider(); 241 | mcpProvider.setSession(session); 242 | } 243 | ``` 244 | 245 | ### Sampling Integration 246 | 247 | AI operations use MCP sampling with different levels of support: 248 | 249 | - `generateText()` → MCP `requestSampling()` with messages (2-minute timeout) ✅ **Full Support** 250 | - `streamText()` → **Limited/No Support** ⚠️ See streaming limitations below 251 | - `generateObject()` → MCP `requestSampling()` with JSON schema instructions (2-minute timeout) ✅ **Full Support** 252 | 253 | **Timeout Configuration**: All MCP sampling requests use a 2-minute (120,000ms) timeout to accommodate complex AI operations. 254 | 255 | #### Streaming Text Limitations ⚠️ 256 | 257 | **Important**: The MCP provider has **no support** for text streaming: 258 | 259 | **MCPProvider**: 260 | - **❌ No Streaming Support**: Throws error "MCP Provider does not support streaming text, use generateText instead" 261 | - **Solution**: Always use `generateText()` instead of `streamText()` with this provider 262 | 263 | **Recommendation**: For streaming functionality, configure a non-MCP fallback provider (like Anthropic or OpenAI) in your fallback role. 264 | 265 | ### Error Handling 266 | 267 | The MCP provider includes comprehensive error handling: 268 | 269 | - Session validation errors (checks for `clientCapabilities.sampling`) 270 | - MCP sampling request failures 271 | - JSON parsing errors (for structured output) 272 | - Automatic fallback to other providers 273 | 274 | ### Best Practices 275 | 276 | ### 1. Configure Fallbacks 277 | 278 | Always configure a non-MCP fallback provider, especially for streaming operations: 279 | 280 | ```json 281 | { 282 | "models": { 283 | "main": { 284 | "provider": "mcp", 285 | "modelId": "mcp-sampling" 286 | }, 287 | "fallback": { 288 | "provider": "anthropic", 289 | "modelId": "claude-3-5-sonnet-20241022" 290 | } 291 | } 292 | } 293 | ``` 294 | 295 | ### 2. Avoid Streaming with MCP 296 | 297 | **Do not use `streamTextService()` with MCP provider**. Use `generateTextService()` instead: 298 | 299 | ```javascript 300 | // ❌ Don't do this with MCP provider 301 | const result = await streamTextService({ 302 | role: 'main', // MCP provider 303 | session: mcpSession, 304 | prompt: 'Generate content' 305 | }); 306 | 307 | // ✅ Do this instead 308 | const result = await generateTextService({ 309 | role: 'main', // MCP provider 310 | session: mcpSession, 311 | prompt: 'Generate content' 312 | }); 313 | ``` 314 | 315 | ### 3. Session Management 316 | 317 | Ensure your MCP session remains active throughout Task Master operations: 318 | 319 | ```javascript 320 | // Check session health before operations 321 | if (!session || !session.capabilities) { 322 | throw new Error('MCP session not available'); 323 | } 324 | ``` 325 | 326 | ### 4. Tool Availability 327 | 328 | Verify required capabilities are available in your MCP session: 329 | 330 | ```javascript 331 | // Check session health and capabilities 332 | if (session && session.clientCapabilities && session.clientCapabilities.sampling) { 333 | console.log('MCP sampling available'); 334 | } else { 335 | console.log('MCP sampling not available'); 336 | } 337 | ``` 338 | 339 | ### 5. Error Recovery 340 | 341 | Handle MCP-specific errors gracefully: 342 | 343 | ```javascript 344 | try { 345 | const result = await generateTextService({ 346 | role: 'main', 347 | session: mcpSession, 348 | prompt: 'Generate content' 349 | }); 350 | } catch (error) { 351 | if (error.message.includes('MCP')) { 352 | // Handle MCP-specific error 353 | console.log('MCP error, falling back to alternate provider'); 354 | } 355 | } 356 | ``` 357 | 358 | ## Troubleshooting 359 | 360 | ### Common Issues 361 | 362 | 1. **"MCP provider requires session context"** 363 | - Ensure `session` parameter is passed to service calls 364 | - Verify session has proper structure 365 | - Check that you're running in an MCP environment 366 | 367 | 2. **"MCP session must have client sampling capabilities"** 368 | - Check that `session.clientCapabilities.sampling` exists 369 | - Verify session has `requestSampling()` method 370 | - Ensure MCP client supports sampling feature 371 | 372 | 3. **"MCP Provider does not support streaming text, use generateText instead"** 373 | - **Common Error**: Occurs when calling `streamTextService()` with MCP provider 374 | - **Solution**: Use `generateTextService()` instead of `streamTextService()` 375 | - **Alternative**: Configure a non-MCP fallback provider for streaming operations 376 | 377 | 4. **"MCP sampling failed"** or **Timeout errors** 378 | - Check MCP client is responding to sampling requests 379 | - Verify session is still active and connected 380 | - Consider if request complexity requires longer processing time 381 | - Check for network connectivity issues 382 | 383 | 5. **"Model ID is required for MCP Remote Provider"** 384 | - Ensure `modelId` is specified in configuration 385 | - Use `mcp-sampling` as the standard model ID 386 | - Verify provider configuration is properly loaded 387 | 388 | 6. **Auto-registration failures** 389 | - Check that MCP session has required sampling capabilities 390 | - Verify server event listeners are properly configured 391 | - Look for provider registry initialization issues 392 | 393 | ### Streaming-Related Issues 394 | 395 | **Error**: `streamTextService()` calls fail with MCP provider 396 | **Cause**: MCP provider has no streaming support 397 | **Solutions**: 398 | - Use `generateTextService()` for all MCP-based text generation 399 | - Configure non-MCP fallback providers for streaming requirements 400 | - Check your provider configuration to ensure fallback chain includes streaming-capable providers 401 | 402 | ### Debug Mode 403 | 404 | Enable debug logging to see MCP provider operations: 405 | 406 | ```javascript 407 | // Set debug flag in config or environment 408 | process.env.DEBUG = 'true'; 409 | 410 | // Or in .taskmasterconfig 411 | { 412 | "debug": true, 413 | "models": { /* ... */ } 414 | } 415 | ``` 416 | 417 | ### Testing MCP Integration 418 | 419 | Test MCP provider functionality: 420 | 421 | ```javascript 422 | // Check if MCP provider is properly registered 423 | import { MCPProvider } from './mcp-server/src/providers/mcp-provider.js'; 424 | 425 | // Test session capabilities 426 | if (session && session.clientCapabilities && session.clientCapabilities.sampling) { 427 | console.log('MCP sampling available'); 428 | 429 | // Test provider creation 430 | const provider = new MCPProvider(); 431 | provider.setSession(session); 432 | console.log('MCP provider created successfully'); 433 | } else { 434 | console.log('MCP session lacks required capabilities'); 435 | } 436 | ``` 437 | 438 | ## Integration with Development Tools 439 | 440 | ### VS Code with MCP Extension 441 | 442 | When using Task Master in VS Code with MCP support: 443 | 444 | 1. Configure Task Master MCP server in your `.vscode/mcp.json` 445 | 2. Set MCP provider as main/research in `.taskmaster/config.json` 446 | 3. Benefit from integrated AI assistance within your development workflow 447 | 4. Use Task Master tools directly from VS Code's MCP interface 448 | 449 | **Example VS Code MCP Configuration:** 450 | ```json 451 | { 452 | "servers": { 453 | "task-master-dev": { 454 | "command": "npx", 455 | "args": ["-y", "task-master-ai"], 456 | "cwd": "/path/to/your/task-master-project", 457 | "env": { 458 | "NODE_ENV": "development", 459 | "ANTHROPIC_API_KEY": "${env:ANTHROPIC_API_KEY}", 460 | "TASK_MASTER_PROJECT_ROOT": "/path/to/your/project" 461 | } 462 | } 463 | } 464 | } 465 | ``` 466 | 467 | ### Claude Desktop 468 | 469 | When using Task Master through Claude Desktop's MCP integration: 470 | 471 | 1. Configure Task Master as MCP provider in Claude Desktop 472 | 2. Use MCP provider for AI operations within Task Master 473 | 3. Benefit from nested MCP tool calling capabilities 474 | 475 | ### Cursor and Other MCP Clients 476 | 477 | The MCP provider works with any MCP-compatible development environment: 478 | 479 | 1. Ensure your IDE has MCP client capabilities 480 | 2. Configure Task Master MCP server endpoint 481 | 3. Use MCP provider for enhanced AI-driven development 482 | 483 | ## Advanced Configuration 484 | 485 | ### Custom Tool Mapping 486 | 487 | Advanced users can use MCP sampling for all roles: 488 | 489 | ```javascript 490 | // MCP sampling for all roles 491 | { 492 | "models": { 493 | "main": { 494 | "provider": "mcp", 495 | "modelId": "mcp-sampling" 496 | } 497 | } 498 | } 499 | ``` 500 | 501 | ### Role-Specific Configuration 502 | 503 | Configure MCP sampling for different roles: 504 | 505 | ```json 506 | { 507 | "models": { 508 | "main": { 509 | "provider": "mcp", 510 | "modelId": "mcp-sampling" 511 | }, 512 | "research": { 513 | "provider": "mcp", 514 | "modelId": "mcp-sampling" 515 | }, 516 | "fallback": { 517 | "provider": "mcp", 518 | "modelId": "backup-server:simple-generation" 519 | } 520 | } 521 | } 522 | ``` 523 | 524 | ### API Reference 525 | 526 | ### MCPProvider Methods 527 | 528 | - `generateText(params)` - Generate text using MCP sampling ✅ **Supported** 529 | - `streamText(params)` - Stream text ❌ **Not supported** (throws error) 530 | - `generateObject(params)` - Generate structured objects ✅ **Supported** 531 | - `setSession(session)` - Update provider session 532 | - `validateAuth(params)` - Validate session capabilities 533 | - `getClient()` - Returns null (not applicable for MCP) 534 | 535 | ### Required Parameters 536 | 537 | All MCP operations require: 538 | - `session` - Active MCP session object (auto-provided when registered) 539 | - `modelId` - MCP model identifier (typically "mcp-sampling") 540 | - `messages` - Array of message objects 541 | 542 | ### Optional Parameters 543 | 544 | - `temperature` - Creativity control (if supported by MCP client) 545 | - `maxTokens` - Maximum response length (if supported) 546 | - `schema` - JSON schema for structured output (generateObject only) 547 | 548 | ## Security Considerations 549 | 550 | 1. **Session Security**: MCP sessions should be properly authenticated 551 | 2. **Server Validation**: Only connect to trusted MCP servers 552 | 3. **Data Privacy**: Ensure MCP clients handle data according to your privacy requirements 553 | 4. **Error Exposure**: Be careful not to expose sensitive session information in error messages 554 | 555 | ## Future Enhancements 556 | 557 | Planned improvements for MCP provider: 558 | 559 | 1. **Native Streaming Support** - True streaming for compatible MCP clients (requires MCP protocol updates) 560 | 2. **Enhanced Session Monitoring** - Automatic session validation and recovery 561 | 3. **Performance Optimization** - Caching and connection pooling 562 | 4. **Advanced Error Recovery** - Intelligent retry and fallback strategies 563 | 564 | **Note**: True streaming support depends on future MCP protocol enhancements. Current implementation provides text generation without streaming capabilities. 565 | ```