This is page 9 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 -------------------------------------------------------------------------------- /src/ai-providers/custom-sdk/claude-code/errors.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * @fileoverview Error handling utilities for Claude Code provider 3 | */ 4 | 5 | import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider'; 6 | 7 | /** 8 | * @typedef {import('./types.js').ClaudeCodeErrorMetadata} ClaudeCodeErrorMetadata 9 | */ 10 | 11 | /** 12 | * Create an API call error with Claude Code specific metadata 13 | * @param {Object} params - Error parameters 14 | * @param {string} params.message - Error message 15 | * @param {string} [params.code] - Error code 16 | * @param {number} [params.exitCode] - Process exit code 17 | * @param {string} [params.stderr] - Standard error output 18 | * @param {string} [params.promptExcerpt] - Excerpt of the prompt 19 | * @param {boolean} [params.isRetryable=false] - Whether the error is retryable 20 | * @returns {APICallError} 21 | */ 22 | export function createAPICallError({ 23 | message, 24 | code, 25 | exitCode, 26 | stderr, 27 | promptExcerpt, 28 | isRetryable = false 29 | }) { 30 | /** @type {ClaudeCodeErrorMetadata} */ 31 | const metadata = { 32 | code, 33 | exitCode, 34 | stderr, 35 | promptExcerpt 36 | }; 37 | 38 | return new APICallError({ 39 | message, 40 | isRetryable, 41 | url: 'claude-code-cli://command', 42 | requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined, 43 | data: metadata 44 | }); 45 | } 46 | 47 | /** 48 | * Create an authentication error 49 | * @param {Object} params - Error parameters 50 | * @param {string} params.message - Error message 51 | * @returns {LoadAPIKeyError} 52 | */ 53 | export function createAuthenticationError({ message }) { 54 | return new LoadAPIKeyError({ 55 | message: 56 | message || 57 | 'Authentication failed. Please ensure Claude Code CLI is properly authenticated.' 58 | }); 59 | } 60 | 61 | /** 62 | * Create a timeout error 63 | * @param {Object} params - Error parameters 64 | * @param {string} params.message - Error message 65 | * @param {string} [params.promptExcerpt] - Excerpt of the prompt 66 | * @param {number} params.timeoutMs - Timeout in milliseconds 67 | * @returns {APICallError} 68 | */ 69 | export function createTimeoutError({ message, promptExcerpt, timeoutMs }) { 70 | // Store timeoutMs in metadata for potential use by error handlers 71 | /** @type {ClaudeCodeErrorMetadata & { timeoutMs: number }} */ 72 | const metadata = { 73 | code: 'TIMEOUT', 74 | promptExcerpt, 75 | timeoutMs 76 | }; 77 | 78 | return new APICallError({ 79 | message, 80 | isRetryable: true, 81 | url: 'claude-code-cli://command', 82 | requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined, 83 | data: metadata 84 | }); 85 | } 86 | 87 | /** 88 | * Check if an error is an authentication error 89 | * @param {unknown} error - Error to check 90 | * @returns {boolean} 91 | */ 92 | export function isAuthenticationError(error) { 93 | if (error instanceof LoadAPIKeyError) return true; 94 | if ( 95 | error instanceof APICallError && 96 | /** @type {ClaudeCodeErrorMetadata} */ (error.data)?.exitCode === 401 97 | ) 98 | return true; 99 | return false; 100 | } 101 | 102 | /** 103 | * Check if an error is a timeout error 104 | * @param {unknown} error - Error to check 105 | * @returns {boolean} 106 | */ 107 | export function isTimeoutError(error) { 108 | if ( 109 | error instanceof APICallError && 110 | /** @type {ClaudeCodeErrorMetadata} */ (error.data)?.code === 'TIMEOUT' 111 | ) 112 | return true; 113 | return false; 114 | } 115 | 116 | /** 117 | * Get error metadata from an error 118 | * @param {unknown} error - Error to extract metadata from 119 | * @returns {ClaudeCodeErrorMetadata|undefined} 120 | */ 121 | export function getErrorMetadata(error) { 122 | if (error instanceof APICallError && error.data) { 123 | return /** @type {ClaudeCodeErrorMetadata} */ (error.data); 124 | } 125 | return undefined; 126 | } 127 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/list-tasks.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * list-tasks.js 3 | * Direct function implementation for listing tasks 4 | */ 5 | 6 | import { listTasks } from '../../../../scripts/modules/task-manager.js'; 7 | import { 8 | enableSilentMode, 9 | disableSilentMode 10 | } from '../../../../scripts/modules/utils.js'; 11 | 12 | /** 13 | * Direct function wrapper for listTasks with error handling and caching. 14 | * 15 | * @param {Object} args - Command arguments (now expecting tasksJsonPath explicitly). 16 | * @param {string} args.tasksJsonPath - Path to the tasks.json file. 17 | * @param {string} args.reportPath - Path to the report file. 18 | * @param {string} args.status - Status of the task. 19 | * @param {boolean} args.withSubtasks - Whether to include subtasks. 20 | * @param {string} args.projectRoot - Project root path (for MCP/env fallback) 21 | * @param {string} args.tag - Tag for the task (optional) 22 | * @param {Object} log - Logger object. 23 | * @returns {Promise<Object>} - Task list result { success: boolean, data?: any, error?: { code: string, message: string } }. 24 | */ 25 | export async function listTasksDirect(args, log, context = {}) { 26 | // Destructure the explicit tasksJsonPath from args 27 | const { tasksJsonPath, reportPath, status, withSubtasks, projectRoot, tag } = 28 | args; 29 | const { session } = context; 30 | 31 | if (!tasksJsonPath) { 32 | log.error('listTasksDirect called without tasksJsonPath'); 33 | return { 34 | success: false, 35 | error: { 36 | code: 'MISSING_ARGUMENT', 37 | message: 'tasksJsonPath is required' 38 | } 39 | }; 40 | } 41 | 42 | // Use the explicit tasksJsonPath for cache key 43 | const statusFilter = status || 'all'; 44 | const withSubtasksFilter = withSubtasks || false; 45 | 46 | // Define the action function to be executed on cache miss 47 | const coreListTasksAction = async () => { 48 | try { 49 | // Enable silent mode to prevent console logs from interfering with JSON response 50 | enableSilentMode(); 51 | 52 | log.info( 53 | `Executing core listTasks function for path: ${tasksJsonPath}, filter: ${statusFilter}, subtasks: ${withSubtasksFilter}` 54 | ); 55 | // Pass the explicit tasksJsonPath to the core function 56 | const resultData = listTasks( 57 | tasksJsonPath, 58 | statusFilter, 59 | reportPath, 60 | withSubtasksFilter, 61 | 'json', 62 | { projectRoot, session, tag } 63 | ); 64 | 65 | if (!resultData || !resultData.tasks) { 66 | log.error('Invalid or empty response from listTasks core function'); 67 | return { 68 | success: false, 69 | error: { 70 | code: 'INVALID_CORE_RESPONSE', 71 | message: 'Invalid or empty response from listTasks core function' 72 | } 73 | }; 74 | } 75 | 76 | log.info( 77 | `Core listTasks function retrieved ${resultData.tasks.length} tasks` 78 | ); 79 | 80 | // Restore normal logging 81 | disableSilentMode(); 82 | 83 | return { success: true, data: resultData }; 84 | } catch (error) { 85 | // Make sure to restore normal logging even if there's an error 86 | disableSilentMode(); 87 | 88 | log.error(`Core listTasks function failed: ${error.message}`); 89 | return { 90 | success: false, 91 | error: { 92 | code: 'LIST_TASKS_CORE_ERROR', 93 | message: error.message || 'Failed to list tasks' 94 | } 95 | }; 96 | } 97 | }; 98 | 99 | try { 100 | const result = await coreListTasksAction(); 101 | log.info('listTasksDirect completed'); 102 | return result; 103 | } catch (error) { 104 | log.error(`Unexpected error during listTasks: ${error.message}`); 105 | console.error(error.stack); 106 | return { 107 | success: false, 108 | error: { 109 | code: 'UNEXPECTED_ERROR', 110 | message: error.message 111 | } 112 | }; 113 | } 114 | } 115 | ``` -------------------------------------------------------------------------------- /.github/scripts/check-pre-release-mode.mjs: -------------------------------------------------------------------------------- ``` 1 | #!/usr/bin/env node 2 | import { readFileSync, existsSync } from 'node:fs'; 3 | import { join, dirname, resolve } from 'node:path'; 4 | import { fileURLToPath } from 'node:url'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | // Get context from command line argument or environment 10 | const context = process.argv[2] || process.env.GITHUB_WORKFLOW || 'manual'; 11 | 12 | function findRootDir(startDir) { 13 | let currentDir = resolve(startDir); 14 | while (currentDir !== '/') { 15 | if (existsSync(join(currentDir, 'package.json'))) { 16 | try { 17 | const pkg = JSON.parse( 18 | readFileSync(join(currentDir, 'package.json'), 'utf8') 19 | ); 20 | if (pkg.name === 'task-master-ai' || pkg.repository) { 21 | return currentDir; 22 | } 23 | } catch {} 24 | } 25 | currentDir = dirname(currentDir); 26 | } 27 | throw new Error('Could not find root directory'); 28 | } 29 | 30 | function checkPreReleaseMode() { 31 | console.log('🔍 Checking if branch is in pre-release mode...'); 32 | 33 | const rootDir = findRootDir(__dirname); 34 | const preJsonPath = join(rootDir, '.changeset', 'pre.json'); 35 | 36 | // Check if pre.json exists 37 | if (!existsSync(preJsonPath)) { 38 | console.log('✅ Not in active pre-release mode - safe to proceed'); 39 | process.exit(0); 40 | } 41 | 42 | try { 43 | // Read and parse pre.json 44 | const preJsonContent = readFileSync(preJsonPath, 'utf8'); 45 | const preJson = JSON.parse(preJsonContent); 46 | 47 | // Check if we're in active pre-release mode 48 | if (preJson.mode === 'pre') { 49 | console.error('❌ ERROR: This branch is in active pre-release mode!'); 50 | console.error(''); 51 | 52 | // Provide context-specific error messages 53 | if (context === 'Release Check' || context === 'pull_request') { 54 | console.error( 55 | 'Pre-release mode must be exited before merging to main.' 56 | ); 57 | console.error(''); 58 | console.error( 59 | 'To fix this, run the following commands in your branch:' 60 | ); 61 | console.error(' npx changeset pre exit'); 62 | console.error(' git add -u'); 63 | console.error(' git commit -m "chore: exit pre-release mode"'); 64 | console.error(' git push'); 65 | console.error(''); 66 | console.error('Then update this pull request.'); 67 | } else if (context === 'Release' || context === 'main') { 68 | console.error( 69 | 'Pre-release mode should only be used on feature branches, not main.' 70 | ); 71 | console.error(''); 72 | console.error('To fix this, run the following commands locally:'); 73 | console.error(' npx changeset pre exit'); 74 | console.error(' git add -u'); 75 | console.error(' git commit -m "chore: exit pre-release mode"'); 76 | console.error(' git push origin main'); 77 | console.error(''); 78 | console.error('Then re-run this workflow.'); 79 | } else { 80 | console.error('Pre-release mode must be exited before proceeding.'); 81 | console.error(''); 82 | console.error('To fix this, run the following commands:'); 83 | console.error(' npx changeset pre exit'); 84 | console.error(' git add -u'); 85 | console.error(' git commit -m "chore: exit pre-release mode"'); 86 | console.error(' git push'); 87 | } 88 | 89 | process.exit(1); 90 | } 91 | 92 | console.log('✅ Not in active pre-release mode - safe to proceed'); 93 | process.exit(0); 94 | } catch (error) { 95 | console.error(`❌ ERROR: Unable to parse .changeset/pre.json – aborting.`); 96 | console.error(`Error details: ${error.message}`); 97 | process.exit(1); 98 | } 99 | } 100 | 101 | // Run the check 102 | checkPreReleaseMode(); 103 | ``` -------------------------------------------------------------------------------- /apps/extension/src/services/config-service.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Config Service 3 | * Manages Task Master config.json file operations 4 | */ 5 | 6 | import * as path from 'path'; 7 | import * as fs from 'fs/promises'; 8 | import * as vscode from 'vscode'; 9 | import type { ExtensionLogger } from '../utils/logger'; 10 | 11 | export interface TaskMasterConfigJson { 12 | anthropicApiKey?: string; 13 | perplexityApiKey?: string; 14 | openaiApiKey?: string; 15 | googleApiKey?: string; 16 | xaiApiKey?: string; 17 | openrouterApiKey?: string; 18 | mistralApiKey?: string; 19 | debug?: boolean; 20 | models?: { 21 | main?: string; 22 | research?: string; 23 | fallback?: string; 24 | }; 25 | } 26 | 27 | export class ConfigService { 28 | private configCache: TaskMasterConfigJson | null = null; 29 | private lastReadTime = 0; 30 | private readonly CACHE_DURATION = 5000; // 5 seconds 31 | 32 | constructor(private logger: ExtensionLogger) {} 33 | 34 | /** 35 | * Read Task Master config.json from the workspace 36 | */ 37 | async readConfig(): Promise<TaskMasterConfigJson | null> { 38 | // Check cache first 39 | if ( 40 | this.configCache && 41 | Date.now() - this.lastReadTime < this.CACHE_DURATION 42 | ) { 43 | return this.configCache; 44 | } 45 | 46 | try { 47 | const workspaceRoot = this.getWorkspaceRoot(); 48 | if (!workspaceRoot) { 49 | this.logger.warn('No workspace folder found'); 50 | return null; 51 | } 52 | 53 | const configPath = path.join(workspaceRoot, '.taskmaster', 'config.json'); 54 | 55 | try { 56 | const configContent = await fs.readFile(configPath, 'utf-8'); 57 | const config = JSON.parse(configContent) as TaskMasterConfigJson; 58 | 59 | // Cache the result 60 | this.configCache = config; 61 | this.lastReadTime = Date.now(); 62 | 63 | this.logger.debug('Successfully read Task Master config', { 64 | hasModels: !!config.models, 65 | debug: config.debug 66 | }); 67 | 68 | return config; 69 | } catch (error) { 70 | if ((error as any).code === 'ENOENT') { 71 | this.logger.debug('Task Master config.json not found'); 72 | } else { 73 | this.logger.error('Failed to read Task Master config', error); 74 | } 75 | return null; 76 | } 77 | } catch (error) { 78 | this.logger.error('Error accessing Task Master config', error); 79 | return null; 80 | } 81 | } 82 | 83 | /** 84 | * Get safe config for display (with sensitive data masked) 85 | */ 86 | async getSafeConfig(): Promise<Record<string, any> | null> { 87 | const config = await this.readConfig(); 88 | if (!config) { 89 | return null; 90 | } 91 | 92 | // Create a safe copy with masked API keys 93 | const safeConfig: Record<string, any> = { 94 | ...config 95 | }; 96 | 97 | // Mask all API keys 98 | const apiKeyFields = [ 99 | 'anthropicApiKey', 100 | 'perplexityApiKey', 101 | 'openaiApiKey', 102 | 'googleApiKey', 103 | 'xaiApiKey', 104 | 'openrouterApiKey', 105 | 'mistralApiKey' 106 | ]; 107 | 108 | for (const field of apiKeyFields) { 109 | if (safeConfig[field]) { 110 | safeConfig[field] = this.maskApiKey(safeConfig[field]); 111 | } 112 | } 113 | 114 | return safeConfig; 115 | } 116 | 117 | /** 118 | * Mask API key for display 119 | * Shows only the last 4 characters for better security 120 | */ 121 | private maskApiKey(key: string): string { 122 | if (key.length <= 4) { 123 | return '****'; 124 | } 125 | const visibleChars = 4; 126 | const maskedLength = key.length - visibleChars; 127 | return ( 128 | '*'.repeat(Math.min(maskedLength, 12)) + 129 | key.substring(key.length - visibleChars) 130 | ); 131 | } 132 | 133 | /** 134 | * Clear cache 135 | */ 136 | clearCache(): void { 137 | this.configCache = null; 138 | this.lastReadTime = 0; 139 | } 140 | 141 | /** 142 | * Get workspace root path 143 | */ 144 | private getWorkspaceRoot(): string | undefined { 145 | return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; 146 | } 147 | } 148 | ``` -------------------------------------------------------------------------------- /src/progress/cli-progress-factory.js: -------------------------------------------------------------------------------- ```javascript 1 | import cliProgress from 'cli-progress'; 2 | 3 | /** 4 | * Default configuration for progress bars 5 | * Extracted to avoid duplication and provide single source of truth 6 | */ 7 | const DEFAULT_CONFIG = { 8 | clearOnComplete: false, 9 | stopOnComplete: true, 10 | hideCursor: true, 11 | barsize: 40 // Standard terminal width for progress bar 12 | }; 13 | 14 | /** 15 | * Available presets for progress bar styling 16 | * Makes it easy to see what options are available 17 | */ 18 | const PRESETS = { 19 | shades_classic: cliProgress.Presets.shades_classic, 20 | shades_grey: cliProgress.Presets.shades_grey, 21 | rect: cliProgress.Presets.rect, 22 | legacy: cliProgress.Presets.legacy 23 | }; 24 | 25 | /** 26 | * Factory class for creating CLI progress bars 27 | * Provides a consistent interface for creating both single and multi-bar instances 28 | */ 29 | export class ProgressBarFactory { 30 | constructor(defaultOptions = {}, defaultPreset = PRESETS.shades_classic) { 31 | this.defaultOptions = { ...DEFAULT_CONFIG, ...defaultOptions }; 32 | this.defaultPreset = defaultPreset; 33 | } 34 | 35 | /** 36 | * Creates a new single progress bar 37 | * @param {Object} opts - Custom options to override defaults 38 | * @param {Object} preset - Progress bar preset for styling 39 | * @returns {cliProgress.SingleBar} Configured single progress bar instance 40 | */ 41 | createSingleBar(opts = {}, preset = null) { 42 | const config = this._mergeConfig(opts); 43 | const barPreset = preset || this.defaultPreset; 44 | 45 | return new cliProgress.SingleBar(config, barPreset); 46 | } 47 | 48 | /** 49 | * Creates a new multi-bar container 50 | * @param {Object} opts - Custom options to override defaults 51 | * @param {Object} preset - Progress bar preset for styling 52 | * @returns {cliProgress.MultiBar} Configured multi-bar instance 53 | */ 54 | createMultiBar(opts = {}, preset = null) { 55 | const config = this._mergeConfig(opts); 56 | const barPreset = preset || this.defaultPreset; 57 | 58 | return new cliProgress.MultiBar(config, barPreset); 59 | } 60 | 61 | /** 62 | * Merges custom options with defaults 63 | * @private 64 | * @param {Object} customOpts - Custom options to merge 65 | * @returns {Object} Merged configuration 66 | */ 67 | _mergeConfig(customOpts) { 68 | return { ...this.defaultOptions, ...customOpts }; 69 | } 70 | 71 | /** 72 | * Updates the default configuration 73 | * @param {Object} options - New default options 74 | */ 75 | setDefaultOptions(options) { 76 | this.defaultOptions = { ...this.defaultOptions, ...options }; 77 | } 78 | 79 | /** 80 | * Updates the default preset 81 | * @param {Object} preset - New default preset 82 | */ 83 | setDefaultPreset(preset) { 84 | this.defaultPreset = preset; 85 | } 86 | } 87 | 88 | // Create a default factory instance for backward compatibility 89 | const defaultFactory = new ProgressBarFactory(); 90 | 91 | /** 92 | * Legacy function for creating a single progress bar 93 | * @deprecated Use ProgressBarFactory.createSingleBar() instead 94 | * @param {Object} opts - Progress bar options 95 | * @returns {cliProgress.SingleBar} Single progress bar instance 96 | */ 97 | export function newSingle(opts = {}) { 98 | return defaultFactory.createSingleBar(opts); 99 | } 100 | 101 | /** 102 | * Legacy function for creating a multi-bar 103 | * @deprecated Use ProgressBarFactory.createMultiBar() instead 104 | * @param {Object} opts - Progress bar options 105 | * @returns {cliProgress.MultiBar} Multi-bar instance 106 | */ 107 | export function newMultiBar(opts = {}) { 108 | return defaultFactory.createMultiBar(opts); 109 | } 110 | 111 | // Export presets for easy access 112 | export { PRESETS }; 113 | 114 | // Export the factory class as default 115 | export default ProgressBarFactory; 116 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/add-subtask.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tools/add-subtask.js 3 | * Tool for adding subtasks to existing tasks 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | handleApiResult, 9 | createErrorResponse, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { addSubtaskDirect } from '../core/task-master-core.js'; 13 | import { findTasksPath } from '../core/utils/path-utils.js'; 14 | import { resolveTag } from '../../../scripts/modules/utils.js'; 15 | 16 | /** 17 | * Register the addSubtask tool with the MCP server 18 | * @param {Object} server - FastMCP server instance 19 | */ 20 | export function registerAddSubtaskTool(server) { 21 | server.addTool({ 22 | name: 'add_subtask', 23 | description: 'Add a subtask to an existing task', 24 | parameters: z.object({ 25 | id: z.string().describe('Parent task ID (required)'), 26 | taskId: z 27 | .string() 28 | .optional() 29 | .describe('Existing task ID to convert to subtask'), 30 | title: z 31 | .string() 32 | .optional() 33 | .describe('Title for the new subtask (when creating a new subtask)'), 34 | description: z 35 | .string() 36 | .optional() 37 | .describe('Description for the new subtask'), 38 | details: z 39 | .string() 40 | .optional() 41 | .describe('Implementation details for the new subtask'), 42 | status: z 43 | .string() 44 | .optional() 45 | .describe("Status for the new subtask (default: 'pending')"), 46 | dependencies: z 47 | .string() 48 | .optional() 49 | .describe('Comma-separated list of dependency IDs for the new subtask'), 50 | file: z 51 | .string() 52 | .optional() 53 | .describe( 54 | 'Absolute path to the tasks file (default: tasks/tasks.json)' 55 | ), 56 | skipGenerate: z 57 | .boolean() 58 | .optional() 59 | .describe('Skip regenerating task files'), 60 | projectRoot: z 61 | .string() 62 | .describe('The directory of the project. Must be an absolute path.'), 63 | tag: z.string().optional().describe('Tag context to operate on') 64 | }), 65 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 66 | try { 67 | const resolvedTag = resolveTag({ 68 | projectRoot: args.projectRoot, 69 | tag: args.tag 70 | }); 71 | log.info(`Adding subtask with args: ${JSON.stringify(args)}`); 72 | 73 | // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) 74 | let tasksJsonPath; 75 | try { 76 | tasksJsonPath = findTasksPath( 77 | { projectRoot: args.projectRoot, file: args.file }, 78 | log 79 | ); 80 | } catch (error) { 81 | log.error(`Error finding tasks.json: ${error.message}`); 82 | return createErrorResponse( 83 | `Failed to find tasks.json: ${error.message}` 84 | ); 85 | } 86 | 87 | const result = await addSubtaskDirect( 88 | { 89 | tasksJsonPath: tasksJsonPath, 90 | id: args.id, 91 | taskId: args.taskId, 92 | title: args.title, 93 | description: args.description, 94 | details: args.details, 95 | status: args.status, 96 | dependencies: args.dependencies, 97 | skipGenerate: args.skipGenerate, 98 | projectRoot: args.projectRoot, 99 | tag: resolvedTag 100 | }, 101 | log, 102 | { session } 103 | ); 104 | 105 | if (result.success) { 106 | log.info(`Subtask added successfully: ${result.data.message}`); 107 | } else { 108 | log.error(`Failed to add subtask: ${result.error.message}`); 109 | } 110 | 111 | return handleApiResult( 112 | result, 113 | log, 114 | 'Error adding subtask', 115 | undefined, 116 | args.projectRoot 117 | ); 118 | } catch (error) { 119 | log.error(`Error in addSubtask tool: ${error.message}`); 120 | return createErrorResponse(error.message); 121 | } 122 | }) 123 | }); 124 | } 125 | ``` -------------------------------------------------------------------------------- /src/provider-registry/index.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Provider Registry - Singleton for managing AI providers 3 | * 4 | * This module implements a singleton registry that allows dynamic registration 5 | * of AI providers at runtime, while maintaining compatibility with the existing 6 | * static PROVIDERS object in ai-services-unified.js. 7 | */ 8 | 9 | // Singleton instance 10 | let instance = null; 11 | 12 | /** 13 | * Provider Registry class - Manages dynamic provider registration 14 | */ 15 | class ProviderRegistry { 16 | constructor() { 17 | // Private provider map 18 | this._providers = new Map(); 19 | 20 | // Flag to track initialization 21 | this._initialized = false; 22 | } 23 | 24 | /** 25 | * Get the singleton instance 26 | * @returns {ProviderRegistry} The singleton instance 27 | */ 28 | static getInstance() { 29 | if (!instance) { 30 | instance = new ProviderRegistry(); 31 | } 32 | return instance; 33 | } 34 | 35 | /** 36 | * Initialize the registry 37 | * @returns {ProviderRegistry} The singleton instance 38 | */ 39 | initialize() { 40 | if (this._initialized) { 41 | return this; 42 | } 43 | 44 | this._initialized = true; 45 | return this; 46 | } 47 | 48 | /** 49 | * Register a provider with the registry 50 | * @param {string} providerName - The name of the provider 51 | * @param {object} provider - The provider instance 52 | * @param {object} options - Additional options for registration 53 | * @returns {ProviderRegistry} The singleton instance for chaining 54 | */ 55 | registerProvider(providerName, provider, options = {}) { 56 | if (!providerName || typeof providerName !== 'string') { 57 | throw new Error('Provider name must be a non-empty string'); 58 | } 59 | 60 | if (!provider) { 61 | throw new Error('Provider instance is required'); 62 | } 63 | 64 | // Validate that provider implements the required interface 65 | if ( 66 | typeof provider.generateText !== 'function' || 67 | typeof provider.streamText !== 'function' || 68 | typeof provider.generateObject !== 'function' 69 | ) { 70 | throw new Error('Provider must implement BaseAIProvider interface'); 71 | } 72 | 73 | // Add provider to the registry 74 | this._providers.set(providerName, { 75 | instance: provider, 76 | options, 77 | registeredAt: new Date() 78 | }); 79 | 80 | return this; 81 | } 82 | 83 | /** 84 | * Check if a provider exists in the registry 85 | * @param {string} providerName - The name of the provider 86 | * @returns {boolean} True if the provider exists 87 | */ 88 | hasProvider(providerName) { 89 | return this._providers.has(providerName); 90 | } 91 | 92 | /** 93 | * Get a provider from the registry 94 | * @param {string} providerName - The name of the provider 95 | * @returns {object|null} The provider instance or null if not found 96 | */ 97 | getProvider(providerName) { 98 | const providerEntry = this._providers.get(providerName); 99 | return providerEntry ? providerEntry.instance : null; 100 | } 101 | 102 | /** 103 | * Get all registered providers 104 | * @returns {Map} Map of all registered providers 105 | */ 106 | getAllProviders() { 107 | return new Map(this._providers); 108 | } 109 | 110 | /** 111 | * Remove a provider from the registry 112 | * @param {string} providerName - The name of the provider 113 | * @returns {boolean} True if the provider was removed 114 | */ 115 | unregisterProvider(providerName) { 116 | if (this._providers.has(providerName)) { 117 | this._providers.delete(providerName); 118 | return true; 119 | } 120 | return false; 121 | } 122 | 123 | /** 124 | * Reset the registry (primarily for testing) 125 | */ 126 | reset() { 127 | this._providers.clear(); 128 | this._initialized = false; 129 | } 130 | } 131 | 132 | ProviderRegistry.getInstance().initialize(); // Ensure singleton is initialized on import 133 | // Export singleton getter 134 | export default ProviderRegistry; 135 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/copy-tag.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * copy-tag.js 3 | * Direct function implementation for copying a tag 4 | */ 5 | 6 | import { copyTag } from '../../../../scripts/modules/task-manager/tag-management.js'; 7 | import { 8 | enableSilentMode, 9 | disableSilentMode 10 | } from '../../../../scripts/modules/utils.js'; 11 | import { createLogWrapper } from '../../tools/utils.js'; 12 | 13 | /** 14 | * Direct function wrapper for copying a tag with error handling. 15 | * 16 | * @param {Object} args - Command arguments 17 | * @param {string} args.sourceName - Name of the source tag to copy from 18 | * @param {string} args.targetName - Name of the new tag to create 19 | * @param {string} [args.description] - Optional description for the new tag 20 | * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) 21 | * @param {string} [args.projectRoot] - Project root path 22 | * @param {Object} log - Logger object 23 | * @param {Object} context - Additional context (session) 24 | * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } } 25 | */ 26 | export async function copyTagDirect(args, log, context = {}) { 27 | // Destructure expected args 28 | const { tasksJsonPath, sourceName, targetName, description, projectRoot } = 29 | args; 30 | const { session } = context; 31 | 32 | // Enable silent mode to prevent console logs from interfering with JSON response 33 | enableSilentMode(); 34 | 35 | // Create logger wrapper using the utility 36 | const mcpLog = createLogWrapper(log); 37 | 38 | try { 39 | // Check if tasksJsonPath was provided 40 | if (!tasksJsonPath) { 41 | log.error('copyTagDirect called without tasksJsonPath'); 42 | disableSilentMode(); 43 | return { 44 | success: false, 45 | error: { 46 | code: 'MISSING_ARGUMENT', 47 | message: 'tasksJsonPath is required' 48 | } 49 | }; 50 | } 51 | 52 | // Check required parameters 53 | if (!sourceName || typeof sourceName !== 'string') { 54 | log.error('Missing required parameter: sourceName'); 55 | disableSilentMode(); 56 | return { 57 | success: false, 58 | error: { 59 | code: 'MISSING_PARAMETER', 60 | message: 'Source tag name is required and must be a string' 61 | } 62 | }; 63 | } 64 | 65 | if (!targetName || typeof targetName !== 'string') { 66 | log.error('Missing required parameter: targetName'); 67 | disableSilentMode(); 68 | return { 69 | success: false, 70 | error: { 71 | code: 'MISSING_PARAMETER', 72 | message: 'Target tag name is required and must be a string' 73 | } 74 | }; 75 | } 76 | 77 | log.info(`Copying tag from "${sourceName}" to "${targetName}"`); 78 | 79 | // Prepare options 80 | const options = { 81 | description 82 | }; 83 | 84 | // Call the copyTag function 85 | const result = await copyTag( 86 | tasksJsonPath, 87 | sourceName, 88 | targetName, 89 | options, 90 | { 91 | session, 92 | mcpLog, 93 | projectRoot 94 | }, 95 | 'json' // outputFormat - use 'json' to suppress CLI UI 96 | ); 97 | 98 | // Restore normal logging 99 | disableSilentMode(); 100 | 101 | return { 102 | success: true, 103 | data: { 104 | sourceName: result.sourceName, 105 | targetName: result.targetName, 106 | copied: result.copied, 107 | tasksCopied: result.tasksCopied, 108 | description: result.description, 109 | message: `Successfully copied tag from "${result.sourceName}" to "${result.targetName}"` 110 | } 111 | }; 112 | } catch (error) { 113 | // Make sure to restore normal logging even if there's an error 114 | disableSilentMode(); 115 | 116 | log.error(`Error in copyTagDirect: ${error.message}`); 117 | return { 118 | success: false, 119 | error: { 120 | code: error.code || 'COPY_TAG_ERROR', 121 | message: error.message 122 | } 123 | }; 124 | } 125 | } 126 | ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/parse-prd/parse-prd-config.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Configuration classes and schemas for PRD parsing 3 | */ 4 | 5 | import { z } from 'zod'; 6 | import { TASK_PRIORITY_OPTIONS } from '../../../../src/constants/task-priority.js'; 7 | import { getCurrentTag, isSilentMode, log } from '../../utils.js'; 8 | import { Duration } from '../../../../src/utils/timeout-manager.js'; 9 | import { hasCodebaseAnalysis } from '../../config-manager.js'; 10 | 11 | // ============================================================================ 12 | // SCHEMAS 13 | // ============================================================================ 14 | 15 | // Define the Zod schema for a SINGLE task object 16 | export const prdSingleTaskSchema = z.object({ 17 | id: z.number(), 18 | title: z.string().min(1), 19 | description: z.string().min(1), 20 | details: z.string(), 21 | testStrategy: z.string(), 22 | priority: z.enum(TASK_PRIORITY_OPTIONS), 23 | dependencies: z.array(z.number()), 24 | status: z.string() 25 | }); 26 | 27 | // Define the Zod schema for the ENTIRE expected AI response object 28 | export const prdResponseSchema = z.object({ 29 | tasks: z.array(prdSingleTaskSchema), 30 | metadata: z.object({ 31 | projectName: z.string(), 32 | totalTasks: z.number(), 33 | sourceFile: z.string(), 34 | generatedAt: z.string() 35 | }) 36 | }); 37 | 38 | // ============================================================================ 39 | // CONFIGURATION CLASSES 40 | // ============================================================================ 41 | 42 | /** 43 | * Configuration object for PRD parsing 44 | */ 45 | export class PrdParseConfig { 46 | constructor(prdPath, tasksPath, numTasks, options = {}) { 47 | this.prdPath = prdPath; 48 | this.tasksPath = tasksPath; 49 | this.numTasks = numTasks; 50 | this.force = options.force || false; 51 | this.append = options.append || false; 52 | this.research = options.research || false; 53 | this.reportProgress = options.reportProgress; 54 | this.mcpLog = options.mcpLog; 55 | this.session = options.session; 56 | this.projectRoot = options.projectRoot; 57 | this.tag = options.tag; 58 | this.streamingTimeout = 59 | options.streamingTimeout || Duration.seconds(180).milliseconds; 60 | 61 | // Derived values 62 | this.targetTag = this.tag || getCurrentTag(this.projectRoot) || 'master'; 63 | this.isMCP = !!this.mcpLog; 64 | this.outputFormat = this.isMCP && !this.reportProgress ? 'json' : 'text'; 65 | 66 | // Feature flag: Temporarily disable streaming, use generateObject instead 67 | // TODO: Re-enable streaming once issues are resolved 68 | const ENABLE_STREAMING = false; 69 | 70 | this.useStreaming = 71 | ENABLE_STREAMING && 72 | (typeof this.reportProgress === 'function' || 73 | this.outputFormat === 'text'); 74 | } 75 | 76 | /** 77 | * Check if codebase analysis is available (Claude Code or Gemini CLI) 78 | */ 79 | hasCodebaseAnalysis() { 80 | return hasCodebaseAnalysis(this.research, this.projectRoot, this.session); 81 | } 82 | } 83 | 84 | /** 85 | * Logging configuration and utilities 86 | */ 87 | export class LoggingConfig { 88 | constructor(mcpLog, reportProgress) { 89 | this.isMCP = !!mcpLog; 90 | this.outputFormat = this.isMCP && !reportProgress ? 'json' : 'text'; 91 | 92 | this.logFn = mcpLog || { 93 | info: (...args) => log('info', ...args), 94 | warn: (...args) => log('warn', ...args), 95 | error: (...args) => log('error', ...args), 96 | debug: (...args) => log('debug', ...args), 97 | success: (...args) => log('success', ...args) 98 | }; 99 | } 100 | 101 | report(message, level = 'info') { 102 | if (this.logFn && typeof this.logFn[level] === 'function') { 103 | this.logFn[level](message); 104 | } else if (!isSilentMode() && this.outputFormat === 'text') { 105 | log(level, message); 106 | } 107 | } 108 | } 109 | ``` -------------------------------------------------------------------------------- /.github/workflows/extension-ci.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Extension CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | paths: 9 | - 'apps/extension/**' 10 | - '.github/workflows/extension-ci.yml' 11 | pull_request: 12 | branches: 13 | - main 14 | - next 15 | paths: 16 | - 'apps/extension/**' 17 | - '.github/workflows/extension-ci.yml' 18 | 19 | permissions: 20 | contents: read 21 | 22 | jobs: 23 | setup: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - uses: actions/setup-node@v4 31 | with: 32 | node-version: 20 33 | 34 | - name: Cache node_modules 35 | uses: actions/cache@v4 36 | with: 37 | path: | 38 | node_modules 39 | */*/node_modules 40 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 41 | restore-keys: | 42 | ${{ runner.os }}-node- 43 | 44 | - name: Install Extension Dependencies 45 | working-directory: apps/extension 46 | run: npm ci 47 | timeout-minutes: 5 48 | 49 | typecheck: 50 | needs: setup 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v4 54 | 55 | - uses: actions/setup-node@v4 56 | with: 57 | node-version: 20 58 | 59 | 60 | - name: Restore node_modules 61 | uses: actions/cache@v4 62 | with: 63 | path: | 64 | node_modules 65 | */*/node_modules 66 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 67 | restore-keys: | 68 | ${{ runner.os }}-node- 69 | 70 | - name: Install if cache miss 71 | working-directory: apps/extension 72 | run: npm ci 73 | timeout-minutes: 3 74 | 75 | - name: Type Check Extension 76 | working-directory: apps/extension 77 | run: npm run check-types 78 | env: 79 | FORCE_COLOR: 1 80 | 81 | build: 82 | needs: setup 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v4 86 | 87 | - uses: actions/setup-node@v4 88 | with: 89 | node-version: 20 90 | 91 | 92 | - name: Restore node_modules 93 | uses: actions/cache@v4 94 | with: 95 | path: | 96 | node_modules 97 | */*/node_modules 98 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 99 | restore-keys: | 100 | ${{ runner.os }}-node- 101 | 102 | - name: Install if cache miss 103 | working-directory: apps/extension 104 | run: npm ci 105 | timeout-minutes: 3 106 | 107 | - name: Build Extension 108 | working-directory: apps/extension 109 | run: npm run build 110 | env: 111 | FORCE_COLOR: 1 112 | 113 | - name: Package Extension 114 | working-directory: apps/extension 115 | run: npm run package 116 | env: 117 | FORCE_COLOR: 1 118 | 119 | - name: Verify Package Contents 120 | working-directory: apps/extension 121 | run: | 122 | echo "Checking vsix-build contents..." 123 | ls -la vsix-build/ 124 | echo "Checking dist contents..." 125 | ls -la vsix-build/dist/ 126 | echo "Checking package.json exists..." 127 | test -f vsix-build/package.json 128 | 129 | - name: Create VSIX Package (Test) 130 | working-directory: apps/extension/vsix-build 131 | run: npx vsce package --no-dependencies 132 | env: 133 | FORCE_COLOR: 1 134 | 135 | - name: Upload Extension Artifact 136 | uses: actions/upload-artifact@v4 137 | with: 138 | name: extension-package 139 | path: | 140 | apps/extension/vsix-build/*.vsix 141 | apps/extension/dist/ 142 | retention-days: 30 143 | 144 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/remove-subtask.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Direct function wrapper for removeSubtask 3 | */ 4 | 5 | import { removeSubtask } from '../../../../scripts/modules/task-manager.js'; 6 | import { 7 | enableSilentMode, 8 | disableSilentMode 9 | } from '../../../../scripts/modules/utils.js'; 10 | 11 | /** 12 | * Remove a subtask from its parent task 13 | * @param {Object} args - Function arguments 14 | * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. 15 | * @param {string} args.id - Subtask ID in format "parentId.subtaskId" (required) 16 | * @param {boolean} [args.convert] - Whether to convert the subtask to a standalone task 17 | * @param {boolean} [args.skipGenerate] - Skip regenerating task files 18 | * @param {string} args.projectRoot - Project root path (for MCP/env fallback) 19 | * @param {string} args.tag - Tag for the task (optional) 20 | * @param {Object} log - Logger object 21 | * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>} 22 | */ 23 | export async function removeSubtaskDirect(args, log) { 24 | // Destructure expected args 25 | const { tasksJsonPath, id, convert, skipGenerate, projectRoot, tag } = args; 26 | try { 27 | // Enable silent mode to prevent console logs from interfering with JSON response 28 | enableSilentMode(); 29 | 30 | log.info(`Removing subtask with args: ${JSON.stringify(args)}`); 31 | 32 | // Check if tasksJsonPath was provided 33 | if (!tasksJsonPath) { 34 | log.error('removeSubtaskDirect called without tasksJsonPath'); 35 | disableSilentMode(); // Disable before returning 36 | return { 37 | success: false, 38 | error: { 39 | code: 'MISSING_ARGUMENT', 40 | message: 'tasksJsonPath is required' 41 | } 42 | }; 43 | } 44 | 45 | if (!id) { 46 | disableSilentMode(); // Disable before returning 47 | return { 48 | success: false, 49 | error: { 50 | code: 'INPUT_VALIDATION_ERROR', 51 | message: 52 | 'Subtask ID is required and must be in format "parentId.subtaskId"' 53 | } 54 | }; 55 | } 56 | 57 | // Validate subtask ID format 58 | if (!id.includes('.')) { 59 | disableSilentMode(); // Disable before returning 60 | return { 61 | success: false, 62 | error: { 63 | code: 'INPUT_VALIDATION_ERROR', 64 | message: `Invalid subtask ID format: ${id}. Expected format: "parentId.subtaskId"` 65 | } 66 | }; 67 | } 68 | 69 | // Use provided path 70 | const tasksPath = tasksJsonPath; 71 | 72 | // Convert convertToTask to a boolean 73 | const convertToTask = convert === true; 74 | 75 | // Determine if we should generate files 76 | const generateFiles = !skipGenerate; 77 | 78 | log.info( 79 | `Removing subtask ${id} (convertToTask: ${convertToTask}, generateFiles: ${generateFiles})` 80 | ); 81 | 82 | // Use the provided tasksPath 83 | const result = await removeSubtask( 84 | tasksPath, 85 | id, 86 | convertToTask, 87 | generateFiles, 88 | { 89 | projectRoot, 90 | tag 91 | } 92 | ); 93 | 94 | // Restore normal logging 95 | disableSilentMode(); 96 | 97 | if (convertToTask && result) { 98 | // Return info about the converted task 99 | return { 100 | success: true, 101 | data: { 102 | message: `Subtask ${id} successfully converted to task #${result.id}`, 103 | task: result 104 | } 105 | }; 106 | } else { 107 | // Return simple success message for deletion 108 | return { 109 | success: true, 110 | data: { 111 | message: `Subtask ${id} successfully removed` 112 | } 113 | }; 114 | } 115 | } catch (error) { 116 | // Ensure silent mode is disabled even if an outer error occurs 117 | disableSilentMode(); 118 | 119 | log.error(`Error in removeSubtaskDirect: ${error.message}`); 120 | return { 121 | success: false, 122 | error: { 123 | code: 'CORE_FUNCTION_ERROR', 124 | message: error.message 125 | } 126 | }; 127 | } 128 | } 129 | ``` -------------------------------------------------------------------------------- /src/ui/confirm.js: -------------------------------------------------------------------------------- ```javascript 1 | import chalk from 'chalk'; 2 | import boxen from 'boxen'; 3 | 4 | /** 5 | * Confirm removing profile rules (destructive operation) 6 | * @param {string[]} profiles - Array of profile names to remove 7 | * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise 8 | */ 9 | async function confirmProfilesRemove(profiles) { 10 | const profileList = profiles 11 | .map((b) => b.charAt(0).toUpperCase() + b.slice(1)) 12 | .join(', '); 13 | console.log( 14 | boxen( 15 | chalk.yellow( 16 | `WARNING: This will selectively remove Task Master components for: ${profileList}. 17 | 18 | What will be removed: 19 | • Task Master specific rule files (e.g., cursor_rules.mdc, taskmaster.mdc, etc.) 20 | • Task Master MCP server configuration (if no other MCP servers exist) 21 | 22 | What will be preserved: 23 | • Your existing custom rule files 24 | • Other MCP server configurations 25 | • The profile directory itself (unless completely empty after removal) 26 | 27 | The .[profile] directory will only be removed if ALL of the following are true: 28 | • All rules in the directory were Task Master rules (no custom rules) 29 | • No other files or folders exist in the profile directory 30 | • The MCP configuration was completely removed (no other servers) 31 | 32 | Are you sure you want to proceed?` 33 | ), 34 | { padding: 1, borderColor: 'yellow', borderStyle: 'round' } 35 | ) 36 | ); 37 | const inquirer = await import('inquirer'); 38 | const { confirm } = await inquirer.default.prompt([ 39 | { 40 | type: 'confirm', 41 | name: 'confirm', 42 | message: 'Type y to confirm selective removal, or n to abort:', 43 | default: false 44 | } 45 | ]); 46 | return confirm; 47 | } 48 | 49 | /** 50 | * Confirm removing ALL remaining profile rules (extremely critical operation) 51 | * @param {string[]} profiles - Array of profile names to remove 52 | * @param {string[]} remainingProfiles - Array of profiles that would be left after removal 53 | * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise 54 | */ 55 | async function confirmRemoveAllRemainingProfiles(profiles, remainingProfiles) { 56 | const profileList = profiles 57 | .map((p) => p.charAt(0).toUpperCase() + p.slice(1)) 58 | .join(', '); 59 | 60 | console.log( 61 | boxen( 62 | chalk.red.bold( 63 | `⚠️ CRITICAL WARNING: REMOVING ALL TASK MASTER RULE PROFILES ⚠️\n\n` + 64 | `You are about to remove Task Master components for: ${profileList}\n` + 65 | `This will leave your project with NO Task Master rule profiles remaining!\n\n` + 66 | `What will be removed:\n` + 67 | `• All Task Master specific rule files\n` + 68 | `• Task Master MCP server configurations\n` + 69 | `• Profile directories (only if completely empty after removal)\n\n` + 70 | `What will be preserved:\n` + 71 | `• Your existing custom rule files\n` + 72 | `• Other MCP server configurations\n` + 73 | `• Profile directories with custom content\n\n` + 74 | `This could impact Task Master functionality but will preserve your custom configurations.\n\n` + 75 | `Are you absolutely sure you want to proceed?` 76 | ), 77 | { 78 | padding: 1, 79 | borderColor: 'red', 80 | borderStyle: 'double', 81 | title: '🚨 CRITICAL OPERATION', 82 | titleAlignment: 'center' 83 | } 84 | ) 85 | ); 86 | 87 | const inquirer = await import('inquirer'); 88 | const { confirm } = await inquirer.default.prompt([ 89 | { 90 | type: 'confirm', 91 | name: 'confirm', 92 | message: 93 | 'Type y to confirm removing ALL Task Master rule profiles, or n to abort:', 94 | default: false 95 | } 96 | ]); 97 | return confirm; 98 | } 99 | 100 | export { confirmProfilesRemove, confirmRemoveAllRemainingProfiles }; 101 | ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/update-single-task-status.js: -------------------------------------------------------------------------------- ```javascript 1 | import chalk from 'chalk'; 2 | 3 | import { log } from '../utils.js'; 4 | import { isValidTaskStatus } from '../../../src/constants/task-status.js'; 5 | 6 | /** 7 | * Update the status of a single task 8 | * @param {string} tasksPath - Path to the tasks.json file 9 | * @param {string} taskIdInput - Task ID to update 10 | * @param {string} newStatus - New status 11 | * @param {Object} data - Tasks data 12 | * @param {boolean} showUi - Whether to show UI elements 13 | */ 14 | async function updateSingleTaskStatus( 15 | tasksPath, 16 | taskIdInput, 17 | newStatus, 18 | data, 19 | showUi = true 20 | ) { 21 | if (!isValidTaskStatus(newStatus)) { 22 | throw new Error( 23 | `Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}` 24 | ); 25 | } 26 | 27 | // Check if it's a subtask (e.g., "1.2") 28 | if (taskIdInput.includes('.')) { 29 | const [parentId, subtaskId] = taskIdInput 30 | .split('.') 31 | .map((id) => parseInt(id, 10)); 32 | 33 | // Find the parent task 34 | const parentTask = data.tasks.find((t) => t.id === parentId); 35 | if (!parentTask) { 36 | throw new Error(`Parent task ${parentId} not found`); 37 | } 38 | 39 | // Find the subtask 40 | if (!parentTask.subtasks) { 41 | throw new Error(`Parent task ${parentId} has no subtasks`); 42 | } 43 | 44 | const subtask = parentTask.subtasks.find((st) => st.id === subtaskId); 45 | if (!subtask) { 46 | throw new Error( 47 | `Subtask ${subtaskId} not found in parent task ${parentId}` 48 | ); 49 | } 50 | 51 | // Update the subtask status 52 | const oldStatus = subtask.status || 'pending'; 53 | subtask.status = newStatus; 54 | 55 | log( 56 | 'info', 57 | `Updated subtask ${parentId}.${subtaskId} status from '${oldStatus}' to '${newStatus}'` 58 | ); 59 | 60 | // Check if all subtasks are done (if setting to 'done') 61 | if ( 62 | newStatus.toLowerCase() === 'done' || 63 | newStatus.toLowerCase() === 'completed' 64 | ) { 65 | const allSubtasksDone = parentTask.subtasks.every( 66 | (st) => st.status === 'done' || st.status === 'completed' 67 | ); 68 | 69 | // Suggest updating parent task if all subtasks are done 70 | if ( 71 | allSubtasksDone && 72 | parentTask.status !== 'done' && 73 | parentTask.status !== 'completed' 74 | ) { 75 | // Only show suggestion in CLI mode 76 | if (showUi) { 77 | console.log( 78 | chalk.yellow( 79 | `All subtasks of parent task ${parentId} are now marked as done.` 80 | ) 81 | ); 82 | console.log( 83 | chalk.yellow( 84 | `Consider updating the parent task status with: task-master set-status --id=${parentId} --status=done` 85 | ) 86 | ); 87 | } 88 | } 89 | } 90 | } else { 91 | // Handle regular task 92 | const taskId = parseInt(taskIdInput, 10); 93 | const task = data.tasks.find((t) => t.id === taskId); 94 | 95 | if (!task) { 96 | throw new Error(`Task ${taskId} not found`); 97 | } 98 | 99 | // Update the task status 100 | const oldStatus = task.status || 'pending'; 101 | task.status = newStatus; 102 | 103 | log( 104 | 'info', 105 | `Updated task ${taskId} status from '${oldStatus}' to '${newStatus}'` 106 | ); 107 | 108 | // If marking as done, also mark all subtasks as done 109 | if ( 110 | (newStatus.toLowerCase() === 'done' || 111 | newStatus.toLowerCase() === 'completed') && 112 | task.subtasks && 113 | task.subtasks.length > 0 114 | ) { 115 | const pendingSubtasks = task.subtasks.filter( 116 | (st) => st.status !== 'done' && st.status !== 'completed' 117 | ); 118 | 119 | if (pendingSubtasks.length > 0) { 120 | log( 121 | 'info', 122 | `Also marking ${pendingSubtasks.length} subtasks as '${newStatus}'` 123 | ); 124 | 125 | pendingSubtasks.forEach((subtask) => { 126 | subtask.status = newStatus; 127 | }); 128 | } 129 | } 130 | } 131 | } 132 | 133 | export default updateSingleTaskStatus; 134 | ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 16 | cancel-in-progress: true 17 | 18 | permissions: 19 | contents: read 20 | 21 | env: 22 | DO_NOT_TRACK: 1 23 | NODE_ENV: development 24 | 25 | jobs: 26 | # Fast checks that can run in parallel 27 | format-check: 28 | name: Format Check 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 2 34 | 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version: 20 38 | cache: "npm" 39 | 40 | - name: Install dependencies 41 | run: npm install --frozen-lockfile --prefer-offline 42 | timeout-minutes: 5 43 | 44 | - name: Format Check 45 | run: npm run format-check 46 | env: 47 | FORCE_COLOR: 1 48 | 49 | typecheck: 50 | name: Typecheck 51 | timeout-minutes: 10 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v4 55 | with: 56 | fetch-depth: 2 57 | 58 | - uses: actions/setup-node@v4 59 | with: 60 | node-version: 20 61 | cache: "npm" 62 | 63 | - name: Install dependencies 64 | run: npm install --frozen-lockfile --prefer-offline 65 | timeout-minutes: 5 66 | 67 | - name: Typecheck 68 | run: npm run turbo:typecheck 69 | env: 70 | FORCE_COLOR: 1 71 | 72 | # Build job to ensure everything compiles 73 | build: 74 | name: Build 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v4 78 | with: 79 | fetch-depth: 2 80 | 81 | - uses: actions/setup-node@v4 82 | with: 83 | node-version: 20 84 | cache: "npm" 85 | 86 | - name: Install dependencies 87 | run: npm install --frozen-lockfile --prefer-offline 88 | timeout-minutes: 5 89 | 90 | - name: Build 91 | run: npm run turbo:build 92 | env: 93 | NODE_ENV: production 94 | FORCE_COLOR: 1 95 | TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }} 96 | TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }} 97 | TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }} 98 | 99 | - name: Upload build artifacts 100 | uses: actions/upload-artifact@v4 101 | with: 102 | name: build-artifacts 103 | path: dist/ 104 | retention-days: 1 105 | 106 | test: 107 | name: Test 108 | timeout-minutes: 15 109 | runs-on: ubuntu-latest 110 | needs: [format-check, typecheck, build] 111 | steps: 112 | - uses: actions/checkout@v4 113 | with: 114 | fetch-depth: 2 115 | 116 | - uses: actions/setup-node@v4 117 | with: 118 | node-version: 20 119 | cache: "npm" 120 | 121 | - name: Install dependencies 122 | run: npm install --frozen-lockfile --prefer-offline 123 | timeout-minutes: 5 124 | 125 | - name: Download build artifacts 126 | uses: actions/download-artifact@v4 127 | with: 128 | name: build-artifacts 129 | path: dist/ 130 | 131 | - name: Run Tests 132 | run: | 133 | npm run test:coverage -- --coverageThreshold '{"global":{"branches":0,"functions":0,"lines":0,"statements":0}}' --detectOpenHandles --forceExit 134 | env: 135 | NODE_ENV: test 136 | CI: true 137 | FORCE_COLOR: 1 138 | 139 | - name: Upload Test Results 140 | if: always() 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: test-results 144 | path: | 145 | test-results 146 | coverage 147 | junit.xml 148 | retention-days: 30 149 | ``` -------------------------------------------------------------------------------- /apps/extension/src/components/TaskDetails/useTaskDetails.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { useMemo } from 'react'; 2 | import { useTaskDetails as useTaskDetailsQuery } from '../../webview/hooks/useTaskQueries'; 3 | import type { TaskMasterTask } from '../../webview/types'; 4 | 5 | interface TaskFileData { 6 | details?: string; 7 | testStrategy?: string; 8 | } 9 | 10 | interface UseTaskDetailsProps { 11 | taskId: string; 12 | sendMessage: (message: any) => Promise<any>; 13 | tasks: TaskMasterTask[]; 14 | } 15 | 16 | export const useTaskDetails = ({ 17 | taskId, 18 | sendMessage, 19 | tasks 20 | }: UseTaskDetailsProps) => { 21 | // Parse task ID to determine if it's a subtask (e.g., "13.2") 22 | const { isSubtask, parentId, subtaskIndex, taskIdForFetch } = useMemo(() => { 23 | // Ensure taskId is a string 24 | const taskIdStr = String(taskId); 25 | const parts = taskIdStr.split('.'); 26 | if (parts.length === 2) { 27 | return { 28 | isSubtask: true, 29 | parentId: parts[0], 30 | subtaskIndex: parseInt(parts[1]) - 1, // Convert to 0-based index 31 | taskIdForFetch: parts[0] // Always fetch parent task for subtasks 32 | }; 33 | } 34 | return { 35 | isSubtask: false, 36 | parentId: taskIdStr, 37 | subtaskIndex: -1, 38 | taskIdForFetch: taskIdStr 39 | }; 40 | }, [taskId]); 41 | 42 | // Use React Query to fetch full task details 43 | const { data: fullTaskData, error: taskDetailsError } = 44 | useTaskDetailsQuery(taskIdForFetch); 45 | 46 | // Find current task from local state for immediate display 47 | const { currentTask, parentTask } = useMemo(() => { 48 | if (isSubtask) { 49 | const parent = tasks.find((t) => t.id === parentId); 50 | if (parent && parent.subtasks && parent.subtasks[subtaskIndex]) { 51 | const subtask = parent.subtasks[subtaskIndex]; 52 | return { currentTask: subtask, parentTask: parent }; 53 | } 54 | } else { 55 | const task = tasks.find((t) => t.id === String(taskId)); 56 | if (task) { 57 | return { currentTask: task, parentTask: null }; 58 | } 59 | } 60 | return { currentTask: null, parentTask: null }; 61 | }, [taskId, tasks, isSubtask, parentId, subtaskIndex]); 62 | 63 | // Merge full task data from React Query with local state 64 | const mergedCurrentTask = useMemo(() => { 65 | if (!currentTask || !fullTaskData) return currentTask; 66 | 67 | if (isSubtask && fullTaskData.subtasks) { 68 | // Find the specific subtask in the full data 69 | const subtaskData = fullTaskData.subtasks.find( 70 | (st: any) => 71 | st.id === currentTask.id || st.id === parseInt(currentTask.id as any) 72 | ); 73 | if (subtaskData) { 74 | return { ...currentTask, ...subtaskData }; 75 | } 76 | } else if (!isSubtask) { 77 | // Merge parent task data 78 | return { ...currentTask, ...fullTaskData }; 79 | } 80 | 81 | return currentTask; 82 | }, [currentTask, fullTaskData, isSubtask]); 83 | 84 | // Extract task file data 85 | const taskFileData: TaskFileData = useMemo(() => { 86 | if (!mergedCurrentTask) return {}; 87 | return { 88 | details: mergedCurrentTask.details || '', 89 | testStrategy: mergedCurrentTask.testStrategy || '' 90 | }; 91 | }, [mergedCurrentTask]); 92 | 93 | // Get complexity score 94 | const complexity = useMemo(() => { 95 | if (mergedCurrentTask?.complexityScore !== undefined) { 96 | return { score: mergedCurrentTask.complexityScore }; 97 | } 98 | return null; 99 | }, [mergedCurrentTask]); 100 | 101 | // Function to refresh data after AI operations 102 | const refreshComplexityAfterAI = () => { 103 | // React Query will automatically refetch when mutations invalidate the query 104 | // No need for manual refresh 105 | }; 106 | 107 | return { 108 | currentTask: mergedCurrentTask, 109 | parentTask, 110 | isSubtask, 111 | taskFileData, 112 | taskFileDataError: taskDetailsError ? 'Failed to load task details' : null, 113 | complexity, 114 | refreshComplexityAfterAI 115 | }; 116 | }; 117 | ``` -------------------------------------------------------------------------------- /tests/unit/kebab-case-validation.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Kebab case validation tests 3 | */ 4 | 5 | import { jest } from '@jest/globals'; 6 | import { toKebabCase } from '../../scripts/modules/utils.js'; 7 | 8 | // Create a test implementation of detectCamelCaseFlags 9 | function testDetectCamelCaseFlags(args) { 10 | const camelCaseFlags = []; 11 | for (const arg of args) { 12 | if (arg.startsWith('--')) { 13 | const flagName = arg.split('=')[0].slice(2); // Remove -- and anything after = 14 | 15 | // Skip single-word flags - they can't be camelCase 16 | if (!flagName.includes('-') && !/[A-Z]/.test(flagName)) { 17 | continue; 18 | } 19 | 20 | // Check for camelCase pattern (lowercase followed by uppercase) 21 | if (/[a-z][A-Z]/.test(flagName)) { 22 | const kebabVersion = toKebabCase(flagName); 23 | if (kebabVersion !== flagName) { 24 | camelCaseFlags.push({ 25 | original: flagName, 26 | kebabCase: kebabVersion 27 | }); 28 | } 29 | } 30 | } 31 | } 32 | return camelCaseFlags; 33 | } 34 | 35 | describe('Kebab Case Validation', () => { 36 | describe('toKebabCase', () => { 37 | test('should convert camelCase to kebab-case', () => { 38 | expect(toKebabCase('promptText')).toBe('prompt-text'); 39 | expect(toKebabCase('userID')).toBe('user-id'); 40 | expect(toKebabCase('numTasks')).toBe('num-tasks'); 41 | }); 42 | 43 | test('should handle already kebab-case strings', () => { 44 | expect(toKebabCase('already-kebab-case')).toBe('already-kebab-case'); 45 | expect(toKebabCase('kebab-case')).toBe('kebab-case'); 46 | }); 47 | 48 | test('should handle single words', () => { 49 | expect(toKebabCase('single')).toBe('single'); 50 | expect(toKebabCase('file')).toBe('file'); 51 | }); 52 | }); 53 | 54 | describe('detectCamelCaseFlags', () => { 55 | test('should properly detect camelCase flags', () => { 56 | const args = [ 57 | 'node', 58 | 'task-master', 59 | 'add-task', 60 | '--promptText=test', 61 | '--userID=123' 62 | ]; 63 | const flags = testDetectCamelCaseFlags(args); 64 | 65 | expect(flags).toHaveLength(2); 66 | expect(flags).toContainEqual({ 67 | original: 'promptText', 68 | kebabCase: 'prompt-text' 69 | }); 70 | expect(flags).toContainEqual({ 71 | original: 'userID', 72 | kebabCase: 'user-id' 73 | }); 74 | }); 75 | 76 | test('should not flag kebab-case or lowercase flags', () => { 77 | const args = [ 78 | 'node', 79 | 'task-master', 80 | 'add-task', 81 | '--prompt=test', 82 | '--user-id=123' 83 | ]; 84 | const flags = testDetectCamelCaseFlags(args); 85 | 86 | expect(flags).toHaveLength(0); 87 | }); 88 | 89 | test('should not flag any single-word flags regardless of case', () => { 90 | const args = [ 91 | 'node', 92 | 'task-master', 93 | 'add-task', 94 | '--prompt=test', // lowercase 95 | '--PROMPT=test', // uppercase 96 | '--Prompt=test', // mixed case 97 | '--file=test', // lowercase 98 | '--FILE=test', // uppercase 99 | '--File=test' // mixed case 100 | ]; 101 | const flags = testDetectCamelCaseFlags(args); 102 | 103 | expect(flags).toHaveLength(0); 104 | }); 105 | 106 | test('should handle mixed case flags correctly', () => { 107 | const args = [ 108 | 'node', 109 | 'task-master', 110 | 'add-task', 111 | '--prompt=test', // single word, should pass 112 | '--promptText=test', // camelCase, should flag 113 | '--prompt-text=test', // kebab-case, should pass 114 | '--ID=123', // single word, should pass 115 | '--userId=123', // camelCase, should flag 116 | '--user-id=123' // kebab-case, should pass 117 | ]; 118 | 119 | const flags = testDetectCamelCaseFlags(args); 120 | 121 | expect(flags).toHaveLength(2); 122 | expect(flags).toContainEqual({ 123 | original: 'promptText', 124 | kebabCase: 'prompt-text' 125 | }); 126 | expect(flags).toContainEqual({ 127 | original: 'userId', 128 | kebabCase: 'user-id' 129 | }); 130 | }); 131 | }); 132 | }); 133 | ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/find-next-task.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Tests for the find-next-task.js module 3 | */ 4 | import { jest } from '@jest/globals'; 5 | import findNextTask from '../../../../../scripts/modules/task-manager/find-next-task.js'; 6 | 7 | describe('findNextTask', () => { 8 | test('should return the highest priority task with all dependencies satisfied', () => { 9 | const tasks = [ 10 | { 11 | id: 1, 12 | title: 'Setup Project', 13 | status: 'done', 14 | dependencies: [], 15 | priority: 'high' 16 | }, 17 | { 18 | id: 2, 19 | title: 'Implement Core Features', 20 | status: 'pending', 21 | dependencies: [1], 22 | priority: 'high' 23 | }, 24 | { 25 | id: 3, 26 | title: 'Create Documentation', 27 | status: 'pending', 28 | dependencies: [1], 29 | priority: 'medium' 30 | }, 31 | { 32 | id: 4, 33 | title: 'Deploy Application', 34 | status: 'pending', 35 | dependencies: [2, 3], 36 | priority: 'high' 37 | } 38 | ]; 39 | 40 | const nextTask = findNextTask(tasks); 41 | 42 | expect(nextTask).toBeDefined(); 43 | expect(nextTask.id).toBe(2); 44 | expect(nextTask.title).toBe('Implement Core Features'); 45 | }); 46 | 47 | test('should prioritize by priority level when dependencies are equal', () => { 48 | const tasks = [ 49 | { 50 | id: 1, 51 | title: 'Setup Project', 52 | status: 'done', 53 | dependencies: [], 54 | priority: 'high' 55 | }, 56 | { 57 | id: 2, 58 | title: 'Low Priority Task', 59 | status: 'pending', 60 | dependencies: [1], 61 | priority: 'low' 62 | }, 63 | { 64 | id: 3, 65 | title: 'Medium Priority Task', 66 | status: 'pending', 67 | dependencies: [1], 68 | priority: 'medium' 69 | }, 70 | { 71 | id: 4, 72 | title: 'High Priority Task', 73 | status: 'pending', 74 | dependencies: [1], 75 | priority: 'high' 76 | } 77 | ]; 78 | 79 | const nextTask = findNextTask(tasks); 80 | 81 | expect(nextTask.id).toBe(4); 82 | expect(nextTask.priority).toBe('high'); 83 | }); 84 | 85 | test('should return null when all tasks are completed', () => { 86 | const tasks = [ 87 | { 88 | id: 1, 89 | title: 'Setup Project', 90 | status: 'done', 91 | dependencies: [], 92 | priority: 'high' 93 | }, 94 | { 95 | id: 2, 96 | title: 'Implement Features', 97 | status: 'done', 98 | dependencies: [1], 99 | priority: 'high' 100 | } 101 | ]; 102 | 103 | const nextTask = findNextTask(tasks); 104 | 105 | expect(nextTask).toBeNull(); 106 | }); 107 | 108 | test('should return null when all pending tasks have unsatisfied dependencies', () => { 109 | const tasks = [ 110 | { 111 | id: 1, 112 | title: 'Setup Project', 113 | status: 'pending', 114 | dependencies: [2], 115 | priority: 'high' 116 | }, 117 | { 118 | id: 2, 119 | title: 'Implement Features', 120 | status: 'pending', 121 | dependencies: [1], 122 | priority: 'high' 123 | } 124 | ]; 125 | 126 | const nextTask = findNextTask(tasks); 127 | 128 | expect(nextTask).toBeNull(); 129 | }); 130 | 131 | test('should handle empty tasks array', () => { 132 | const nextTask = findNextTask([]); 133 | 134 | expect(nextTask).toBeNull(); 135 | }); 136 | 137 | test('should consider subtask dependencies when finding next task', () => { 138 | const tasks = [ 139 | { 140 | id: 1, 141 | title: 'Parent Task', 142 | status: 'in-progress', 143 | dependencies: [], 144 | priority: 'high', 145 | subtasks: [ 146 | { 147 | id: 1, 148 | title: 'Subtask 1', 149 | status: 'done', 150 | dependencies: [] 151 | }, 152 | { 153 | id: 2, 154 | title: 'Subtask 2', 155 | status: 'pending', 156 | dependencies: [] 157 | } 158 | ] 159 | }, 160 | { 161 | id: 2, 162 | title: 'Dependent Task', 163 | status: 'pending', 164 | dependencies: [1], 165 | priority: 'high' 166 | } 167 | ]; 168 | 169 | const nextTask = findNextTask(tasks); 170 | 171 | // Task 2 should not be returned because Task 1 is not completely done 172 | // (it has a pending subtask) 173 | expect(nextTask).not.toEqual(expect.objectContaining({ id: 2 })); 174 | }); 175 | }); 176 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/scope-up.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * scope-up.js 3 | * Direct function implementation for scoping up task complexity 4 | */ 5 | 6 | import { scopeUpTask } from '../../../../scripts/modules/task-manager.js'; 7 | import { 8 | enableSilentMode, 9 | disableSilentMode 10 | } from '../../../../scripts/modules/utils.js'; 11 | import { createLogWrapper } from '../../tools/utils.js'; 12 | 13 | /** 14 | * Direct function wrapper for scoping up task complexity with error handling. 15 | * 16 | * @param {Object} args - Command arguments 17 | * @param {string} args.id - Comma-separated list of task IDs to scope up 18 | * @param {string} [args.strength='regular'] - Strength level (light, regular, heavy) 19 | * @param {string} [args.prompt] - Custom prompt for scoping adjustments 20 | * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) 21 | * @param {boolean} [args.research=false] - Whether to use research capabilities for scoping 22 | * @param {string} args.projectRoot - Project root path 23 | * @param {string} [args.tag] - Tag for the task context (optional) 24 | * @param {Object} log - Logger object 25 | * @param {Object} context - Additional context (session) 26 | * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } } 27 | */ 28 | export async function scopeUpDirect(args, log, context = {}) { 29 | // Destructure expected args 30 | const { 31 | tasksJsonPath, 32 | id, 33 | strength = 'regular', 34 | prompt: customPrompt, 35 | research = false, 36 | projectRoot, 37 | tag 38 | } = args; 39 | const { session } = context; // Destructure session from context 40 | 41 | // Enable silent mode to prevent console logs from interfering with JSON response 42 | enableSilentMode(); 43 | 44 | // Create logger wrapper using the utility 45 | const mcpLog = createLogWrapper(log); 46 | 47 | try { 48 | // Check if tasksJsonPath was provided 49 | if (!tasksJsonPath) { 50 | log.error('scopeUpDirect called without tasksJsonPath'); 51 | disableSilentMode(); // Disable before returning 52 | return { 53 | success: false, 54 | error: { 55 | code: 'MISSING_ARGUMENT', 56 | message: 'tasksJsonPath is required' 57 | } 58 | }; 59 | } 60 | 61 | // Check required parameters 62 | if (!id) { 63 | log.error('Missing required parameter: id'); 64 | disableSilentMode(); 65 | return { 66 | success: false, 67 | error: { 68 | code: 'MISSING_PARAMETER', 69 | message: 'The id parameter is required for scoping up tasks' 70 | } 71 | }; 72 | } 73 | 74 | // Parse task IDs - convert to numbers as expected by scopeUpTask 75 | const taskIds = id.split(',').map((taskId) => parseInt(taskId.trim(), 10)); 76 | 77 | log.info( 78 | `Scoping up tasks: ${taskIds.join(', ')}, strength: ${strength}, research: ${research}` 79 | ); 80 | 81 | // Call the scopeUpTask function 82 | const result = await scopeUpTask( 83 | tasksJsonPath, 84 | taskIds, 85 | strength, 86 | customPrompt, 87 | { 88 | session, 89 | mcpLog, 90 | projectRoot, 91 | commandName: 'scope-up', 92 | outputType: 'mcp', 93 | tag, 94 | research 95 | }, 96 | 'json' // outputFormat 97 | ); 98 | 99 | // Restore normal logging 100 | disableSilentMode(); 101 | 102 | return { 103 | success: true, 104 | data: { 105 | updatedTasks: result.updatedTasks, 106 | tasksUpdated: result.updatedTasks.length, 107 | message: `Successfully scoped up ${result.updatedTasks.length} task(s)`, 108 | telemetryData: result.telemetryData 109 | } 110 | }; 111 | } catch (error) { 112 | // Make sure to restore normal logging even if there's an error 113 | disableSilentMode(); 114 | 115 | log.error(`Error in scopeUpDirect: ${error.message}`); 116 | return { 117 | success: false, 118 | error: { 119 | code: error.code || 'SCOPE_UP_ERROR', 120 | message: error.message 121 | } 122 | }; 123 | } 124 | } 125 | ``` -------------------------------------------------------------------------------- /src/ai-providers/custom-sdk/claude-code/types.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * @fileoverview Type definitions for Claude Code AI SDK provider 3 | * These JSDoc types mirror the TypeScript interfaces from the original provider 4 | */ 5 | 6 | /** 7 | * Claude Code provider settings 8 | * @typedef {Object} ClaudeCodeSettings 9 | * @property {string} [pathToClaudeCodeExecutable='claude'] - Custom path to Claude Code CLI executable 10 | * @property {string} [customSystemPrompt] - Custom system prompt to use 11 | * @property {string} [appendSystemPrompt] - Append additional content to the system prompt 12 | * @property {number} [maxTurns] - Maximum number of turns for the conversation 13 | * @property {number} [maxThinkingTokens] - Maximum thinking tokens for the model 14 | * @property {string} [cwd] - Working directory for CLI operations 15 | * @property {'bun'|'deno'|'node'} [executable='node'] - JavaScript runtime to use 16 | * @property {string[]} [executableArgs] - Additional arguments for the JavaScript runtime 17 | * @property {'default'|'acceptEdits'|'bypassPermissions'|'plan'} [permissionMode='default'] - Permission mode for tool usage 18 | * @property {string} [permissionPromptToolName] - Custom tool name for permission prompts 19 | * @property {boolean} [continue] - Continue the most recent conversation 20 | * @property {string} [resume] - Resume a specific session by ID 21 | * @property {string[]} [allowedTools] - Tools to explicitly allow during execution (e.g., ['Read', 'LS', 'Bash(git log:*)']) 22 | * @property {string[]} [disallowedTools] - Tools to disallow during execution (e.g., ['Write', 'Edit', 'Bash(rm:*)']) 23 | * @property {Object.<string, MCPServerConfig>} [mcpServers] - MCP server configuration 24 | * @property {boolean} [verbose] - Enable verbose logging for debugging 25 | */ 26 | 27 | /** 28 | * MCP Server configuration 29 | * @typedef {Object} MCPServerConfig 30 | * @property {'stdio'|'sse'} [type='stdio'] - Server type 31 | * @property {string} command - Command to execute (for stdio type) 32 | * @property {string[]} [args] - Arguments for the command 33 | * @property {Object.<string, string>} [env] - Environment variables 34 | * @property {string} url - URL for SSE type servers 35 | * @property {Object.<string, string>} [headers] - Headers for SSE type servers 36 | */ 37 | 38 | /** 39 | * Model ID type - either 'opus', 'sonnet', or any string 40 | * @typedef {'opus'|'sonnet'|string} ClaudeCodeModelId 41 | */ 42 | 43 | /** 44 | * Language model options 45 | * @typedef {Object} ClaudeCodeLanguageModelOptions 46 | * @property {ClaudeCodeModelId} id - The model ID 47 | * @property {ClaudeCodeSettings} [settings] - Optional settings 48 | */ 49 | 50 | /** 51 | * Error metadata for Claude Code errors 52 | * @typedef {Object} ClaudeCodeErrorMetadata 53 | * @property {string} [code] - Error code 54 | * @property {number} [exitCode] - Process exit code 55 | * @property {string} [stderr] - Standard error output 56 | * @property {string} [promptExcerpt] - Excerpt of the prompt that caused the error 57 | */ 58 | 59 | /** 60 | * Claude Code provider interface 61 | * @typedef {Object} ClaudeCodeProvider 62 | * @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} languageModel - Create a language model 63 | * @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} chat - Alias for languageModel 64 | * @property {function(string): never} textEmbeddingModel - Throws NoSuchModelError (not supported) 65 | */ 66 | 67 | /** 68 | * Claude Code provider settings 69 | * @typedef {Object} ClaudeCodeProviderSettings 70 | * @property {ClaudeCodeSettings} [defaultSettings] - Default settings to use for all models 71 | */ 72 | 73 | export {}; // This ensures the file is treated as a module 74 | ``` -------------------------------------------------------------------------------- /tests/unit/profiles/opencode-integration.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import os from 'os'; 5 | 6 | describe('OpenCode Profile Integration', () => { 7 | let tempDir; 8 | 9 | beforeEach(() => { 10 | jest.clearAllMocks(); 11 | 12 | // Create a temporary directory for testing 13 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); 14 | 15 | // Spy on fs methods 16 | jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); 17 | jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { 18 | if (filePath.toString().includes('AGENTS.md')) { 19 | return 'Sample AGENTS.md content for OpenCode integration'; 20 | } 21 | if (filePath.toString().includes('opencode.json')) { 22 | return JSON.stringify({ mcpServers: {} }, null, 2); 23 | } 24 | return '{}'; 25 | }); 26 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false); 27 | jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); 28 | }); 29 | 30 | afterEach(() => { 31 | // Clean up the temporary directory 32 | try { 33 | fs.rmSync(tempDir, { recursive: true, force: true }); 34 | } catch (err) { 35 | console.error(`Error cleaning up: ${err.message}`); 36 | } 37 | }); 38 | 39 | // Test function that simulates the OpenCode profile file copying behavior 40 | function mockCreateOpenCodeStructure() { 41 | // OpenCode profile copies AGENTS.md to AGENTS.md in project root (same name) 42 | const sourceContent = 'Sample AGENTS.md content for OpenCode integration'; 43 | fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), sourceContent); 44 | 45 | // OpenCode profile creates opencode.json config file 46 | const configContent = JSON.stringify({ mcpServers: {} }, null, 2); 47 | fs.writeFileSync(path.join(tempDir, 'opencode.json'), configContent); 48 | } 49 | 50 | test('creates AGENTS.md file in project root', () => { 51 | // Act 52 | mockCreateOpenCodeStructure(); 53 | 54 | // Assert 55 | expect(fs.writeFileSync).toHaveBeenCalledWith( 56 | path.join(tempDir, 'AGENTS.md'), 57 | 'Sample AGENTS.md content for OpenCode integration' 58 | ); 59 | }); 60 | 61 | test('creates opencode.json config file in project root', () => { 62 | // Act 63 | mockCreateOpenCodeStructure(); 64 | 65 | // Assert 66 | expect(fs.writeFileSync).toHaveBeenCalledWith( 67 | path.join(tempDir, 'opencode.json'), 68 | JSON.stringify({ mcpServers: {} }, null, 2) 69 | ); 70 | }); 71 | 72 | test('does not create any profile directories', () => { 73 | // Act 74 | mockCreateOpenCodeStructure(); 75 | 76 | // Assert - OpenCode profile should not create any directories 77 | // Only the temp directory creation calls should exist 78 | const mkdirCalls = fs.mkdirSync.mock.calls.filter( 79 | (call) => !call[0].includes('task-master-test-') 80 | ); 81 | expect(mkdirCalls).toHaveLength(0); 82 | }); 83 | 84 | test('handles transformation of MCP config format', () => { 85 | // This test simulates the transformation behavior that would happen in onPostConvert 86 | const standardMcpConfig = { 87 | mcpServers: { 88 | 'taskmaster-ai': { 89 | command: 'node', 90 | args: ['path/to/server.js'], 91 | env: { 92 | API_KEY: 'test-key' 93 | } 94 | } 95 | } 96 | }; 97 | 98 | const expectedOpenCodeConfig = { 99 | $schema: 'https://opencode.ai/config.json', 100 | mcp: { 101 | 'taskmaster-ai': { 102 | type: 'local', 103 | command: ['node', 'path/to/server.js'], 104 | enabled: true, 105 | environment: { 106 | API_KEY: 'test-key' 107 | } 108 | } 109 | } 110 | }; 111 | 112 | // Mock the transformation behavior 113 | fs.writeFileSync( 114 | path.join(tempDir, 'opencode.json'), 115 | JSON.stringify(expectedOpenCodeConfig, null, 2) 116 | ); 117 | 118 | expect(fs.writeFileSync).toHaveBeenCalledWith( 119 | path.join(tempDir, 'opencode.json'), 120 | JSON.stringify(expectedOpenCodeConfig, null, 2) 121 | ); 122 | }); 123 | }); 124 | ``` -------------------------------------------------------------------------------- /apps/cli/src/ui/components/next-task.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Next task recommendation component 3 | * Displays detailed information about the recommended next task 4 | */ 5 | 6 | import chalk from 'chalk'; 7 | import boxen from 'boxen'; 8 | import type { Task } from '@tm/core/types'; 9 | 10 | /** 11 | * Next task display options 12 | */ 13 | export interface NextTaskDisplayOptions { 14 | id: string | number; 15 | title: string; 16 | priority?: string; 17 | status?: string; 18 | dependencies?: (string | number)[]; 19 | description?: string; 20 | } 21 | 22 | /** 23 | * Display the recommended next task section 24 | */ 25 | export function displayRecommendedNextTask( 26 | task: NextTaskDisplayOptions | undefined 27 | ): void { 28 | if (!task) { 29 | // If no task available, show a message 30 | console.log( 31 | boxen( 32 | chalk.yellow( 33 | 'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.' 34 | ), 35 | { 36 | padding: 1, 37 | borderStyle: 'round', 38 | borderColor: 'yellow', 39 | title: '⚠ NO TASKS AVAILABLE ⚠', 40 | titleAlignment: 'center' 41 | } 42 | ) 43 | ); 44 | return; 45 | } 46 | 47 | // Build the content for the next task box 48 | const content = []; 49 | 50 | // Task header with ID and title 51 | content.push( 52 | `🔥 ${chalk.hex('#FF8800').bold('Next Task to Work On:')} ${chalk.yellow(`#${task.id}`)}${chalk.hex('#FF8800').bold(` - ${task.title}`)}` 53 | ); 54 | content.push(''); 55 | 56 | // Priority and Status line 57 | const statusLine = []; 58 | if (task.priority) { 59 | const priorityColor = 60 | task.priority === 'high' 61 | ? chalk.red 62 | : task.priority === 'medium' 63 | ? chalk.yellow 64 | : chalk.gray; 65 | statusLine.push(`Priority: ${priorityColor.bold(task.priority)}`); 66 | } 67 | if (task.status) { 68 | const statusDisplay = 69 | task.status === 'pending' 70 | ? chalk.yellow('○ pending') 71 | : task.status === 'in-progress' 72 | ? chalk.blue('▶ in-progress') 73 | : chalk.gray(task.status); 74 | statusLine.push(`Status: ${statusDisplay}`); 75 | } 76 | content.push(statusLine.join(' ')); 77 | 78 | // Dependencies 79 | const depsDisplay = 80 | !task.dependencies || task.dependencies.length === 0 81 | ? chalk.gray('None') 82 | : chalk.cyan(task.dependencies.join(', ')); 83 | content.push(`Dependencies: ${depsDisplay}`); 84 | 85 | // Description if available 86 | if (task.description) { 87 | content.push(''); 88 | content.push(`Description: ${chalk.white(task.description)}`); 89 | } 90 | 91 | // Action commands 92 | content.push(''); 93 | content.push( 94 | `${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}` 95 | ); 96 | content.push( 97 | `${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${task.id}`)}` 98 | ); 99 | 100 | // Display in a styled box with orange border 101 | console.log( 102 | boxen(content.join('\n'), { 103 | padding: 1, 104 | margin: { top: 1, bottom: 1 }, 105 | borderStyle: 'round', 106 | borderColor: '#FFA500', // Orange color 107 | title: chalk.hex('#FFA500')('⚡ RECOMMENDED NEXT TASK ⚡'), 108 | titleAlignment: 'center', 109 | width: process.stdout.columns * 0.97, 110 | fullscreen: false 111 | }) 112 | ); 113 | } 114 | 115 | /** 116 | * Get task description from the full task object 117 | */ 118 | export function getTaskDescription(task: Task): string | undefined { 119 | // Try to get description from the task 120 | // This could be from task.description or the first line of task.details 121 | if ('description' in task && task.description) { 122 | return task.description as string; 123 | } 124 | 125 | if ('details' in task && task.details) { 126 | // Take first sentence or line from details 127 | const details = task.details as string; 128 | const firstLine = details.split('\n')[0]; 129 | const firstSentence = firstLine.split('.')[0]; 130 | return firstSentence; 131 | } 132 | 133 | return undefined; 134 | } 135 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/scope-down.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * scope-down.js 3 | * Direct function implementation for scoping down task complexity 4 | */ 5 | 6 | import { scopeDownTask } from '../../../../scripts/modules/task-manager.js'; 7 | import { 8 | enableSilentMode, 9 | disableSilentMode 10 | } from '../../../../scripts/modules/utils.js'; 11 | import { createLogWrapper } from '../../tools/utils.js'; 12 | 13 | /** 14 | * Direct function wrapper for scoping down task complexity with error handling. 15 | * 16 | * @param {Object} args - Command arguments 17 | * @param {string} args.id - Comma-separated list of task IDs to scope down 18 | * @param {string} [args.strength='regular'] - Strength level (light, regular, heavy) 19 | * @param {string} [args.prompt] - Custom prompt for scoping adjustments 20 | * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) 21 | * @param {boolean} [args.research=false] - Whether to use research capabilities for scoping 22 | * @param {string} args.projectRoot - Project root path 23 | * @param {string} [args.tag] - Tag for the task context (optional) 24 | * @param {Object} log - Logger object 25 | * @param {Object} context - Additional context (session) 26 | * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } } 27 | */ 28 | export async function scopeDownDirect(args, log, context = {}) { 29 | // Destructure expected args 30 | const { 31 | tasksJsonPath, 32 | id, 33 | strength = 'regular', 34 | prompt: customPrompt, 35 | research = false, 36 | projectRoot, 37 | tag 38 | } = args; 39 | const { session } = context; // Destructure session from context 40 | 41 | // Enable silent mode to prevent console logs from interfering with JSON response 42 | enableSilentMode(); 43 | 44 | // Create logger wrapper using the utility 45 | const mcpLog = createLogWrapper(log); 46 | 47 | try { 48 | // Check if tasksJsonPath was provided 49 | if (!tasksJsonPath) { 50 | log.error('scopeDownDirect called without tasksJsonPath'); 51 | disableSilentMode(); // Disable before returning 52 | return { 53 | success: false, 54 | error: { 55 | code: 'MISSING_ARGUMENT', 56 | message: 'tasksJsonPath is required' 57 | } 58 | }; 59 | } 60 | 61 | // Check required parameters 62 | if (!id) { 63 | log.error('Missing required parameter: id'); 64 | disableSilentMode(); 65 | return { 66 | success: false, 67 | error: { 68 | code: 'MISSING_PARAMETER', 69 | message: 'The id parameter is required for scoping down tasks' 70 | } 71 | }; 72 | } 73 | 74 | // Parse task IDs - convert to numbers as expected by scopeDownTask 75 | const taskIds = id.split(',').map((taskId) => parseInt(taskId.trim(), 10)); 76 | 77 | log.info( 78 | `Scoping down tasks: ${taskIds.join(', ')}, strength: ${strength}, research: ${research}` 79 | ); 80 | 81 | // Call the scopeDownTask function 82 | const result = await scopeDownTask( 83 | tasksJsonPath, 84 | taskIds, 85 | strength, 86 | customPrompt, 87 | { 88 | session, 89 | mcpLog, 90 | projectRoot, 91 | commandName: 'scope-down', 92 | outputType: 'mcp', 93 | tag, 94 | research 95 | }, 96 | 'json' // outputFormat 97 | ); 98 | 99 | // Restore normal logging 100 | disableSilentMode(); 101 | 102 | return { 103 | success: true, 104 | data: { 105 | updatedTasks: result.updatedTasks, 106 | tasksUpdated: result.updatedTasks.length, 107 | message: `Successfully scoped down ${result.updatedTasks.length} task(s)`, 108 | telemetryData: result.telemetryData 109 | } 110 | }; 111 | } catch (error) { 112 | // Make sure to restore normal logging even if there's an error 113 | disableSilentMode(); 114 | 115 | log.error(`Error in scopeDownDirect: ${error.message}`); 116 | return { 117 | success: false, 118 | error: { 119 | code: error.code || 'SCOPE_DOWN_ERROR', 120 | message: error.message 121 | } 122 | }; 123 | } 124 | } 125 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/expand-all-tasks.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Direct function wrapper for expandAllTasks 3 | */ 4 | 5 | import { expandAllTasks } from '../../../../scripts/modules/task-manager.js'; 6 | import { 7 | enableSilentMode, 8 | disableSilentMode 9 | } from '../../../../scripts/modules/utils.js'; 10 | import { createLogWrapper } from '../../tools/utils.js'; 11 | 12 | /** 13 | * Expand all pending tasks with subtasks (Direct Function Wrapper) 14 | * @param {Object} args - Function arguments 15 | * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. 16 | * @param {number|string} [args.num] - Number of subtasks to generate 17 | * @param {boolean} [args.research] - Enable research-backed subtask generation 18 | * @param {string} [args.prompt] - Additional context to guide subtask generation 19 | * @param {boolean} [args.force] - Force regeneration of subtasks for tasks that already have them 20 | * @param {string} [args.projectRoot] - Project root path. 21 | * @param {string} [args.tag] - Tag for the task (optional) 22 | * @param {Object} log - Logger object from FastMCP 23 | * @param {Object} context - Context object containing session 24 | * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>} 25 | */ 26 | export async function expandAllTasksDirect(args, log, context = {}) { 27 | const { session } = context; // Extract session 28 | // Destructure expected args, including projectRoot 29 | const { tasksJsonPath, num, research, prompt, force, projectRoot, tag } = 30 | args; 31 | 32 | // Create logger wrapper using the utility 33 | const mcpLog = createLogWrapper(log); 34 | 35 | if (!tasksJsonPath) { 36 | log.error('expandAllTasksDirect called without tasksJsonPath'); 37 | return { 38 | success: false, 39 | error: { 40 | code: 'MISSING_ARGUMENT', 41 | message: 'tasksJsonPath is required' 42 | } 43 | }; 44 | } 45 | 46 | enableSilentMode(); // Enable silent mode for the core function call 47 | try { 48 | log.info( 49 | `Calling core expandAllTasks with args: ${JSON.stringify({ num, research, prompt, force, projectRoot, tag })}` 50 | ); 51 | 52 | // Parse parameters (ensure correct types) 53 | const numSubtasks = num ? parseInt(num, 10) : undefined; 54 | const useResearch = research === true; 55 | const additionalContext = prompt || ''; 56 | const forceFlag = force === true; 57 | 58 | // Call the core function, passing options and the context object { session, mcpLog, projectRoot } 59 | const result = await expandAllTasks( 60 | tasksJsonPath, 61 | numSubtasks, 62 | useResearch, 63 | additionalContext, 64 | forceFlag, 65 | { session, mcpLog, projectRoot, tag }, 66 | 'json' 67 | ); 68 | 69 | // Core function now returns a summary object including the *aggregated* telemetryData 70 | return { 71 | success: true, 72 | data: { 73 | message: `Expand all operation completed. Expanded: ${result.expandedCount}, Failed: ${result.failedCount}, Skipped: ${result.skippedCount}`, 74 | details: { 75 | expandedCount: result.expandedCount, 76 | failedCount: result.failedCount, 77 | skippedCount: result.skippedCount, 78 | tasksToExpand: result.tasksToExpand 79 | }, 80 | telemetryData: result.telemetryData // Pass the aggregated object 81 | } 82 | }; 83 | } catch (error) { 84 | // Log the error using the MCP logger 85 | log.error(`Error during core expandAllTasks execution: ${error.message}`); 86 | // Optionally log stack trace if available and debug enabled 87 | // if (error.stack && log.debug) { log.debug(error.stack); } 88 | 89 | return { 90 | success: false, 91 | error: { 92 | code: 'CORE_FUNCTION_ERROR', // Or a more specific code if possible 93 | message: error.message 94 | } 95 | }; 96 | } finally { 97 | disableSilentMode(); // IMPORTANT: Ensure silent mode is always disabled 98 | } 99 | } 100 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/list-tags.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * list-tags.js 3 | * Direct function implementation for listing all tags 4 | */ 5 | 6 | import { tags } from '../../../../scripts/modules/task-manager/tag-management.js'; 7 | import { 8 | enableSilentMode, 9 | disableSilentMode 10 | } from '../../../../scripts/modules/utils.js'; 11 | import { createLogWrapper } from '../../tools/utils.js'; 12 | 13 | /** 14 | * Direct function wrapper for listing all tags with error handling. 15 | * 16 | * @param {Object} args - Command arguments 17 | * @param {boolean} [args.showMetadata=false] - Whether to include metadata in the output 18 | * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) 19 | * @param {string} [args.projectRoot] - Project root path 20 | * @param {Object} log - Logger object 21 | * @param {Object} context - Additional context (session) 22 | * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } } 23 | */ 24 | export async function listTagsDirect(args, log, context = {}) { 25 | // Destructure expected args 26 | const { tasksJsonPath, showMetadata = false, projectRoot } = args; 27 | const { session } = context; 28 | 29 | // Enable silent mode to prevent console logs from interfering with JSON response 30 | enableSilentMode(); 31 | 32 | // Create logger wrapper using the utility 33 | const mcpLog = createLogWrapper(log); 34 | 35 | try { 36 | // Check if tasksJsonPath was provided 37 | if (!tasksJsonPath) { 38 | log.error('listTagsDirect called without tasksJsonPath'); 39 | disableSilentMode(); 40 | return { 41 | success: false, 42 | error: { 43 | code: 'MISSING_ARGUMENT', 44 | message: 'tasksJsonPath is required' 45 | } 46 | }; 47 | } 48 | 49 | log.info('Listing all tags'); 50 | 51 | // Prepare options 52 | const options = { 53 | showMetadata 54 | }; 55 | 56 | // Call the tags function 57 | const result = await tags( 58 | tasksJsonPath, 59 | options, 60 | { 61 | session, 62 | mcpLog, 63 | projectRoot 64 | }, 65 | 'json' // outputFormat - use 'json' to suppress CLI UI 66 | ); 67 | 68 | // Transform the result to remove full task data and provide summary info 69 | const tagsSummary = result.tags.map((tag) => { 70 | const tasks = tag.tasks || []; 71 | 72 | // Calculate status breakdown 73 | const statusBreakdown = tasks.reduce((acc, task) => { 74 | const status = task.status || 'pending'; 75 | acc[status] = (acc[status] || 0) + 1; 76 | return acc; 77 | }, {}); 78 | 79 | // Calculate subtask counts 80 | const subtaskCounts = tasks.reduce( 81 | (acc, task) => { 82 | if (task.subtasks && task.subtasks.length > 0) { 83 | acc.totalSubtasks += task.subtasks.length; 84 | task.subtasks.forEach((subtask) => { 85 | const subStatus = subtask.status || 'pending'; 86 | acc.subtasksByStatus[subStatus] = 87 | (acc.subtasksByStatus[subStatus] || 0) + 1; 88 | }); 89 | } 90 | return acc; 91 | }, 92 | { totalSubtasks: 0, subtasksByStatus: {} } 93 | ); 94 | 95 | return { 96 | name: tag.name, 97 | isCurrent: tag.isCurrent, 98 | taskCount: tasks.length, 99 | completedTasks: tag.completedTasks, 100 | statusBreakdown, 101 | subtaskCounts, 102 | created: tag.created, 103 | description: tag.description 104 | }; 105 | }); 106 | 107 | // Restore normal logging 108 | disableSilentMode(); 109 | 110 | return { 111 | success: true, 112 | data: { 113 | tags: tagsSummary, 114 | currentTag: result.currentTag, 115 | totalTags: result.totalTags, 116 | message: `Found ${result.totalTags} tag(s)` 117 | } 118 | }; 119 | } catch (error) { 120 | // Make sure to restore normal logging even if there's an error 121 | disableSilentMode(); 122 | 123 | log.error(`Error in listTagsDirect: ${error.message}`); 124 | return { 125 | success: false, 126 | error: { 127 | code: error.code || 'LIST_TAGS_ERROR', 128 | message: error.message 129 | } 130 | }; 131 | } 132 | } 133 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/set-task-status.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tools/setTaskStatus.js 3 | * Tool to set the status of a task 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | handleApiResult, 9 | createErrorResponse, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { 13 | setTaskStatusDirect, 14 | nextTaskDirect 15 | } from '../core/task-master-core.js'; 16 | import { 17 | findTasksPath, 18 | findComplexityReportPath 19 | } from '../core/utils/path-utils.js'; 20 | import { TASK_STATUS_OPTIONS } from '../../../src/constants/task-status.js'; 21 | import { resolveTag } from '../../../scripts/modules/utils.js'; 22 | 23 | /** 24 | * Register the setTaskStatus tool with the MCP server 25 | * @param {Object} server - FastMCP server instance 26 | */ 27 | export function registerSetTaskStatusTool(server) { 28 | server.addTool({ 29 | name: 'set_task_status', 30 | description: 'Set the status of one or more tasks or subtasks.', 31 | parameters: z.object({ 32 | id: z 33 | .string() 34 | .describe( 35 | "Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated to update multiple tasks/subtasks at once." 36 | ), 37 | status: z 38 | .enum(TASK_STATUS_OPTIONS) 39 | .describe( 40 | "New status to set (e.g., 'pending', 'done', 'in-progress', 'review', 'deferred', 'cancelled'." 41 | ), 42 | file: z.string().optional().describe('Absolute path to the tasks file'), 43 | complexityReport: z 44 | .string() 45 | .optional() 46 | .describe( 47 | 'Path to the complexity report file (relative to project root or absolute)' 48 | ), 49 | projectRoot: z 50 | .string() 51 | .describe('The directory of the project. Must be an absolute path.'), 52 | tag: z.string().optional().describe('Optional tag context to operate on') 53 | }), 54 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 55 | try { 56 | log.info( 57 | `Setting status of task(s) ${args.id} to: ${args.status} ${ 58 | args.tag ? `in tag: ${args.tag}` : 'in current tag' 59 | }` 60 | ); 61 | const resolvedTag = resolveTag({ 62 | projectRoot: args.projectRoot, 63 | tag: args.tag 64 | }); 65 | // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) 66 | let tasksJsonPath; 67 | try { 68 | tasksJsonPath = findTasksPath( 69 | { projectRoot: args.projectRoot, file: args.file }, 70 | log 71 | ); 72 | } catch (error) { 73 | log.error(`Error finding tasks.json: ${error.message}`); 74 | return createErrorResponse( 75 | `Failed to find tasks.json: ${error.message}` 76 | ); 77 | } 78 | 79 | let complexityReportPath; 80 | try { 81 | complexityReportPath = findComplexityReportPath( 82 | { 83 | projectRoot: args.projectRoot, 84 | complexityReport: args.complexityReport, 85 | tag: resolvedTag 86 | }, 87 | log 88 | ); 89 | } catch (error) { 90 | log.error(`Error finding complexity report: ${error.message}`); 91 | } 92 | 93 | const result = await setTaskStatusDirect( 94 | { 95 | tasksJsonPath: tasksJsonPath, 96 | id: args.id, 97 | status: args.status, 98 | complexityReportPath, 99 | projectRoot: args.projectRoot, 100 | tag: resolvedTag 101 | }, 102 | log, 103 | { session } 104 | ); 105 | 106 | if (result.success) { 107 | log.info( 108 | `Successfully updated status for task(s) ${args.id} to "${args.status}": ${result.data.message}` 109 | ); 110 | } else { 111 | log.error( 112 | `Failed to update task status: ${result.error?.message || 'Unknown error'}` 113 | ); 114 | } 115 | 116 | return handleApiResult( 117 | result, 118 | log, 119 | 'Error setting task status', 120 | undefined, 121 | args.projectRoot 122 | ); 123 | } catch (error) { 124 | log.error(`Error in setTaskStatus tool: ${error.message}`); 125 | return createErrorResponse( 126 | `Error setting task status: ${error.message}` 127 | ); 128 | } 129 | }) 130 | }); 131 | } 132 | ``` -------------------------------------------------------------------------------- /tests/unit/profiles/claude-integration.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import os from 'os'; 5 | import { claudeProfile } from '../../../src/profiles/claude.js'; 6 | 7 | // Mock external modules 8 | jest.mock('child_process', () => ({ 9 | execSync: jest.fn() 10 | })); 11 | 12 | // Mock console methods 13 | jest.mock('console', () => ({ 14 | log: jest.fn(), 15 | info: jest.fn(), 16 | warn: jest.fn(), 17 | error: jest.fn(), 18 | clear: jest.fn() 19 | })); 20 | 21 | describe('Claude Profile Integration', () => { 22 | let tempDir; 23 | 24 | beforeEach(() => { 25 | jest.clearAllMocks(); 26 | 27 | // Create a temporary directory for testing 28 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); 29 | 30 | // Spy on fs methods 31 | jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); 32 | jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { 33 | if (filePath.toString().includes('AGENTS.md')) { 34 | return 'Sample AGENTS.md content for Claude integration'; 35 | } 36 | return '{}'; 37 | }); 38 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false); 39 | jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); 40 | }); 41 | 42 | afterEach(() => { 43 | // Clean up the temporary directory 44 | try { 45 | fs.rmSync(tempDir, { recursive: true, force: true }); 46 | } catch (err) { 47 | console.error(`Error cleaning up: ${err.message}`); 48 | } 49 | }); 50 | 51 | // Test function that simulates the Claude profile file copying behavior 52 | function mockCreateClaudeStructure() { 53 | // Claude profile copies AGENTS.md to CLAUDE.md in project root 54 | const sourceContent = 'Sample AGENTS.md content for Claude integration'; 55 | fs.writeFileSync(path.join(tempDir, 'CLAUDE.md'), sourceContent); 56 | } 57 | 58 | test('creates CLAUDE.md file in project root', () => { 59 | // Act 60 | mockCreateClaudeStructure(); 61 | 62 | // Assert 63 | expect(fs.writeFileSync).toHaveBeenCalledWith( 64 | path.join(tempDir, 'CLAUDE.md'), 65 | 'Sample AGENTS.md content for Claude integration' 66 | ); 67 | }); 68 | 69 | test('does not create any profile directories', () => { 70 | // Act 71 | mockCreateClaudeStructure(); 72 | 73 | // Assert - Claude profile should not create any directories 74 | // Only the temp directory creation calls should exist 75 | const mkdirCalls = fs.mkdirSync.mock.calls.filter( 76 | (call) => !call[0].includes('task-master-test-') 77 | ); 78 | expect(mkdirCalls).toHaveLength(0); 79 | }); 80 | 81 | test('supports MCP configuration when using rule transformer', () => { 82 | // This test verifies that the Claude profile is configured to support MCP 83 | // The actual MCP file creation is handled by the rule transformer 84 | 85 | // Assert - Claude profile should now support MCP configuration 86 | expect(claudeProfile.mcpConfig).toBe(true); 87 | expect(claudeProfile.mcpConfigName).toBe('.mcp.json'); 88 | expect(claudeProfile.mcpConfigPath).toBe('.mcp.json'); 89 | }); 90 | 91 | test('mock function does not create MCP configuration files', () => { 92 | // Act 93 | mockCreateClaudeStructure(); 94 | 95 | // Assert - The mock function should not create MCP config files 96 | // (This is expected since the mock doesn't use the rule transformer) 97 | const writeFileCalls = fs.writeFileSync.mock.calls; 98 | const mcpConfigCalls = writeFileCalls.filter( 99 | (call) => 100 | call[0].toString().includes('mcp.json') || 101 | call[0].toString().includes('mcp_settings.json') 102 | ); 103 | expect(mcpConfigCalls).toHaveLength(0); 104 | }); 105 | 106 | test('only creates the target integration guide file', () => { 107 | // Act 108 | mockCreateClaudeStructure(); 109 | 110 | // Assert - Should only create CLAUDE.md 111 | const writeFileCalls = fs.writeFileSync.mock.calls; 112 | expect(writeFileCalls).toHaveLength(1); 113 | expect(writeFileCalls[0][0]).toBe(path.join(tempDir, 'CLAUDE.md')); 114 | }); 115 | }); 116 | ``` -------------------------------------------------------------------------------- /src/ai-providers/custom-sdk/grok-cli/message-converter.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * @fileoverview Message format conversion utilities for Grok CLI provider 3 | */ 4 | 5 | /** 6 | * @typedef {import('./types.js').GrokCliMessage} GrokCliMessage 7 | */ 8 | 9 | /** 10 | * Convert AI SDK messages to Grok CLI compatible format 11 | * @param {Array<Object>} messages - AI SDK message array 12 | * @returns {Array<GrokCliMessage>} Grok CLI compatible messages 13 | */ 14 | export function convertToGrokCliMessages(messages) { 15 | return messages.map((message) => { 16 | // Handle different message content types 17 | let content = ''; 18 | 19 | if (typeof message.content === 'string') { 20 | content = message.content; 21 | } else if (Array.isArray(message.content)) { 22 | // Handle multi-part content (text and images) 23 | content = message.content 24 | .filter((part) => part.type === 'text') 25 | .map((part) => part.text) 26 | .join('\n'); 27 | } else if (message.content && typeof message.content === 'object') { 28 | // Handle object content 29 | content = message.content.text || JSON.stringify(message.content); 30 | } 31 | 32 | return { 33 | role: message.role, 34 | content: content.trim() 35 | }; 36 | }); 37 | } 38 | 39 | /** 40 | * Convert Grok CLI response to AI SDK format 41 | * @param {string} responseText - Raw response text from Grok CLI (JSONL format) 42 | * @returns {Object} AI SDK compatible response object 43 | */ 44 | export function convertFromGrokCliResponse(responseText) { 45 | try { 46 | // Grok CLI outputs JSONL format - each line is a separate JSON message 47 | const lines = responseText 48 | .trim() 49 | .split('\n') 50 | .filter((line) => line.trim()); 51 | 52 | // Parse each line as JSON and find assistant messages 53 | const messages = []; 54 | for (const line of lines) { 55 | try { 56 | const message = JSON.parse(line); 57 | messages.push(message); 58 | } catch (parseError) { 59 | // Skip invalid JSON lines 60 | continue; 61 | } 62 | } 63 | 64 | // Find the last assistant message 65 | const assistantMessage = messages 66 | .filter((msg) => msg.role === 'assistant') 67 | .pop(); 68 | 69 | if (assistantMessage && assistantMessage.content) { 70 | return { 71 | text: assistantMessage.content, 72 | usage: assistantMessage.usage 73 | ? { 74 | promptTokens: assistantMessage.usage.prompt_tokens || 0, 75 | completionTokens: assistantMessage.usage.completion_tokens || 0, 76 | totalTokens: assistantMessage.usage.total_tokens || 0 77 | } 78 | : undefined 79 | }; 80 | } 81 | 82 | // Fallback: if no assistant message found, return the raw text 83 | return { 84 | text: responseText.trim(), 85 | usage: undefined 86 | }; 87 | } catch (error) { 88 | // If parsing fails completely, treat as plain text response 89 | return { 90 | text: responseText.trim(), 91 | usage: undefined 92 | }; 93 | } 94 | } 95 | 96 | /** 97 | * Create a prompt string for Grok CLI from messages 98 | * @param {Array<Object>} messages - AI SDK message array 99 | * @returns {string} Formatted prompt string 100 | */ 101 | export function createPromptFromMessages(messages) { 102 | const grokMessages = convertToGrokCliMessages(messages); 103 | 104 | // Create a conversation-style prompt 105 | const prompt = grokMessages 106 | .map((message) => { 107 | switch (message.role) { 108 | case 'system': 109 | return `System: ${message.content}`; 110 | case 'user': 111 | return `User: ${message.content}`; 112 | case 'assistant': 113 | return `Assistant: ${message.content}`; 114 | default: 115 | return `${message.role}: ${message.content}`; 116 | } 117 | }) 118 | .join('\n\n'); 119 | 120 | return prompt; 121 | } 122 | 123 | /** 124 | * Escape shell arguments for safe CLI execution 125 | * @param {string} arg - Argument to escape 126 | * @returns {string} Shell-escaped argument 127 | */ 128 | export function escapeShellArg(arg) { 129 | if (typeof arg !== 'string') { 130 | arg = String(arg); 131 | } 132 | 133 | // Replace single quotes with '\'' 134 | return "'" + arg.replace(/'/g, "'\\''") + "'"; 135 | } 136 | ``` -------------------------------------------------------------------------------- /packages/tm-core/src/executors/claude-executor.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Claude executor implementation for Task Master 3 | */ 4 | 5 | import { spawn } from 'child_process'; 6 | import type { Task } from '../types/index.js'; 7 | import type { 8 | ExecutorType, 9 | ExecutionResult, 10 | ClaudeExecutorConfig 11 | } from './types.js'; 12 | import { BaseExecutor } from './base-executor.js'; 13 | 14 | export class ClaudeExecutor extends BaseExecutor { 15 | private claudeConfig: ClaudeExecutorConfig; 16 | private currentProcess: any = null; 17 | 18 | constructor(projectRoot: string, config: ClaudeExecutorConfig = {}) { 19 | super(projectRoot, config); 20 | this.claudeConfig = { 21 | command: config.command || 'claude', 22 | systemPrompt: 23 | config.systemPrompt || 24 | 'You are a helpful AI assistant helping to complete a software development task.', 25 | additionalFlags: config.additionalFlags || [] 26 | }; 27 | } 28 | 29 | getType(): ExecutorType { 30 | return 'claude'; 31 | } 32 | 33 | async isAvailable(): Promise<boolean> { 34 | return new Promise((resolve) => { 35 | const checkProcess = spawn('which', [this.claudeConfig.command!], { 36 | shell: true 37 | }); 38 | 39 | checkProcess.on('close', (code) => { 40 | resolve(code === 0); 41 | }); 42 | 43 | checkProcess.on('error', () => { 44 | resolve(false); 45 | }); 46 | }); 47 | } 48 | 49 | async execute(task: Task): Promise<ExecutionResult> { 50 | const startTime = new Date().toISOString(); 51 | 52 | try { 53 | // Check if Claude is available 54 | const isAvailable = await this.isAvailable(); 55 | if (!isAvailable) { 56 | return this.createResult( 57 | task.id, 58 | false, 59 | undefined, 60 | `Claude CLI not found. Please ensure 'claude' command is available in PATH.` 61 | ); 62 | } 63 | 64 | // Format the task into a prompt 65 | const taskPrompt = this.formatTaskPrompt(task); 66 | const fullPrompt = `${this.claudeConfig.systemPrompt}\n\nHere is the task to complete:\n\n${taskPrompt}`; 67 | 68 | // Execute Claude with the task details 69 | const result = await this.runClaude(fullPrompt, task.id); 70 | 71 | return { 72 | ...result, 73 | startTime, 74 | endTime: new Date().toISOString() 75 | }; 76 | } catch (error: any) { 77 | this.logger.error(`Failed to execute task ${task.id}:`, error); 78 | return this.createResult( 79 | task.id, 80 | false, 81 | undefined, 82 | error.message || 'Unknown error occurred' 83 | ); 84 | } 85 | } 86 | 87 | private runClaude(prompt: string, taskId: string): Promise<ExecutionResult> { 88 | return new Promise((resolve) => { 89 | const args = [prompt, ...this.claudeConfig.additionalFlags!]; 90 | 91 | this.logger.info(`Executing Claude for task ${taskId}`); 92 | this.logger.debug( 93 | `Command: ${this.claudeConfig.command} ${args.join(' ')}` 94 | ); 95 | 96 | this.currentProcess = spawn(this.claudeConfig.command!, args, { 97 | cwd: this.projectRoot, 98 | shell: false, 99 | stdio: 'inherit' // Let Claude handle its own I/O 100 | }); 101 | 102 | this.currentProcess.on('close', (code: number) => { 103 | this.currentProcess = null; 104 | 105 | if (code === 0) { 106 | resolve( 107 | this.createResult( 108 | taskId, 109 | true, 110 | 'Claude session completed successfully' 111 | ) 112 | ); 113 | } else { 114 | resolve( 115 | this.createResult( 116 | taskId, 117 | false, 118 | undefined, 119 | `Claude exited with code ${code}` 120 | ) 121 | ); 122 | } 123 | }); 124 | 125 | this.currentProcess.on('error', (error: any) => { 126 | this.currentProcess = null; 127 | this.logger.error(`Claude process error:`, error); 128 | resolve( 129 | this.createResult( 130 | taskId, 131 | false, 132 | undefined, 133 | `Failed to spawn Claude: ${error.message}` 134 | ) 135 | ); 136 | }); 137 | }); 138 | } 139 | 140 | async stop(): Promise<void> { 141 | if (this.currentProcess) { 142 | this.logger.info('Stopping Claude process...'); 143 | this.currentProcess.kill('SIGTERM'); 144 | this.currentProcess = null; 145 | } 146 | } 147 | } 148 | ``` -------------------------------------------------------------------------------- /tests/integration/profiles/roo-init-functionality.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { rooProfile } from '../../../src/profiles/roo.js'; 5 | import { COMMON_TOOL_MAPPINGS } from '../../../src/profiles/base-profile.js'; 6 | 7 | describe('Roo Profile Initialization Functionality', () => { 8 | let rooProfileContent; 9 | 10 | beforeAll(() => { 11 | // Read the roo.js profile file content once for all tests 12 | const rooJsPath = path.join(process.cwd(), 'src', 'profiles', 'roo.js'); 13 | rooProfileContent = fs.readFileSync(rooJsPath, 'utf8'); 14 | }); 15 | 16 | test('roo.js uses factory pattern with correct configuration', () => { 17 | // Check for explicit, non-default values in the source file 18 | expect(rooProfileContent).toContain("name: 'roo'"); 19 | expect(rooProfileContent).toContain("displayName: 'Roo Code'"); 20 | expect(rooProfileContent).toContain( 21 | 'toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE' 22 | ); 23 | 24 | // Check the final computed properties on the profile object 25 | expect(rooProfile.profileName).toBe('roo'); 26 | expect(rooProfile.displayName).toBe('Roo Code'); 27 | expect(rooProfile.profileDir).toBe('.roo'); // default 28 | expect(rooProfile.rulesDir).toBe('.roo/rules'); // default 29 | expect(rooProfile.mcpConfig).toBe(true); // default 30 | }); 31 | 32 | test('roo.js uses custom ROO_STYLE tool mappings', () => { 33 | // Check that the profile uses the correct, non-standard tool mappings 34 | expect(rooProfileContent).toContain( 35 | 'toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE' 36 | ); 37 | 38 | // Verify the result: roo uses custom tool names 39 | expect(rooProfile.conversionConfig.toolNames.edit_file).toBe('apply_diff'); 40 | expect(rooProfile.conversionConfig.toolNames.search).toBe('search_files'); 41 | }); 42 | 43 | test('roo.js profile ensures Roo directory structure via onAddRulesProfile', () => { 44 | // Check if onAddRulesProfile function exists 45 | expect(rooProfileContent).toContain( 46 | 'onAddRulesProfile(targetDir, assetsDir)' 47 | ); 48 | 49 | // Check for the general copy of assets/roocode which includes .roo base structure 50 | expect(rooProfileContent).toContain( 51 | "const sourceDir = path.join(assetsDir, 'roocode');" 52 | ); 53 | expect(rooProfileContent).toContain( 54 | 'copyRecursiveSync(sourceDir, targetDir);' 55 | ); 56 | 57 | // Check for the specific .roo modes directory handling 58 | expect(rooProfileContent).toContain( 59 | "const rooModesDir = path.join(sourceDir, '.roo');" 60 | ); 61 | 62 | // Check for import of ROO_MODES from profiles.js instead of local definition 63 | expect(rooProfileContent).toContain( 64 | "import { ROO_MODES } from '../constants/profiles.js';" 65 | ); 66 | }); 67 | 68 | test('roo.js profile copies .roomodes file via onAddRulesProfile', () => { 69 | expect(rooProfileContent).toContain( 70 | 'onAddRulesProfile(targetDir, assetsDir)' 71 | ); 72 | 73 | // Check for the specific .roomodes copy logic 74 | expect(rooProfileContent).toContain( 75 | "const roomodesSrc = path.join(sourceDir, '.roomodes');" 76 | ); 77 | expect(rooProfileContent).toContain( 78 | "const roomodesDest = path.join(targetDir, '.roomodes');" 79 | ); 80 | expect(rooProfileContent).toContain( 81 | 'fs.copyFileSync(roomodesSrc, roomodesDest);' 82 | ); 83 | }); 84 | 85 | test('roo.js profile copies mode-specific rule files via onAddRulesProfile', () => { 86 | expect(rooProfileContent).toContain( 87 | 'onAddRulesProfile(targetDir, assetsDir)' 88 | ); 89 | expect(rooProfileContent).toContain('for (const mode of ROO_MODES)'); 90 | 91 | // Check for the specific mode rule file copy logic 92 | expect(rooProfileContent).toContain( 93 | 'const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`);' 94 | ); 95 | expect(rooProfileContent).toContain( 96 | "const dest = path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`);" 97 | ); 98 | }); 99 | }); 100 | ```