This is page 13 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 -------------------------------------------------------------------------------- /apps/docs/archive/ai-development-workflow.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "AI Development Workflow" 3 | description: "Learn how Task Master and Cursor AI work together to streamline your development workflow" 4 | --- 5 | 6 | <Tip>The Cursor agent is pre-configured (via the rules file) to follow this workflow</Tip> 7 | 8 | <AccordionGroup> 9 | <Accordion title="1. Task Discovery and Selection"> 10 | Ask the agent to list available tasks: 11 | 12 | ``` 13 | What tasks are available to work on next? 14 | ``` 15 | 16 | The agent will: 17 | 18 | - Run `task-master list` to see all tasks 19 | - Run `task-master next` to determine the next task to work on 20 | - Analyze dependencies to determine which tasks are ready to be worked on 21 | - Prioritize tasks based on priority level and ID order 22 | - Suggest the next task(s) to implement 23 | </Accordion> 24 | 25 | <Accordion title="2. Task Implementation"> 26 | When implementing a task, the agent will: 27 | 28 | - Reference the task's details section for implementation specifics 29 | - Consider dependencies on previous tasks 30 | - Follow the project's coding standards 31 | - Create appropriate tests based on the task's testStrategy 32 | 33 | You can ask: 34 | 35 | ``` 36 | Let's implement task 3. What does it involve? 37 | ``` 38 | </Accordion> 39 | 40 | <Accordion title="3. Task Verification"> 41 | Before marking a task as complete, verify it according to: 42 | 43 | - The task's specified testStrategy 44 | - Any automated tests in the codebase 45 | - Manual verification if required 46 | </Accordion> 47 | 48 | <Accordion title="4. Task Completion"> 49 | When a task is completed, tell the agent: 50 | 51 | ``` 52 | Task 3 is now complete. Please update its status. 53 | ``` 54 | 55 | The agent will execute: 56 | 57 | ```bash 58 | task-master set-status --id=3 --status=done 59 | ``` 60 | </Accordion> 61 | 62 | <Accordion title="5. Handling Implementation Drift"> 63 | If during implementation, you discover that: 64 | 65 | - The current approach differs significantly from what was planned 66 | - Future tasks need to be modified due to current implementation choices 67 | - New dependencies or requirements have emerged 68 | 69 | Tell the agent: 70 | 71 | ``` 72 | We've changed our approach. We're now using Express instead of Fastify. Please update all future tasks to reflect this change. 73 | ``` 74 | 75 | The agent will execute: 76 | 77 | ```bash 78 | task-master update --from=4 --prompt="Now we are using Express instead of Fastify." 79 | ``` 80 | 81 | This will rewrite or re-scope subsequent tasks in tasks.json while preserving completed work. 82 | </Accordion> 83 | 84 | <Accordion title="6. Breaking Down Complex Tasks"> 85 | For complex tasks that need more granularity: 86 | 87 | ``` 88 | Task 5 seems complex. Can you break it down into subtasks? 89 | ``` 90 | 91 | The agent will execute: 92 | 93 | ```bash 94 | task-master expand --id=5 --num=3 95 | ``` 96 | 97 | You can provide additional context: 98 | 99 | ``` 100 | Please break down task 5 with a focus on security considerations. 101 | ``` 102 | 103 | The agent will execute: 104 | 105 | ```bash 106 | task-master expand --id=5 --prompt="Focus on security aspects" 107 | ``` 108 | 109 | You can also expand all pending tasks: 110 | 111 | ``` 112 | Please break down all pending tasks into subtasks. 113 | ``` 114 | 115 | The agent will execute: 116 | 117 | ```bash 118 | task-master expand --all 119 | ``` 120 | 121 | For research-backed subtask generation using Perplexity AI: 122 | 123 | ``` 124 | Please break down task 5 using research-backed generation. 125 | ``` 126 | 127 | The agent will execute: 128 | 129 | ```bash 130 | task-master expand --id=5 --research 131 | ``` 132 | </Accordion> 133 | </AccordionGroup> 134 | 135 | ## Example Cursor AI Interactions 136 | 137 | <AccordionGroup> 138 | <Accordion title="Starting a new project"> 139 | ``` 140 | I've just initialized a new project with Claude Task Master. I have a PRD at scripts/prd.txt. 141 | Can you help me parse it and set up the initial tasks? 142 | ``` 143 | </Accordion> 144 | <Accordion title="Working on tasks"> 145 | ``` 146 | What's the next task I should work on? Please consider dependencies and priorities. 147 | ``` 148 | </Accordion> 149 | <Accordion title="Implementing a specific task"> 150 | ``` 151 | I'd like to implement task 4. Can you help me understand what needs to be done and how to approach it? 152 | ``` 153 | </Accordion> 154 | <Accordion title="Managing subtasks"> 155 | ``` 156 | I need to regenerate the subtasks for task 3 with a different approach. Can you help me clear and regenerate them? 157 | ``` 158 | </Accordion> 159 | <Accordion title="Handling changes"> 160 | ``` 161 | We've decided to use MongoDB instead of PostgreSQL. Can you update all future tasks to reflect this change? 162 | ``` 163 | </Accordion> 164 | <Accordion title="Completing work"> 165 | ``` 166 | I've finished implementing the authentication system described in task 2. All tests are passing. 167 | Please mark it as complete and tell me what I should work on next. 168 | ``` 169 | </Accordion> 170 | <Accordion title="Analyzing complexity"> 171 | ``` 172 | Can you analyze the complexity of our tasks to help me understand which ones need to be broken down further? 173 | ``` 174 | </Accordion> 175 | <Accordion title="Viewing complexity report"> 176 | ``` 177 | Can you show me the complexity report in a more readable format? 178 | ``` 179 | </Accordion> 180 | </AccordionGroup> 181 | ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/reducers/appReducer.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Main application state reducer 3 | */ 4 | 5 | import type { AppState, AppAction } from '../types'; 6 | import { logger } from '../utils/logger'; 7 | 8 | export const appReducer = (state: AppState, action: AppAction): AppState => { 9 | logger.debug( 10 | 'Reducer action:', 11 | action.type, 12 | 'payload' in action ? action.payload : 'no payload' 13 | ); 14 | switch (action.type) { 15 | case 'SET_TASKS': 16 | const newTasks = Array.isArray(action.payload) ? action.payload : []; 17 | logger.debug('SET_TASKS reducer - updating tasks:', { 18 | oldCount: state.tasks.length, 19 | newCount: newTasks.length, 20 | newTasks 21 | }); 22 | return { 23 | ...state, 24 | tasks: newTasks, 25 | loading: false, 26 | error: undefined 27 | }; 28 | case 'SET_LOADING': 29 | return { ...state, loading: action.payload }; 30 | case 'SET_ERROR': 31 | return { ...state, error: action.payload, loading: false }; 32 | case 'CLEAR_ERROR': 33 | return { ...state, error: undefined }; 34 | case 'INCREMENT_REQUEST_ID': 35 | return { ...state, requestId: state.requestId + 1 }; 36 | case 'UPDATE_TASK_STATUS': { 37 | const { taskId, newStatus } = action.payload; 38 | return { 39 | ...state, 40 | tasks: state.tasks.map((task) => 41 | task.id === taskId ? { ...task, status: newStatus } : task 42 | ) 43 | }; 44 | } 45 | case 'UPDATE_TASK_CONTENT': { 46 | const { taskId, updates } = action.payload; 47 | return { 48 | ...state, 49 | tasks: state.tasks.map((task) => 50 | task.id === taskId ? { ...task, ...updates } : task 51 | ) 52 | }; 53 | } 54 | case 'SET_CONNECTION_STATUS': 55 | return { 56 | ...state, 57 | isConnected: action.payload.isConnected, 58 | connectionStatus: action.payload.status 59 | }; 60 | case 'SET_EDITING_TASK': 61 | return { 62 | ...state, 63 | editingTask: action.payload 64 | }; 65 | case 'SET_POLLING_STATUS': 66 | return { 67 | ...state, 68 | polling: { 69 | ...state.polling, 70 | isActive: action.payload.isActive, 71 | errorCount: action.payload.errorCount ?? state.polling.errorCount, 72 | lastUpdate: action.payload.isActive 73 | ? Date.now() 74 | : state.polling.lastUpdate 75 | } 76 | }; 77 | case 'SET_USER_INTERACTING': 78 | return { 79 | ...state, 80 | polling: { 81 | ...state.polling, 82 | isUserInteracting: action.payload 83 | } 84 | }; 85 | case 'TASKS_UPDATED_FROM_POLLING': 86 | return { 87 | ...state, 88 | tasks: Array.isArray(action.payload) ? action.payload : [], 89 | polling: { 90 | ...state.polling, 91 | lastUpdate: Date.now() 92 | } 93 | }; 94 | case 'SET_NETWORK_STATUS': 95 | return { 96 | ...state, 97 | polling: { 98 | ...state.polling, 99 | isOfflineMode: action.payload.isOfflineMode, 100 | connectionStatus: action.payload.connectionStatus, 101 | reconnectAttempts: 102 | action.payload.reconnectAttempts !== undefined 103 | ? action.payload.reconnectAttempts 104 | : state.polling.reconnectAttempts, 105 | maxReconnectAttempts: 106 | action.payload.maxReconnectAttempts !== undefined 107 | ? action.payload.maxReconnectAttempts 108 | : state.polling.maxReconnectAttempts, 109 | lastSuccessfulConnection: 110 | action.payload.lastSuccessfulConnection !== undefined 111 | ? action.payload.lastSuccessfulConnection 112 | : state.polling.lastSuccessfulConnection 113 | } 114 | }; 115 | case 'LOAD_CACHED_TASKS': 116 | return { 117 | ...state, 118 | tasks: Array.isArray(action.payload) ? action.payload : [] 119 | }; 120 | case 'ADD_TOAST': 121 | return { 122 | ...state, 123 | toastNotifications: [...state.toastNotifications, action.payload] 124 | }; 125 | case 'REMOVE_TOAST': 126 | return { 127 | ...state, 128 | toastNotifications: state.toastNotifications.filter( 129 | (notification) => notification.id !== action.payload 130 | ) 131 | }; 132 | case 'CLEAR_ALL_TOASTS': 133 | return { ...state, toastNotifications: [] }; 134 | case 'NAVIGATE_TO_TASK': 135 | logger.debug('📍 Reducer: Navigating to task:', action.payload); 136 | return { 137 | ...state, 138 | currentView: 'task-details', 139 | selectedTaskId: action.payload 140 | }; 141 | case 'NAVIGATE_TO_KANBAN': 142 | logger.debug('📍 Reducer: Navigating to kanban'); 143 | return { ...state, currentView: 'kanban', selectedTaskId: undefined }; 144 | case 'NAVIGATE_TO_CONFIG': 145 | logger.debug('📍 Reducer: Navigating to config'); 146 | return { ...state, currentView: 'config', selectedTaskId: undefined }; 147 | case 'SET_CURRENT_TAG': 148 | return { 149 | ...state, 150 | currentTag: action.payload 151 | }; 152 | case 'SET_AVAILABLE_TAGS': 153 | return { 154 | ...state, 155 | availableTags: action.payload 156 | }; 157 | case 'SET_TAG_DATA': 158 | return { 159 | ...state, 160 | currentTag: action.payload.currentTag, 161 | availableTags: action.payload.availableTags 162 | }; 163 | default: 164 | return state; 165 | } 166 | }; 167 | 168 | export const initialState: AppState = { 169 | tasks: [], 170 | loading: true, 171 | requestId: 0, 172 | isConnected: false, 173 | connectionStatus: 'Connecting...', 174 | editingTask: { taskId: null }, 175 | polling: { 176 | isActive: false, 177 | errorCount: 0, 178 | lastUpdate: undefined, 179 | isUserInteracting: false, 180 | isOfflineMode: false, 181 | reconnectAttempts: 0, 182 | maxReconnectAttempts: 0, 183 | lastSuccessfulConnection: undefined, 184 | connectionStatus: 'online' 185 | }, 186 | toastNotifications: [], 187 | currentView: 'kanban', 188 | selectedTaskId: undefined, 189 | // Tag-related state 190 | currentTag: 'master', 191 | availableTags: ['master'] 192 | }; 193 | ``` -------------------------------------------------------------------------------- /src/prompts/add-task.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "id": "add-task", 3 | "version": "1.0.0", 4 | "description": "Generate a new task based on description", 5 | "metadata": { 6 | "author": "system", 7 | "created": "2024-01-01T00:00:00Z", 8 | "updated": "2024-01-01T00:00:00Z", 9 | "tags": ["task-creation", "generation"] 10 | }, 11 | "parameters": { 12 | "prompt": { 13 | "type": "string", 14 | "required": true, 15 | "description": "User's task description" 16 | }, 17 | "newTaskId": { 18 | "type": "number", 19 | "required": true, 20 | "description": "ID for the new task" 21 | }, 22 | "existingTasks": { 23 | "type": "array", 24 | "description": "List of existing tasks for context" 25 | }, 26 | "gatheredContext": { 27 | "type": "string", 28 | "description": "Context gathered from codebase analysis" 29 | }, 30 | "contextFromArgs": { 31 | "type": "string", 32 | "description": "Additional context from manual args" 33 | }, 34 | "priority": { 35 | "type": "string", 36 | "default": "medium", 37 | "enum": ["high", "medium", "low"], 38 | "description": "Task priority" 39 | }, 40 | "dependencies": { 41 | "type": "array", 42 | "description": "Task dependency IDs" 43 | }, 44 | "useResearch": { 45 | "type": "boolean", 46 | "default": false, 47 | "description": "Use research mode" 48 | }, 49 | "hasCodebaseAnalysis": { 50 | "type": "boolean", 51 | "required": false, 52 | "default": false, 53 | "description": "Whether codebase analysis is available" 54 | }, 55 | "projectRoot": { 56 | "type": "string", 57 | "required": false, 58 | "default": "", 59 | "description": "Project root path for context" 60 | } 61 | }, 62 | "prompts": { 63 | "default": { 64 | "system": "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\nWhen determining dependencies for a new task, follow these principles:\n1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n2. Prioritize task dependencies that are semantically related to the functionality being built.\n3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\nThe dependencies array should contain task IDs (numbers) of prerequisite tasks.{{#if useResearch}}\n\nResearch current best practices and technologies relevant to this task.{{/if}}", 65 | "user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the task:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine key files like package.json, main entry points, and relevant source files\n4. Analyze the current implementation to understand what already exists\n\nBased on your analysis:\n- Identify existing components/features that relate to this new task\n- Understand the technology stack, frameworks, and patterns in use\n- Generate implementation details that align with the project's current architecture\n- Reference specific files, functions, or patterns from the codebase in your details\n\nProject Root: {{projectRoot}}\n\n{{/if}}You are generating the details for Task #{{newTaskId}}. Based on the user's request: \"{{prompt}}\", create a comprehensive new task for a software development project.\n \n {{gatheredContext}}\n \n {{#if useResearch}}Research current best practices, technologies, and implementation patterns relevant to this task. {{/if}}Based on the information about existing tasks provided above, include appropriate dependencies in the \"dependencies\" array. Only include task IDs that this new task directly depends on.\n \n Return your answer as a single JSON object matching the schema precisely:\n \n {\n \"title\": \"Task title goes here\",\n \"description\": \"A concise one or two sentence description of what the task involves\",\n \"details\": \"Detailed implementation steps, considerations, code examples, or technical approach\",\n \"testStrategy\": \"Specific steps to verify correct implementation and functionality\",\n \"dependencies\": [1, 3] // Example: IDs of tasks that must be completed before this task\n }\n \n Make sure the details and test strategy are comprehensive and specific{{#if useResearch}}, incorporating current best practices from your research{{/if}}. DO NOT include the task ID in the title.\n {{#if contextFromArgs}}{{contextFromArgs}}{{/if}}" 66 | } 67 | } 68 | } 69 | ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/add-subtask.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Tests for the addSubtask function 3 | */ 4 | import { jest } from '@jest/globals'; 5 | 6 | // Mock dependencies before importing the module 7 | const mockUtils = { 8 | readJSON: jest.fn(), 9 | writeJSON: jest.fn(), 10 | log: jest.fn(), 11 | getCurrentTag: jest.fn() 12 | }; 13 | const mockTaskManager = { 14 | isTaskDependentOn: jest.fn() 15 | }; 16 | const mockGenerateTaskFiles = jest.fn(); 17 | 18 | jest.unstable_mockModule( 19 | '../../../../../scripts/modules/utils.js', 20 | () => mockUtils 21 | ); 22 | jest.unstable_mockModule( 23 | '../../../../../scripts/modules/task-manager.js', 24 | () => mockTaskManager 25 | ); 26 | jest.unstable_mockModule( 27 | '../../../../../scripts/modules/task-manager/generate-task-files.js', 28 | () => ({ 29 | default: mockGenerateTaskFiles 30 | }) 31 | ); 32 | 33 | const addSubtask = ( 34 | await import('../../../../../scripts/modules/task-manager/add-subtask.js') 35 | ).default; 36 | 37 | describe('addSubtask function', () => { 38 | const multiTagData = { 39 | master: { 40 | tasks: [{ id: 1, title: 'Master Task', subtasks: [] }], 41 | metadata: { description: 'Master tasks' } 42 | }, 43 | 'feature-branch': { 44 | tasks: [{ id: 1, title: 'Feature Task', subtasks: [] }], 45 | metadata: { description: 'Feature tasks' } 46 | } 47 | }; 48 | 49 | beforeEach(() => { 50 | jest.clearAllMocks(); 51 | mockTaskManager.isTaskDependentOn.mockReturnValue(false); 52 | }); 53 | 54 | test('should add a new subtask and preserve other tags', async () => { 55 | const context = { projectRoot: '/fake/root', tag: 'feature-branch' }; 56 | const newSubtaskData = { title: 'My New Subtask' }; 57 | mockUtils.readJSON.mockReturnValueOnce({ 58 | tasks: [{ id: 1, title: 'Feature Task', subtasks: [] }], 59 | metadata: { description: 'Feature tasks' } 60 | }); 61 | 62 | await addSubtask('tasks.json', '1', null, newSubtaskData, true, context); 63 | 64 | expect(mockUtils.writeJSON).toHaveBeenCalledWith( 65 | 'tasks.json', 66 | expect.any(Object), 67 | '/fake/root', 68 | 'feature-branch' 69 | ); 70 | const writtenData = mockUtils.writeJSON.mock.calls[0][1]; 71 | const parentTask = writtenData.tasks.find((t) => t.id === 1); 72 | expect(parentTask.subtasks).toHaveLength(1); 73 | expect(parentTask.subtasks[0].title).toBe('My New Subtask'); 74 | }); 75 | 76 | test('should add a new subtask to a parent task', async () => { 77 | mockUtils.readJSON.mockReturnValueOnce({ 78 | tasks: [{ id: 1, title: 'Parent Task', subtasks: [] }] 79 | }); 80 | const context = {}; 81 | const newSubtask = await addSubtask( 82 | 'tasks.json', 83 | '1', 84 | null, 85 | { title: 'New Subtask' }, 86 | true, 87 | context 88 | ); 89 | expect(newSubtask).toBeDefined(); 90 | expect(newSubtask.id).toBe(1); 91 | expect(newSubtask.parentTaskId).toBe(1); 92 | expect(mockUtils.writeJSON).toHaveBeenCalled(); 93 | const writeCallArgs = mockUtils.writeJSON.mock.calls[0][1]; // data is the second arg now 94 | const parentTask = writeCallArgs.tasks.find((t) => t.id === 1); 95 | expect(parentTask.subtasks).toHaveLength(1); 96 | expect(parentTask.subtasks[0].title).toBe('New Subtask'); 97 | expect(mockGenerateTaskFiles).toHaveBeenCalled(); 98 | }); 99 | 100 | test('should convert an existing task to a subtask', async () => { 101 | mockUtils.readJSON.mockReturnValueOnce({ 102 | tasks: [ 103 | { id: 1, title: 'Parent Task', subtasks: [] }, 104 | { id: 2, title: 'Existing Task 2', subtasks: [] } 105 | ] 106 | }); 107 | const context = {}; 108 | const convertedSubtask = await addSubtask( 109 | 'tasks.json', 110 | '1', 111 | '2', 112 | null, 113 | true, 114 | context 115 | ); 116 | expect(convertedSubtask.id).toBe(1); 117 | expect(convertedSubtask.parentTaskId).toBe(1); 118 | expect(convertedSubtask.title).toBe('Existing Task 2'); 119 | expect(mockUtils.writeJSON).toHaveBeenCalled(); 120 | const writeCallArgs = mockUtils.writeJSON.mock.calls[0][1]; 121 | const parentTask = writeCallArgs.tasks.find((t) => t.id === 1); 122 | expect(parentTask.subtasks).toHaveLength(1); 123 | expect(parentTask.subtasks[0].title).toBe('Existing Task 2'); 124 | }); 125 | 126 | test('should throw an error if parent task does not exist', async () => { 127 | mockUtils.readJSON.mockReturnValueOnce({ 128 | tasks: [{ id: 1, title: 'Task 1', subtasks: [] }] 129 | }); 130 | const context = {}; 131 | await expect( 132 | addSubtask( 133 | 'tasks.json', 134 | '99', 135 | null, 136 | { title: 'New Subtask' }, 137 | true, 138 | context 139 | ) 140 | ).rejects.toThrow('Parent task with ID 99 not found'); 141 | }); 142 | 143 | test('should throw an error if trying to convert a non-existent task', async () => { 144 | mockUtils.readJSON.mockReturnValueOnce({ 145 | tasks: [{ id: 1, title: 'Parent Task', subtasks: [] }] 146 | }); 147 | const context = {}; 148 | await expect( 149 | addSubtask('tasks.json', '1', '99', null, true, context) 150 | ).rejects.toThrow('Task with ID 99 not found'); 151 | }); 152 | 153 | test('should throw an error for circular dependency', async () => { 154 | mockUtils.readJSON.mockReturnValueOnce({ 155 | tasks: [ 156 | { id: 1, title: 'Parent Task', subtasks: [] }, 157 | { id: 2, title: 'Child Task', subtasks: [] } 158 | ] 159 | }); 160 | mockTaskManager.isTaskDependentOn.mockImplementation( 161 | (tasks, parentTask, existingTaskIdNum) => { 162 | return parentTask.id === 1 && existingTaskIdNum === 2; 163 | } 164 | ); 165 | const context = {}; 166 | await expect( 167 | addSubtask('tasks.json', '1', '2', null, true, context) 168 | ).rejects.toThrow( 169 | 'Cannot create circular dependency: task 1 is already a subtask or dependent of task 2' 170 | ); 171 | }); 172 | }); 173 | ``` -------------------------------------------------------------------------------- /packages/tm-core/src/logger/logger.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Core logger implementation 3 | */ 4 | 5 | import chalk from 'chalk'; 6 | 7 | export enum LogLevel { 8 | SILENT = 0, 9 | ERROR = 1, 10 | WARN = 2, 11 | INFO = 3, 12 | DEBUG = 4 13 | } 14 | 15 | export interface LoggerConfig { 16 | level?: LogLevel; 17 | silent?: boolean; 18 | prefix?: string; 19 | timestamp?: boolean; 20 | colors?: boolean; 21 | // MCP mode silences all output 22 | mcpMode?: boolean; 23 | } 24 | 25 | export class Logger { 26 | private config: Required<LoggerConfig>; 27 | private static readonly DEFAULT_CONFIG: Required<LoggerConfig> = { 28 | level: LogLevel.WARN, 29 | silent: false, 30 | prefix: '', 31 | timestamp: false, 32 | colors: true, 33 | mcpMode: false 34 | }; 35 | 36 | constructor(config: LoggerConfig = {}) { 37 | // Check environment variables 38 | const envConfig: LoggerConfig = {}; 39 | 40 | // Check for MCP mode 41 | if ( 42 | process.env.MCP_MODE === 'true' || 43 | process.env.TASK_MASTER_MCP === 'true' 44 | ) { 45 | envConfig.mcpMode = true; 46 | } 47 | 48 | // Check for silent mode 49 | if ( 50 | process.env.TASK_MASTER_SILENT === 'true' || 51 | process.env.TM_SILENT === 'true' 52 | ) { 53 | envConfig.silent = true; 54 | } 55 | 56 | // Check for log level 57 | if (process.env.TASK_MASTER_LOG_LEVEL || process.env.TM_LOG_LEVEL) { 58 | const levelStr = ( 59 | process.env.TASK_MASTER_LOG_LEVEL || 60 | process.env.TM_LOG_LEVEL || 61 | '' 62 | ).toUpperCase(); 63 | if (levelStr in LogLevel) { 64 | envConfig.level = LogLevel[levelStr as keyof typeof LogLevel]; 65 | } 66 | } 67 | 68 | // Check for no colors 69 | if ( 70 | process.env.NO_COLOR === 'true' || 71 | process.env.TASK_MASTER_NO_COLOR === 'true' 72 | ) { 73 | envConfig.colors = false; 74 | } 75 | 76 | // Merge configs: defaults < constructor < environment 77 | this.config = { 78 | ...Logger.DEFAULT_CONFIG, 79 | ...config, 80 | ...envConfig 81 | }; 82 | 83 | // MCP mode overrides everything to be silent 84 | if (this.config.mcpMode) { 85 | this.config.silent = true; 86 | } 87 | } 88 | 89 | /** 90 | * Check if logging is enabled for a given level 91 | */ 92 | private shouldLog(level: LogLevel): boolean { 93 | if (this.config.silent || this.config.mcpMode) { 94 | return false; 95 | } 96 | return level <= this.config.level; 97 | } 98 | 99 | /** 100 | * Format a log message 101 | */ 102 | private formatMessage( 103 | level: LogLevel, 104 | message: string, 105 | ...args: any[] 106 | ): string { 107 | let formatted = ''; 108 | 109 | // Add timestamp if enabled 110 | if (this.config.timestamp) { 111 | const timestamp = new Date().toISOString(); 112 | formatted += this.config.colors 113 | ? chalk.gray(`[${timestamp}] `) 114 | : `[${timestamp}] `; 115 | } 116 | 117 | // Add prefix if configured 118 | if (this.config.prefix) { 119 | formatted += this.config.colors 120 | ? chalk.cyan(`[${this.config.prefix}] `) 121 | : `[${this.config.prefix}] `; 122 | } 123 | 124 | // Skip level indicator for cleaner output 125 | // We can still color the message based on level 126 | if (this.config.colors) { 127 | switch (level) { 128 | case LogLevel.ERROR: 129 | message = chalk.red(message); 130 | break; 131 | case LogLevel.WARN: 132 | message = chalk.yellow(message); 133 | break; 134 | case LogLevel.INFO: 135 | // Info stays default color 136 | break; 137 | case LogLevel.DEBUG: 138 | message = chalk.gray(message); 139 | break; 140 | } 141 | } 142 | 143 | // Add the message 144 | formatted += message; 145 | 146 | // Add any additional arguments 147 | if (args.length > 0) { 148 | formatted += 149 | ' ' + 150 | args 151 | .map((arg) => 152 | typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) 153 | ) 154 | .join(' '); 155 | } 156 | 157 | return formatted; 158 | } 159 | 160 | /** 161 | * Log an error message 162 | */ 163 | error(message: string, ...args: any[]): void { 164 | if (!this.shouldLog(LogLevel.ERROR)) return; 165 | console.error(this.formatMessage(LogLevel.ERROR, message, ...args)); 166 | } 167 | 168 | /** 169 | * Log a warning message 170 | */ 171 | warn(message: string, ...args: any[]): void { 172 | if (!this.shouldLog(LogLevel.WARN)) return; 173 | console.warn(this.formatMessage(LogLevel.WARN, message, ...args)); 174 | } 175 | 176 | /** 177 | * Log an info message 178 | */ 179 | info(message: string, ...args: any[]): void { 180 | if (!this.shouldLog(LogLevel.INFO)) return; 181 | console.log(this.formatMessage(LogLevel.INFO, message, ...args)); 182 | } 183 | 184 | /** 185 | * Log a debug message 186 | */ 187 | debug(message: string, ...args: any[]): void { 188 | if (!this.shouldLog(LogLevel.DEBUG)) return; 189 | console.log(this.formatMessage(LogLevel.DEBUG, message, ...args)); 190 | } 191 | 192 | /** 193 | * Log a message without any formatting (raw output) 194 | * Useful for CLI output that should appear as-is 195 | */ 196 | log(message: string, ...args: any[]): void { 197 | if (this.config.silent || this.config.mcpMode) return; 198 | 199 | if (args.length > 0) { 200 | console.log(message, ...args); 201 | } else { 202 | console.log(message); 203 | } 204 | } 205 | 206 | /** 207 | * Update logger configuration 208 | */ 209 | setConfig(config: Partial<LoggerConfig>): void { 210 | this.config = { 211 | ...this.config, 212 | ...config 213 | }; 214 | 215 | // MCP mode always overrides to silent 216 | if (this.config.mcpMode) { 217 | this.config.silent = true; 218 | } 219 | } 220 | 221 | /** 222 | * Get current configuration 223 | */ 224 | getConfig(): Readonly<Required<LoggerConfig>> { 225 | return { ...this.config }; 226 | } 227 | 228 | /** 229 | * Create a child logger with a prefix 230 | */ 231 | child(prefix: string, config?: Partial<LoggerConfig>): Logger { 232 | const childPrefix = this.config.prefix 233 | ? `${this.config.prefix}:${prefix}` 234 | : prefix; 235 | 236 | return new Logger({ 237 | ...this.config, 238 | ...config, 239 | prefix: childPrefix 240 | }); 241 | } 242 | } 243 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/add-tag.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * add-tag.js 3 | * Direct function implementation for creating a new tag 4 | */ 5 | 6 | import { 7 | createTag, 8 | createTagFromBranch 9 | } from '../../../../scripts/modules/task-manager/tag-management.js'; 10 | import { 11 | enableSilentMode, 12 | disableSilentMode 13 | } from '../../../../scripts/modules/utils.js'; 14 | import { createLogWrapper } from '../../tools/utils.js'; 15 | 16 | /** 17 | * Direct function wrapper for creating a new tag with error handling. 18 | * 19 | * @param {Object} args - Command arguments 20 | * @param {string} args.name - Name of the new tag to create 21 | * @param {boolean} [args.copyFromCurrent=false] - Whether to copy tasks from current tag 22 | * @param {string} [args.copyFromTag] - Specific tag to copy tasks from 23 | * @param {boolean} [args.fromBranch=false] - Create tag name from current git branch 24 | * @param {string} [args.description] - Optional description for the tag 25 | * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) 26 | * @param {string} [args.projectRoot] - Project root path 27 | * @param {Object} log - Logger object 28 | * @param {Object} context - Additional context (session) 29 | * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } } 30 | */ 31 | export async function addTagDirect(args, log, context = {}) { 32 | // Destructure expected args 33 | const { 34 | tasksJsonPath, 35 | name, 36 | copyFromCurrent = false, 37 | copyFromTag, 38 | fromBranch = false, 39 | description, 40 | projectRoot 41 | } = args; 42 | const { session } = context; 43 | 44 | // Enable silent mode to prevent console logs from interfering with JSON response 45 | enableSilentMode(); 46 | 47 | // Create logger wrapper using the utility 48 | const mcpLog = createLogWrapper(log); 49 | 50 | try { 51 | // Check if tasksJsonPath was provided 52 | if (!tasksJsonPath) { 53 | log.error('addTagDirect called without tasksJsonPath'); 54 | disableSilentMode(); 55 | return { 56 | success: false, 57 | error: { 58 | code: 'MISSING_ARGUMENT', 59 | message: 'tasksJsonPath is required' 60 | } 61 | }; 62 | } 63 | 64 | // Handle --from-branch option 65 | if (fromBranch) { 66 | log.info('Creating tag from current git branch'); 67 | 68 | // Import git utilities 69 | const gitUtils = await import( 70 | '../../../../scripts/modules/utils/git-utils.js' 71 | ); 72 | 73 | // Check if we're in a git repository 74 | if (!(await gitUtils.isGitRepository(projectRoot))) { 75 | log.error('Not in a git repository'); 76 | disableSilentMode(); 77 | return { 78 | success: false, 79 | error: { 80 | code: 'NOT_GIT_REPO', 81 | message: 'Not in a git repository. Cannot use fromBranch option.' 82 | } 83 | }; 84 | } 85 | 86 | // Get current git branch 87 | const currentBranch = await gitUtils.getCurrentBranch(projectRoot); 88 | if (!currentBranch) { 89 | log.error('Could not determine current git branch'); 90 | disableSilentMode(); 91 | return { 92 | success: false, 93 | error: { 94 | code: 'NO_CURRENT_BRANCH', 95 | message: 'Could not determine current git branch.' 96 | } 97 | }; 98 | } 99 | 100 | // Prepare options for branch-based tag creation 101 | const branchOptions = { 102 | copyFromCurrent, 103 | copyFromTag, 104 | description: 105 | description || `Tag created from git branch "${currentBranch}"` 106 | }; 107 | 108 | // Call the createTagFromBranch function 109 | const result = await createTagFromBranch( 110 | tasksJsonPath, 111 | currentBranch, 112 | branchOptions, 113 | { 114 | session, 115 | mcpLog, 116 | projectRoot 117 | }, 118 | 'json' // outputFormat - use 'json' to suppress CLI UI 119 | ); 120 | 121 | // Restore normal logging 122 | disableSilentMode(); 123 | 124 | return { 125 | success: true, 126 | data: { 127 | branchName: result.branchName, 128 | tagName: result.tagName, 129 | created: result.created, 130 | mappingUpdated: result.mappingUpdated, 131 | message: `Successfully created tag "${result.tagName}" from git branch "${result.branchName}"` 132 | } 133 | }; 134 | } else { 135 | // Check required parameters for regular tag creation 136 | if (!name || typeof name !== 'string') { 137 | log.error('Missing required parameter: name'); 138 | disableSilentMode(); 139 | return { 140 | success: false, 141 | error: { 142 | code: 'MISSING_PARAMETER', 143 | message: 'Tag name is required and must be a string' 144 | } 145 | }; 146 | } 147 | 148 | log.info(`Creating new tag: ${name}`); 149 | 150 | // Prepare options 151 | const options = { 152 | copyFromCurrent, 153 | copyFromTag, 154 | description 155 | }; 156 | 157 | // Call the createTag function 158 | const result = await createTag( 159 | tasksJsonPath, 160 | name, 161 | options, 162 | { 163 | session, 164 | mcpLog, 165 | projectRoot 166 | }, 167 | 'json' // outputFormat - use 'json' to suppress CLI UI 168 | ); 169 | 170 | // Restore normal logging 171 | disableSilentMode(); 172 | 173 | return { 174 | success: true, 175 | data: { 176 | tagName: result.tagName, 177 | created: result.created, 178 | tasksCopied: result.tasksCopied, 179 | sourceTag: result.sourceTag, 180 | description: result.description, 181 | message: `Successfully created tag "${result.tagName}"` 182 | } 183 | }; 184 | } 185 | } catch (error) { 186 | // Make sure to restore normal logging even if there's an error 187 | disableSilentMode(); 188 | 189 | log.error(`Error in addTagDirect: ${error.message}`); 190 | return { 191 | success: false, 192 | error: { 193 | code: error.code || 'ADD_TAG_ERROR', 194 | message: error.message 195 | } 196 | }; 197 | } 198 | } 199 | ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/components/EmptyState.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from 'react'; 2 | import { ExternalLink, Terminal, MessageSquare, Plus } from 'lucide-react'; 3 | import { TaskMasterLogo } from '../../components/TaskMasterLogo'; 4 | 5 | interface EmptyStateProps { 6 | currentTag: string; 7 | } 8 | 9 | export const EmptyState: React.FC<EmptyStateProps> = ({ currentTag }) => { 10 | return ( 11 | <div className="flex items-center justify-center h-full overflow-auto"> 12 | <div className="max-w-2xl mx-auto text-center p-8"> 13 | {/* Empty state illustration */} 14 | <div className="mb-8 max-w-96 mx-auto"> 15 | <TaskMasterLogo className="w-32 h-32 mx-auto text-vscode-foreground/20" /> 16 | </div> 17 | 18 | <h2 className="text-2xl font-semibold mb-2 text-vscode-foreground"> 19 | No tasks in "{currentTag}" tag 20 | </h2> 21 | <p className="text-vscode-foreground/70 mb-8"> 22 | Get started by adding tasks to this tag using the commands below 23 | </p> 24 | 25 | {/* Command suggestions */} 26 | <div className="space-y-4 text-left"> 27 | <div className="bg-vscode-editor-background/50 border border-vscode-panel-border rounded-lg p-4"> 28 | <div className="flex items-center gap-2 mb-2"> 29 | <Terminal className="w-4 h-4 text-vscode-terminal-ansiGreen" /> 30 | <h3 className="font-medium">CLI Commands</h3> 31 | </div> 32 | <div className="space-y-2"> 33 | <div className="bg-vscode-editor-background rounded p-2 font-mono text-sm"> 34 | <span className="text-vscode-terminal-ansiYellow"> 35 | task-master 36 | </span>{' '} 37 | <span className="text-vscode-terminal-ansiCyan">parse-prd</span>{' '} 38 | <span className="text-vscode-foreground/70"> 39 | <path-to-prd> 40 | </span>{' '} 41 | <span className="text-vscode-terminal-ansiMagenta"> 42 | --append 43 | </span> 44 | <div className="text-xs text-vscode-foreground/50 mt-1"> 45 | Parse a PRD and append tasks to current tag 46 | </div> 47 | </div> 48 | <div className="bg-vscode-editor-background rounded p-2 font-mono text-sm"> 49 | <span className="text-vscode-terminal-ansiYellow"> 50 | task-master 51 | </span>{' '} 52 | <span className="text-vscode-terminal-ansiCyan">add-task</span>{' '} 53 | <span className="text-vscode-terminal-ansiMagenta"> 54 | --prompt 55 | </span>{' '} 56 | <span className="text-vscode-foreground/70"> 57 | "Your task description" 58 | </span> 59 | <div className="text-xs text-vscode-foreground/50 mt-1"> 60 | Add a single task with AI assistance 61 | </div> 62 | </div> 63 | <div className="bg-vscode-editor-background rounded p-2 font-mono text-sm"> 64 | <span className="text-vscode-terminal-ansiYellow"> 65 | task-master 66 | </span>{' '} 67 | <span className="text-vscode-terminal-ansiCyan">add-task</span>{' '} 68 | <span className="text-vscode-terminal-ansiMagenta">--help</span> 69 | <div className="text-xs text-vscode-foreground/50 mt-1"> 70 | View all options for adding tasks 71 | </div> 72 | </div> 73 | </div> 74 | </div> 75 | 76 | <div className="bg-vscode-editor-background/50 border border-vscode-panel-border rounded-lg p-4"> 77 | <div className="flex items-center gap-2 mb-2"> 78 | <MessageSquare className="w-4 h-4 text-vscode-textLink-foreground" /> 79 | <h3 className="font-medium">MCP Examples</h3> 80 | </div> 81 | <div className="space-y-2 text-sm"> 82 | <div className="flex items-start gap-2"> 83 | <Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" /> 84 | <div> 85 | <div className="text-vscode-foreground"> 86 | "Add a task to tag {currentTag}: Implement user 87 | authentication" 88 | </div> 89 | </div> 90 | </div> 91 | <div className="flex items-start gap-2"> 92 | <Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" /> 93 | <div> 94 | <div className="text-vscode-foreground"> 95 | "Parse this PRD and add tasks to {currentTag}: [paste PRD 96 | content]" 97 | </div> 98 | </div> 99 | </div> 100 | <div className="flex items-start gap-2"> 101 | <Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" /> 102 | <div> 103 | <div className="text-vscode-foreground"> 104 | "Create 5 tasks for building a REST API in tag {currentTag}" 105 | </div> 106 | </div> 107 | </div> 108 | </div> 109 | </div> 110 | 111 | {/* Documentation link */} 112 | <div className="flex justify-center pt-4"> 113 | <a 114 | href="https://docs.task-master.dev" 115 | className="inline-flex items-center gap-2 text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground transition-colors" 116 | onClick={(e) => { 117 | e.preventDefault(); 118 | // Use VS Code API to open external link 119 | if (window.acquireVsCodeApi) { 120 | const vscode = window.acquireVsCodeApi(); 121 | vscode.postMessage({ 122 | type: 'openExternal', 123 | url: 'https://docs.task-master.dev' 124 | }); 125 | } 126 | }} 127 | > 128 | <ExternalLink className="w-4 h-4" /> 129 | <span className="text-sm font-medium"> 130 | View TaskMaster Documentation 131 | </span> 132 | </a> 133 | </div> 134 | </div> 135 | </div> 136 | </div> 137 | ); 138 | }; 139 | ``` -------------------------------------------------------------------------------- /tests/unit/profiles/roo-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 | // Mock external modules 7 | jest.mock('child_process', () => ({ 8 | execSync: jest.fn() 9 | })); 10 | 11 | // Mock console methods 12 | jest.mock('console', () => ({ 13 | log: jest.fn(), 14 | info: jest.fn(), 15 | warn: jest.fn(), 16 | error: jest.fn(), 17 | clear: jest.fn() 18 | })); 19 | 20 | describe('Roo Integration', () => { 21 | let tempDir; 22 | 23 | beforeEach(() => { 24 | jest.clearAllMocks(); 25 | 26 | // Create a temporary directory for testing 27 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); 28 | 29 | // Spy on fs methods 30 | jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); 31 | jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { 32 | if (filePath.toString().includes('.roomodes')) { 33 | return 'Existing roomodes content'; 34 | } 35 | if (filePath.toString().includes('-rules')) { 36 | return 'Existing mode rules content'; 37 | } 38 | return '{}'; 39 | }); 40 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false); 41 | jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); 42 | }); 43 | 44 | afterEach(() => { 45 | // Clean up the temporary directory 46 | try { 47 | fs.rmSync(tempDir, { recursive: true, force: true }); 48 | } catch (err) { 49 | console.error(`Error cleaning up: ${err.message}`); 50 | } 51 | }); 52 | 53 | // Test function that simulates the createProjectStructure behavior for Roo files 54 | function mockCreateRooStructure() { 55 | // Create main .roo directory 56 | fs.mkdirSync(path.join(tempDir, '.roo'), { recursive: true }); 57 | 58 | // Create rules directory 59 | fs.mkdirSync(path.join(tempDir, '.roo', 'rules'), { recursive: true }); 60 | 61 | // Create mode-specific rule directories 62 | const rooModes = [ 63 | 'architect', 64 | 'ask', 65 | 'orchestrator', 66 | 'code', 67 | 'debug', 68 | 'test' 69 | ]; 70 | for (const mode of rooModes) { 71 | fs.mkdirSync(path.join(tempDir, '.roo', `rules-${mode}`), { 72 | recursive: true 73 | }); 74 | fs.writeFileSync( 75 | path.join(tempDir, '.roo', `rules-${mode}`, `${mode}-rules`), 76 | `Content for ${mode} rules` 77 | ); 78 | } 79 | 80 | // Create additional directories 81 | fs.mkdirSync(path.join(tempDir, '.roo', 'config'), { recursive: true }); 82 | fs.mkdirSync(path.join(tempDir, '.roo', 'templates'), { recursive: true }); 83 | fs.mkdirSync(path.join(tempDir, '.roo', 'logs'), { recursive: true }); 84 | 85 | // Copy .roomodes file 86 | fs.writeFileSync(path.join(tempDir, '.roomodes'), 'Roomodes file content'); 87 | } 88 | 89 | test('creates all required .roo directories', () => { 90 | // Act 91 | mockCreateRooStructure(); 92 | 93 | // Assert 94 | expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.roo'), { 95 | recursive: true 96 | }); 97 | expect(fs.mkdirSync).toHaveBeenCalledWith( 98 | path.join(tempDir, '.roo', 'rules'), 99 | { recursive: true } 100 | ); 101 | 102 | // Verify all mode directories are created 103 | expect(fs.mkdirSync).toHaveBeenCalledWith( 104 | path.join(tempDir, '.roo', 'rules-architect'), 105 | { recursive: true } 106 | ); 107 | expect(fs.mkdirSync).toHaveBeenCalledWith( 108 | path.join(tempDir, '.roo', 'rules-ask'), 109 | { recursive: true } 110 | ); 111 | expect(fs.mkdirSync).toHaveBeenCalledWith( 112 | path.join(tempDir, '.roo', 'rules-orchestrator'), 113 | { recursive: true } 114 | ); 115 | expect(fs.mkdirSync).toHaveBeenCalledWith( 116 | path.join(tempDir, '.roo', 'rules-code'), 117 | { recursive: true } 118 | ); 119 | expect(fs.mkdirSync).toHaveBeenCalledWith( 120 | path.join(tempDir, '.roo', 'rules-debug'), 121 | { recursive: true } 122 | ); 123 | expect(fs.mkdirSync).toHaveBeenCalledWith( 124 | path.join(tempDir, '.roo', 'rules-test'), 125 | { recursive: true } 126 | ); 127 | }); 128 | 129 | test('creates rule files for all modes', () => { 130 | // Act 131 | mockCreateRooStructure(); 132 | 133 | // Assert - check all rule files are created 134 | expect(fs.writeFileSync).toHaveBeenCalledWith( 135 | path.join(tempDir, '.roo', 'rules-architect', 'architect-rules'), 136 | expect.any(String) 137 | ); 138 | expect(fs.writeFileSync).toHaveBeenCalledWith( 139 | path.join(tempDir, '.roo', 'rules-ask', 'ask-rules'), 140 | expect.any(String) 141 | ); 142 | expect(fs.writeFileSync).toHaveBeenCalledWith( 143 | path.join(tempDir, '.roo', 'rules-orchestrator', 'orchestrator-rules'), 144 | expect.any(String) 145 | ); 146 | expect(fs.writeFileSync).toHaveBeenCalledWith( 147 | path.join(tempDir, '.roo', 'rules-code', 'code-rules'), 148 | expect.any(String) 149 | ); 150 | expect(fs.writeFileSync).toHaveBeenCalledWith( 151 | path.join(tempDir, '.roo', 'rules-debug', 'debug-rules'), 152 | expect.any(String) 153 | ); 154 | expect(fs.writeFileSync).toHaveBeenCalledWith( 155 | path.join(tempDir, '.roo', 'rules-test', 'test-rules'), 156 | expect.any(String) 157 | ); 158 | }); 159 | 160 | test('creates .roomodes file in project root', () => { 161 | // Act 162 | mockCreateRooStructure(); 163 | 164 | // Assert 165 | expect(fs.writeFileSync).toHaveBeenCalledWith( 166 | path.join(tempDir, '.roomodes'), 167 | expect.any(String) 168 | ); 169 | }); 170 | 171 | test('creates additional required Roo directories', () => { 172 | // Act 173 | mockCreateRooStructure(); 174 | 175 | // Assert 176 | expect(fs.mkdirSync).toHaveBeenCalledWith( 177 | path.join(tempDir, '.roo', 'config'), 178 | { recursive: true } 179 | ); 180 | expect(fs.mkdirSync).toHaveBeenCalledWith( 181 | path.join(tempDir, '.roo', 'templates'), 182 | { recursive: true } 183 | ); 184 | expect(fs.mkdirSync).toHaveBeenCalledWith( 185 | path.join(tempDir, '.roo', 'logs'), 186 | { recursive: true } 187 | ); 188 | }); 189 | }); 190 | ``` -------------------------------------------------------------------------------- /apps/extension/src/components/TaskDetails/DetailsSection.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type React from 'react'; 2 | import { CollapsibleSection } from '@/components/ui/CollapsibleSection'; 3 | 4 | interface MarkdownRendererProps { 5 | content: string; 6 | className?: string; 7 | } 8 | 9 | const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ 10 | content, 11 | className = '' 12 | }) => { 13 | const parseMarkdown = (text: string) => { 14 | const parts = []; 15 | const lines = text.split('\n'); 16 | let currentBlock = []; 17 | let inCodeBlock = false; 18 | let codeLanguage = ''; 19 | 20 | for (let i = 0; i < lines.length; i++) { 21 | const line = lines[i]; 22 | 23 | if (line.startsWith('```')) { 24 | if (inCodeBlock) { 25 | if (currentBlock.length > 0) { 26 | parts.push({ 27 | type: 'code', 28 | content: currentBlock.join('\n'), 29 | language: codeLanguage 30 | }); 31 | currentBlock = []; 32 | } 33 | inCodeBlock = false; 34 | codeLanguage = ''; 35 | } else { 36 | if (currentBlock.length > 0) { 37 | parts.push({ 38 | type: 'text', 39 | content: currentBlock.join('\n') 40 | }); 41 | currentBlock = []; 42 | } 43 | inCodeBlock = true; 44 | codeLanguage = line.substring(3).trim(); 45 | } 46 | } else { 47 | currentBlock.push(line); 48 | } 49 | } 50 | 51 | if (currentBlock.length > 0) { 52 | parts.push({ 53 | type: inCodeBlock ? 'code' : 'text', 54 | content: currentBlock.join('\n'), 55 | language: codeLanguage 56 | }); 57 | } 58 | 59 | return parts; 60 | }; 61 | 62 | const parts = parseMarkdown(content); 63 | 64 | return ( 65 | <div className={className}> 66 | {parts.map((part, index) => { 67 | if (part.type === 'code') { 68 | return ( 69 | <pre 70 | key={index} 71 | className="bg-vscode-editor-background rounded-md p-4 overflow-x-auto mb-4 border border-vscode-editor-lineHighlightBorder" 72 | > 73 | <code className="text-sm text-vscode-editor-foreground font-mono"> 74 | {part.content} 75 | </code> 76 | </pre> 77 | ); 78 | } 79 | return ( 80 | <div key={index} className="whitespace-pre-wrap mb-4 last:mb-0"> 81 | {part.content.split('\n').map((line, lineIndex) => { 82 | const bulletMatch = line.match(/^(\s*)([-*])\s(.+)$/); 83 | if (bulletMatch) { 84 | const indent = bulletMatch[1].length; 85 | return ( 86 | <div 87 | key={lineIndex} 88 | className="flex gap-2 mb-1" 89 | style={{ paddingLeft: `${indent * 16}px` }} 90 | > 91 | <span className="text-vscode-foreground/60">•</span> 92 | <span className="flex-1">{bulletMatch[3]}</span> 93 | </div> 94 | ); 95 | } 96 | 97 | const numberedMatch = line.match(/^(\s*)(\d+\.)\s(.+)$/); 98 | if (numberedMatch) { 99 | const indent = numberedMatch[1].length; 100 | return ( 101 | <div 102 | key={lineIndex} 103 | className="flex gap-2 mb-1" 104 | style={{ paddingLeft: `${indent * 16}px` }} 105 | > 106 | <span className="text-vscode-foreground/60 font-mono"> 107 | {numberedMatch[2]} 108 | </span> 109 | <span className="flex-1">{numberedMatch[3]}</span> 110 | </div> 111 | ); 112 | } 113 | 114 | const headingMatch = line.match(/^(#{1,6})\s(.+)$/); 115 | if (headingMatch) { 116 | const level = headingMatch[1].length; 117 | const headingLevel = Math.min(level + 2, 6); 118 | const headingClassName = 119 | 'font-semibold text-vscode-foreground mb-2 mt-4 first:mt-0'; 120 | 121 | switch (headingLevel) { 122 | case 3: 123 | return ( 124 | <h3 key={lineIndex} className={headingClassName}> 125 | {headingMatch[2]} 126 | </h3> 127 | ); 128 | case 4: 129 | return ( 130 | <h4 key={lineIndex} className={headingClassName}> 131 | {headingMatch[2]} 132 | </h4> 133 | ); 134 | case 5: 135 | return ( 136 | <h5 key={lineIndex} className={headingClassName}> 137 | {headingMatch[2]} 138 | </h5> 139 | ); 140 | case 6: 141 | return ( 142 | <h6 key={lineIndex} className={headingClassName}> 143 | {headingMatch[2]} 144 | </h6> 145 | ); 146 | default: 147 | return ( 148 | <h3 key={lineIndex} className={headingClassName}> 149 | {headingMatch[2]} 150 | </h3> 151 | ); 152 | } 153 | } 154 | 155 | if (line.trim() === '') { 156 | return <div key={lineIndex} className="h-2" />; 157 | } 158 | 159 | return ( 160 | <div key={lineIndex} className="mb-2 last:mb-0"> 161 | {line} 162 | </div> 163 | ); 164 | })} 165 | </div> 166 | ); 167 | })} 168 | </div> 169 | ); 170 | }; 171 | 172 | interface DetailsSectionProps { 173 | title: string; 174 | content?: string; 175 | error?: string | null; 176 | emptyMessage?: string; 177 | defaultExpanded?: boolean; 178 | } 179 | 180 | export const DetailsSection: React.FC<DetailsSectionProps> = ({ 181 | title, 182 | content, 183 | error, 184 | emptyMessage = 'No details available', 185 | defaultExpanded = false 186 | }) => { 187 | return ( 188 | <CollapsibleSection title={title} defaultExpanded={defaultExpanded}> 189 | <div className={title.toLowerCase().replace(/\s+/g, '-') + '-content'}> 190 | {error ? ( 191 | <div className="text-sm text-red-400 py-2"> 192 | Error loading {title.toLowerCase()}: {error} 193 | </div> 194 | ) : content !== undefined && content !== '' ? ( 195 | <MarkdownRenderer content={content} /> 196 | ) : ( 197 | <div className="text-sm text-vscode-foreground/50 py-2"> 198 | {emptyMessage} 199 | </div> 200 | )} 201 | </div> 202 | </CollapsibleSection> 203 | ); 204 | }; 205 | ``` -------------------------------------------------------------------------------- /src/utils/timeout-manager.js: -------------------------------------------------------------------------------- ```javascript 1 | import { StreamingError, STREAMING_ERROR_CODES } from './stream-parser.js'; 2 | 3 | /** 4 | * Utility class for managing timeouts in async operations 5 | * Reduces code duplication for timeout handling patterns 6 | */ 7 | export class TimeoutManager { 8 | /** 9 | * Wraps a promise with a timeout that will reject if not resolved in time 10 | * 11 | * @param {Promise} promise - The promise to wrap with timeout 12 | * @param {number} timeoutMs - Timeout duration in milliseconds 13 | * @param {string} operationName - Name of the operation for error messages 14 | * @returns {Promise} The result of the promise or throws timeout error 15 | * 16 | * @example 17 | * const result = await TimeoutManager.withTimeout( 18 | * fetchData(), 19 | * 5000, 20 | * 'Data fetch operation' 21 | * ); 22 | */ 23 | static async withTimeout(promise, timeoutMs, operationName = 'Operation') { 24 | let timeoutHandle; 25 | 26 | const timeoutPromise = new Promise((_, reject) => { 27 | timeoutHandle = setTimeout(() => { 28 | reject( 29 | new StreamingError( 30 | `${operationName} timed out after ${timeoutMs / 1000} seconds`, 31 | STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED 32 | ) 33 | ); 34 | }, timeoutMs); 35 | }); 36 | 37 | try { 38 | // Race between the actual promise and the timeout 39 | const result = await Promise.race([promise, timeoutPromise]); 40 | // Clear timeout if promise resolved first 41 | clearTimeout(timeoutHandle); 42 | return result; 43 | } catch (error) { 44 | // Always clear timeout on error 45 | clearTimeout(timeoutHandle); 46 | throw error; 47 | } 48 | } 49 | 50 | /** 51 | * Wraps a promise with a timeout, but returns undefined instead of throwing on timeout 52 | * Useful for optional operations that shouldn't fail the main flow 53 | * 54 | * @param {Promise} promise - The promise to wrap with timeout 55 | * @param {number} timeoutMs - Timeout duration in milliseconds 56 | * @param {*} defaultValue - Value to return on timeout (default: undefined) 57 | * @returns {Promise} The result of the promise or defaultValue on timeout 58 | * 59 | * @example 60 | * const usage = await TimeoutManager.withSoftTimeout( 61 | * getUsageStats(), 62 | * 1000, 63 | * { tokens: 0 } 64 | * ); 65 | */ 66 | static async withSoftTimeout(promise, timeoutMs, defaultValue = undefined) { 67 | let timeoutHandle; 68 | 69 | const timeoutPromise = new Promise((resolve) => { 70 | timeoutHandle = setTimeout(() => { 71 | resolve(defaultValue); 72 | }, timeoutMs); 73 | }); 74 | 75 | try { 76 | const result = await Promise.race([promise, timeoutPromise]); 77 | clearTimeout(timeoutHandle); 78 | return result; 79 | } catch (error) { 80 | // On error, clear timeout and return default value 81 | clearTimeout(timeoutHandle); 82 | return defaultValue; 83 | } 84 | } 85 | 86 | /** 87 | * Creates a reusable timeout controller for multiple operations 88 | * Useful when you need to apply the same timeout to multiple promises 89 | * 90 | * @param {number} timeoutMs - Timeout duration in milliseconds 91 | * @param {string} operationName - Base name for operations 92 | * @returns {Object} Controller with wrap method 93 | * 94 | * @example 95 | * const controller = TimeoutManager.createController(60000, 'AI Service'); 96 | * const result1 = await controller.wrap(service.call1(), 'call 1'); 97 | * const result2 = await controller.wrap(service.call2(), 'call 2'); 98 | */ 99 | static createController(timeoutMs, operationName = 'Operation') { 100 | return { 101 | timeoutMs, 102 | operationName, 103 | 104 | async wrap(promise, specificName = null) { 105 | const fullName = specificName 106 | ? `${operationName} - ${specificName}` 107 | : operationName; 108 | return TimeoutManager.withTimeout(promise, timeoutMs, fullName); 109 | }, 110 | 111 | async wrapSoft(promise, defaultValue = undefined) { 112 | return TimeoutManager.withSoftTimeout(promise, timeoutMs, defaultValue); 113 | } 114 | }; 115 | } 116 | 117 | /** 118 | * Checks if an error is a timeout error from this manager 119 | * 120 | * @param {Error} error - The error to check 121 | * @returns {boolean} True if this is a timeout error 122 | */ 123 | static isTimeoutError(error) { 124 | return ( 125 | error instanceof StreamingError && 126 | error.code === STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED && 127 | error.message.includes('timed out') 128 | ); 129 | } 130 | } 131 | 132 | /** 133 | * Duration helper class for more readable timeout specifications 134 | */ 135 | export class Duration { 136 | constructor(value, unit = 'ms') { 137 | this.milliseconds = this._toMilliseconds(value, unit); 138 | } 139 | 140 | static milliseconds(value) { 141 | return new Duration(value, 'ms'); 142 | } 143 | 144 | static seconds(value) { 145 | return new Duration(value, 's'); 146 | } 147 | 148 | static minutes(value) { 149 | return new Duration(value, 'm'); 150 | } 151 | 152 | static hours(value) { 153 | return new Duration(value, 'h'); 154 | } 155 | 156 | get seconds() { 157 | return this.milliseconds / 1000; 158 | } 159 | 160 | get minutes() { 161 | return this.milliseconds / 60000; 162 | } 163 | 164 | get hours() { 165 | return this.milliseconds / 3600000; 166 | } 167 | 168 | toString() { 169 | if (this.milliseconds < 1000) { 170 | return `${this.milliseconds}ms`; 171 | } else if (this.milliseconds < 60000) { 172 | return `${this.seconds}s`; 173 | } else if (this.milliseconds < 3600000) { 174 | return `${Math.floor(this.minutes)}m ${Math.floor(this.seconds % 60)}s`; 175 | } else { 176 | return `${Math.floor(this.hours)}h ${Math.floor(this.minutes % 60)}m`; 177 | } 178 | } 179 | 180 | _toMilliseconds(value, unit) { 181 | const conversions = { 182 | ms: 1, 183 | s: 1000, 184 | m: 60000, 185 | h: 3600000 186 | }; 187 | return value * (conversions[unit] || 1); 188 | } 189 | } 190 | ``` -------------------------------------------------------------------------------- /tests/unit/profiles/kilo-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 | // Mock external modules 7 | jest.mock('child_process', () => ({ 8 | execSync: jest.fn() 9 | })); 10 | 11 | // Mock console methods 12 | jest.mock('console', () => ({ 13 | log: jest.fn(), 14 | info: jest.fn(), 15 | warn: jest.fn(), 16 | error: jest.fn(), 17 | clear: jest.fn() 18 | })); 19 | 20 | describe('Kilo Integration', () => { 21 | let tempDir; 22 | 23 | beforeEach(() => { 24 | jest.clearAllMocks(); 25 | 26 | // Create a temporary directory for testing 27 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); 28 | 29 | // Spy on fs methods 30 | jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); 31 | jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { 32 | if (filePath.toString().includes('.kilocodemodes')) { 33 | return 'Existing kilocodemodes content'; 34 | } 35 | if (filePath.toString().includes('-rules')) { 36 | return 'Existing mode rules content'; 37 | } 38 | return '{}'; 39 | }); 40 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false); 41 | jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); 42 | }); 43 | 44 | afterEach(() => { 45 | // Clean up the temporary directory 46 | try { 47 | fs.rmSync(tempDir, { recursive: true, force: true }); 48 | } catch (err) { 49 | console.error(`Error cleaning up: ${err.message}`); 50 | } 51 | }); 52 | 53 | // Test function that simulates the createProjectStructure behavior for Kilo files 54 | function mockCreateKiloStructure() { 55 | // Create main .kilo directory 56 | fs.mkdirSync(path.join(tempDir, '.kilo'), { recursive: true }); 57 | 58 | // Create rules directory 59 | fs.mkdirSync(path.join(tempDir, '.kilo', 'rules'), { recursive: true }); 60 | 61 | // Create mode-specific rule directories 62 | const kiloModes = [ 63 | 'architect', 64 | 'ask', 65 | 'orchestrator', 66 | 'code', 67 | 'debug', 68 | 'test' 69 | ]; 70 | for (const mode of kiloModes) { 71 | fs.mkdirSync(path.join(tempDir, '.kilo', `rules-${mode}`), { 72 | recursive: true 73 | }); 74 | fs.writeFileSync( 75 | path.join(tempDir, '.kilo', `rules-${mode}`, `${mode}-rules`), 76 | `Content for ${mode} rules` 77 | ); 78 | } 79 | 80 | // Create additional directories 81 | fs.mkdirSync(path.join(tempDir, '.kilo', 'config'), { recursive: true }); 82 | fs.mkdirSync(path.join(tempDir, '.kilo', 'templates'), { recursive: true }); 83 | fs.mkdirSync(path.join(tempDir, '.kilo', 'logs'), { recursive: true }); 84 | 85 | // Copy .kilocodemodes file 86 | fs.writeFileSync( 87 | path.join(tempDir, '.kilocodemodes'), 88 | 'Kilocodemodes file content' 89 | ); 90 | } 91 | 92 | test('creates all required .kilo directories', () => { 93 | // Act 94 | mockCreateKiloStructure(); 95 | 96 | // Assert 97 | expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.kilo'), { 98 | recursive: true 99 | }); 100 | expect(fs.mkdirSync).toHaveBeenCalledWith( 101 | path.join(tempDir, '.kilo', 'rules'), 102 | { recursive: true } 103 | ); 104 | 105 | // Verify all mode directories are created 106 | expect(fs.mkdirSync).toHaveBeenCalledWith( 107 | path.join(tempDir, '.kilo', 'rules-architect'), 108 | { recursive: true } 109 | ); 110 | expect(fs.mkdirSync).toHaveBeenCalledWith( 111 | path.join(tempDir, '.kilo', 'rules-ask'), 112 | { recursive: true } 113 | ); 114 | expect(fs.mkdirSync).toHaveBeenCalledWith( 115 | path.join(tempDir, '.kilo', 'rules-orchestrator'), 116 | { recursive: true } 117 | ); 118 | expect(fs.mkdirSync).toHaveBeenCalledWith( 119 | path.join(tempDir, '.kilo', 'rules-code'), 120 | { recursive: true } 121 | ); 122 | expect(fs.mkdirSync).toHaveBeenCalledWith( 123 | path.join(tempDir, '.kilo', 'rules-debug'), 124 | { recursive: true } 125 | ); 126 | expect(fs.mkdirSync).toHaveBeenCalledWith( 127 | path.join(tempDir, '.kilo', 'rules-test'), 128 | { recursive: true } 129 | ); 130 | }); 131 | 132 | test('creates rule files for all modes', () => { 133 | // Act 134 | mockCreateKiloStructure(); 135 | 136 | // Assert - check all rule files are created 137 | expect(fs.writeFileSync).toHaveBeenCalledWith( 138 | path.join(tempDir, '.kilo', 'rules-architect', 'architect-rules'), 139 | expect.any(String) 140 | ); 141 | expect(fs.writeFileSync).toHaveBeenCalledWith( 142 | path.join(tempDir, '.kilo', 'rules-ask', 'ask-rules'), 143 | expect.any(String) 144 | ); 145 | expect(fs.writeFileSync).toHaveBeenCalledWith( 146 | path.join(tempDir, '.kilo', 'rules-orchestrator', 'orchestrator-rules'), 147 | expect.any(String) 148 | ); 149 | expect(fs.writeFileSync).toHaveBeenCalledWith( 150 | path.join(tempDir, '.kilo', 'rules-code', 'code-rules'), 151 | expect.any(String) 152 | ); 153 | expect(fs.writeFileSync).toHaveBeenCalledWith( 154 | path.join(tempDir, '.kilo', 'rules-debug', 'debug-rules'), 155 | expect.any(String) 156 | ); 157 | expect(fs.writeFileSync).toHaveBeenCalledWith( 158 | path.join(tempDir, '.kilo', 'rules-test', 'test-rules'), 159 | expect.any(String) 160 | ); 161 | }); 162 | 163 | test('creates .kilocodemodes file in project root', () => { 164 | // Act 165 | mockCreateKiloStructure(); 166 | 167 | // Assert 168 | expect(fs.writeFileSync).toHaveBeenCalledWith( 169 | path.join(tempDir, '.kilocodemodes'), 170 | expect.any(String) 171 | ); 172 | }); 173 | 174 | test('creates additional required Kilo directories', () => { 175 | // Act 176 | mockCreateKiloStructure(); 177 | 178 | // Assert 179 | expect(fs.mkdirSync).toHaveBeenCalledWith( 180 | path.join(tempDir, '.kilo', 'config'), 181 | { recursive: true } 182 | ); 183 | expect(fs.mkdirSync).toHaveBeenCalledWith( 184 | path.join(tempDir, '.kilo', 'templates'), 185 | { recursive: true } 186 | ); 187 | expect(fs.mkdirSync).toHaveBeenCalledWith( 188 | path.join(tempDir, '.kilo', 'logs'), 189 | { recursive: true } 190 | ); 191 | }); 192 | }); 193 | ``` -------------------------------------------------------------------------------- /apps/extension/package.mjs: -------------------------------------------------------------------------------- ``` 1 | import { execSync } from 'child_process'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import fs from 'fs-extra'; 5 | 6 | // --- Configuration --- 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | 10 | const packageDir = path.resolve(__dirname, 'vsix-build'); 11 | // --- End Configuration --- 12 | 13 | try { 14 | console.log('🚀 Starting packaging process...'); 15 | 16 | // 1. Build Project 17 | console.log('\nBuilding JavaScript...'); 18 | execSync('npm run build:js', { stdio: 'inherit' }); 19 | console.log('\nBuilding CSS...'); 20 | execSync('npm run build:css', { stdio: 'inherit' }); 21 | 22 | // 2. Prepare Clean Directory 23 | console.log(`\nPreparing clean directory at: ${packageDir}`); 24 | fs.emptyDirSync(packageDir); 25 | 26 | // 3. Copy Build Artifacts (excluding source maps) 27 | console.log('Copying build artifacts...'); 28 | const distDir = path.resolve(__dirname, 'dist'); 29 | const targetDistDir = path.resolve(packageDir, 'dist'); 30 | fs.ensureDirSync(targetDistDir); 31 | 32 | // Only copy the files we need (exclude .map files) 33 | const filesToCopy = ['extension.js', 'index.js', 'index.css', 'sidebar.js']; 34 | for (const file of filesToCopy) { 35 | const srcFile = path.resolve(distDir, file); 36 | const destFile = path.resolve(targetDistDir, file); 37 | if (fs.existsSync(srcFile)) { 38 | fs.copySync(srcFile, destFile); 39 | console.log(` - Copied dist/${file}`); 40 | } 41 | } 42 | 43 | // 4. Copy additional files 44 | const additionalFiles = ['README.md', 'CHANGELOG.md', 'AGENTS.md']; 45 | for (const file of additionalFiles) { 46 | if (fs.existsSync(path.resolve(__dirname, file))) { 47 | fs.copySync( 48 | path.resolve(__dirname, file), 49 | path.resolve(packageDir, file) 50 | ); 51 | console.log(` - Copied ${file}`); 52 | } 53 | } 54 | 55 | // 5. Sync versions and prepare the final package.json 56 | console.log('Syncing versions and preparing the final package.json...'); 57 | 58 | // Read current versions 59 | const devPackagePath = path.resolve(__dirname, 'package.json'); 60 | const publishPackagePath = path.resolve(__dirname, 'package.publish.json'); 61 | 62 | const devPackage = JSON.parse(fs.readFileSync(devPackagePath, 'utf8')); 63 | const publishPackage = JSON.parse( 64 | fs.readFileSync(publishPackagePath, 'utf8') 65 | ); 66 | 67 | // Handle RC versions for VS Code Marketplace 68 | let finalVersion = devPackage.version; 69 | if (finalVersion.includes('-rc.')) { 70 | console.log( 71 | ' - Detected RC version, transforming for VS Code Marketplace...' 72 | ); 73 | 74 | // Extract base version and RC number 75 | const baseVersion = finalVersion.replace(/-rc\.\d+$/, ''); 76 | const rcMatch = finalVersion.match(/rc\.(\d+)/); 77 | const rcNumber = rcMatch ? parseInt(rcMatch[1]) : 0; 78 | 79 | // For each RC iteration, increment the patch version 80 | // This ensures unique versions in VS Code Marketplace 81 | if (rcNumber > 0) { 82 | const [major, minor, patch] = baseVersion.split('.').map(Number); 83 | finalVersion = `${major}.${minor}.${patch + rcNumber}`; 84 | console.log( 85 | ` - RC version mapping: ${devPackage.version} → ${finalVersion}` 86 | ); 87 | } else { 88 | finalVersion = baseVersion; 89 | console.log( 90 | ` - RC version mapping: ${devPackage.version} → ${finalVersion}` 91 | ); 92 | } 93 | } 94 | 95 | // Check if versions need updating 96 | if (publishPackage.version !== finalVersion) { 97 | console.log( 98 | ` - Version sync needed: ${publishPackage.version} → ${finalVersion}` 99 | ); 100 | publishPackage.version = finalVersion; 101 | 102 | // Update the source package.publish.json file with the final version 103 | fs.writeFileSync( 104 | publishPackagePath, 105 | JSON.stringify(publishPackage, null, '\t') + '\n' 106 | ); 107 | console.log(` - Updated package.publish.json version to ${finalVersion}`); 108 | } else { 109 | console.log(` - Versions already in sync: ${finalVersion}`); 110 | } 111 | 112 | // Copy the (now synced) package.publish.json as package.json 113 | fs.copySync(publishPackagePath, path.resolve(packageDir, 'package.json')); 114 | console.log(' - Copied package.publish.json as package.json'); 115 | 116 | // 6. Copy .vscodeignore if it exists 117 | if (fs.existsSync(path.resolve(__dirname, '.vscodeignore'))) { 118 | fs.copySync( 119 | path.resolve(__dirname, '.vscodeignore'), 120 | path.resolve(packageDir, '.vscodeignore') 121 | ); 122 | console.log(' - Copied .vscodeignore'); 123 | } 124 | 125 | // 7. Copy LICENSE if it exists 126 | if (fs.existsSync(path.resolve(__dirname, 'LICENSE'))) { 127 | fs.copySync( 128 | path.resolve(__dirname, 'LICENSE'), 129 | path.resolve(packageDir, 'LICENSE') 130 | ); 131 | console.log(' - Copied LICENSE'); 132 | } 133 | 134 | // 7a. Copy assets directory if it exists 135 | const assetsDir = path.resolve(__dirname, 'assets'); 136 | if (fs.existsSync(assetsDir)) { 137 | const targetAssetsDir = path.resolve(packageDir, 'assets'); 138 | fs.copySync(assetsDir, targetAssetsDir); 139 | console.log(' - Copied assets directory'); 140 | } 141 | 142 | // Small delay to ensure file system operations complete 143 | await new Promise((resolve) => setTimeout(resolve, 100)); 144 | 145 | // 8. Final step - manual packaging 146 | console.log('\n✅ Build preparation complete!'); 147 | console.log('\nTo create the VSIX package, run:'); 148 | console.log( 149 | '\x1b[36m%s\x1b[0m', 150 | `cd vsix-build && npx vsce package --no-dependencies` 151 | ); 152 | 153 | // Use the transformed version for output 154 | console.log( 155 | `\nYour extension will be packaged to: vsix-build/task-master-${finalVersion}.vsix` 156 | ); 157 | } catch (error) { 158 | console.error('\n❌ Packaging failed!'); 159 | console.error(error.message); 160 | process.exit(1); 161 | } 162 | ``` -------------------------------------------------------------------------------- /src/profiles/opencode.js: -------------------------------------------------------------------------------- ```javascript 1 | // Opencode profile for rule-transformer 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import { log } from '../../scripts/modules/utils.js'; 5 | import { createProfile } from './base-profile.js'; 6 | 7 | /** 8 | * Transform standard MCP config format to OpenCode format 9 | * @param {Object} mcpConfig - Standard MCP configuration object 10 | * @returns {Object} - Transformed OpenCode configuration object 11 | */ 12 | function transformToOpenCodeFormat(mcpConfig) { 13 | const openCodeConfig = { 14 | $schema: 'https://opencode.ai/config.json' 15 | }; 16 | 17 | // Transform mcpServers to mcp 18 | if (mcpConfig.mcpServers) { 19 | openCodeConfig.mcp = {}; 20 | 21 | for (const [serverName, serverConfig] of Object.entries( 22 | mcpConfig.mcpServers 23 | )) { 24 | // Transform server configuration 25 | const transformedServer = { 26 | type: 'local' 27 | }; 28 | 29 | // Combine command and args into single command array 30 | if (serverConfig.command && serverConfig.args) { 31 | transformedServer.command = [ 32 | serverConfig.command, 33 | ...serverConfig.args 34 | ]; 35 | } else if (serverConfig.command) { 36 | transformedServer.command = [serverConfig.command]; 37 | } 38 | 39 | // Add enabled flag 40 | transformedServer.enabled = true; 41 | 42 | // Transform env to environment 43 | if (serverConfig.env) { 44 | transformedServer.environment = serverConfig.env; 45 | } 46 | 47 | // update with transformed config 48 | openCodeConfig.mcp[serverName] = transformedServer; 49 | } 50 | } 51 | 52 | return openCodeConfig; 53 | } 54 | 55 | /** 56 | * Lifecycle function called after MCP config generation to transform to OpenCode format 57 | * @param {string} targetDir - Target project directory 58 | * @param {string} assetsDir - Assets directory (unused for OpenCode) 59 | */ 60 | function onPostConvertRulesProfile(targetDir, assetsDir) { 61 | const openCodeConfigPath = path.join(targetDir, 'opencode.json'); 62 | 63 | if (!fs.existsSync(openCodeConfigPath)) { 64 | log('debug', '[OpenCode] No opencode.json found to transform'); 65 | return; 66 | } 67 | 68 | try { 69 | // Read the generated standard MCP config 70 | const mcpConfigContent = fs.readFileSync(openCodeConfigPath, 'utf8'); 71 | const mcpConfig = JSON.parse(mcpConfigContent); 72 | 73 | // Check if it's already in OpenCode format (has $schema) 74 | if (mcpConfig.$schema) { 75 | log( 76 | 'info', 77 | '[OpenCode] opencode.json already in OpenCode format, skipping transformation' 78 | ); 79 | return; 80 | } 81 | 82 | // Transform to OpenCode format 83 | const openCodeConfig = transformToOpenCodeFormat(mcpConfig); 84 | 85 | // Write back the transformed config with proper formatting 86 | fs.writeFileSync( 87 | openCodeConfigPath, 88 | JSON.stringify(openCodeConfig, null, 2) + '\n' 89 | ); 90 | 91 | log('info', '[OpenCode] Transformed opencode.json to OpenCode format'); 92 | log( 93 | 'debug', 94 | `[OpenCode] Added schema, renamed mcpServers->mcp, combined command+args, added type/enabled, renamed env->environment` 95 | ); 96 | } catch (error) { 97 | log( 98 | 'error', 99 | `[OpenCode] Failed to transform opencode.json: ${error.message}` 100 | ); 101 | } 102 | } 103 | 104 | /** 105 | * Lifecycle function called when removing OpenCode profile 106 | * @param {string} targetDir - Target project directory 107 | */ 108 | function onRemoveRulesProfile(targetDir) { 109 | const openCodeConfigPath = path.join(targetDir, 'opencode.json'); 110 | 111 | if (!fs.existsSync(openCodeConfigPath)) { 112 | log('debug', '[OpenCode] No opencode.json found to clean up'); 113 | return; 114 | } 115 | 116 | try { 117 | // Read the current config 118 | const configContent = fs.readFileSync(openCodeConfigPath, 'utf8'); 119 | const config = JSON.parse(configContent); 120 | 121 | // Check if it has the mcp section and taskmaster-ai server 122 | if (config.mcp && config.mcp['taskmaster-ai']) { 123 | // Remove taskmaster-ai server 124 | delete config.mcp['taskmaster-ai']; 125 | 126 | // Check if there are other MCP servers 127 | const remainingServers = Object.keys(config.mcp); 128 | 129 | if (remainingServers.length === 0) { 130 | // No other servers, remove entire mcp section 131 | delete config.mcp; 132 | } 133 | 134 | // Check if config is now empty (only has $schema) 135 | const remainingKeys = Object.keys(config).filter( 136 | (key) => key !== '$schema' 137 | ); 138 | 139 | if (remainingKeys.length === 0) { 140 | // Config only has schema left, remove entire file 141 | fs.rmSync(openCodeConfigPath, { force: true }); 142 | log('info', '[OpenCode] Removed empty opencode.json file'); 143 | } else { 144 | // Write back the modified config 145 | fs.writeFileSync( 146 | openCodeConfigPath, 147 | JSON.stringify(config, null, 2) + '\n' 148 | ); 149 | log( 150 | 'info', 151 | '[OpenCode] Removed TaskMaster from opencode.json, preserved other configurations' 152 | ); 153 | } 154 | } else { 155 | log('debug', '[OpenCode] TaskMaster not found in opencode.json'); 156 | } 157 | } catch (error) { 158 | log( 159 | 'error', 160 | `[OpenCode] Failed to clean up opencode.json: ${error.message}` 161 | ); 162 | } 163 | } 164 | 165 | // Create and export opencode profile using the base factory 166 | export const opencodeProfile = createProfile({ 167 | name: 'opencode', 168 | displayName: 'OpenCode', 169 | url: 'opencode.ai', 170 | docsUrl: 'opencode.ai/docs/', 171 | profileDir: '.', // Root directory 172 | rulesDir: '.', // Root directory for AGENTS.md 173 | mcpConfigName: 'opencode.json', // Override default 'mcp.json' 174 | includeDefaultRules: false, 175 | fileMap: { 176 | 'AGENTS.md': 'AGENTS.md' 177 | }, 178 | onPostConvert: onPostConvertRulesProfile, 179 | onRemove: onRemoveRulesProfile 180 | }); 181 | 182 | // Export lifecycle functions separately to avoid naming conflicts 183 | export { onPostConvertRulesProfile, onRemoveRulesProfile }; 184 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/update-subtask-by-id.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * update-subtask-by-id.js 3 | * Direct function implementation for appending information to a specific subtask 4 | */ 5 | 6 | import { updateSubtaskById } from '../../../../scripts/modules/task-manager.js'; 7 | import { 8 | enableSilentMode, 9 | disableSilentMode, 10 | isSilentMode 11 | } from '../../../../scripts/modules/utils.js'; 12 | import { createLogWrapper } from '../../tools/utils.js'; 13 | 14 | /** 15 | * Direct function wrapper for updateSubtaskById with error handling. 16 | * 17 | * @param {Object} args - Command arguments containing id, prompt, useResearch, tasksJsonPath, and projectRoot. 18 | * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. 19 | * @param {string} args.id - Subtask ID in format "parent.sub". 20 | * @param {string} args.prompt - Information to append to the subtask. 21 | * @param {boolean} [args.research] - Whether to use research role. 22 | * @param {string} [args.projectRoot] - Project root path. 23 | * @param {string} [args.tag] - Tag for the task (optional) 24 | * @param {Object} log - Logger object. 25 | * @param {Object} context - Context object containing session data. 26 | * @returns {Promise<Object>} - Result object with success status and data/error information. 27 | */ 28 | export async function updateSubtaskByIdDirect(args, log, context = {}) { 29 | const { session } = context; 30 | // Destructure expected args, including projectRoot 31 | const { tasksJsonPath, id, prompt, research, projectRoot, tag } = args; 32 | 33 | const logWrapper = createLogWrapper(log); 34 | 35 | try { 36 | logWrapper.info( 37 | `Updating subtask by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}` 38 | ); 39 | 40 | // Check if tasksJsonPath was provided 41 | if (!tasksJsonPath) { 42 | const errorMessage = 'tasksJsonPath is required but was not provided.'; 43 | logWrapper.error(errorMessage); 44 | return { 45 | success: false, 46 | error: { code: 'MISSING_ARGUMENT', message: errorMessage } 47 | }; 48 | } 49 | 50 | // Basic validation for ID format (e.g., '5.2') 51 | if (!id || typeof id !== 'string' || !id.includes('.')) { 52 | const errorMessage = 53 | 'Invalid subtask ID format. Must be in format "parentId.subtaskId" (e.g., "5.2").'; 54 | logWrapper.error(errorMessage); 55 | return { 56 | success: false, 57 | error: { code: 'INVALID_SUBTASK_ID', message: errorMessage } 58 | }; 59 | } 60 | 61 | if (!prompt) { 62 | const errorMessage = 63 | 'No prompt specified. Please provide the information to append.'; 64 | logWrapper.error(errorMessage); 65 | return { 66 | success: false, 67 | error: { code: 'MISSING_PROMPT', message: errorMessage } 68 | }; 69 | } 70 | 71 | // Validate subtask ID format 72 | const subtaskId = id; 73 | if (typeof subtaskId !== 'string' && typeof subtaskId !== 'number') { 74 | const errorMessage = `Invalid subtask ID type: ${typeof subtaskId}. Subtask ID must be a string or number.`; 75 | log.error(errorMessage); 76 | return { 77 | success: false, 78 | error: { code: 'INVALID_SUBTASK_ID_TYPE', message: errorMessage } 79 | }; 80 | } 81 | 82 | const subtaskIdStr = String(subtaskId); 83 | if (!subtaskIdStr.includes('.')) { 84 | const errorMessage = `Invalid subtask ID format: ${subtaskIdStr}. Subtask ID must be in format "parentId.subtaskId" (e.g., "5.2").`; 85 | log.error(errorMessage); 86 | return { 87 | success: false, 88 | error: { code: 'INVALID_SUBTASK_ID_FORMAT', message: errorMessage } 89 | }; 90 | } 91 | 92 | // Use the provided path 93 | const tasksPath = tasksJsonPath; 94 | const useResearch = research === true; 95 | 96 | log.info( 97 | `Updating subtask with ID ${subtaskIdStr} with prompt "${prompt}" and research: ${useResearch}` 98 | ); 99 | 100 | const wasSilent = isSilentMode(); 101 | if (!wasSilent) { 102 | enableSilentMode(); 103 | } 104 | 105 | try { 106 | // Execute core updateSubtaskById function 107 | const coreResult = await updateSubtaskById( 108 | tasksPath, 109 | subtaskIdStr, 110 | prompt, 111 | useResearch, 112 | { 113 | mcpLog: logWrapper, 114 | session, 115 | projectRoot, 116 | tag, 117 | commandName: 'update-subtask', 118 | outputType: 'mcp' 119 | }, 120 | 'json' 121 | ); 122 | 123 | if (!coreResult || coreResult.updatedSubtask === null) { 124 | const message = `Subtask ${id} or its parent task not found.`; 125 | logWrapper.error(message); 126 | return { 127 | success: false, 128 | error: { code: 'SUBTASK_NOT_FOUND', message: message } 129 | }; 130 | } 131 | 132 | // Subtask updated successfully 133 | const successMessage = `Successfully updated subtask with ID ${subtaskIdStr}`; 134 | logWrapper.success(successMessage); 135 | return { 136 | success: true, 137 | data: { 138 | message: `Successfully updated subtask with ID ${subtaskIdStr}`, 139 | subtaskId: subtaskIdStr, 140 | parentId: subtaskIdStr.split('.')[0], 141 | subtask: coreResult.updatedSubtask, 142 | tasksPath, 143 | useResearch, 144 | telemetryData: coreResult.telemetryData, 145 | tagInfo: coreResult.tagInfo 146 | } 147 | }; 148 | } catch (error) { 149 | logWrapper.error(`Error updating subtask by ID: ${error.message}`); 150 | return { 151 | success: false, 152 | error: { 153 | code: 'UPDATE_SUBTASK_CORE_ERROR', 154 | message: error.message || 'Unknown error updating subtask' 155 | } 156 | }; 157 | } finally { 158 | if (!wasSilent && isSilentMode()) { 159 | disableSilentMode(); 160 | } 161 | } 162 | } catch (error) { 163 | logWrapper.error( 164 | `Setup error in updateSubtaskByIdDirect: ${error.message}` 165 | ); 166 | if (isSilentMode()) disableSilentMode(); 167 | return { 168 | success: false, 169 | error: { 170 | code: 'DIRECT_FUNCTION_SETUP_ERROR', 171 | message: error.message || 'Unknown setup error' 172 | } 173 | }; 174 | } 175 | } 176 | ``` -------------------------------------------------------------------------------- /src/progress/parse-prd-tracker.js: -------------------------------------------------------------------------------- ```javascript 1 | import chalk from 'chalk'; 2 | import { newMultiBar } from './cli-progress-factory.js'; 3 | import { BaseProgressTracker } from './base-progress-tracker.js'; 4 | import { 5 | createProgressHeader, 6 | createProgressRow, 7 | createBorder 8 | } from './tracker-ui.js'; 9 | import { 10 | getCliPriorityIndicators, 11 | getPriorityIndicator, 12 | getStatusBarPriorityIndicators, 13 | getPriorityColors 14 | } from '../ui/indicators.js'; 15 | 16 | // Get centralized priority indicators 17 | const PRIORITY_INDICATORS = getCliPriorityIndicators(); 18 | const PRIORITY_DOTS = getStatusBarPriorityIndicators(); 19 | const PRIORITY_COLORS = getPriorityColors(); 20 | 21 | // Constants 22 | const CONSTANTS = { 23 | DEBOUNCE_DELAY: 100, 24 | MAX_TITLE_LENGTH: 57, 25 | TRUNCATED_LENGTH: 54, 26 | TASK_ID_PAD_START: 3, 27 | TASK_ID_PAD_END: 4, 28 | PRIORITY_PAD_END: 3, 29 | VALID_PRIORITIES: ['high', 'medium', 'low'], 30 | DEFAULT_PRIORITY: 'medium' 31 | }; 32 | 33 | /** 34 | * Helper class to manage update debouncing 35 | */ 36 | class UpdateDebouncer { 37 | constructor(delay = CONSTANTS.DEBOUNCE_DELAY) { 38 | this.delay = delay; 39 | this.pendingTimeout = null; 40 | } 41 | 42 | debounce(callback) { 43 | this.clear(); 44 | this.pendingTimeout = setTimeout(() => { 45 | callback(); 46 | this.pendingTimeout = null; 47 | }, this.delay); 48 | } 49 | 50 | clear() { 51 | if (this.pendingTimeout) { 52 | clearTimeout(this.pendingTimeout); 53 | this.pendingTimeout = null; 54 | } 55 | } 56 | 57 | hasPending() { 58 | return this.pendingTimeout !== null; 59 | } 60 | } 61 | 62 | /** 63 | * Helper class to manage priority counts 64 | */ 65 | class PriorityManager { 66 | constructor() { 67 | this.priorities = { high: 0, medium: 0, low: 0 }; 68 | } 69 | 70 | increment(priority) { 71 | const normalized = this.normalize(priority); 72 | this.priorities[normalized]++; 73 | return normalized; 74 | } 75 | 76 | normalize(priority) { 77 | const lowercased = priority 78 | ? priority.toLowerCase() 79 | : CONSTANTS.DEFAULT_PRIORITY; 80 | return CONSTANTS.VALID_PRIORITIES.includes(lowercased) 81 | ? lowercased 82 | : CONSTANTS.DEFAULT_PRIORITY; 83 | } 84 | 85 | getCounts() { 86 | return { ...this.priorities }; 87 | } 88 | } 89 | 90 | /** 91 | * Helper class for formatting task display elements 92 | */ 93 | class TaskFormatter { 94 | static formatTitle(title, taskNumber) { 95 | if (!title) return `Task ${taskNumber}`; 96 | return title.length > CONSTANTS.MAX_TITLE_LENGTH 97 | ? title.substring(0, CONSTANTS.TRUNCATED_LENGTH) + '...' 98 | : title; 99 | } 100 | 101 | static formatPriority(priority) { 102 | return getPriorityIndicator(priority, false).padEnd( 103 | CONSTANTS.PRIORITY_PAD_END, 104 | ' ' 105 | ); 106 | } 107 | 108 | static formatTaskId(taskNumber) { 109 | return taskNumber 110 | .toString() 111 | .padStart(CONSTANTS.TASK_ID_PAD_START, ' ') 112 | .padEnd(CONSTANTS.TASK_ID_PAD_END, ' '); 113 | } 114 | } 115 | 116 | /** 117 | * Tracks progress for PRD parsing operations with multibar display 118 | */ 119 | class ParsePrdTracker extends BaseProgressTracker { 120 | _initializeCustomProperties(options) { 121 | this.append = options.append; 122 | this.priorityManager = new PriorityManager(); 123 | this.debouncer = new UpdateDebouncer(); 124 | this.headerShown = false; 125 | } 126 | 127 | _getTimeTokensBarFormat() { 128 | return `{clock} {elapsed} | ${PRIORITY_DOTS.high} {high} ${PRIORITY_DOTS.medium} {medium} ${PRIORITY_DOTS.low} {low} | Tokens (I/O): {in}/{out} | Est: {remaining}`; 129 | } 130 | 131 | _getProgressBarFormat() { 132 | return 'Tasks {tasks} |{bar}| {percentage}%'; 133 | } 134 | 135 | _getCustomTimeTokensPayload() { 136 | return this.priorityManager.getCounts(); 137 | } 138 | 139 | addTaskLine(taskNumber, title, priority = 'medium') { 140 | if (!this.multibar || this.isFinished) return; 141 | 142 | this._ensureHeaderShown(); 143 | const normalizedPriority = this._updateTaskCounters(taskNumber, priority); 144 | 145 | // Immediately update the time/tokens bar to show the new priority count 146 | this._updateTimeTokensBar(); 147 | 148 | this.debouncer.debounce(() => { 149 | this._updateProgressDisplay(taskNumber, title, normalizedPriority); 150 | }); 151 | } 152 | 153 | _ensureHeaderShown() { 154 | if (!this.headerShown) { 155 | this.headerShown = true; 156 | createProgressHeader( 157 | this.multibar, 158 | ' TASK | PRI | TITLE', 159 | '------+-----+----------------------------------------------------------------' 160 | ); 161 | } 162 | } 163 | 164 | _updateTaskCounters(taskNumber, priority) { 165 | const normalizedPriority = this.priorityManager.increment(priority); 166 | this.completedUnits = taskNumber; 167 | return normalizedPriority; 168 | } 169 | 170 | _updateProgressDisplay(taskNumber, title, normalizedPriority) { 171 | this.progressBar.update(this.completedUnits, { 172 | tasks: `${this.completedUnits}/${this.numUnits}` 173 | }); 174 | 175 | const displayTitle = TaskFormatter.formatTitle(title, taskNumber); 176 | const priorityDisplay = TaskFormatter.formatPriority(normalizedPriority); 177 | const taskIdCentered = TaskFormatter.formatTaskId(taskNumber); 178 | 179 | createProgressRow( 180 | this.multibar, 181 | ` ${taskIdCentered} | ${priorityDisplay} | {title}`, 182 | { title: displayTitle } 183 | ); 184 | 185 | createBorder( 186 | this.multibar, 187 | '------+-----+----------------------------------------------------------------' 188 | ); 189 | 190 | this._updateTimeTokensBar(); 191 | } 192 | 193 | finish() { 194 | // Flush any pending updates before finishing 195 | if (this.debouncer.hasPending()) { 196 | this.debouncer.clear(); 197 | this._updateTimeTokensBar(); 198 | } 199 | this.cleanup(); 200 | super.finish(); 201 | } 202 | 203 | /** 204 | * Override cleanup to handle pending updates 205 | */ 206 | _performCustomCleanup() { 207 | this.debouncer.clear(); 208 | } 209 | 210 | getSummary() { 211 | return { 212 | ...super.getSummary(), 213 | taskPriorities: this.priorityManager.getCounts(), 214 | actionVerb: this.append ? 'appended' : 'generated' 215 | }; 216 | } 217 | } 218 | 219 | export function createParsePrdTracker(options = {}) { 220 | return new ParsePrdTracker(options); 221 | } 222 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/initialize-project.js: -------------------------------------------------------------------------------- ```javascript 1 | import { initializeProject } from '../../../../scripts/init.js'; // Import core function and its logger if needed separately 2 | import { 3 | enableSilentMode, 4 | disableSilentMode 5 | // isSilentMode // Not used directly here 6 | } from '../../../../scripts/modules/utils.js'; 7 | import os from 'os'; // Import os module for home directory check 8 | import { RULE_PROFILES } from '../../../../src/constants/profiles.js'; 9 | import { convertAllRulesToProfileRules } from '../../../../src/utils/rule-transformer.js'; 10 | 11 | /** 12 | * Direct function wrapper for initializing a project. 13 | * Derives target directory from session, sets CWD, and calls core init logic. 14 | * @param {object} args - Arguments containing initialization options (addAliases, initGit, storeTasksInGit, skipInstall, yes, projectRoot, rules) 15 | * @param {object} log - The FastMCP logger instance. 16 | * @param {object} context - The context object, must contain { session }. 17 | * @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object. 18 | */ 19 | export async function initializeProjectDirect(args, log, context = {}) { 20 | const { session } = context; // Keep session if core logic needs it 21 | const homeDir = os.homedir(); 22 | 23 | log.info(`Args received in direct function: ${JSON.stringify(args)}`); 24 | 25 | // --- Determine Target Directory --- 26 | // TRUST the projectRoot passed from the tool layer via args 27 | // The HOF in the tool layer already normalized and validated it came from a reliable source (args or session) 28 | const targetDirectory = args.projectRoot; 29 | 30 | // --- Validate the targetDirectory (basic sanity checks) --- 31 | if ( 32 | !targetDirectory || 33 | typeof targetDirectory !== 'string' || // Ensure it's a string 34 | targetDirectory === '/' || 35 | targetDirectory === homeDir 36 | ) { 37 | log.error( 38 | `Invalid target directory received from tool layer: '${targetDirectory}'` 39 | ); 40 | return { 41 | success: false, 42 | error: { 43 | code: 'INVALID_TARGET_DIRECTORY', 44 | message: `Cannot initialize project: Invalid target directory '${targetDirectory}' received. Please ensure a valid workspace/folder is open or specified.`, 45 | details: `Received args.projectRoot: ${args.projectRoot}` // Show what was received 46 | } 47 | }; 48 | } 49 | 50 | // --- Proceed with validated targetDirectory --- 51 | log.info(`Validated target directory for initialization: ${targetDirectory}`); 52 | 53 | const originalCwd = process.cwd(); 54 | let resultData; 55 | let success = false; 56 | let errorResult = null; 57 | 58 | log.info( 59 | `Temporarily changing CWD to ${targetDirectory} for initialization.` 60 | ); 61 | process.chdir(targetDirectory); // Change CWD to the HOF-provided root 62 | 63 | enableSilentMode(); 64 | try { 65 | // Construct options ONLY from the relevant flags in args 66 | // The core initializeProject operates in the current CWD, which we just set 67 | const options = { 68 | addAliases: args.addAliases, 69 | initGit: args.initGit, 70 | storeTasksInGit: args.storeTasksInGit, 71 | skipInstall: args.skipInstall, 72 | yes: true // Force yes mode 73 | }; 74 | 75 | // Handle rules option with MCP-specific defaults 76 | if (Array.isArray(args.rules) && args.rules.length > 0) { 77 | options.rules = args.rules; 78 | options.rulesExplicitlyProvided = true; 79 | log.info(`Including rules: ${args.rules.join(', ')}`); 80 | } else { 81 | // For MCP initialization, default to Cursor profile only 82 | options.rules = ['cursor']; 83 | options.rulesExplicitlyProvided = true; 84 | log.info(`No rule profiles specified, defaulting to: Cursor`); 85 | } 86 | 87 | log.info(`Initializing project with options: ${JSON.stringify(options)}`); 88 | const result = await initializeProject(options); // Call core logic 89 | 90 | resultData = { 91 | message: 'Project initialized successfully.', 92 | next_step: 93 | 'Now that the project is initialized, the next step is to create the tasks by parsing a PRD. This will create the tasks folder and the initial task files (tasks folder will be created when parse-prd is run). The parse-prd tool will require a prd.txt file as input (typically found in .taskmaster/docs/ directory). You can create a prd.txt file by asking the user about their idea, and then using the .taskmaster/templates/example_prd.txt file as a template to generate a prd.txt file in .taskmaster/docs/. You may skip all of this if the user already has a prd.txt file. You can THEN use the parse-prd tool to create the tasks. So: step 1 after initialization is to create a prd.txt file in .taskmaster/docs/prd.txt or confirm the user already has one. Step 2 is to use the parse-prd tool to create the tasks. Do not bother looking for tasks after initialization, just use the parse-prd tool to create the tasks after creating a prd.txt from which to parse the tasks. You do NOT need to reinitialize the project to parse-prd.', 94 | ...result 95 | }; 96 | success = true; 97 | log.info( 98 | `Project initialization completed successfully in ${targetDirectory}.` 99 | ); 100 | } catch (error) { 101 | log.error(`Core initializeProject failed: ${error.message}`); 102 | errorResult = { 103 | code: 'INITIALIZATION_FAILED', 104 | message: `Core project initialization failed: ${error.message}`, 105 | details: error.stack 106 | }; 107 | success = false; 108 | } finally { 109 | disableSilentMode(); 110 | log.info(`Restoring original CWD: ${originalCwd}`); 111 | process.chdir(originalCwd); 112 | } 113 | 114 | if (success) { 115 | return { success: true, data: resultData }; 116 | } else { 117 | return { success: false, error: errorResult }; 118 | } 119 | } 120 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/task-master-core.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * task-master-core.js 3 | * Central module that imports and re-exports all direct function implementations 4 | * for improved organization and maintainability. 5 | */ 6 | 7 | // Import direct function implementations 8 | import { listTasksDirect } from './direct-functions/list-tasks.js'; 9 | import { getCacheStatsDirect } from './direct-functions/cache-stats.js'; 10 | import { parsePRDDirect } from './direct-functions/parse-prd.js'; 11 | import { updateTasksDirect } from './direct-functions/update-tasks.js'; 12 | import { updateTaskByIdDirect } from './direct-functions/update-task-by-id.js'; 13 | import { updateSubtaskByIdDirect } from './direct-functions/update-subtask-by-id.js'; 14 | import { generateTaskFilesDirect } from './direct-functions/generate-task-files.js'; 15 | import { setTaskStatusDirect } from './direct-functions/set-task-status.js'; 16 | import { showTaskDirect } from './direct-functions/show-task.js'; 17 | import { nextTaskDirect } from './direct-functions/next-task.js'; 18 | import { expandTaskDirect } from './direct-functions/expand-task.js'; 19 | import { addTaskDirect } from './direct-functions/add-task.js'; 20 | import { addSubtaskDirect } from './direct-functions/add-subtask.js'; 21 | import { removeSubtaskDirect } from './direct-functions/remove-subtask.js'; 22 | import { analyzeTaskComplexityDirect } from './direct-functions/analyze-task-complexity.js'; 23 | import { clearSubtasksDirect } from './direct-functions/clear-subtasks.js'; 24 | import { expandAllTasksDirect } from './direct-functions/expand-all-tasks.js'; 25 | import { removeDependencyDirect } from './direct-functions/remove-dependency.js'; 26 | import { validateDependenciesDirect } from './direct-functions/validate-dependencies.js'; 27 | import { fixDependenciesDirect } from './direct-functions/fix-dependencies.js'; 28 | import { complexityReportDirect } from './direct-functions/complexity-report.js'; 29 | import { addDependencyDirect } from './direct-functions/add-dependency.js'; 30 | import { removeTaskDirect } from './direct-functions/remove-task.js'; 31 | import { initializeProjectDirect } from './direct-functions/initialize-project.js'; 32 | import { modelsDirect } from './direct-functions/models.js'; 33 | import { moveTaskDirect } from './direct-functions/move-task.js'; 34 | import { moveTaskCrossTagDirect } from './direct-functions/move-task-cross-tag.js'; 35 | import { researchDirect } from './direct-functions/research.js'; 36 | import { addTagDirect } from './direct-functions/add-tag.js'; 37 | import { deleteTagDirect } from './direct-functions/delete-tag.js'; 38 | import { listTagsDirect } from './direct-functions/list-tags.js'; 39 | import { useTagDirect } from './direct-functions/use-tag.js'; 40 | import { renameTagDirect } from './direct-functions/rename-tag.js'; 41 | import { copyTagDirect } from './direct-functions/copy-tag.js'; 42 | import { scopeUpDirect } from './direct-functions/scope-up.js'; 43 | import { scopeDownDirect } from './direct-functions/scope-down.js'; 44 | 45 | // Re-export utility functions 46 | export { findTasksPath } from './utils/path-utils.js'; 47 | 48 | // Use Map for potential future enhancements like introspection or dynamic dispatch 49 | export const directFunctions = new Map([ 50 | ['listTasksDirect', listTasksDirect], 51 | ['getCacheStatsDirect', getCacheStatsDirect], 52 | ['parsePRDDirect', parsePRDDirect], 53 | ['updateTasksDirect', updateTasksDirect], 54 | ['updateTaskByIdDirect', updateTaskByIdDirect], 55 | ['updateSubtaskByIdDirect', updateSubtaskByIdDirect], 56 | ['generateTaskFilesDirect', generateTaskFilesDirect], 57 | ['setTaskStatusDirect', setTaskStatusDirect], 58 | ['showTaskDirect', showTaskDirect], 59 | ['nextTaskDirect', nextTaskDirect], 60 | ['expandTaskDirect', expandTaskDirect], 61 | ['addTaskDirect', addTaskDirect], 62 | ['addSubtaskDirect', addSubtaskDirect], 63 | ['removeSubtaskDirect', removeSubtaskDirect], 64 | ['analyzeTaskComplexityDirect', analyzeTaskComplexityDirect], 65 | ['clearSubtasksDirect', clearSubtasksDirect], 66 | ['expandAllTasksDirect', expandAllTasksDirect], 67 | ['removeDependencyDirect', removeDependencyDirect], 68 | ['validateDependenciesDirect', validateDependenciesDirect], 69 | ['fixDependenciesDirect', fixDependenciesDirect], 70 | ['complexityReportDirect', complexityReportDirect], 71 | ['addDependencyDirect', addDependencyDirect], 72 | ['removeTaskDirect', removeTaskDirect], 73 | ['initializeProjectDirect', initializeProjectDirect], 74 | ['modelsDirect', modelsDirect], 75 | ['moveTaskDirect', moveTaskDirect], 76 | ['moveTaskCrossTagDirect', moveTaskCrossTagDirect], 77 | ['researchDirect', researchDirect], 78 | ['addTagDirect', addTagDirect], 79 | ['deleteTagDirect', deleteTagDirect], 80 | ['listTagsDirect', listTagsDirect], 81 | ['useTagDirect', useTagDirect], 82 | ['renameTagDirect', renameTagDirect], 83 | ['copyTagDirect', copyTagDirect], 84 | ['scopeUpDirect', scopeUpDirect], 85 | ['scopeDownDirect', scopeDownDirect] 86 | ]); 87 | 88 | // Re-export all direct function implementations 89 | export { 90 | listTasksDirect, 91 | getCacheStatsDirect, 92 | parsePRDDirect, 93 | updateTasksDirect, 94 | updateTaskByIdDirect, 95 | updateSubtaskByIdDirect, 96 | generateTaskFilesDirect, 97 | setTaskStatusDirect, 98 | showTaskDirect, 99 | nextTaskDirect, 100 | expandTaskDirect, 101 | addTaskDirect, 102 | addSubtaskDirect, 103 | removeSubtaskDirect, 104 | analyzeTaskComplexityDirect, 105 | clearSubtasksDirect, 106 | expandAllTasksDirect, 107 | removeDependencyDirect, 108 | validateDependenciesDirect, 109 | fixDependenciesDirect, 110 | complexityReportDirect, 111 | addDependencyDirect, 112 | removeTaskDirect, 113 | initializeProjectDirect, 114 | modelsDirect, 115 | moveTaskDirect, 116 | moveTaskCrossTagDirect, 117 | researchDirect, 118 | addTagDirect, 119 | deleteTagDirect, 120 | listTagsDirect, 121 | useTagDirect, 122 | renameTagDirect, 123 | copyTagDirect, 124 | scopeUpDirect, 125 | scopeDownDirect 126 | }; 127 | ``` -------------------------------------------------------------------------------- /docs/providers/gemini-cli.md: -------------------------------------------------------------------------------- ```markdown 1 | # Gemini CLI Provider 2 | 3 | The Gemini CLI provider allows you to use Google's Gemini models through the Gemini CLI tool, leveraging your existing Gemini subscription and OAuth authentication. 4 | 5 | ## Why Use Gemini CLI? 6 | 7 | The primary benefit of using the `gemini-cli` provider is to leverage your existing Personal Gemini Code Assist license/usage Google offers for free, or Gemini Code Assist Standard/Enterprise subscription you may already have, via OAuth configured through the Gemini CLI. This is ideal for users who: 8 | 9 | - Have an active Gemini Code Assist license (including those using the free tier offere by Google) 10 | - Want to use OAuth authentication instead of managing API keys 11 | - Have already configured authentication via `gemini` OAuth login 12 | 13 | ## Installation 14 | 15 | The provider is already included in Task Master. However, you need to install the Gemini CLI tool: 16 | 17 | ```bash 18 | # Install gemini CLI globally 19 | npm install -g @google/gemini-cli 20 | ``` 21 | 22 | ## Authentication 23 | 24 | ### Primary Method: CLI Authentication (Recommended) 25 | 26 | The Gemini CLI provider is designed to use your pre-configured OAuth authentication: 27 | 28 | ```bash 29 | # Launch Gemini CLI and go through the authentication procedure 30 | gemini 31 | ``` 32 | 33 | For OAuth use, select `Login with Google` - This will open a browser window for OAuth authentication. Once authenticated, Task Master will automatically use these credentials when you select the `gemini-cli` provider and models. 34 | 35 | ### Alternative Method: API Key 36 | 37 | While the primary use case is OAuth authentication, you can also use an API key if needed: 38 | 39 | ```bash 40 | export GEMINI_API_KEY="your-gemini-api-key" 41 | ``` 42 | 43 | **Note:** If you want to use API keys, consider using the standard `google` provider instead, as `gemini-cli` is specifically designed for OAuth/subscription users. 44 | 45 | More details on authentication steps and options can be found in the [gemini-cli GitHub README](https://github.com/google-gemini/gemini-cli). 46 | 47 | ## Configuration 48 | 49 | Use the `task-master init` command to run through the guided initialization: 50 | 51 | ```bash 52 | task-master init 53 | ``` 54 | 55 | **OR** 56 | 57 | Configure `gemini-cli` as a provider using the Task Master models command: 58 | 59 | ```bash 60 | # Set gemini-cli as your main provider with gemini-2.5-pro 61 | task-master models --set-main gemini-2.5-pro --gemini-cli 62 | 63 | # Or use the faster gemini-2.5-flash model 64 | task-master models --set-main gemini-2.5-flash --gemini-cli 65 | ``` 66 | 67 | You can also manually edit your `.taskmaster/config.json`: 68 | 69 | ```json 70 | { 71 | "models": { 72 | "main": { 73 | "provider": "gemini-cli", 74 | "modelId": "gemini-2.5-pro", 75 | "maxTokens": 65536, 76 | "temperature": 0.2 77 | }, 78 | "research": { 79 | "provider": "gemini-cli", 80 | "modelId": "gemini-2.5-pro", 81 | "maxTokens": 65536, 82 | "temperature": 0.1 83 | }, 84 | "fallback": { 85 | "provider": "gemini-cli", 86 | "modelId": "gemini-2.5-flash", 87 | "maxTokens": 65536, 88 | "temperature": 0.2 89 | } 90 | }, 91 | "global": { 92 | "logLevel": "info", 93 | "debug": false, 94 | "defaultNumTasks": 10, 95 | "defaultSubtasks": 5, 96 | "defaultPriority": "medium", 97 | "projectName": "Taskmaster", 98 | "ollamaBaseURL": "http://localhost:11434/api", 99 | "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", 100 | "responseLanguage": "English", 101 | "defaultTag": "master", 102 | "azureOpenaiBaseURL": "https://your-endpoint.openai.azure.com/" 103 | }, 104 | "claudeCode": {} 105 | } 106 | ``` 107 | 108 | ### Available Models 109 | 110 | The gemini-cli provider supports only two models: 111 | - `gemini-2.5-pro` - High performance model (1M token context window, 65,536 max output tokens) 112 | - `gemini-2.5-flash` - Fast, efficient model (1M token context window, 65,536 max output tokens) 113 | 114 | ## Usage Examples 115 | 116 | ### Basic Usage 117 | 118 | Once gemini-cli is installed and authenticated, and Task Master simply use Task Master as normal: 119 | 120 | ```bash 121 | # The provider will automatically use your OAuth credentials 122 | task-master parse-prd my-prd.txt 123 | ``` 124 | 125 | ## Troubleshooting 126 | 127 | ### "Authentication failed" Error 128 | 129 | If you get an authentication error: 130 | 131 | 1. **Primary solution**: Run `gemini` to authenticate with your Google account - use `/auth` slash command in **gemini-cli** to change authentication method if desired. 132 | 2. **Check authentication status**: Run `gemini` and use `/about` to verify your Auth Method and GCP Project if applicable. 133 | 3. **If using API key** (not recommended): Ensure `GEMINI_API_KEY` env variable is set correctly, see the gemini-cli README.md for more info. 134 | 135 | ### "Model not found" Error 136 | 137 | The gemini-cli provider only supports two models: 138 | - `gemini-2.5-pro` 139 | - `gemini-2.5-flash` 140 | 141 | If you need other Gemini models, use the standard `google` provider with an API key instead. 142 | 143 | ### Gemini CLI Not Found 144 | 145 | If you get a "gemini: command not found" error: 146 | 147 | ```bash 148 | # Install the Gemini CLI globally 149 | npm install -g @google/gemini-cli 150 | 151 | # Verify installation 152 | gemini --version 153 | ``` 154 | 155 | ## Important Notes 156 | 157 | - **OAuth vs API Key**: This provider is specifically designed for users who want to use OAuth authentication via gemini-cli. If you prefer using API keys, consider using the standard `google` provider instead. 158 | - **Limited Model Support**: Only `gemini-2.5-pro` and `gemini-2.5-flash` are available through gemini-cli. 159 | - **Subscription Benefits**: Using OAuth authentication allows you to leverage any subscription benefits associated with your Google account. 160 | - The provider uses the `ai-sdk-provider-gemini-cli` npm package internally. 161 | - Supports all standard Task Master features: text generation, streaming, and structured object generation. ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/update-task-by-id.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * update-task-by-id.js 3 | * Direct function implementation for updating a single task by ID with new information 4 | */ 5 | 6 | import { updateTaskById } from '../../../../scripts/modules/task-manager.js'; 7 | import { 8 | enableSilentMode, 9 | disableSilentMode, 10 | isSilentMode 11 | } from '../../../../scripts/modules/utils.js'; 12 | import { createLogWrapper } from '../../tools/utils.js'; 13 | 14 | /** 15 | * Direct function wrapper for updateTaskById with error handling. 16 | * 17 | * @param {Object} args - Command arguments containing id, prompt, useResearch, tasksJsonPath, and projectRoot. 18 | * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. 19 | * @param {string} args.id - Task ID (or subtask ID like "1.2"). 20 | * @param {string} args.prompt - New information/context prompt. 21 | * @param {boolean} [args.research] - Whether to use research role. 22 | * @param {boolean} [args.append] - Whether to append timestamped information instead of full update. 23 | * @param {string} [args.projectRoot] - Project root path. 24 | * @param {string} [args.tag] - Tag for the task (optional) 25 | * @param {Object} log - Logger object. 26 | * @param {Object} context - Context object containing session data. 27 | * @returns {Promise<Object>} - Result object with success status and data/error information. 28 | */ 29 | export async function updateTaskByIdDirect(args, log, context = {}) { 30 | const { session } = context; 31 | // Destructure expected args, including projectRoot 32 | const { tasksJsonPath, id, prompt, research, append, projectRoot, tag } = 33 | args; 34 | 35 | const logWrapper = createLogWrapper(log); 36 | 37 | try { 38 | logWrapper.info( 39 | `Updating task by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}` 40 | ); 41 | 42 | // Check if tasksJsonPath was provided 43 | if (!tasksJsonPath) { 44 | const errorMessage = 'tasksJsonPath is required but was not provided.'; 45 | logWrapper.error(errorMessage); 46 | return { 47 | success: false, 48 | error: { code: 'MISSING_ARGUMENT', message: errorMessage } 49 | }; 50 | } 51 | 52 | // Check required parameters (id and prompt) 53 | if (!id) { 54 | const errorMessage = 55 | 'No task ID specified. Please provide a task ID to update.'; 56 | logWrapper.error(errorMessage); 57 | return { 58 | success: false, 59 | error: { code: 'MISSING_TASK_ID', message: errorMessage } 60 | }; 61 | } 62 | 63 | if (!prompt) { 64 | const errorMessage = 65 | 'No prompt specified. Please provide a prompt with new information for the task update.'; 66 | logWrapper.error(errorMessage); 67 | return { 68 | success: false, 69 | error: { code: 'MISSING_PROMPT', message: errorMessage } 70 | }; 71 | } 72 | 73 | // Parse taskId - handle both string and number values 74 | let taskId; 75 | if (typeof id === 'string') { 76 | // Handle subtask IDs (e.g., "5.2") 77 | if (id.includes('.')) { 78 | taskId = id; // Keep as string for subtask IDs 79 | } else { 80 | // Parse as integer for main task IDs 81 | taskId = parseInt(id, 10); 82 | if (Number.isNaN(taskId)) { 83 | const errorMessage = `Invalid task ID: ${id}. Task ID must be a positive integer or subtask ID (e.g., "5.2").`; 84 | logWrapper.error(errorMessage); 85 | return { 86 | success: false, 87 | error: { code: 'INVALID_TASK_ID', message: errorMessage } 88 | }; 89 | } 90 | } 91 | } else { 92 | taskId = id; 93 | } 94 | 95 | // Use the provided path 96 | const tasksPath = tasksJsonPath; 97 | 98 | // Get research flag 99 | const useResearch = research === true; 100 | 101 | logWrapper.info( 102 | `Updating task with ID ${taskId} with prompt "${prompt}" and research: ${useResearch}` 103 | ); 104 | 105 | const wasSilent = isSilentMode(); 106 | if (!wasSilent) { 107 | enableSilentMode(); 108 | } 109 | 110 | try { 111 | // Execute core updateTaskById function with proper parameters 112 | const coreResult = await updateTaskById( 113 | tasksPath, 114 | taskId, 115 | prompt, 116 | useResearch, 117 | { 118 | mcpLog: logWrapper, 119 | session, 120 | projectRoot, 121 | tag, 122 | commandName: 'update-task', 123 | outputType: 'mcp' 124 | }, 125 | 'json', 126 | append || false 127 | ); 128 | 129 | // Check if the core function returned null or an object without success 130 | if (!coreResult || coreResult.updatedTask === null) { 131 | // Core function logs the reason, just return success with info 132 | const message = `Task ${taskId} was not updated (likely already completed).`; 133 | logWrapper.info(message); 134 | return { 135 | success: true, 136 | data: { 137 | message: message, 138 | taskId: taskId, 139 | updated: false, 140 | telemetryData: coreResult?.telemetryData, 141 | tagInfo: coreResult?.tagInfo 142 | } 143 | }; 144 | } 145 | 146 | // Task was updated successfully 147 | const successMessage = `Successfully updated task with ID ${taskId} based on the prompt`; 148 | logWrapper.success(successMessage); 149 | return { 150 | success: true, 151 | data: { 152 | message: successMessage, 153 | taskId: taskId, 154 | tasksPath: tasksPath, 155 | useResearch: useResearch, 156 | updated: true, 157 | updatedTask: coreResult.updatedTask, 158 | telemetryData: coreResult.telemetryData, 159 | tagInfo: coreResult.tagInfo 160 | } 161 | }; 162 | } catch (error) { 163 | logWrapper.error(`Error updating task by ID: ${error.message}`); 164 | return { 165 | success: false, 166 | error: { 167 | code: 'UPDATE_TASK_CORE_ERROR', 168 | message: error.message || 'Unknown error updating task' 169 | } 170 | }; 171 | } finally { 172 | if (!wasSilent && isSilentMode()) { 173 | disableSilentMode(); 174 | } 175 | } 176 | } catch (error) { 177 | logWrapper.error(`Setup error in updateTaskByIdDirect: ${error.message}`); 178 | if (isSilentMode()) disableSilentMode(); 179 | return { 180 | success: false, 181 | error: { 182 | code: 'DIRECT_FUNCTION_SETUP_ERROR', 183 | message: error.message || 'Unknown setup error' 184 | } 185 | }; 186 | } 187 | } 188 | ``` -------------------------------------------------------------------------------- /.github/workflows/claude-docs-updater.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Claude Documentation Updater 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | commit_sha: 7 | description: 'The commit SHA that triggered this update' 8 | required: true 9 | type: string 10 | commit_message: 11 | description: 'The commit message' 12 | required: true 13 | type: string 14 | changed_files: 15 | description: 'List of changed files' 16 | required: true 17 | type: string 18 | commit_diff: 19 | description: 'Diff summary of changes' 20 | required: true 21 | type: string 22 | 23 | jobs: 24 | update-docs: 25 | runs-on: ubuntu-latest 26 | permissions: 27 | contents: write 28 | pull-requests: write 29 | issues: write 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v4 33 | with: 34 | ref: next 35 | fetch-depth: 0 # Need full history to checkout specific commit 36 | 37 | - name: Create docs update branch 38 | id: create-branch 39 | run: | 40 | BRANCH_NAME="docs/auto-update-$(date +%Y%m%d-%H%M%S)" 41 | git checkout -b $BRANCH_NAME 42 | echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT 43 | 44 | - name: Run Claude Code to Update Documentation 45 | uses: anthropics/claude-code-action@beta 46 | with: 47 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 48 | timeout_minutes: "30" 49 | mode: "agent" 50 | github_token: ${{ secrets.GITHUB_TOKEN }} 51 | experimental_allowed_domains: | 52 | .anthropic.com 53 | .github.com 54 | api.github.com 55 | .githubusercontent.com 56 | registry.npmjs.org 57 | .task-master.dev 58 | base_branch: "next" 59 | direct_prompt: | 60 | You are a documentation specialist. Analyze the recent changes pushed to the 'next' branch and update the documentation accordingly. 61 | 62 | Recent changes: 63 | - Commit: ${{ inputs.commit_message }} 64 | - Changed files: 65 | ${{ inputs.changed_files }} 66 | 67 | - Changes summary: 68 | ${{ inputs.commit_diff }} 69 | 70 | Your task: 71 | 1. Analyze the changes to understand what functionality was added, modified, or removed 72 | 2. Check if these changes require documentation updates in apps/docs/ 73 | 3. If documentation updates are needed: 74 | - Update relevant documentation files in apps/docs/ 75 | - Ensure examples are updated if APIs changed 76 | - Update any configuration documentation if config options changed 77 | - Add new documentation pages if new features were added 78 | - Update the changelog or release notes if applicable 79 | 4. If no documentation updates are needed, skip creating changes 80 | 81 | Guidelines: 82 | - Focus only on user-facing changes that need documentation 83 | - Keep documentation clear, concise, and helpful 84 | - Include code examples where appropriate 85 | - Maintain consistent documentation style with existing docs 86 | - Don't document internal implementation details unless they affect users 87 | - Update navigation/menu files if new pages are added 88 | 89 | Only make changes if the documentation truly needs updating based on the code changes. 90 | 91 | - name: Check if changes were made 92 | id: check-changes 93 | run: | 94 | if git diff --quiet; then 95 | echo "has_changes=false" >> $GITHUB_OUTPUT 96 | else 97 | echo "has_changes=true" >> $GITHUB_OUTPUT 98 | git add -A 99 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 100 | git config --local user.name "github-actions[bot]" 101 | git commit -m "docs: auto-update documentation based on changes in next branch 102 | 103 | This PR was automatically generated to update documentation based on recent changes. 104 | 105 | Original commit: ${{ inputs.commit_message }} 106 | 107 | Co-authored-by: Claude <[email protected]>" 108 | fi 109 | 110 | - name: Push changes and create PR 111 | if: steps.check-changes.outputs.has_changes == 'true' 112 | env: 113 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 114 | run: | 115 | git push origin ${{ steps.create-branch.outputs.branch_name }} 116 | 117 | # Create PR using GitHub CLI 118 | gh pr create \ 119 | --title "docs: update documentation for recent changes" \ 120 | --body "## 📚 Documentation Update 121 | 122 | This PR automatically updates documentation based on recent changes merged to the \`next\` branch. 123 | 124 | ### Original Changes 125 | **Commit:** ${{ inputs.commit_sha }} 126 | **Message:** ${{ inputs.commit_message }} 127 | 128 | ### Changed Files in Original Commit 129 | \`\`\` 130 | ${{ inputs.changed_files }} 131 | \`\`\` 132 | 133 | ### Documentation Updates 134 | This PR includes documentation updates to reflect the changes above. Please review to ensure: 135 | - [ ] Documentation accurately reflects the changes 136 | - [ ] Examples are correct and working 137 | - [ ] No important details are missing 138 | - [ ] Style is consistent with existing documentation 139 | 140 | --- 141 | *This PR was automatically generated by Claude Code GitHub Action*" \ 142 | --base next \ 143 | --head ${{ steps.create-branch.outputs.branch_name }} \ 144 | --label "documentation" \ 145 | --label "automated" 146 | ```