This is page 10 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 -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/models.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * models.js 3 | * Direct function for managing AI model configurations via MCP 4 | */ 5 | 6 | import { 7 | getModelConfiguration, 8 | getAvailableModelsList, 9 | setModel 10 | } from '../../../../scripts/modules/task-manager/models.js'; 11 | import { 12 | enableSilentMode, 13 | disableSilentMode 14 | } from '../../../../scripts/modules/utils.js'; 15 | import { createLogWrapper } from '../../tools/utils.js'; 16 | import { CUSTOM_PROVIDERS_ARRAY } from '../../../../src/constants/providers.js'; 17 | 18 | // Define supported roles for model setting 19 | const MODEL_ROLES = ['main', 'research', 'fallback']; 20 | 21 | /** 22 | * Determine provider hint from custom provider flags 23 | * @param {Object} args - Arguments containing provider flags 24 | * @returns {string|undefined} Provider hint or undefined if no custom provider flag is set 25 | */ 26 | function getProviderHint(args) { 27 | return CUSTOM_PROVIDERS_ARRAY.find((provider) => args[provider]); 28 | } 29 | 30 | /** 31 | * Handle setting models for different roles 32 | * @param {Object} args - Arguments containing role-specific model IDs 33 | * @param {Object} context - Context object with session, mcpLog, projectRoot 34 | * @returns {Object|null} Result if a model was set, null if no model setting was requested 35 | */ 36 | async function handleModelSetting(args, context) { 37 | for (const role of MODEL_ROLES) { 38 | const roleKey = `set${role.charAt(0).toUpperCase() + role.slice(1)}`; // setMain, setResearch, setFallback 39 | 40 | if (args[roleKey]) { 41 | const providerHint = getProviderHint(args); 42 | 43 | return await setModel(role, args[roleKey], { 44 | ...context, 45 | providerHint 46 | }); 47 | } 48 | } 49 | return null; // No model setting was requested 50 | } 51 | 52 | /** 53 | * Get or update model configuration 54 | * @param {Object} args - Arguments passed by the MCP tool 55 | * @param {Object} log - MCP logger 56 | * @param {Object} context - MCP context (contains session) 57 | * @returns {Object} Result object with success, data/error fields 58 | */ 59 | export async function modelsDirect(args, log, context = {}) { 60 | const { session } = context; 61 | const { projectRoot } = args; // Extract projectRoot from args 62 | 63 | // Create a logger wrapper that the core functions can use 64 | const mcpLog = createLogWrapper(log); 65 | 66 | log.info(`Executing models_direct with args: ${JSON.stringify(args)}`); 67 | log.info(`Using project root: ${projectRoot}`); 68 | 69 | // Validate flags: only one custom provider flag can be used simultaneously 70 | const customProviderFlags = CUSTOM_PROVIDERS_ARRAY.filter( 71 | (provider) => args[provider] 72 | ); 73 | 74 | if (customProviderFlags.length > 1) { 75 | log.error( 76 | 'Error: Cannot use multiple custom provider flags simultaneously.' 77 | ); 78 | return { 79 | success: false, 80 | error: { 81 | code: 'INVALID_ARGS', 82 | message: 83 | 'Cannot use multiple custom provider flags simultaneously. Choose only one: openrouter, ollama, bedrock, azure, or vertex.' 84 | } 85 | }; 86 | } 87 | 88 | try { 89 | enableSilentMode(); 90 | 91 | try { 92 | // Check for the listAvailableModels flag 93 | if (args.listAvailableModels === true) { 94 | return await getAvailableModelsList({ 95 | session, 96 | mcpLog, 97 | projectRoot 98 | }); 99 | } 100 | 101 | // Handle setting any model role using unified function 102 | const modelContext = { session, mcpLog, projectRoot }; 103 | const modelSetResult = await handleModelSetting(args, modelContext); 104 | if (modelSetResult) { 105 | return modelSetResult; 106 | } 107 | 108 | // Default action: get current configuration 109 | return await getModelConfiguration({ 110 | session, 111 | mcpLog, 112 | projectRoot 113 | }); 114 | } finally { 115 | disableSilentMode(); 116 | } 117 | } catch (error) { 118 | log.error(`Error in models_direct: ${error.message}`); 119 | return { 120 | success: false, 121 | error: { 122 | code: 'DIRECT_FUNCTION_ERROR', 123 | message: error.message, 124 | details: error.stack 125 | } 126 | }; 127 | } 128 | } 129 | ``` -------------------------------------------------------------------------------- /packages/tm-core/src/config/services/runtime-state-manager.service.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Runtime State Manager Service 3 | * Manages runtime state separate from configuration 4 | */ 5 | 6 | import { promises as fs } from 'node:fs'; 7 | import path from 'node:path'; 8 | import { 9 | ERROR_CODES, 10 | TaskMasterError 11 | } from '../../errors/task-master-error.js'; 12 | import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js'; 13 | 14 | /** 15 | * Runtime state data structure 16 | */ 17 | export interface RuntimeState { 18 | /** Currently active tag */ 19 | currentTag: string; 20 | /** Last updated timestamp */ 21 | lastUpdated?: string; 22 | /** Additional metadata */ 23 | metadata?: Record<string, unknown>; 24 | } 25 | 26 | /** 27 | * RuntimeStateManager handles runtime state persistence 28 | * Single responsibility: Runtime state management (separate from config) 29 | */ 30 | export class RuntimeStateManager { 31 | private stateFilePath: string; 32 | private currentState: RuntimeState; 33 | 34 | constructor(projectRoot: string) { 35 | this.stateFilePath = path.join(projectRoot, '.taskmaster', 'state.json'); 36 | this.currentState = { 37 | currentTag: DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG 38 | }; 39 | } 40 | 41 | /** 42 | * Load runtime state from disk 43 | */ 44 | async loadState(): Promise<RuntimeState> { 45 | try { 46 | const stateData = await fs.readFile(this.stateFilePath, 'utf-8'); 47 | const rawState = JSON.parse(stateData); 48 | 49 | // Map legacy field names to current interface 50 | const state: RuntimeState = { 51 | currentTag: 52 | rawState.currentTag || 53 | rawState.activeTag || 54 | DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG, 55 | lastUpdated: rawState.lastUpdated, 56 | metadata: rawState.metadata 57 | }; 58 | 59 | // Apply environment variable override for current tag 60 | if (process.env.TASKMASTER_TAG) { 61 | state.currentTag = process.env.TASKMASTER_TAG; 62 | } 63 | 64 | this.currentState = state; 65 | return state; 66 | } catch (error: any) { 67 | if (error.code === 'ENOENT') { 68 | // State file doesn't exist, use defaults 69 | console.debug('No state.json found, using default state'); 70 | 71 | // Check environment variable 72 | if (process.env.TASKMASTER_TAG) { 73 | this.currentState.currentTag = process.env.TASKMASTER_TAG; 74 | } 75 | 76 | return this.currentState; 77 | } 78 | 79 | console.warn('Failed to load state file:', error.message); 80 | return this.currentState; 81 | } 82 | } 83 | 84 | /** 85 | * Save runtime state to disk 86 | */ 87 | async saveState(): Promise<void> { 88 | const stateDir = path.dirname(this.stateFilePath); 89 | 90 | try { 91 | await fs.mkdir(stateDir, { recursive: true }); 92 | 93 | const stateToSave = { 94 | ...this.currentState, 95 | lastUpdated: new Date().toISOString() 96 | }; 97 | 98 | await fs.writeFile( 99 | this.stateFilePath, 100 | JSON.stringify(stateToSave, null, 2), 101 | 'utf-8' 102 | ); 103 | } catch (error) { 104 | throw new TaskMasterError( 105 | 'Failed to save runtime state', 106 | ERROR_CODES.CONFIG_ERROR, 107 | { statePath: this.stateFilePath }, 108 | error as Error 109 | ); 110 | } 111 | } 112 | 113 | /** 114 | * Get the currently active tag 115 | */ 116 | getCurrentTag(): string { 117 | return this.currentState.currentTag; 118 | } 119 | 120 | /** 121 | * Set the current tag 122 | */ 123 | async setCurrentTag(tag: string): Promise<void> { 124 | this.currentState.currentTag = tag; 125 | await this.saveState(); 126 | } 127 | 128 | /** 129 | * Get current state 130 | */ 131 | getState(): RuntimeState { 132 | return { ...this.currentState }; 133 | } 134 | 135 | /** 136 | * Update metadata 137 | */ 138 | async updateMetadata(metadata: Record<string, unknown>): Promise<void> { 139 | this.currentState.metadata = { 140 | ...this.currentState.metadata, 141 | ...metadata 142 | }; 143 | await this.saveState(); 144 | } 145 | 146 | /** 147 | * Clear state file 148 | */ 149 | async clearState(): Promise<void> { 150 | try { 151 | await fs.unlink(this.stateFilePath); 152 | } catch (error: any) { 153 | if (error.code !== 'ENOENT') { 154 | throw error; 155 | } 156 | } 157 | this.currentState = { 158 | currentTag: DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG 159 | }; 160 | } 161 | } 162 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/update-tasks.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * update-tasks.js 3 | * Direct function implementation for updating tasks based on new context 4 | */ 5 | 6 | import path from 'path'; 7 | import { updateTasks } from '../../../../scripts/modules/task-manager.js'; 8 | import { createLogWrapper } from '../../tools/utils.js'; 9 | import { 10 | enableSilentMode, 11 | disableSilentMode 12 | } from '../../../../scripts/modules/utils.js'; 13 | 14 | /** 15 | * Direct function wrapper for updating tasks based on new context. 16 | * 17 | * @param {Object} args - Command arguments containing projectRoot, from, prompt, research options. 18 | * @param {string} args.from - The ID of the task to update. 19 | * @param {string} args.prompt - The prompt to update the task with. 20 | * @param {boolean} args.research - Whether to use research mode. 21 | * @param {string} args.tasksJsonPath - Path to the tasks.json file. 22 | * @param {string} args.projectRoot - Project root path (for MCP/env fallback) 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 updateTasksDirect(args, log, context = {}) { 29 | const { session } = context; 30 | const { from, prompt, research, tasksJsonPath, projectRoot, tag } = args; 31 | 32 | // Create the standard logger wrapper 33 | const logWrapper = createLogWrapper(log); 34 | 35 | // --- Input Validation --- 36 | if (!projectRoot) { 37 | logWrapper.error('updateTasksDirect requires a projectRoot argument.'); 38 | return { 39 | success: false, 40 | error: { 41 | code: 'MISSING_ARGUMENT', 42 | message: 'projectRoot is required.' 43 | } 44 | }; 45 | } 46 | 47 | if (!from) { 48 | logWrapper.error('updateTasksDirect called without from ID'); 49 | return { 50 | success: false, 51 | error: { 52 | code: 'MISSING_ARGUMENT', 53 | message: 'Starting task ID (from) is required' 54 | } 55 | }; 56 | } 57 | 58 | if (!prompt) { 59 | logWrapper.error('updateTasksDirect called without prompt'); 60 | return { 61 | success: false, 62 | error: { 63 | code: 'MISSING_ARGUMENT', 64 | message: 'Update prompt is required' 65 | } 66 | }; 67 | } 68 | 69 | logWrapper.info( 70 | `Updating tasks via direct function. From: ${from}, Research: ${research}, File: ${tasksJsonPath}, ProjectRoot: ${projectRoot}` 71 | ); 72 | 73 | enableSilentMode(); // Enable silent mode 74 | try { 75 | // Call the core updateTasks function 76 | const result = await updateTasks( 77 | tasksJsonPath, 78 | from, 79 | prompt, 80 | research, 81 | { 82 | session, 83 | mcpLog: logWrapper, 84 | projectRoot, 85 | tag 86 | }, 87 | 'json' 88 | ); 89 | 90 | if (result && result.success && Array.isArray(result.updatedTasks)) { 91 | logWrapper.success( 92 | `Successfully updated ${result.updatedTasks.length} tasks.` 93 | ); 94 | return { 95 | success: true, 96 | data: { 97 | message: `Successfully updated ${result.updatedTasks.length} tasks.`, 98 | tasksPath: tasksJsonPath, 99 | updatedCount: result.updatedTasks.length, 100 | telemetryData: result.telemetryData, 101 | tagInfo: result.tagInfo 102 | } 103 | }; 104 | } else { 105 | // Handle case where core function didn't return expected success structure 106 | logWrapper.error( 107 | 'Core updateTasks function did not return a successful structure.' 108 | ); 109 | return { 110 | success: false, 111 | error: { 112 | code: 'CORE_FUNCTION_ERROR', 113 | message: 114 | result?.message || 115 | 'Core function failed to update tasks or returned unexpected result.' 116 | } 117 | }; 118 | } 119 | } catch (error) { 120 | logWrapper.error(`Error executing core updateTasks: ${error.message}`); 121 | return { 122 | success: false, 123 | error: { 124 | code: 'UPDATE_TASKS_CORE_ERROR', 125 | message: error.message || 'Unknown error updating tasks' 126 | } 127 | }; 128 | } finally { 129 | disableSilentMode(); // Ensure silent mode is disabled 130 | } 131 | } 132 | ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/clear-subtasks.js: -------------------------------------------------------------------------------- ```javascript 1 | import path from 'path'; 2 | import chalk from 'chalk'; 3 | import boxen from 'boxen'; 4 | import Table from 'cli-table3'; 5 | 6 | import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js'; 7 | import { displayBanner } from '../ui.js'; 8 | 9 | /** 10 | * Clear subtasks from specified tasks 11 | * @param {string} tasksPath - Path to the tasks.json file 12 | * @param {string} taskIds - Task IDs to clear subtasks from 13 | * @param {Object} context - Context object containing projectRoot and tag 14 | * @param {string} [context.projectRoot] - Project root path 15 | * @param {string} [context.tag] - Tag for the task 16 | */ 17 | function clearSubtasks(tasksPath, taskIds, context = {}) { 18 | const { projectRoot, tag } = context; 19 | log('info', `Reading tasks from ${tasksPath}...`); 20 | const data = readJSON(tasksPath, projectRoot, tag); 21 | if (!data || !data.tasks) { 22 | log('error', 'No valid tasks found.'); 23 | process.exit(1); 24 | } 25 | 26 | if (!isSilentMode()) { 27 | console.log( 28 | boxen(chalk.white.bold('Clearing Subtasks'), { 29 | padding: 1, 30 | borderColor: 'blue', 31 | borderStyle: 'round', 32 | margin: { top: 1, bottom: 1 } 33 | }) 34 | ); 35 | } 36 | 37 | // Handle multiple task IDs (comma-separated) 38 | const taskIdArray = taskIds.split(',').map((id) => id.trim()); 39 | let clearedCount = 0; 40 | 41 | // Create a summary table for the cleared subtasks 42 | const summaryTable = new Table({ 43 | head: [ 44 | chalk.cyan.bold('Task ID'), 45 | chalk.cyan.bold('Task Title'), 46 | chalk.cyan.bold('Subtasks Cleared') 47 | ], 48 | colWidths: [10, 50, 20], 49 | style: { head: [], border: [] } 50 | }); 51 | 52 | taskIdArray.forEach((taskId) => { 53 | const id = parseInt(taskId, 10); 54 | if (Number.isNaN(id)) { 55 | log('error', `Invalid task ID: ${taskId}`); 56 | return; 57 | } 58 | 59 | const task = data.tasks.find((t) => t.id === id); 60 | if (!task) { 61 | log('error', `Task ${id} not found`); 62 | return; 63 | } 64 | 65 | if (!task.subtasks || task.subtasks.length === 0) { 66 | log('info', `Task ${id} has no subtasks to clear`); 67 | summaryTable.push([ 68 | id.toString(), 69 | truncate(task.title, 47), 70 | chalk.yellow('No subtasks') 71 | ]); 72 | return; 73 | } 74 | 75 | const subtaskCount = task.subtasks.length; 76 | task.subtasks = []; 77 | clearedCount++; 78 | log('info', `Cleared ${subtaskCount} subtasks from task ${id}`); 79 | 80 | summaryTable.push([ 81 | id.toString(), 82 | truncate(task.title, 47), 83 | chalk.green(`${subtaskCount} subtasks cleared`) 84 | ]); 85 | }); 86 | 87 | if (clearedCount > 0) { 88 | writeJSON(tasksPath, data, projectRoot, tag); 89 | 90 | // Show summary table 91 | if (!isSilentMode()) { 92 | console.log( 93 | boxen(chalk.white.bold('Subtask Clearing Summary:'), { 94 | padding: { left: 2, right: 2, top: 0, bottom: 0 }, 95 | margin: { top: 1, bottom: 0 }, 96 | borderColor: 'blue', 97 | borderStyle: 'round' 98 | }) 99 | ); 100 | console.log(summaryTable.toString()); 101 | } 102 | 103 | // Success message 104 | if (!isSilentMode()) { 105 | console.log( 106 | boxen( 107 | chalk.green( 108 | `Successfully cleared subtasks from ${chalk.bold(clearedCount)} task(s)` 109 | ), 110 | { 111 | padding: 1, 112 | borderColor: 'green', 113 | borderStyle: 'round', 114 | margin: { top: 1 } 115 | } 116 | ) 117 | ); 118 | 119 | // Next steps suggestion 120 | console.log( 121 | boxen( 122 | chalk.white.bold('Next Steps:') + 123 | '\n\n' + 124 | `${chalk.cyan('1.')} Run ${chalk.yellow('task-master expand --id=<id>')} to generate new subtasks\n` + 125 | `${chalk.cyan('2.')} Run ${chalk.yellow('task-master list --with-subtasks')} to verify changes`, 126 | { 127 | padding: 1, 128 | borderColor: 'cyan', 129 | borderStyle: 'round', 130 | margin: { top: 1 } 131 | } 132 | ) 133 | ); 134 | } 135 | } else { 136 | if (!isSilentMode()) { 137 | console.log( 138 | boxen(chalk.yellow('No subtasks were cleared'), { 139 | padding: 1, 140 | borderColor: 'yellow', 141 | borderStyle: 'round', 142 | margin: { top: 1 } 143 | }) 144 | ); 145 | } 146 | } 147 | } 148 | 149 | export default clearSubtasks; 150 | ``` -------------------------------------------------------------------------------- /.taskmaster/docs/prd-tm-start.txt: -------------------------------------------------------------------------------- ``` 1 | <context> 2 | # Overview 3 | Add a new CLI command: `task-master start <task_id>` (alias: `tm start <task_id>`). This command hard-codes `claude-code` as the executor, fetches task details, builds a standardized prompt, runs claude-code, shows the result, checks for git changes, and auto-marks the task as done if successful. 4 | 5 | We follow the Commander class pattern, reuse task retrieval from `show` command flow. Extremely minimal for 1-hour hackathon timeline. 6 | 7 | # Core Features 8 | - `start` command (Commander class style) 9 | - Hard-coded executor: `claude-code` 10 | - Standardized prompt designed for minimal changes following existing patterns 11 | - Shows claude-code output (no streaming) 12 | - Git status check for success detection 13 | - Auto-mark task done if successful 14 | 15 | # User Experience 16 | ``` 17 | task-master start 12 18 | ``` 19 | 1) Fetches Task #12 details 20 | 2) Builds standardized prompt with task context 21 | 3) Runs claude-code with the prompt 22 | 4) Shows output 23 | 5) Checks git status for changes 24 | 6) Auto-marks task done if changes detected 25 | </context> 26 | 27 | <PRD> 28 | # Technical Architecture 29 | 30 | - Command pattern: 31 | - Create `apps/cli/src/commands/start.command.ts` modeled on [list.command.ts](mdc:apps/cli/src/commands/list.command.ts) and task lookup from [show.command.ts](mdc:apps/cli/src/commands/show.command.ts) 32 | 33 | - Task retrieval: 34 | - Use `@tm/core` via `createTaskMasterCore` to get task by ID 35 | - Extract: id, title, description, details 36 | 37 | - Executor (ultra-simple approach): 38 | - Execute `claude "full prompt here"` command directly 39 | - The prompt tells Claude to first run `tm show <task_id>` to get task details 40 | - Then tells Claude to implement the code changes 41 | - This opens Claude CLI interface naturally in the current terminal 42 | - No subprocess management needed - just execute the command 43 | 44 | - Execution flow: 45 | 1) Validate `<task_id>` exists; exit with error if not 46 | 2) Build standardized prompt that includes instructions to run `tm show <task_id>` 47 | 3) Execute `claude "prompt"` command directly in terminal 48 | 4) Claude CLI opens, runs `tm show`, then implements changes 49 | 5) After Claude session ends, run `git status --porcelain` to detect changes 50 | 6) If changes detected, auto-run `task-master set-status --id=<task_id> --status=done` 51 | 52 | - Success criteria: 53 | - Success = exit code 0 AND git shows modified/created files 54 | - Print changed file paths; warn if no changes detected 55 | 56 | # Development Roadmap 57 | 58 | MVP (ship in ~1 hour): 59 | 1) Implement `start.command.ts` (Commander class), parse `<task_id>` 60 | 2) Validate task exists via tm-core 61 | 3) Build prompt that tells Claude to run `tm show <task_id>` then implement 62 | 4) Execute `claude "prompt"` command, then check git status and auto-mark done 63 | 64 | # Risks and Mitigations 65 | - Executor availability: Error clearly if `claude-code` provider fails 66 | - False success: Git-change heuristic acceptable for hackathon MVP 67 | 68 | # Appendix 69 | 70 | **Standardized Prompt Template:** 71 | ``` 72 | You are an AI coding assistant with access to this repository's codebase. 73 | 74 | First, run this command to get the task details: 75 | tm show <task_id> 76 | 77 | Then implement the task with these requirements: 78 | - Make the SMALLEST number of code changes possible 79 | - Follow ALL existing patterns in the codebase (you have access to analyze the code) 80 | - Do NOT over-engineer the solution 81 | - Use existing files/functions/patterns wherever possible 82 | - When complete, print: COMPLETED: <brief summary of changes> 83 | 84 | Begin by running tm show <task_id> to understand what needs to be implemented. 85 | ``` 86 | 87 | **Key References:** 88 | - [list.command.ts](mdc:apps/cli/src/commands/list.command.ts) - Command structure 89 | - [show.command.ts](mdc:apps/cli/src/commands/show.command.ts) - Task validation 90 | - Node.js `child_process.exec()` - For executing `claude "prompt"` command 91 | </PRD> ``` -------------------------------------------------------------------------------- /packages/tm-core/src/subpath-exports.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Test file documenting subpath export usage 3 | * This demonstrates how consumers can use granular imports for better tree-shaking 4 | */ 5 | 6 | import { describe, it, expect } from 'vitest'; 7 | 8 | describe('Subpath Exports', () => { 9 | it('should allow importing from auth subpath', async () => { 10 | // Instead of: import { AuthManager } from '@tm/core'; 11 | // Use: import { AuthManager } from '@tm/core/auth'; 12 | const authModule = await import('./auth'); 13 | expect(authModule.AuthManager).toBeDefined(); 14 | expect(authModule.AuthenticationError).toBeDefined(); 15 | }); 16 | 17 | it('should allow importing from storage subpath', async () => { 18 | // Instead of: import { FileStorage } from '@tm/core'; 19 | // Use: import { FileStorage } from '@tm/core/storage'; 20 | const storageModule = await import('./storage'); 21 | expect(storageModule.FileStorage).toBeDefined(); 22 | expect(storageModule.ApiStorage).toBeDefined(); 23 | expect(storageModule.StorageFactory).toBeDefined(); 24 | }); 25 | 26 | it('should allow importing from config subpath', async () => { 27 | // Instead of: import { ConfigManager } from '@tm/core'; 28 | // Use: import { ConfigManager } from '@tm/core/config'; 29 | const configModule = await import('./config'); 30 | expect(configModule.ConfigManager).toBeDefined(); 31 | }); 32 | 33 | it('should allow importing from errors subpath', async () => { 34 | // Instead of: import { TaskMasterError } from '@tm/core'; 35 | // Use: import { TaskMasterError } from '@tm/core/errors'; 36 | const errorsModule = await import('./errors'); 37 | expect(errorsModule.TaskMasterError).toBeDefined(); 38 | expect(errorsModule.ERROR_CODES).toBeDefined(); 39 | }); 40 | 41 | it('should allow importing from logger subpath', async () => { 42 | // Instead of: import { getLogger } from '@tm/core'; 43 | // Use: import { getLogger } from '@tm/core/logger'; 44 | const loggerModule = await import('./logger'); 45 | expect(loggerModule.getLogger).toBeDefined(); 46 | expect(loggerModule.createLogger).toBeDefined(); 47 | }); 48 | 49 | it('should allow importing from providers subpath', async () => { 50 | // Instead of: import { BaseProvider } from '@tm/core'; 51 | // Use: import { BaseProvider } from '@tm/core/providers'; 52 | const providersModule = await import('./providers'); 53 | expect(providersModule.BaseProvider).toBeDefined(); 54 | }); 55 | 56 | it('should allow importing from services subpath', async () => { 57 | // Instead of: import { TaskService } from '@tm/core'; 58 | // Use: import { TaskService } from '@tm/core/services'; 59 | const servicesModule = await import('./services'); 60 | expect(servicesModule.TaskService).toBeDefined(); 61 | }); 62 | 63 | it('should allow importing from utils subpath', async () => { 64 | // Instead of: import { generateId } from '@tm/core'; 65 | // Use: import { generateId } from '@tm/core/utils'; 66 | const utilsModule = await import('./utils'); 67 | expect(utilsModule.generateId).toBeDefined(); 68 | }); 69 | }); 70 | 71 | /** 72 | * Usage Examples for Consumers: 73 | * 74 | * 1. Import only authentication (smaller bundle): 75 | * ```typescript 76 | * import { AuthManager, AuthenticationError } from '@tm/core/auth'; 77 | * ``` 78 | * 79 | * 2. Import only storage (no auth code bundled): 80 | * ```typescript 81 | * import { FileStorage, StorageFactory } from '@tm/core/storage'; 82 | * ``` 83 | * 84 | * 3. Import only errors (minimal bundle): 85 | * ```typescript 86 | * import { TaskMasterError, ERROR_CODES } from '@tm/core/errors'; 87 | * ``` 88 | * 89 | * 4. Still support convenience imports (larger bundle but better DX): 90 | * ```typescript 91 | * import { AuthManager, FileStorage, TaskMasterError } from '@tm/core'; 92 | * ``` 93 | * 94 | * Benefits: 95 | * - Better tree-shaking: unused modules are not bundled 96 | * - Clearer dependencies: explicit about what parts of the library you use 97 | * - Faster builds: bundlers can optimize better with granular imports 98 | * - Smaller bundles: especially important for browser/edge deployments 99 | */ 100 | ``` -------------------------------------------------------------------------------- /src/prompts/research.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "id": "research", 3 | "version": "1.0.0", 4 | "description": "Perform AI-powered research with project context", 5 | "metadata": { 6 | "author": "system", 7 | "created": "2024-01-01T00:00:00Z", 8 | "updated": "2024-01-01T00:00:00Z", 9 | "tags": ["research", "context-aware", "information-gathering"] 10 | }, 11 | "parameters": { 12 | "query": { 13 | "type": "string", 14 | "required": true, 15 | "description": "Research query" 16 | }, 17 | "gatheredContext": { 18 | "type": "string", 19 | "default": "", 20 | "description": "Gathered project context" 21 | }, 22 | "detailLevel": { 23 | "type": "string", 24 | "enum": ["low", "medium", "high"], 25 | "default": "medium", 26 | "description": "Level of detail for the response" 27 | }, 28 | "projectInfo": { 29 | "type": "object", 30 | "description": "Project information", 31 | "properties": { 32 | "root": { 33 | "type": "string", 34 | "description": "Project root path" 35 | }, 36 | "taskCount": { 37 | "type": "number", 38 | "description": "Number of related tasks" 39 | }, 40 | "fileCount": { 41 | "type": "number", 42 | "description": "Number of related files" 43 | } 44 | } 45 | } 46 | }, 47 | "prompts": { 48 | "default": { 49 | "system": "You are an expert AI research assistant helping with a software development project. You have access to project context including tasks, files, and project structure.\n\nYour role is to provide comprehensive, accurate, and actionable research responses based on the user's query and the provided project context.\n{{#if (eq detailLevel \"low\")}}\n**Response Style: Concise & Direct**\n- Provide brief, focused answers (2-4 paragraphs maximum)\n- Focus on the most essential information\n- Use bullet points for key takeaways\n- Avoid lengthy explanations unless critical\n- Skip pleasantries, introductions, and conclusions\n- No phrases like \"Based on your project context\" or \"I'll provide guidance\"\n- No summary outros or alignment statements\n- Get straight to the actionable information\n- Use simple, direct language - users want info, not explanation{{/if}}{{#if (eq detailLevel \"medium\")}}\n**Response Style: Balanced & Comprehensive**\n- Provide thorough but well-structured responses (4-8 paragraphs)\n- Include relevant examples and explanations\n- Balance depth with readability\n- Use headings and bullet points for organization{{/if}}{{#if (eq detailLevel \"high\")}}\n**Response Style: Detailed & Exhaustive**\n- Provide comprehensive, in-depth analysis (8+ paragraphs)\n- Include multiple perspectives and approaches\n- Provide detailed examples, code snippets, and step-by-step guidance\n- Cover edge cases and potential pitfalls\n- Use clear structure with headings, subheadings, and lists{{/if}}\n\n**Guidelines:**\n- Always consider the project context when formulating responses\n- Reference specific tasks, files, or project elements when relevant\n- Provide actionable insights that can be applied to the project\n- If the query relates to existing project tasks, suggest how the research applies to those tasks\n- Use markdown formatting for better readability\n- Be precise and avoid speculation unless clearly marked as such\n{{#if (eq detailLevel \"low\")}}\n**For LOW detail level specifically:**\n- Start immediately with the core information\n- No introductory phrases or context acknowledgments\n- No concluding summaries or project alignment statements\n- Focus purely on facts, steps, and actionable items{{/if}}", 50 | "user": "# Research Query\n\n{{query}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\n# Instructions\n\nPlease research and provide a {{detailLevel}}-detail response to the query above. Consider the project context provided and make your response as relevant and actionable as possible for this specific project." 51 | } 52 | } 53 | } 54 | ``` -------------------------------------------------------------------------------- /packages/tm-core/src/storage/file-storage/file-operations.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview File operations with atomic writes and locking 3 | */ 4 | 5 | import { promises as fs } from 'node:fs'; 6 | import type { FileStorageData } from './format-handler.js'; 7 | 8 | /** 9 | * Handles atomic file operations with locking mechanism 10 | */ 11 | export class FileOperations { 12 | private fileLocks: Map<string, Promise<void>> = new Map(); 13 | 14 | /** 15 | * Read and parse JSON file 16 | */ 17 | async readJson(filePath: string): Promise<any> { 18 | try { 19 | const content = await fs.readFile(filePath, 'utf-8'); 20 | return JSON.parse(content); 21 | } catch (error: any) { 22 | if (error.code === 'ENOENT') { 23 | throw error; // Re-throw ENOENT for caller to handle 24 | } 25 | if (error instanceof SyntaxError) { 26 | throw new Error(`Invalid JSON in file ${filePath}: ${error.message}`); 27 | } 28 | throw new Error(`Failed to read file ${filePath}: ${error.message}`); 29 | } 30 | } 31 | 32 | /** 33 | * Write JSON file with atomic operation and locking 34 | */ 35 | async writeJson( 36 | filePath: string, 37 | data: FileStorageData | any 38 | ): Promise<void> { 39 | // Use file locking to prevent concurrent writes 40 | const lockKey = filePath; 41 | const existingLock = this.fileLocks.get(lockKey); 42 | 43 | if (existingLock) { 44 | await existingLock; 45 | } 46 | 47 | const lockPromise = this.performAtomicWrite(filePath, data); 48 | this.fileLocks.set(lockKey, lockPromise); 49 | 50 | try { 51 | await lockPromise; 52 | } finally { 53 | this.fileLocks.delete(lockKey); 54 | } 55 | } 56 | 57 | /** 58 | * Perform atomic write operation using temporary file 59 | */ 60 | private async performAtomicWrite(filePath: string, data: any): Promise<void> { 61 | const tempPath = `${filePath}.tmp`; 62 | 63 | try { 64 | // Write to temp file first 65 | const content = JSON.stringify(data, null, 2); 66 | await fs.writeFile(tempPath, content, 'utf-8'); 67 | 68 | // Atomic rename 69 | await fs.rename(tempPath, filePath); 70 | } catch (error: any) { 71 | // Clean up temp file if it exists 72 | try { 73 | await fs.unlink(tempPath); 74 | } catch { 75 | // Ignore cleanup errors 76 | } 77 | 78 | throw new Error(`Failed to write file ${filePath}: ${error.message}`); 79 | } 80 | } 81 | 82 | /** 83 | * Check if file exists 84 | */ 85 | async exists(filePath: string): Promise<boolean> { 86 | try { 87 | await fs.access(filePath, fs.constants.F_OK); 88 | return true; 89 | } catch { 90 | return false; 91 | } 92 | } 93 | 94 | /** 95 | * Get file stats 96 | */ 97 | async getStats(filePath: string) { 98 | return fs.stat(filePath); 99 | } 100 | 101 | /** 102 | * Read directory contents 103 | */ 104 | async readDir(dirPath: string): Promise<string[]> { 105 | return fs.readdir(dirPath); 106 | } 107 | 108 | /** 109 | * Create directory recursively 110 | */ 111 | async ensureDir(dirPath: string): Promise<void> { 112 | try { 113 | await fs.mkdir(dirPath, { recursive: true }); 114 | } catch (error: any) { 115 | throw new Error( 116 | `Failed to create directory ${dirPath}: ${error.message}` 117 | ); 118 | } 119 | } 120 | 121 | /** 122 | * Delete file 123 | */ 124 | async deleteFile(filePath: string): Promise<void> { 125 | try { 126 | await fs.unlink(filePath); 127 | } catch (error: any) { 128 | if (error.code !== 'ENOENT') { 129 | throw new Error(`Failed to delete file ${filePath}: ${error.message}`); 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * Rename/move file 136 | */ 137 | async moveFile(oldPath: string, newPath: string): Promise<void> { 138 | try { 139 | await fs.rename(oldPath, newPath); 140 | } catch (error: any) { 141 | throw new Error( 142 | `Failed to move file from ${oldPath} to ${newPath}: ${error.message}` 143 | ); 144 | } 145 | } 146 | 147 | /** 148 | * Copy file 149 | */ 150 | async copyFile(srcPath: string, destPath: string): Promise<void> { 151 | try { 152 | await fs.copyFile(srcPath, destPath); 153 | } catch (error: any) { 154 | throw new Error( 155 | `Failed to copy file from ${srcPath} to ${destPath}: ${error.message}` 156 | ); 157 | } 158 | } 159 | 160 | /** 161 | * Clean up all pending file operations 162 | */ 163 | async cleanup(): Promise<void> { 164 | const locks = Array.from(this.fileLocks.values()); 165 | if (locks.length > 0) { 166 | await Promise.all(locks); 167 | } 168 | this.fileLocks.clear(); 169 | } 170 | } 171 | ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/components/TagDropdown.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | 3 | interface TagDropdownProps { 4 | currentTag: string; 5 | availableTags: string[]; 6 | onTagSwitch: (tagName: string) => Promise<void>; 7 | sendMessage: (message: any) => Promise<any>; 8 | dispatch: React.Dispatch<any>; 9 | } 10 | 11 | export const TagDropdown: React.FC<TagDropdownProps> = ({ 12 | currentTag, 13 | availableTags, 14 | onTagSwitch, 15 | sendMessage, 16 | dispatch 17 | }) => { 18 | const [isOpen, setIsOpen] = useState(false); 19 | const [isLoading, setIsLoading] = useState(false); 20 | const dropdownRef = useRef<HTMLDivElement>(null); 21 | 22 | // Fetch tags when component mounts 23 | useEffect(() => { 24 | fetchTags(); 25 | }, []); 26 | 27 | // Handle click outside to close dropdown 28 | useEffect(() => { 29 | const handleClickOutside = (event: MouseEvent) => { 30 | if ( 31 | dropdownRef.current && 32 | !dropdownRef.current.contains(event.target as Node) 33 | ) { 34 | setIsOpen(false); 35 | } 36 | }; 37 | 38 | if (isOpen) { 39 | document.addEventListener('mousedown', handleClickOutside); 40 | return () => { 41 | document.removeEventListener('mousedown', handleClickOutside); 42 | }; 43 | } 44 | }, [isOpen]); 45 | 46 | const fetchTags = async () => { 47 | try { 48 | const result = await sendMessage({ type: 'getTags' }); 49 | 50 | if (result?.tags && result?.currentTag) { 51 | const tagNames = result.tags.map((tag: any) => tag.name || tag); 52 | dispatch({ 53 | type: 'SET_TAG_DATA', 54 | payload: { 55 | currentTag: result.currentTag, 56 | availableTags: tagNames 57 | } 58 | }); 59 | } 60 | } catch (error) { 61 | console.error('Failed to fetch tags:', error); 62 | } 63 | }; 64 | 65 | const handleTagSwitch = async (tagName: string) => { 66 | if (tagName === currentTag) { 67 | setIsOpen(false); 68 | return; 69 | } 70 | 71 | setIsLoading(true); 72 | try { 73 | await onTagSwitch(tagName); 74 | dispatch({ type: 'SET_CURRENT_TAG', payload: tagName }); 75 | setIsOpen(false); 76 | } catch (error) { 77 | console.error('Failed to switch tag:', error); 78 | } finally { 79 | setIsLoading(false); 80 | } 81 | }; 82 | 83 | return ( 84 | <div className="relative" ref={dropdownRef}> 85 | <button 86 | onClick={() => setIsOpen(!isOpen)} 87 | disabled={isLoading} 88 | className="flex items-center gap-2 px-3 py-1.5 text-sm bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border rounded hover:bg-vscode-list-hoverBackground transition-colors" 89 | > 90 | <span className="font-medium">{currentTag}</span> 91 | <svg 92 | className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} 93 | fill="none" 94 | stroke="currentColor" 95 | viewBox="0 0 24 24" 96 | > 97 | <path 98 | strokeLinecap="round" 99 | strokeLinejoin="round" 100 | strokeWidth={2} 101 | d="M19 9l-7 7-7-7" 102 | /> 103 | </svg> 104 | </button> 105 | 106 | {isOpen && ( 107 | <div className="absolute top-full mt-1 right-0 bg-background border border-vscode-dropdown-border rounded shadow-lg z-50 min-w-[200px] py-1"> 108 | {availableTags.map((tag) => ( 109 | <button 110 | key={tag} 111 | onClick={() => handleTagSwitch(tag)} 112 | className={`w-full text-left px-3 py-2 text-sm transition-colors flex items-center justify-between group 113 | ${ 114 | tag === currentTag 115 | ? 'bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground' 116 | : 'hover:bg-vscode-list-hoverBackground text-vscode-dropdown-foreground' 117 | }`} 118 | > 119 | <span className="truncate pr-2">{tag}</span> 120 | {tag === currentTag && ( 121 | <svg 122 | className="w-4 h-4 flex-shrink-0 text-vscode-textLink-foreground" 123 | fill="none" 124 | stroke="currentColor" 125 | viewBox="0 0 24 24" 126 | > 127 | <path 128 | strokeLinecap="round" 129 | strokeLinejoin="round" 130 | strokeWidth={2} 131 | d="M5 13l4 4L19 7" 132 | /> 133 | </svg> 134 | )} 135 | </button> 136 | ))} 137 | </div> 138 | )} 139 | </div> 140 | ); 141 | }; 142 | ``` -------------------------------------------------------------------------------- /packages/tm-core/src/auth/auth-manager.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for AuthManager singleton behavior 3 | */ 4 | 5 | import { describe, it, expect, beforeEach, vi } from 'vitest'; 6 | 7 | // Mock the logger to verify warnings (must be hoisted before SUT import) 8 | const mockLogger = { 9 | warn: vi.fn(), 10 | info: vi.fn(), 11 | debug: vi.fn(), 12 | error: vi.fn() 13 | }; 14 | 15 | vi.mock('../logger/index.js', () => ({ 16 | getLogger: () => mockLogger 17 | })); 18 | 19 | // Spy on CredentialStore constructor to verify config propagation 20 | const CredentialStoreSpy = vi.fn(); 21 | vi.mock('./credential-store.js', () => { 22 | return { 23 | CredentialStore: class { 24 | constructor(config: any) { 25 | CredentialStoreSpy(config); 26 | this.getCredentials = vi.fn(() => null); 27 | } 28 | getCredentials() { 29 | return null; 30 | } 31 | saveCredentials() {} 32 | clearCredentials() {} 33 | hasValidCredentials() { 34 | return false; 35 | } 36 | } 37 | }; 38 | }); 39 | 40 | // Mock OAuthService to avoid side effects 41 | vi.mock('./oauth-service.js', () => { 42 | return { 43 | OAuthService: class { 44 | constructor() {} 45 | authenticate() { 46 | return Promise.resolve({}); 47 | } 48 | getAuthorizationUrl() { 49 | return null; 50 | } 51 | } 52 | }; 53 | }); 54 | 55 | // Mock SupabaseAuthClient to avoid side effects 56 | vi.mock('../clients/supabase-client.js', () => { 57 | return { 58 | SupabaseAuthClient: class { 59 | constructor() {} 60 | refreshSession() { 61 | return Promise.resolve({}); 62 | } 63 | signOut() { 64 | return Promise.resolve(); 65 | } 66 | } 67 | }; 68 | }); 69 | 70 | // Import SUT after mocks 71 | import { AuthManager } from './auth-manager.js'; 72 | 73 | describe('AuthManager Singleton', () => { 74 | beforeEach(() => { 75 | // Reset singleton before each test 76 | AuthManager.resetInstance(); 77 | vi.clearAllMocks(); 78 | CredentialStoreSpy.mockClear(); 79 | }); 80 | 81 | it('should return the same instance on multiple calls', () => { 82 | const instance1 = AuthManager.getInstance(); 83 | const instance2 = AuthManager.getInstance(); 84 | 85 | expect(instance1).toBe(instance2); 86 | }); 87 | 88 | it('should use config on first call', () => { 89 | const config = { 90 | baseUrl: 'https://test.auth.com', 91 | configDir: '/test/config', 92 | configFile: '/test/config/auth.json' 93 | }; 94 | 95 | const instance = AuthManager.getInstance(config); 96 | expect(instance).toBeDefined(); 97 | 98 | // Assert that CredentialStore was constructed with the provided config 99 | expect(CredentialStoreSpy).toHaveBeenCalledTimes(1); 100 | expect(CredentialStoreSpy).toHaveBeenCalledWith(config); 101 | 102 | // Verify the config is passed to internal components through observable behavior 103 | // getCredentials would look in the configured file path 104 | const credentials = instance.getCredentials(); 105 | expect(credentials).toBeNull(); // File doesn't exist, but config was propagated correctly 106 | }); 107 | 108 | it('should warn when config is provided after initialization', () => { 109 | // Clear previous calls 110 | mockLogger.warn.mockClear(); 111 | 112 | // First call with config 113 | AuthManager.getInstance({ baseUrl: 'https://first.auth.com' }); 114 | 115 | // Second call with different config 116 | AuthManager.getInstance({ baseUrl: 'https://second.auth.com' }); 117 | 118 | // Verify warning was logged 119 | expect(mockLogger.warn).toHaveBeenCalledWith( 120 | expect.stringMatching(/config.*after initialization.*ignored/i) 121 | ); 122 | }); 123 | 124 | it('should not warn when no config is provided after initialization', () => { 125 | // Clear previous calls 126 | mockLogger.warn.mockClear(); 127 | 128 | // First call with config 129 | AuthManager.getInstance({ configDir: '/test/config' }); 130 | 131 | // Second call without config 132 | AuthManager.getInstance(); 133 | 134 | // Verify no warning was logged 135 | expect(mockLogger.warn).not.toHaveBeenCalled(); 136 | }); 137 | 138 | it('should allow resetting the instance', () => { 139 | const instance1 = AuthManager.getInstance(); 140 | 141 | // Reset the instance 142 | AuthManager.resetInstance(); 143 | 144 | // Get new instance 145 | const instance2 = AuthManager.getInstance(); 146 | 147 | // They should be different instances 148 | expect(instance1).not.toBe(instance2); 149 | }); 150 | }); 151 | ``` -------------------------------------------------------------------------------- /tests/integration/profiles/opencode-init-functionality.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { opencodeProfile } from '../../../src/profiles/opencode.js'; 4 | 5 | describe('OpenCode Profile Initialization Functionality', () => { 6 | let opencodeProfileContent; 7 | 8 | beforeAll(() => { 9 | const opencodeJsPath = path.join( 10 | process.cwd(), 11 | 'src', 12 | 'profiles', 13 | 'opencode.js' 14 | ); 15 | opencodeProfileContent = fs.readFileSync(opencodeJsPath, 'utf8'); 16 | }); 17 | 18 | test('opencode.js has correct asset-only profile configuration', () => { 19 | // Check for explicit, non-default values in the source file 20 | expect(opencodeProfileContent).toContain("name: 'opencode'"); 21 | expect(opencodeProfileContent).toContain("displayName: 'OpenCode'"); 22 | expect(opencodeProfileContent).toContain("url: 'opencode.ai'"); 23 | expect(opencodeProfileContent).toContain("docsUrl: 'opencode.ai/docs/'"); 24 | expect(opencodeProfileContent).toContain("profileDir: '.'"); // non-default 25 | expect(opencodeProfileContent).toContain("rulesDir: '.'"); // non-default 26 | expect(opencodeProfileContent).toContain("mcpConfigName: 'opencode.json'"); // non-default 27 | expect(opencodeProfileContent).toContain('includeDefaultRules: false'); // non-default 28 | expect(opencodeProfileContent).toContain("'AGENTS.md': 'AGENTS.md'"); 29 | 30 | // Check the final computed properties on the profile object 31 | expect(opencodeProfile.profileName).toBe('opencode'); 32 | expect(opencodeProfile.displayName).toBe('OpenCode'); 33 | expect(opencodeProfile.profileDir).toBe('.'); 34 | expect(opencodeProfile.rulesDir).toBe('.'); 35 | expect(opencodeProfile.mcpConfig).toBe(true); // computed from mcpConfigName 36 | expect(opencodeProfile.mcpConfigName).toBe('opencode.json'); 37 | expect(opencodeProfile.mcpConfigPath).toBe('opencode.json'); // computed 38 | expect(opencodeProfile.includeDefaultRules).toBe(false); 39 | expect(opencodeProfile.fileMap['AGENTS.md']).toBe('AGENTS.md'); 40 | }); 41 | 42 | test('opencode.js has lifecycle functions for MCP config transformation', () => { 43 | expect(opencodeProfileContent).toContain( 44 | 'function onPostConvertRulesProfile' 45 | ); 46 | expect(opencodeProfileContent).toContain('function onRemoveRulesProfile'); 47 | expect(opencodeProfileContent).toContain('transformToOpenCodeFormat'); 48 | }); 49 | 50 | test('opencode.js handles opencode.json transformation in lifecycle functions', () => { 51 | expect(opencodeProfileContent).toContain('opencode.json'); 52 | expect(opencodeProfileContent).toContain('transformToOpenCodeFormat'); 53 | expect(opencodeProfileContent).toContain('$schema'); 54 | expect(opencodeProfileContent).toContain('mcpServers'); 55 | expect(opencodeProfileContent).toContain('mcp'); 56 | }); 57 | 58 | test('opencode.js has proper error handling in lifecycle functions', () => { 59 | expect(opencodeProfileContent).toContain('try {'); 60 | expect(opencodeProfileContent).toContain('} catch (error) {'); 61 | expect(opencodeProfileContent).toContain('log('); 62 | }); 63 | 64 | test('opencode.js uses custom MCP config name', () => { 65 | // OpenCode uses opencode.json instead of mcp.json 66 | expect(opencodeProfileContent).toContain("mcpConfigName: 'opencode.json'"); 67 | // Should not contain mcp.json as a config value (comments are OK) 68 | expect(opencodeProfileContent).not.toMatch( 69 | /mcpConfigName:\s*['"]mcp\.json['"]/ 70 | ); 71 | }); 72 | 73 | test('opencode.js has transformation logic for OpenCode format', () => { 74 | // Check for transformation function 75 | expect(opencodeProfileContent).toContain('transformToOpenCodeFormat'); 76 | 77 | // Check for specific transformation logic 78 | expect(opencodeProfileContent).toContain('mcpServers'); 79 | expect(opencodeProfileContent).toContain('command'); 80 | expect(opencodeProfileContent).toContain('args'); 81 | expect(opencodeProfileContent).toContain('environment'); 82 | expect(opencodeProfileContent).toContain('enabled'); 83 | expect(opencodeProfileContent).toContain('type'); 84 | }); 85 | }); 86 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/remove-task.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * remove-task.js 3 | * Direct function implementation for removing a task 4 | */ 5 | 6 | import { 7 | removeTask, 8 | taskExists 9 | } from '../../../../scripts/modules/task-manager.js'; 10 | import { 11 | enableSilentMode, 12 | disableSilentMode, 13 | readJSON 14 | } from '../../../../scripts/modules/utils.js'; 15 | 16 | /** 17 | * Direct function wrapper for removeTask with error handling. 18 | * Supports removing multiple tasks at once with comma-separated IDs. 19 | * 20 | * @param {Object} args - Command arguments 21 | * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. 22 | * @param {string} args.id - The ID(s) of the task(s) or subtask(s) to remove (comma-separated for multiple). 23 | * @param {string} args.projectRoot - Project root path (for MCP/env fallback) 24 | * @param {string} args.tag - Tag for the task (optional) 25 | * @param {Object} log - Logger object 26 | * @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string } } 27 | */ 28 | export async function removeTaskDirect(args, log, context = {}) { 29 | // Destructure expected args 30 | const { tasksJsonPath, id, projectRoot, tag } = args; 31 | const { session } = context; 32 | try { 33 | // Check if tasksJsonPath was provided 34 | if (!tasksJsonPath) { 35 | log.error('removeTaskDirect called without tasksJsonPath'); 36 | return { 37 | success: false, 38 | error: { 39 | code: 'MISSING_ARGUMENT', 40 | message: 'tasksJsonPath is required' 41 | } 42 | }; 43 | } 44 | 45 | // Validate task ID parameter 46 | if (!id) { 47 | log.error('Task ID is required'); 48 | return { 49 | success: false, 50 | error: { 51 | code: 'INPUT_VALIDATION_ERROR', 52 | message: 'Task ID is required' 53 | } 54 | }; 55 | } 56 | 57 | // Split task IDs if comma-separated 58 | const taskIdArray = id.split(',').map((taskId) => taskId.trim()); 59 | 60 | log.info( 61 | `Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}${tag ? ` in tag '${tag}'` : ''}` 62 | ); 63 | 64 | // Validate all task IDs exist before proceeding 65 | const data = readJSON(tasksJsonPath, projectRoot, tag); 66 | if (!data || !data.tasks) { 67 | return { 68 | success: false, 69 | error: { 70 | code: 'INVALID_TASKS_FILE', 71 | message: `No valid tasks found in ${tasksJsonPath}${tag ? ` for tag '${tag}'` : ''}` 72 | } 73 | }; 74 | } 75 | 76 | const invalidTasks = taskIdArray.filter( 77 | (taskId) => !taskExists(data.tasks, taskId) 78 | ); 79 | 80 | if (invalidTasks.length > 0) { 81 | return { 82 | success: false, 83 | error: { 84 | code: 'INVALID_TASK_ID', 85 | message: `The following tasks were not found${tag ? ` in tag '${tag}'` : ''}: ${invalidTasks.join(', ')}` 86 | } 87 | }; 88 | } 89 | 90 | // Enable silent mode to prevent console logs from interfering with JSON response 91 | enableSilentMode(); 92 | 93 | try { 94 | // Call removeTask with proper context including tag 95 | const result = await removeTask(tasksJsonPath, id, { 96 | projectRoot, 97 | tag 98 | }); 99 | 100 | if (!result.success) { 101 | return { 102 | success: false, 103 | error: { 104 | code: 'REMOVE_TASK_ERROR', 105 | message: result.error || 'Failed to remove tasks' 106 | } 107 | }; 108 | } 109 | 110 | log.info(`Successfully removed ${result.removedTasks.length} task(s)`); 111 | 112 | return { 113 | success: true, 114 | data: { 115 | totalTasks: taskIdArray.length, 116 | successful: result.removedTasks.length, 117 | failed: taskIdArray.length - result.removedTasks.length, 118 | removedTasks: result.removedTasks, 119 | message: result.message, 120 | tasksPath: tasksJsonPath, 121 | tag 122 | } 123 | }; 124 | } finally { 125 | // Restore normal logging 126 | disableSilentMode(); 127 | } 128 | } catch (error) { 129 | // Ensure silent mode is disabled even if an outer error occurs 130 | disableSilentMode(); 131 | 132 | // Catch any unexpected errors 133 | log.error(`Unexpected error in removeTaskDirect: ${error.message}`); 134 | return { 135 | success: false, 136 | error: { 137 | code: 'UNEXPECTED_ERROR', 138 | message: error.message 139 | } 140 | }; 141 | } 142 | } 143 | ``` -------------------------------------------------------------------------------- /packages/tm-core/tests/unit/smoke.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Smoke tests to verify basic package functionality and imports 3 | */ 4 | 5 | import { 6 | PlaceholderParser, 7 | PlaceholderStorage, 8 | StorageError, 9 | TaskNotFoundError, 10 | TmCoreError, 11 | ValidationError, 12 | formatDate, 13 | generateTaskId, 14 | isValidTaskId, 15 | name, 16 | version 17 | } from '@tm/core'; 18 | 19 | import type { 20 | PlaceholderTask, 21 | TaskId, 22 | TaskPriority, 23 | TaskStatus 24 | } from '@tm/core'; 25 | 26 | describe('tm-core smoke tests', () => { 27 | describe('package metadata', () => { 28 | it('should export correct package name and version', () => { 29 | expect(name).toBe('@task-master/tm-core'); 30 | expect(version).toBe('1.0.0'); 31 | }); 32 | }); 33 | 34 | describe('utility functions', () => { 35 | it('should generate valid task IDs', () => { 36 | const id1 = generateTaskId(); 37 | const id2 = generateTaskId(); 38 | 39 | expect(typeof id1).toBe('string'); 40 | expect(typeof id2).toBe('string'); 41 | expect(id1).not.toBe(id2); // Should be unique 42 | expect(isValidTaskId(id1)).toBe(true); 43 | expect(isValidTaskId('')).toBe(false); 44 | }); 45 | 46 | it('should format dates', () => { 47 | const date = new Date('2023-01-01T00:00:00.000Z'); 48 | const formatted = formatDate(date); 49 | expect(formatted).toBe('2023-01-01T00:00:00.000Z'); 50 | }); 51 | }); 52 | 53 | describe('placeholder storage', () => { 54 | it('should perform basic storage operations', async () => { 55 | const storage = new PlaceholderStorage(); 56 | const testPath = 'test/path'; 57 | const testData = 'test data'; 58 | 59 | // Initially should not exist 60 | expect(await storage.exists(testPath)).toBe(false); 61 | expect(await storage.read(testPath)).toBe(null); 62 | 63 | // Write and verify 64 | await storage.write(testPath, testData); 65 | expect(await storage.exists(testPath)).toBe(true); 66 | expect(await storage.read(testPath)).toBe(testData); 67 | 68 | // Delete and verify 69 | await storage.delete(testPath); 70 | expect(await storage.exists(testPath)).toBe(false); 71 | }); 72 | }); 73 | 74 | describe('placeholder parser', () => { 75 | it('should parse simple task lists', async () => { 76 | const parser = new PlaceholderParser(); 77 | const content = ` 78 | - Task 1 79 | - Task 2 80 | - Task 3 81 | `; 82 | 83 | const isValid = await parser.validate(content); 84 | expect(isValid).toBe(true); 85 | 86 | const tasks = await parser.parse(content); 87 | expect(tasks).toHaveLength(3); 88 | expect(tasks[0]?.title).toBe('Task 1'); 89 | expect(tasks[1]?.title).toBe('Task 2'); 90 | expect(tasks[2]?.title).toBe('Task 3'); 91 | 92 | tasks.forEach((task) => { 93 | expect(task.status).toBe('pending'); 94 | expect(task.priority).toBe('medium'); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('error classes', () => { 100 | it('should create and throw custom errors', () => { 101 | const baseError = new TmCoreError('Base error'); 102 | expect(baseError.name).toBe('TmCoreError'); 103 | expect(baseError.message).toBe('Base error'); 104 | 105 | const taskNotFound = new TaskNotFoundError('task-123'); 106 | expect(taskNotFound.name).toBe('TaskNotFoundError'); 107 | expect(taskNotFound.code).toBe('TASK_NOT_FOUND'); 108 | expect(taskNotFound.message).toContain('task-123'); 109 | 110 | const validationError = new ValidationError('Invalid data'); 111 | expect(validationError.name).toBe('ValidationError'); 112 | expect(validationError.code).toBe('VALIDATION_ERROR'); 113 | 114 | const storageError = new StorageError('Storage failed'); 115 | expect(storageError.name).toBe('StorageError'); 116 | expect(storageError.code).toBe('STORAGE_ERROR'); 117 | }); 118 | }); 119 | 120 | describe('type definitions', () => { 121 | it('should have correct types available', () => { 122 | // These are compile-time checks that verify types exist 123 | const taskId: TaskId = 'test-id'; 124 | const status: TaskStatus = 'pending'; 125 | const priority: TaskPriority = 'high'; 126 | 127 | const task: PlaceholderTask = { 128 | id: taskId, 129 | title: 'Test Task', 130 | status: status, 131 | priority: priority 132 | }; 133 | 134 | expect(task.id).toBe('test-id'); 135 | expect(task.status).toBe('pending'); 136 | expect(task.priority).toBe('high'); 137 | }); 138 | }); 139 | }); 140 | ``` -------------------------------------------------------------------------------- /.taskmaster/docs/research/2025-06-14_test-the-fix-for-duplicate-saves-final-test.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Research Session 3 | query: "test the fix for duplicate saves - final test" 4 | date: 6/14/2025 5 | time: 6:10:00 PM 6 | timestamp: 2025-06-14T22:10:00.394Z 7 | exchanges: 1 8 | --- 9 | 10 | # Research Session 11 | 12 | ## Initial Query 13 | 14 | **Question:** test the fix for duplicate saves - final test 15 | 16 | **Response:** 17 | 18 | ## Steps to Test the Fix for Duplicate Saves 19 | 20 | **1. Prepare a Clean Test Environment** 21 | - Ensure the tasks.json file and any related data stores are in a known, clean state with no pre-existing duplicate entries. 22 | - Back up the current tasks.json in case rollback is needed. 23 | 24 | **2. Define Test Scenarios** 25 | - Attempt to save a new task with unique data. 26 | - Attempt to save a task with the same ID or unique identifier as an existing task (simulate a duplicate). 27 | - Attempt to save a task with the same title or content but a different ID (to test if the fix is based on ID or content). 28 | - Simultaneously trigger multiple save operations (if the system supports concurrency) to test race conditions. 29 | 30 | **3. Execute Manual and Automated Tests** 31 | - Use the application's UI or API to create and save tasks as per the scenarios above. 32 | - For each save operation, check the tasks.json file or the data store to verify: 33 | - No duplicate entries are created. 34 | - Existing tasks are not overwritten unless intended (e.g., for updates). 35 | - The system returns appropriate error messages or warnings when a duplicate save is attempted. 36 | 37 | **4. Validate System Behavior** 38 | - Confirm that after all test operations, each task in tasks.json is unique according to the defined criteria (ID, title, or other unique fields). 39 | - If the system is designed to merge duplicates, verify that merging occurs as expected. 40 | - If the system is designed to reject duplicates, ensure that the rejection is clear and no duplicate is present. 41 | 42 | **5. Test Edge Cases** 43 | - Attempt to save tasks with minor variations (e.g., whitespace differences, case sensitivity) to ensure the duplicate detection logic is robust. 44 | - Test with large numbers of tasks to ensure performance and correctness at scale. 45 | 46 | **6. Review Logs and Error Handling** 47 | - Check application logs for any errors or warnings related to duplicate saves. 48 | - Ensure that error handling is user-friendly and provides actionable feedback. 49 | 50 | **7. Regression Testing** 51 | - Run the full suite of task operations (create, update, delete) to ensure the fix for duplicate saves does not introduce new issues elsewhere in the system. 52 | 53 | ## Additional Best Practices 54 | 55 | - Communicate test results with the team to confirm the fix is effective and to prevent future regressions[1][2][5]. 56 | - Consider implementing automated duplicate detection as part of the save operation to prevent future issues[2][3]. 57 | - Document the test cases and results for future reference and audits. 58 | 59 | ## Example Test Case Table 60 | 61 | | Test Scenario | Expected Result | Actual Result | Pass/Fail | 62 | |--------------------------------------|-------------------------------|---------------|-----------| 63 | | Save unique task | Task saved | | | 64 | | Save duplicate task (same ID) | Duplicate rejected/merged | | | 65 | | Save duplicate task (same title) | Duplicate rejected/merged | | | 66 | | Simultaneous saves (race condition) | Only one unique task present | | | 67 | | Save with minor variations | No false positives/negatives | | | 68 | 69 | Fill in the "Actual Result" and "Pass/Fail" columns during testing. 70 | 71 | ## Action Items 72 | 73 | - Complete all test scenarios above. 74 | - Document any issues found and retest after fixes. 75 | - Confirm with stakeholders before closing the issue. 76 | 77 | 78 | --- 79 | 80 | *Generated by Task Master Research Command* 81 | *Timestamp: 2025-06-14T22:10:00.394Z* 82 | ``` -------------------------------------------------------------------------------- /apps/docs/getting-started/quick-start/configuration-quick.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Configuration 3 | sidebarTitle: "Configuration" 4 | 5 | --- 6 | 7 | Before getting started with Task Master, you'll need to set up your API keys. There are a couple of ways to do this depending on whether you're using the CLI or working inside MCP. It's also a good time to start getting familiar with the other configuration options available — even if you don’t need to adjust them yet, knowing what’s possible will help down the line. 8 | 9 | ## API Key Setup 10 | 11 | Task Master uses environment variables to securely store provider API keys and optional endpoint URLs. 12 | 13 | ### MCP Usage: mcp.json file 14 | 15 | For MCP/Cursor usage: Configure keys in the env section of your .cursor/mcp.json file. 16 | 17 | ```java .env lines icon="java" 18 | { 19 | "mcpServers": { 20 | "task-master-ai": { 21 | "command": "npx", 22 | "args": ["-y", "task-master-ai"], 23 | "env": { 24 | "ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY_HERE", 25 | "PERPLEXITY_API_KEY": "PERPLEXITY_API_KEY_HERE", 26 | "OPENAI_API_KEY": "OPENAI_API_KEY_HERE", 27 | "GOOGLE_API_KEY": "GOOGLE_API_KEY_HERE", 28 | "XAI_API_KEY": "XAI_API_KEY_HERE", 29 | "OPENROUTER_API_KEY": "OPENROUTER_API_KEY_HERE", 30 | "MISTRAL_API_KEY": "MISTRAL_API_KEY_HERE", 31 | "AZURE_OPENAI_API_KEY": "AZURE_OPENAI_API_KEY_HERE", 32 | "OLLAMA_API_KEY": "OLLAMA_API_KEY_HERE", 33 | "GITHUB_API_KEY": "GITHUB_API_KEY_HERE" 34 | } 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | ### CLI Usage: `.env` File 41 | 42 | Create a `.env` file in your project root and include the keys for the providers you plan to use: 43 | 44 | 45 | 46 | ```java .env lines icon="java" 47 | # Required API keys for providers configured in .taskmaster/config.json 48 | ANTHROPIC_API_KEY=sk-ant-api03-your-key-here 49 | PERPLEXITY_API_KEY=pplx-your-key-here 50 | # OPENAI_API_KEY=sk-your-key-here 51 | # GOOGLE_API_KEY=AIzaSy... 52 | # AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here 53 | # etc. 54 | 55 | # Optional Endpoint Overrides 56 | # Use a specific provider's base URL, e.g., for an OpenAI-compatible API 57 | # OPENAI_BASE_URL=https://api.third-party.com/v1 58 | # 59 | # Azure OpenAI Configuration 60 | # AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/ or https://your-endpoint-name.cognitiveservices.azure.com/openai/deployments 61 | # OLLAMA_BASE_URL=http://custom-ollama-host:11434/api 62 | 63 | # Google Vertex AI Configuration (Required if using 'vertex' provider) 64 | # VERTEX_PROJECT_ID=your-gcp-project-id 65 | ``` 66 | 67 | ## What Else Can Be Configured? 68 | 69 | The main configuration file (`.taskmaster/config.json`) allows you to control nearly every aspect of Task Master’s behavior. Here’s a high-level look at what you can customize: 70 | 71 | <Tip> 72 | You don’t need to configure everything up front. Most settings can be left as defaults or updated later as your workflow evolves. 73 | </Tip> 74 | 75 | <Accordion title="View Configuration Options"> 76 | 77 | ### Models and Providers 78 | - Role-based model setup: `main`, `research`, `fallback` 79 | - Provider selection (Anthropic, OpenAI, Perplexity, etc.) 80 | - Model IDs per role 81 | - Temperature, max tokens, and other generation settings 82 | - Custom base URLs for OpenAI-compatible APIs 83 | 84 | ### Global Settings 85 | - `logLevel`: Logging verbosity 86 | - `debug`: Enable/disable debug mode 87 | - `projectName`: Optional name for your project 88 | - `defaultTag`: Default tag for task grouping 89 | - `defaultSubtasks`: Number of subtasks to auto-generate 90 | - `defaultPriority`: Priority level for new tasks 91 | 92 | ### API Endpoint Overrides 93 | - `ollamaBaseURL`: Custom Ollama server URL 94 | - `azureBaseURL`: Global Azure endpoint 95 | - `vertexProjectId`: Google Vertex AI project ID 96 | - `vertexLocation`: Region for Vertex AI models 97 | 98 | ### Tag and Git Integration 99 | - Default tag context per project 100 | - Support for task isolation by tag 101 | - Manual tag creation from Git branches 102 | 103 | ### State Management 104 | - Active tag tracking 105 | - Migration state 106 | - Last tag switch timestamp 107 | 108 | </Accordion> 109 | 110 | <Note> 111 | For advanced configuration options and detailed customization, see our [Advanced Configuration Guide](/docs/best-practices/configuration-advanced) page. 112 | </Note> ``` -------------------------------------------------------------------------------- /src/progress/tracker-ui.js: -------------------------------------------------------------------------------- ```javascript 1 | import chalk from 'chalk'; 2 | 3 | /** 4 | * Factory for creating progress bar elements 5 | */ 6 | class ProgressBarFactory { 7 | constructor(multibar) { 8 | if (!multibar) { 9 | throw new Error('Multibar instance is required'); 10 | } 11 | this.multibar = multibar; 12 | } 13 | 14 | /** 15 | * Creates a progress bar with the given format 16 | */ 17 | createBar(format, payload = {}) { 18 | if (typeof format !== 'string') { 19 | throw new Error('Format must be a string'); 20 | } 21 | 22 | const bar = this.multibar.create( 23 | 1, // total 24 | 1, // current 25 | {}, 26 | { 27 | format, 28 | barsize: 1, 29 | hideCursor: true, 30 | clearOnComplete: false 31 | } 32 | ); 33 | 34 | bar.update(1, payload); 35 | return bar; 36 | } 37 | 38 | /** 39 | * Creates a header with borders 40 | */ 41 | createHeader(headerFormat, borderFormat) { 42 | this.createBar(borderFormat); // Top border 43 | this.createBar(headerFormat); // Header 44 | this.createBar(borderFormat); // Bottom border 45 | } 46 | 47 | /** 48 | * Creates a data row 49 | */ 50 | createRow(rowFormat, payload) { 51 | if (!payload || typeof payload !== 'object') { 52 | throw new Error('Payload must be an object'); 53 | } 54 | return this.createBar(rowFormat, payload); 55 | } 56 | 57 | /** 58 | * Creates a border element 59 | */ 60 | createBorder(borderFormat) { 61 | return this.createBar(borderFormat); 62 | } 63 | } 64 | 65 | /** 66 | * Creates a bordered header for progress tables. 67 | * @param {Object} multibar - The multibar instance. 68 | * @param {string} headerFormat - Format string for the header row. 69 | * @param {string} borderFormat - Format string for the top and bottom borders. 70 | * @returns {void} 71 | */ 72 | export function createProgressHeader(multibar, headerFormat, borderFormat) { 73 | const factory = new ProgressBarFactory(multibar); 74 | factory.createHeader(headerFormat, borderFormat); 75 | } 76 | 77 | /** 78 | * Creates a formatted data row for progress tables. 79 | * @param {Object} multibar - The multibar instance. 80 | * @param {string} rowFormat - Format string for the row. 81 | * @param {Object} payload - Data payload for the row format. 82 | * @returns {void} 83 | */ 84 | export function createProgressRow(multibar, rowFormat, payload) { 85 | const factory = new ProgressBarFactory(multibar); 86 | factory.createRow(rowFormat, payload); 87 | } 88 | 89 | /** 90 | * Creates a border row for progress tables. 91 | * @param {Object} multibar - The multibar instance. 92 | * @param {string} borderFormat - Format string for the border. 93 | * @returns {void} 94 | */ 95 | export function createBorder(multibar, borderFormat) { 96 | const factory = new ProgressBarFactory(multibar); 97 | factory.createBorder(borderFormat); 98 | } 99 | 100 | /** 101 | * Builder for creating progress tables with consistent formatting 102 | */ 103 | export class ProgressTableBuilder { 104 | constructor(multibar) { 105 | this.factory = new ProgressBarFactory(multibar); 106 | this.borderStyle = '─'; 107 | this.columnSeparator = '|'; 108 | } 109 | 110 | /** 111 | * Shows a formatted table header 112 | */ 113 | showHeader(columns = null) { 114 | // Default columns for task display 115 | const defaultColumns = [ 116 | { text: 'TASK', width: 6 }, 117 | { text: 'PRI', width: 5 }, 118 | { text: 'TITLE', width: 64 } 119 | ]; 120 | 121 | const cols = columns || defaultColumns; 122 | const headerText = ' ' + cols.map((c) => c.text).join(' | ') + ' '; 123 | const borderLine = this.createBorderLine(cols.map((c) => c.width)); 124 | 125 | this.factory.createHeader(headerText, borderLine); 126 | return this; 127 | } 128 | 129 | /** 130 | * Creates a border line based on column widths 131 | */ 132 | createBorderLine(columnWidths) { 133 | return columnWidths 134 | .map((width) => this.borderStyle.repeat(width)) 135 | .join('─┼─'); 136 | } 137 | 138 | /** 139 | * Adds a task row to the table 140 | */ 141 | addTaskRow(taskId, priority, title) { 142 | const format = ` ${taskId} | ${priority} | {title}`; 143 | this.factory.createRow(format, { title }); 144 | 145 | // Add separator after each row 146 | const borderLine = '------+-----+' + '─'.repeat(64); 147 | this.factory.createBorder(borderLine); 148 | return this; 149 | } 150 | 151 | /** 152 | * Creates a summary row 153 | */ 154 | addSummaryRow(label, value) { 155 | const format = ` ${label}: {value}`; 156 | this.factory.createRow(format, { value }); 157 | return this; 158 | } 159 | } 160 | ``` -------------------------------------------------------------------------------- /tests/unit/mcp/tools/move-task-cross-tag-options.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | 3 | // Mocks 4 | const mockFindTasksPath = jest 5 | .fn() 6 | .mockReturnValue('/test/path/.taskmaster/tasks/tasks.json'); 7 | jest.unstable_mockModule( 8 | '../../../../mcp-server/src/core/utils/path-utils.js', 9 | () => ({ 10 | findTasksPath: mockFindTasksPath 11 | }) 12 | ); 13 | 14 | const mockEnableSilentMode = jest.fn(); 15 | const mockDisableSilentMode = jest.fn(); 16 | jest.unstable_mockModule('../../../../scripts/modules/utils.js', () => ({ 17 | enableSilentMode: mockEnableSilentMode, 18 | disableSilentMode: mockDisableSilentMode 19 | })); 20 | 21 | // Spyable mock for moveTasksBetweenTags 22 | const mockMoveTasksBetweenTags = jest.fn(); 23 | jest.unstable_mockModule( 24 | '../../../../scripts/modules/task-manager/move-task.js', 25 | () => ({ 26 | moveTasksBetweenTags: mockMoveTasksBetweenTags 27 | }) 28 | ); 29 | 30 | // Import after mocks 31 | const { moveTaskCrossTagDirect } = await import( 32 | '../../../../mcp-server/src/core/direct-functions/move-task-cross-tag.js' 33 | ); 34 | 35 | describe('MCP Cross-Tag Move Direct Function - options & suggestions', () => { 36 | const mockLog = { info: jest.fn(), warn: jest.fn(), error: jest.fn() }; 37 | 38 | beforeEach(() => { 39 | jest.clearAllMocks(); 40 | }); 41 | 42 | it('passes only withDependencies/ignoreDependencies (no force) to core', async () => { 43 | // Arrange: make core throw tag validation after call to capture params 44 | mockMoveTasksBetweenTags.mockImplementation(() => { 45 | const err = new Error('Source tag "invalid" not found or invalid'); 46 | err.code = 'INVALID_SOURCE_TAG'; 47 | throw err; 48 | }); 49 | 50 | // Act 51 | await moveTaskCrossTagDirect( 52 | { 53 | sourceIds: '1,2', 54 | sourceTag: 'backlog', 55 | targetTag: 'in-progress', 56 | withDependencies: true, 57 | projectRoot: '/test' 58 | }, 59 | mockLog 60 | ); 61 | 62 | // Assert options argument (5th param) 63 | expect(mockMoveTasksBetweenTags).toHaveBeenCalled(); 64 | const args = mockMoveTasksBetweenTags.mock.calls[0]; 65 | const moveOptions = args[4]; 66 | expect(moveOptions).toEqual({ 67 | withDependencies: true, 68 | ignoreDependencies: false 69 | }); 70 | expect('force' in moveOptions).toBe(false); 71 | }); 72 | 73 | it('returns conflict suggestions on cross-tag dependency conflicts', async () => { 74 | // Arrange: core throws cross-tag dependency conflicts 75 | mockMoveTasksBetweenTags.mockImplementation(() => { 76 | const err = new Error( 77 | 'Cannot move tasks: 2 cross-tag dependency conflicts found' 78 | ); 79 | err.code = 'CROSS_TAG_DEPENDENCY_CONFLICTS'; 80 | throw err; 81 | }); 82 | 83 | // Act 84 | const result = await moveTaskCrossTagDirect( 85 | { 86 | sourceIds: '1', 87 | sourceTag: 'backlog', 88 | targetTag: 'in-progress', 89 | projectRoot: '/test' 90 | }, 91 | mockLog 92 | ); 93 | 94 | // Assert 95 | expect(result.success).toBe(false); 96 | expect(result.error.code).toBe('CROSS_TAG_DEPENDENCY_CONFLICT'); 97 | expect(Array.isArray(result.error.suggestions)).toBe(true); 98 | // Key suggestions 99 | const s = result.error.suggestions.join(' '); 100 | expect(s).toContain('--with-dependencies'); 101 | expect(s).toContain('--ignore-dependencies'); 102 | expect(s).toContain('validate-dependencies'); 103 | expect(s).toContain('Move dependencies first'); 104 | }); 105 | 106 | it('returns ID collision suggestions when target tag already has the ID', async () => { 107 | // Arrange: core throws TASK_ALREADY_EXISTS structured error 108 | mockMoveTasksBetweenTags.mockImplementation(() => { 109 | const err = new Error( 110 | 'Task 1 already exists in target tag "in-progress"' 111 | ); 112 | err.code = 'TASK_ALREADY_EXISTS'; 113 | throw err; 114 | }); 115 | 116 | // Act 117 | const result = await moveTaskCrossTagDirect( 118 | { 119 | sourceIds: '1', 120 | sourceTag: 'backlog', 121 | targetTag: 'in-progress', 122 | projectRoot: '/test' 123 | }, 124 | mockLog 125 | ); 126 | 127 | // Assert 128 | expect(result.success).toBe(false); 129 | expect(result.error.code).toBe('TASK_ALREADY_EXISTS'); 130 | const joined = (result.error.suggestions || []).join(' '); 131 | expect(joined).toContain('different target tag'); 132 | expect(joined).toContain('different set of IDs'); 133 | expect(joined).toContain('within-tag'); 134 | }); 135 | }); 136 | ``` -------------------------------------------------------------------------------- /.github/workflows/weekly-metrics-discord.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Weekly Metrics to Discord 2 | # description: Sends weekly metrics summary to Discord channel 3 | 4 | on: 5 | schedule: 6 | - cron: "0 9 * * 1" # Every Monday at 9 AM 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | issues: read 12 | pull-requests: read 13 | 14 | jobs: 15 | weekly-metrics: 16 | runs-on: ubuntu-latest 17 | env: 18 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }} 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: '20' 27 | 28 | - name: Get dates for last 14 days 29 | run: | 30 | set -Eeuo pipefail 31 | # Last 14 days 32 | first_day=$(date -d "14 days ago" +%Y-%m-%d) 33 | last_day=$(date +%Y-%m-%d) 34 | 35 | echo "first_day=$first_day" >> $GITHUB_ENV 36 | echo "last_day=$last_day" >> $GITHUB_ENV 37 | echo "week_of=$(date -d '7 days ago' +'Week of %B %d, %Y')" >> $GITHUB_ENV 38 | echo "date_range=Past 14 days ($first_day to $last_day)" >> $GITHUB_ENV 39 | 40 | - name: Generate issue metrics 41 | uses: github/issue-metrics@v3 42 | env: 43 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | SEARCH_QUERY: "repo:${{ github.repository }} is:issue created:${{ env.first_day }}..${{ env.last_day }}" 45 | HIDE_TIME_TO_ANSWER: true 46 | HIDE_LABEL_METRICS: false 47 | OUTPUT_FILE: issue_metrics.md 48 | 49 | - name: Generate PR created metrics 50 | uses: github/issue-metrics@v3 51 | env: 52 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | SEARCH_QUERY: "repo:${{ github.repository }} is:pr created:${{ env.first_day }}..${{ env.last_day }}" 54 | OUTPUT_FILE: pr_created_metrics.md 55 | 56 | - name: Generate PR merged metrics 57 | uses: github/issue-metrics@v3 58 | env: 59 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | SEARCH_QUERY: "repo:${{ github.repository }} is:pr is:merged merged:${{ env.first_day }}..${{ env.last_day }}" 61 | OUTPUT_FILE: pr_merged_metrics.md 62 | 63 | - name: Debug generated metrics 64 | run: | 65 | set -Eeuo pipefail 66 | echo "Listing markdown files in workspace:" 67 | ls -la *.md || true 68 | for f in issue_metrics.md pr_created_metrics.md pr_merged_metrics.md; do 69 | if [ -f "$f" ]; then 70 | echo "== $f (first 10 lines) ==" 71 | head -n 10 "$f" 72 | else 73 | echo "Missing $f" 74 | fi 75 | done 76 | 77 | - name: Parse metrics 78 | id: metrics 79 | run: node .github/scripts/parse-metrics.mjs 80 | 81 | - name: Send to Discord 82 | uses: sarisia/actions-status-discord@v1 83 | if: env.DISCORD_WEBHOOK != '' 84 | with: 85 | webhook: ${{ env.DISCORD_WEBHOOK }} 86 | status: Success 87 | title: "📊 Weekly Metrics Report" 88 | description: | 89 | **${{ env.week_of }}** 90 | *${{ env.date_range }}* 91 | 92 | **🎯 Issues** 93 | • Created: ${{ steps.metrics.outputs.issues_created }} 94 | • Closed: ${{ steps.metrics.outputs.issues_closed }} 95 | • Avg Response Time: ${{ steps.metrics.outputs.issue_avg_first_response }} 96 | • Avg Time to Close: ${{ steps.metrics.outputs.issue_avg_time_to_close }} 97 | 98 | **🔀 Pull Requests** 99 | • Created: ${{ steps.metrics.outputs.prs_created }} 100 | • Merged: ${{ steps.metrics.outputs.prs_merged }} 101 | • Avg Response Time: ${{ steps.metrics.outputs.pr_avg_first_response }} 102 | • Avg Time to Merge: ${{ steps.metrics.outputs.pr_avg_merge_time }} 103 | 104 | **📈 Visual Analytics** 105 | https://repobeats.axiom.co/api/embed/b439f28f0ab5bd7a2da19505355693cd2c55bfd4.svg 106 | color: 0x58AFFF 107 | username: Task Master Metrics Bot 108 | avatar_url: https://raw.githubusercontent.com/eyaltoledano/claude-task-master/main/images/logo.png 109 | ``` -------------------------------------------------------------------------------- /src/ai-providers/custom-sdk/claude-code/message-converter.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * @fileoverview Converts AI SDK prompt format to Claude Code message format 3 | */ 4 | 5 | /** 6 | * Convert AI SDK prompt to Claude Code messages format 7 | * @param {Array} prompt - AI SDK prompt array 8 | * @param {Object} [mode] - Generation mode 9 | * @param {string} mode.type - Mode type ('regular', 'object-json', 'object-tool') 10 | * @returns {{messagesPrompt: string, systemPrompt?: string}} 11 | */ 12 | export function convertToClaudeCodeMessages(prompt, mode) { 13 | const messages = []; 14 | let systemPrompt; 15 | 16 | for (const message of prompt) { 17 | switch (message.role) { 18 | case 'system': 19 | systemPrompt = message.content; 20 | break; 21 | 22 | case 'user': 23 | if (typeof message.content === 'string') { 24 | messages.push(message.content); 25 | } else { 26 | // Handle multi-part content 27 | const textParts = message.content 28 | .filter((part) => part.type === 'text') 29 | .map((part) => part.text) 30 | .join('\n'); 31 | 32 | if (textParts) { 33 | messages.push(textParts); 34 | } 35 | 36 | // Note: Image parts are not supported by Claude Code CLI 37 | const imageParts = message.content.filter( 38 | (part) => part.type === 'image' 39 | ); 40 | if (imageParts.length > 0) { 41 | console.warn( 42 | 'Claude Code CLI does not support image inputs. Images will be ignored.' 43 | ); 44 | } 45 | } 46 | break; 47 | 48 | case 'assistant': 49 | if (typeof message.content === 'string') { 50 | messages.push(`Assistant: ${message.content}`); 51 | } else { 52 | const textParts = message.content 53 | .filter((part) => part.type === 'text') 54 | .map((part) => part.text) 55 | .join('\n'); 56 | 57 | if (textParts) { 58 | messages.push(`Assistant: ${textParts}`); 59 | } 60 | 61 | // Handle tool calls if present 62 | const toolCalls = message.content.filter( 63 | (part) => part.type === 'tool-call' 64 | ); 65 | if (toolCalls.length > 0) { 66 | // For now, we'll just note that tool calls were made 67 | messages.push(`Assistant: [Tool calls made]`); 68 | } 69 | } 70 | break; 71 | 72 | case 'tool': 73 | // Tool results could be included in the conversation 74 | messages.push( 75 | `Tool Result (${message.content[0].toolName}): ${JSON.stringify( 76 | message.content[0].result 77 | )}` 78 | ); 79 | break; 80 | } 81 | } 82 | 83 | // For the SDK, we need to provide a single prompt string 84 | // Format the conversation history properly 85 | 86 | // Combine system prompt with messages 87 | let finalPrompt = ''; 88 | 89 | // Add system prompt at the beginning if present 90 | if (systemPrompt) { 91 | finalPrompt = systemPrompt; 92 | } 93 | 94 | if (messages.length === 0) { 95 | return { messagesPrompt: finalPrompt, systemPrompt }; 96 | } 97 | 98 | // Format messages 99 | const formattedMessages = []; 100 | for (let i = 0; i < messages.length; i++) { 101 | const msg = messages[i]; 102 | // Check if this is a user or assistant message based on content 103 | if (msg.startsWith('Assistant:') || msg.startsWith('Tool Result')) { 104 | formattedMessages.push(msg); 105 | } else { 106 | // User messages 107 | formattedMessages.push(`Human: ${msg}`); 108 | } 109 | } 110 | 111 | // Combine system prompt with messages 112 | if (finalPrompt) { 113 | finalPrompt = finalPrompt + '\n\n' + formattedMessages.join('\n\n'); 114 | } else { 115 | finalPrompt = formattedMessages.join('\n\n'); 116 | } 117 | 118 | // For JSON mode, add explicit instruction to ensure JSON output 119 | if (mode?.type === 'object-json') { 120 | // Make the JSON instruction even more explicit 121 | finalPrompt = `${finalPrompt} 122 | 123 | CRITICAL INSTRUCTION: You MUST respond with ONLY valid JSON. Follow these rules EXACTLY: 124 | 1. Start your response with an opening brace { 125 | 2. End your response with a closing brace } 126 | 3. Do NOT include any text before the opening brace 127 | 4. Do NOT include any text after the closing brace 128 | 5. Do NOT use markdown code blocks or backticks 129 | 6. Do NOT include explanations or commentary 130 | 7. The ENTIRE response must be valid JSON that can be parsed with JSON.parse() 131 | 132 | Begin your response with { and end with }`; 133 | } 134 | 135 | return { 136 | messagesPrompt: finalPrompt, 137 | systemPrompt 138 | }; 139 | } 140 | ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/utils/logger.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Webview Logger Utility 3 | * Provides conditional logging based on environment 4 | */ 5 | 6 | type LogLevel = 'log' | 'warn' | 'error' | 'debug' | 'info'; 7 | 8 | interface LogEntry { 9 | level: LogLevel; 10 | message: string; 11 | data?: any; 12 | timestamp: number; 13 | } 14 | 15 | class WebviewLogger { 16 | private static instance: WebviewLogger; 17 | private enabled: boolean; 18 | private logHistory: LogEntry[] = []; 19 | private maxHistorySize = 100; 20 | 21 | private constructor() { 22 | // Enable logging in development, disable in production 23 | // Check for development mode via various indicators 24 | this.enabled = this.isDevelopment(); 25 | } 26 | 27 | static getInstance(): WebviewLogger { 28 | if (!WebviewLogger.instance) { 29 | WebviewLogger.instance = new WebviewLogger(); 30 | } 31 | return WebviewLogger.instance; 32 | } 33 | 34 | private isDevelopment(): boolean { 35 | // Check various indicators for development mode 36 | // VS Code webviews don't have process.env, so we check other indicators 37 | return ( 38 | // Check if running in localhost (development server) 39 | window.location.hostname === 'localhost' || 40 | // Check for development query parameter 41 | window.location.search.includes('debug=true') || 42 | // Check for VS Code development mode indicator 43 | (window as any).__VSCODE_DEV_MODE__ === true || 44 | // Default to false in production 45 | false 46 | ); 47 | } 48 | 49 | private addToHistory(entry: LogEntry): void { 50 | this.logHistory.push(entry); 51 | if (this.logHistory.length > this.maxHistorySize) { 52 | this.logHistory.shift(); 53 | } 54 | } 55 | 56 | private logMessage(level: LogLevel, message: string, ...args: any[]): void { 57 | const entry: LogEntry = { 58 | level, 59 | message, 60 | data: args.length > 0 ? args : undefined, 61 | timestamp: Date.now() 62 | }; 63 | 64 | this.addToHistory(entry); 65 | 66 | if (!this.enabled) { 67 | return; 68 | } 69 | 70 | // Format the message with timestamp 71 | const timestamp = new Date().toISOString(); 72 | const prefix = `[${timestamp}] [${level.toUpperCase()}]`; 73 | 74 | // Use appropriate console method 75 | switch (level) { 76 | case 'error': 77 | console.error(prefix, message, ...args); 78 | break; 79 | case 'warn': 80 | console.warn(prefix, message, ...args); 81 | break; 82 | case 'debug': 83 | console.debug(prefix, message, ...args); 84 | break; 85 | case 'info': 86 | console.info(prefix, message, ...args); 87 | break; 88 | default: 89 | console.log(prefix, message, ...args); 90 | } 91 | } 92 | 93 | log(message: string, ...args: any[]): void { 94 | this.logMessage('log', message, ...args); 95 | } 96 | 97 | error(message: string, ...args: any[]): void { 98 | // Always log errors, even in production 99 | const entry: LogEntry = { 100 | level: 'error', 101 | message, 102 | data: args.length > 0 ? args : undefined, 103 | timestamp: Date.now() 104 | }; 105 | this.addToHistory(entry); 106 | console.error(`[${new Date().toISOString()}] [ERROR]`, message, ...args); 107 | } 108 | 109 | warn(message: string, ...args: any[]): void { 110 | this.logMessage('warn', message, ...args); 111 | } 112 | 113 | debug(message: string, ...args: any[]): void { 114 | this.logMessage('debug', message, ...args); 115 | } 116 | 117 | info(message: string, ...args: any[]): void { 118 | this.logMessage('info', message, ...args); 119 | } 120 | 121 | // Enable/disable logging dynamically 122 | setEnabled(enabled: boolean): void { 123 | this.enabled = enabled; 124 | if (enabled) { 125 | console.log('[WebviewLogger] Logging enabled'); 126 | } 127 | } 128 | 129 | // Get log history (useful for debugging) 130 | getHistory(): LogEntry[] { 131 | return [...this.logHistory]; 132 | } 133 | 134 | // Clear log history 135 | clearHistory(): void { 136 | this.logHistory = []; 137 | } 138 | 139 | // Export logs as string (useful for bug reports) 140 | exportLogs(): string { 141 | return this.logHistory 142 | .map((entry) => { 143 | const timestamp = new Date(entry.timestamp).toISOString(); 144 | const data = entry.data ? JSON.stringify(entry.data) : ''; 145 | return `[${timestamp}] [${entry.level.toUpperCase()}] ${entry.message} ${data}`; 146 | }) 147 | .join('\n'); 148 | } 149 | } 150 | 151 | // Export singleton instance 152 | export const logger = WebviewLogger.getInstance(); 153 | 154 | // Export type for use in other files 155 | export type { WebviewLogger }; 156 | ``` -------------------------------------------------------------------------------- /src/profiles/roo.js: -------------------------------------------------------------------------------- ```javascript 1 | // Roo Code conversion profile for rule-transformer 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import { isSilentMode, log } from '../../scripts/modules/utils.js'; 5 | import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js'; 6 | import { ROO_MODES } from '../constants/profiles.js'; 7 | 8 | // Lifecycle functions for Roo profile 9 | function onAddRulesProfile(targetDir, assetsDir) { 10 | // Use the provided assets directory to find the roocode directory 11 | const sourceDir = path.join(assetsDir, 'roocode'); 12 | 13 | if (!fs.existsSync(sourceDir)) { 14 | log('error', `[Roo] Source directory does not exist: ${sourceDir}`); 15 | return; 16 | } 17 | 18 | copyRecursiveSync(sourceDir, targetDir); 19 | log('debug', `[Roo] Copied roocode directory to ${targetDir}`); 20 | 21 | const rooModesDir = path.join(sourceDir, '.roo'); 22 | 23 | // Copy .roomodes to project root 24 | const roomodesSrc = path.join(sourceDir, '.roomodes'); 25 | const roomodesDest = path.join(targetDir, '.roomodes'); 26 | if (fs.existsSync(roomodesSrc)) { 27 | try { 28 | fs.copyFileSync(roomodesSrc, roomodesDest); 29 | log('debug', `[Roo] Copied .roomodes to ${roomodesDest}`); 30 | } catch (err) { 31 | log('error', `[Roo] Failed to copy .roomodes: ${err.message}`); 32 | } 33 | } 34 | 35 | for (const mode of ROO_MODES) { 36 | const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`); 37 | const dest = path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`); 38 | if (fs.existsSync(src)) { 39 | try { 40 | const destDir = path.dirname(dest); 41 | if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true }); 42 | fs.copyFileSync(src, dest); 43 | log('debug', `[Roo] Copied ${mode}-rules to ${dest}`); 44 | } catch (err) { 45 | log('error', `[Roo] Failed to copy ${src} to ${dest}: ${err.message}`); 46 | } 47 | } 48 | } 49 | } 50 | 51 | function copyRecursiveSync(src, dest) { 52 | const exists = fs.existsSync(src); 53 | const stats = exists && fs.statSync(src); 54 | const isDirectory = exists && stats.isDirectory(); 55 | if (isDirectory) { 56 | if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); 57 | fs.readdirSync(src).forEach((childItemName) => { 58 | copyRecursiveSync( 59 | path.join(src, childItemName), 60 | path.join(dest, childItemName) 61 | ); 62 | }); 63 | } else { 64 | fs.copyFileSync(src, dest); 65 | } 66 | } 67 | 68 | function onRemoveRulesProfile(targetDir) { 69 | const roomodesPath = path.join(targetDir, '.roomodes'); 70 | if (fs.existsSync(roomodesPath)) { 71 | try { 72 | fs.rmSync(roomodesPath, { force: true }); 73 | log('debug', `[Roo] Removed .roomodes from ${roomodesPath}`); 74 | } catch (err) { 75 | log('error', `[Roo] Failed to remove .roomodes: ${err.message}`); 76 | } 77 | } 78 | 79 | const rooDir = path.join(targetDir, '.roo'); 80 | if (fs.existsSync(rooDir)) { 81 | fs.readdirSync(rooDir).forEach((entry) => { 82 | if (entry.startsWith('rules-')) { 83 | const modeDir = path.join(rooDir, entry); 84 | try { 85 | fs.rmSync(modeDir, { recursive: true, force: true }); 86 | log('debug', `[Roo] Removed ${entry} directory from ${modeDir}`); 87 | } catch (err) { 88 | log('error', `[Roo] Failed to remove ${modeDir}: ${err.message}`); 89 | } 90 | } 91 | }); 92 | if (fs.readdirSync(rooDir).length === 0) { 93 | try { 94 | fs.rmSync(rooDir, { recursive: true, force: true }); 95 | log('debug', `[Roo] Removed empty .roo directory from ${rooDir}`); 96 | } catch (err) { 97 | log('error', `[Roo] Failed to remove .roo directory: ${err.message}`); 98 | } 99 | } 100 | } 101 | } 102 | 103 | function onPostConvertRulesProfile(targetDir, assetsDir) { 104 | onAddRulesProfile(targetDir, assetsDir); 105 | } 106 | 107 | // Create and export roo profile using the base factory 108 | export const rooProfile = createProfile({ 109 | name: 'roo', 110 | displayName: 'Roo Code', 111 | url: 'roocode.com', 112 | docsUrl: 'docs.roocode.com', 113 | toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE, 114 | onAdd: onAddRulesProfile, 115 | onRemove: onRemoveRulesProfile, 116 | onPostConvert: onPostConvertRulesProfile 117 | }); 118 | 119 | // Export lifecycle functions separately to avoid naming conflicts 120 | export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile }; 121 | ``` -------------------------------------------------------------------------------- /tests/integration/profiles/roo-files-inclusion.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import os from 'os'; 5 | import { execSync } from 'child_process'; 6 | 7 | describe('Roo Files Inclusion in Package', () => { 8 | // This test verifies that the required Roo files are included in the final package 9 | 10 | test('package.json includes dist/** in the "files" array for bundled files', () => { 11 | // Read the package.json file 12 | const packageJsonPath = path.join(process.cwd(), 'package.json'); 13 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 14 | 15 | // Check if dist/** is included in the files array (which contains bundled output including Roo files) 16 | expect(packageJson.files).toContain('dist/**'); 17 | }); 18 | 19 | test('roo.js profile contains logic for Roo directory creation and file copying', () => { 20 | // Read the roo.js profile file 21 | const rooJsPath = path.join(process.cwd(), 'src', 'profiles', 'roo.js'); 22 | const rooJsContent = fs.readFileSync(rooJsPath, 'utf8'); 23 | 24 | // Check for the main handler function 25 | expect( 26 | rooJsContent.includes('onAddRulesProfile(targetDir, assetsDir)') 27 | ).toBe(true); 28 | 29 | // Check for general recursive copy of assets/roocode 30 | expect( 31 | rooJsContent.includes('copyRecursiveSync(sourceDir, targetDir)') 32 | ).toBe(true); 33 | 34 | // Check for updated path handling 35 | expect(rooJsContent.includes("path.join(assetsDir, 'roocode')")).toBe(true); 36 | 37 | // Check for .roomodes file copying logic (source and destination paths) 38 | expect(rooJsContent.includes("path.join(sourceDir, '.roomodes')")).toBe( 39 | true 40 | ); 41 | expect(rooJsContent.includes("path.join(targetDir, '.roomodes')")).toBe( 42 | true 43 | ); 44 | 45 | // Check for mode-specific rule file copying logic 46 | expect(rooJsContent.includes('for (const mode of ROO_MODES)')).toBe(true); 47 | expect( 48 | rooJsContent.includes( 49 | 'path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`)' 50 | ) 51 | ).toBe(true); 52 | expect( 53 | rooJsContent.includes( 54 | "path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`)" 55 | ) 56 | ).toBe(true); 57 | 58 | // Check for import of ROO_MODES from profiles.js instead of local definition 59 | expect( 60 | rooJsContent.includes( 61 | "import { ROO_MODES } from '../constants/profiles.js'" 62 | ) 63 | ).toBe(true); 64 | 65 | // Verify ROO_MODES is used in the for loop 66 | expect(rooJsContent.includes('for (const mode of ROO_MODES)')).toBe(true); 67 | 68 | // Verify mode variable is used in the template strings (this confirms modes are being processed) 69 | expect(rooJsContent.includes('rules-${mode}')).toBe(true); 70 | expect(rooJsContent.includes('${mode}-rules')).toBe(true); 71 | 72 | // Verify that the ROO_MODES constant is properly imported and used 73 | // We should be able to find the template literals that use the mode variable 74 | expect(rooJsContent.includes('`rules-${mode}`')).toBe(true); 75 | expect(rooJsContent.includes('`${mode}-rules`')).toBe(true); 76 | expect(rooJsContent.includes('Copied ${mode}-rules to ${dest}')).toBe(true); 77 | 78 | // Also verify that the expected mode names are defined in the imported constant 79 | // by checking that the import is from the correct file that contains all 6 modes 80 | const profilesConstantsPath = path.join( 81 | process.cwd(), 82 | 'src', 83 | 'constants', 84 | 'profiles.js' 85 | ); 86 | const profilesContent = fs.readFileSync(profilesConstantsPath, 'utf8'); 87 | 88 | // Check that ROO_MODES is exported and contains all expected modes 89 | expect(profilesContent.includes('export const ROO_MODES')).toBe(true); 90 | const expectedModes = [ 91 | 'architect', 92 | 'ask', 93 | 'orchestrator', 94 | 'code', 95 | 'debug', 96 | 'test' 97 | ]; 98 | expectedModes.forEach((mode) => { 99 | expect(profilesContent.includes(`'${mode}'`)).toBe(true); 100 | }); 101 | }); 102 | 103 | test('source Roo files exist in assets directory', () => { 104 | // Verify that the source files for Roo integration exist 105 | expect( 106 | fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roo')) 107 | ).toBe(true); 108 | expect( 109 | fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roomodes')) 110 | ).toBe(true); 111 | }); 112 | }); 113 | ``` -------------------------------------------------------------------------------- /apps/extension/src/services/terminal-manager.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Terminal Manager - Handles task execution in VS Code terminals 3 | * Uses @tm/core for consistent task management with the CLI 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { createTaskMasterCore, type TaskMasterCore } from '@tm/core'; 8 | import type { ExtensionLogger } from '../utils/logger'; 9 | 10 | export interface TerminalExecutionOptions { 11 | taskId: string; 12 | taskTitle: string; 13 | tag?: string; 14 | } 15 | 16 | export interface TerminalExecutionResult { 17 | success: boolean; 18 | error?: string; 19 | terminalName?: string; 20 | } 21 | 22 | export class TerminalManager { 23 | private terminals = new Map<string, vscode.Terminal>(); 24 | private tmCore?: TaskMasterCore; 25 | 26 | constructor( 27 | private context: vscode.ExtensionContext, 28 | private logger: ExtensionLogger 29 | ) {} 30 | 31 | /** 32 | * Execute a task in a new VS Code terminal with Claude 33 | * Uses @tm/core for consistent task management with the CLI 34 | */ 35 | async executeTask( 36 | options: TerminalExecutionOptions 37 | ): Promise<TerminalExecutionResult> { 38 | const { taskTitle, tag } = options; 39 | // Ensure taskId is always a string 40 | const taskId = String(options.taskId); 41 | 42 | this.logger.log( 43 | `Starting task execution for ${taskId}: ${taskTitle}${tag ? ` (tag: ${tag})` : ''}` 44 | ); 45 | this.logger.log(`TaskId type: ${typeof taskId}, value: ${taskId}`); 46 | 47 | try { 48 | // Initialize tm-core if needed 49 | await this.initializeCore(); 50 | 51 | // Use tm-core to start the task (same as CLI) 52 | const startResult = await this.tmCore!.startTask(taskId, { 53 | dryRun: false, 54 | force: false, 55 | updateStatus: true 56 | }); 57 | 58 | if (!startResult.started || !startResult.executionOutput) { 59 | throw new Error( 60 | startResult.error || 'Failed to start task with tm-core' 61 | ); 62 | } 63 | 64 | // Create terminal with custom TaskMaster icon 65 | const terminalName = `Task ${taskId}: ${taskTitle}`; 66 | const terminal = this.createTerminal(terminalName); 67 | 68 | // Store terminal reference for potential cleanup 69 | this.terminals.set(taskId, terminal); 70 | 71 | // Show terminal and run Claude command 72 | terminal.show(); 73 | const command = `claude "${startResult.executionOutput}"`; 74 | terminal.sendText(command); 75 | 76 | this.logger.log(`Launched Claude for task ${taskId} using tm-core`); 77 | 78 | return { 79 | success: true, 80 | terminalName 81 | }; 82 | } catch (error) { 83 | this.logger.error('Failed to execute task:', error); 84 | return { 85 | success: false, 86 | error: error instanceof Error ? error.message : 'Unknown error' 87 | }; 88 | } 89 | } 90 | 91 | /** 92 | * Create a new terminal with TaskMaster branding 93 | */ 94 | private createTerminal(name: string): vscode.Terminal { 95 | const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; 96 | 97 | return vscode.window.createTerminal({ 98 | name, 99 | cwd: workspaceRoot, 100 | iconPath: new vscode.ThemeIcon('play') // Use a VS Code built-in icon for now 101 | }); 102 | } 103 | 104 | /** 105 | * Initialize TaskMaster Core (same as CLI) 106 | */ 107 | private async initializeCore(): Promise<void> { 108 | if (!this.tmCore) { 109 | const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; 110 | if (!workspaceRoot) { 111 | throw new Error('No workspace folder found'); 112 | } 113 | this.tmCore = await createTaskMasterCore({ projectPath: workspaceRoot }); 114 | } 115 | } 116 | 117 | /** 118 | * Get terminal by task ID (if still active) 119 | */ 120 | getTerminalByTaskId(taskId: string): vscode.Terminal | undefined { 121 | return this.terminals.get(taskId); 122 | } 123 | 124 | /** 125 | * Clean up terminated terminals 126 | */ 127 | cleanupTerminal(taskId: string): void { 128 | const terminal = this.terminals.get(taskId); 129 | if (terminal) { 130 | this.terminals.delete(taskId); 131 | } 132 | } 133 | 134 | /** 135 | * Dispose all managed terminals and clean up tm-core 136 | */ 137 | async dispose(): Promise<void> { 138 | this.terminals.forEach((terminal) => { 139 | try { 140 | terminal.dispose(); 141 | } catch (error) { 142 | this.logger.error('Failed to dispose terminal:', error); 143 | } 144 | }); 145 | this.terminals.clear(); 146 | 147 | if (this.tmCore) { 148 | try { 149 | await this.tmCore.close(); 150 | this.tmCore = undefined; 151 | } catch (error) { 152 | this.logger.error('Failed to close tm-core:', error); 153 | } 154 | } 155 | } 156 | } 157 | ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/remove-task.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | 3 | // --- Mock dependencies BEFORE module import --- 4 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ 5 | readJSON: jest.fn(), 6 | writeJSON: jest.fn(), 7 | log: jest.fn(), 8 | CONFIG: { 9 | model: 'mock-model', 10 | maxTokens: 4000, 11 | temperature: 0.7, 12 | debug: false 13 | }, 14 | findTaskById: jest.fn(), 15 | truncate: jest.fn((t) => t), 16 | isSilentMode: jest.fn(() => false) 17 | })); 18 | 19 | jest.unstable_mockModule( 20 | '../../../../../scripts/modules/task-manager/generate-task-files.js', 21 | () => ({ 22 | default: jest.fn().mockResolvedValue() 23 | }) 24 | ); 25 | 26 | // fs is used for file deletion side-effects – stub the methods we touch 27 | jest.unstable_mockModule('fs', () => ({ 28 | existsSync: jest.fn(() => true), 29 | unlinkSync: jest.fn() 30 | })); 31 | 32 | // path is fine to keep as real since only join/dirname used – no side effects 33 | 34 | // Import mocked modules 35 | const { readJSON, writeJSON, log } = await import( 36 | '../../../../../scripts/modules/utils.js' 37 | ); 38 | const generateTaskFiles = ( 39 | await import( 40 | '../../../../../scripts/modules/task-manager/generate-task-files.js' 41 | ) 42 | ).default; 43 | const fs = await import('fs'); 44 | 45 | // Import module under test (AFTER mocks in place) 46 | const { default: removeTask } = await import( 47 | '../../../../../scripts/modules/task-manager/remove-task.js' 48 | ); 49 | 50 | // ---- Test data helpers ---- 51 | const buildSampleTaggedTasks = () => ({ 52 | master: { 53 | tasks: [ 54 | { id: 1, title: 'Task 1', status: 'pending', dependencies: [] }, 55 | { id: 2, title: 'Task 2', status: 'pending', dependencies: [1] }, 56 | { 57 | id: 3, 58 | title: 'Parent', 59 | status: 'pending', 60 | dependencies: [], 61 | subtasks: [ 62 | { id: 1, title: 'Sub 3.1', status: 'pending', dependencies: [] } 63 | ] 64 | } 65 | ] 66 | }, 67 | other: { 68 | tasks: [{ id: 99, title: 'Shadow', status: 'pending', dependencies: [1] }] 69 | } 70 | }); 71 | 72 | // Utility to deep clone sample each test 73 | const getFreshData = () => JSON.parse(JSON.stringify(buildSampleTaggedTasks())); 74 | 75 | // ----- Tests ----- 76 | 77 | describe('removeTask', () => { 78 | beforeEach(() => { 79 | jest.clearAllMocks(); 80 | // readJSON returns deep copy so each test isolated 81 | readJSON.mockImplementation(() => { 82 | return { 83 | ...getFreshData().master, 84 | tag: 'master', 85 | _rawTaggedData: getFreshData() 86 | }; 87 | }); 88 | writeJSON.mockResolvedValue(); 89 | log.mockImplementation(() => {}); 90 | fs.unlinkSync.mockImplementation(() => {}); 91 | }); 92 | 93 | test('removes a main task and cleans dependencies across tags', async () => { 94 | const result = await removeTask('tasks/tasks.json', '1', { tag: 'master' }); 95 | 96 | // Expect success true 97 | expect(result.success).toBe(true); 98 | // writeJSON called with data where task 1 is gone in master & dependencies removed in other tags 99 | const written = writeJSON.mock.calls[0][1]; 100 | expect(written.master.tasks.find((t) => t.id === 1)).toBeUndefined(); 101 | // deps removed from child tasks 102 | const task2 = written.master.tasks.find((t) => t.id === 2); 103 | expect(task2.dependencies).not.toContain(1); 104 | const shadow = written.other.tasks.find((t) => t.id === 99); 105 | expect(shadow.dependencies).not.toContain(1); 106 | // Task file deletion attempted 107 | expect(fs.unlinkSync).toHaveBeenCalled(); 108 | }); 109 | 110 | test('removes a subtask only and leaves parent intact', async () => { 111 | const result = await removeTask('tasks/tasks.json', '3.1', { 112 | tag: 'master' 113 | }); 114 | 115 | expect(result.success).toBe(true); 116 | const written = writeJSON.mock.calls[0][1]; 117 | const parent = written.master.tasks.find((t) => t.id === 3); 118 | expect(parent.subtasks || []).toHaveLength(0); 119 | // Ensure parent still exists 120 | expect(parent).toBeDefined(); 121 | // No task files should be deleted for subtasks 122 | expect(fs.unlinkSync).not.toHaveBeenCalled(); 123 | }); 124 | 125 | test('handles non-existent task gracefully', async () => { 126 | const result = await removeTask('tasks/tasks.json', '42', { 127 | tag: 'master' 128 | }); 129 | expect(result.success).toBe(false); 130 | expect(result.error).toContain('not found'); 131 | // writeJSON not called because nothing changed 132 | expect(writeJSON).not.toHaveBeenCalled(); 133 | }); 134 | }); 135 | ``` -------------------------------------------------------------------------------- /tests/unit/prompts/expand-task-prompt.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import { PromptManager } from '../../../scripts/modules/prompt-manager.js'; 3 | 4 | describe('expand-task prompt template', () => { 5 | let promptManager; 6 | 7 | beforeEach(() => { 8 | promptManager = new PromptManager(); 9 | }); 10 | 11 | const testTask = { 12 | id: 1, 13 | title: 'Setup AWS Infrastructure', 14 | description: 'Provision core AWS services', 15 | details: 'Create VPC, subnets, and security groups' 16 | }; 17 | 18 | const baseParams = { 19 | task: testTask, 20 | subtaskCount: 3, 21 | nextSubtaskId: 1, 22 | additionalContext: '', 23 | complexityReasoningContext: '', 24 | gatheredContext: '', 25 | useResearch: false, 26 | expansionPrompt: undefined 27 | }; 28 | 29 | test('default variant includes task context', () => { 30 | const { userPrompt } = promptManager.loadPrompt( 31 | 'expand-task', 32 | baseParams, 33 | 'default' 34 | ); 35 | 36 | expect(userPrompt).toContain(testTask.title); 37 | expect(userPrompt).toContain(testTask.description); 38 | expect(userPrompt).toContain(testTask.details); 39 | expect(userPrompt).toContain('Task ID: 1'); 40 | }); 41 | 42 | test('research variant includes task context', () => { 43 | const params = { ...baseParams, useResearch: true }; 44 | const { userPrompt } = promptManager.loadPrompt( 45 | 'expand-task', 46 | params, 47 | 'research' 48 | ); 49 | 50 | expect(userPrompt).toContain(testTask.title); 51 | expect(userPrompt).toContain(testTask.description); 52 | expect(userPrompt).toContain(testTask.details); 53 | expect(userPrompt).toContain('Parent Task:'); 54 | expect(userPrompt).toContain('ID: 1'); 55 | }); 56 | 57 | test('complexity-report variant includes task context', () => { 58 | const params = { 59 | ...baseParams, 60 | expansionPrompt: 'Focus on security best practices', 61 | complexityReasoningContext: 'High complexity due to security requirements' 62 | }; 63 | const { userPrompt } = promptManager.loadPrompt( 64 | 'expand-task', 65 | params, 66 | 'complexity-report' 67 | ); 68 | 69 | // The fix ensures task context is included 70 | expect(userPrompt).toContain('Parent Task:'); 71 | expect(userPrompt).toContain(`ID: ${testTask.id}`); 72 | expect(userPrompt).toContain(`Title: ${testTask.title}`); 73 | expect(userPrompt).toContain(`Description: ${testTask.description}`); 74 | expect(userPrompt).toContain(`Current details: ${testTask.details}`); 75 | 76 | // Also includes the expansion prompt 77 | expect(userPrompt).toContain('Expansion Guidance:'); 78 | expect(userPrompt).toContain(params.expansionPrompt); 79 | expect(userPrompt).toContain(params.complexityReasoningContext); 80 | }); 81 | 82 | test('all variants request JSON format with subtasks array', () => { 83 | const variants = ['default', 'research', 'complexity-report']; 84 | 85 | variants.forEach((variant) => { 86 | const params = 87 | variant === 'complexity-report' 88 | ? { ...baseParams, expansionPrompt: 'test' } 89 | : baseParams; 90 | 91 | const { systemPrompt, userPrompt } = promptManager.loadPrompt( 92 | 'expand-task', 93 | params, 94 | variant 95 | ); 96 | const combined = systemPrompt + userPrompt; 97 | 98 | expect(combined.toLowerCase()).toContain('subtasks'); 99 | expect(combined).toContain('JSON'); 100 | }); 101 | }); 102 | 103 | test('complexity-report variant fails without task context regression test', () => { 104 | // This test ensures we don't regress to the old behavior where 105 | // complexity-report variant only used expansionPrompt without task context 106 | const params = { 107 | ...baseParams, 108 | expansionPrompt: 'Generic expansion prompt' 109 | }; 110 | 111 | const { userPrompt } = promptManager.loadPrompt( 112 | 'expand-task', 113 | params, 114 | 'complexity-report' 115 | ); 116 | 117 | // Count occurrences of task-specific content 118 | const titleOccurrences = ( 119 | userPrompt.match(new RegExp(testTask.title, 'g')) || [] 120 | ).length; 121 | const descriptionOccurrences = ( 122 | userPrompt.match(new RegExp(testTask.description, 'g')) || [] 123 | ).length; 124 | 125 | // Should have at least one occurrence of title and description 126 | expect(titleOccurrences).toBeGreaterThanOrEqual(1); 127 | expect(descriptionOccurrences).toBeGreaterThanOrEqual(1); 128 | 129 | // Should not be ONLY the expansion prompt 130 | expect(userPrompt.length).toBeGreaterThan( 131 | params.expansionPrompt.length + 100 132 | ); 133 | }); 134 | }); 135 | ``` -------------------------------------------------------------------------------- /src/ai-providers/custom-sdk/grok-cli/errors.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * @fileoverview Error handling utilities for Grok CLI provider 3 | */ 4 | 5 | import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider'; 6 | 7 | /** 8 | * @typedef {import('./types.js').GrokCliErrorMetadata} GrokCliErrorMetadata 9 | */ 10 | 11 | /** 12 | * Create an API call error with Grok CLI specific metadata 13 | * @param {Object} params - Error parameters 14 | * @param {string} params.message - Error message 15 | * @param {string} [params.code] - Error code 16 | * @param {number} [params.exitCode] - Process exit code 17 | * @param {string} [params.stderr] - Standard error output 18 | * @param {string} [params.stdout] - Standard output 19 | * @param {string} [params.promptExcerpt] - Excerpt of the prompt 20 | * @param {boolean} [params.isRetryable=false] - Whether the error is retryable 21 | * @returns {APICallError} 22 | */ 23 | export function createAPICallError({ 24 | message, 25 | code, 26 | exitCode, 27 | stderr, 28 | stdout, 29 | promptExcerpt, 30 | isRetryable = false 31 | }) { 32 | /** @type {GrokCliErrorMetadata} */ 33 | const metadata = { 34 | code, 35 | exitCode, 36 | stderr, 37 | stdout, 38 | promptExcerpt 39 | }; 40 | 41 | return new APICallError({ 42 | message, 43 | isRetryable, 44 | url: 'grok-cli://command', 45 | requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined, 46 | data: metadata 47 | }); 48 | } 49 | 50 | /** 51 | * Create an authentication error 52 | * @param {Object} params - Error parameters 53 | * @param {string} params.message - Error message 54 | * @returns {LoadAPIKeyError} 55 | */ 56 | export function createAuthenticationError({ message }) { 57 | return new LoadAPIKeyError({ 58 | message: 59 | message || 60 | 'Authentication failed. Please ensure Grok CLI is properly configured with API key.' 61 | }); 62 | } 63 | 64 | /** 65 | * Create a timeout error 66 | * @param {Object} params - Error parameters 67 | * @param {string} params.message - Error message 68 | * @param {string} [params.promptExcerpt] - Excerpt of the prompt 69 | * @param {number} params.timeoutMs - Timeout in milliseconds 70 | * @returns {APICallError} 71 | */ 72 | export function createTimeoutError({ message, promptExcerpt, timeoutMs }) { 73 | /** @type {GrokCliErrorMetadata & { timeoutMs: number }} */ 74 | const metadata = { 75 | code: 'TIMEOUT', 76 | promptExcerpt, 77 | timeoutMs 78 | }; 79 | 80 | return new APICallError({ 81 | message, 82 | isRetryable: true, 83 | url: 'grok-cli://command', 84 | requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined, 85 | data: metadata 86 | }); 87 | } 88 | 89 | /** 90 | * Create a CLI installation error 91 | * @param {Object} params - Error parameters 92 | * @param {string} [params.message] - Error message 93 | * @returns {APICallError} 94 | */ 95 | export function createInstallationError({ message }) { 96 | return new APICallError({ 97 | message: 98 | message || 99 | 'Grok CLI is not installed or not found in PATH. Please install with: npm install -g @vibe-kit/grok-cli', 100 | isRetryable: false, 101 | url: 'grok-cli://installation' 102 | }); 103 | } 104 | 105 | /** 106 | * Check if an error is an authentication error 107 | * @param {unknown} error - Error to check 108 | * @returns {boolean} 109 | */ 110 | export function isAuthenticationError(error) { 111 | if (error instanceof LoadAPIKeyError) return true; 112 | if ( 113 | error instanceof APICallError && 114 | /** @type {GrokCliErrorMetadata} */ (error.data)?.exitCode === 401 115 | ) 116 | return true; 117 | return false; 118 | } 119 | 120 | /** 121 | * Check if an error is a timeout error 122 | * @param {unknown} error - Error to check 123 | * @returns {boolean} 124 | */ 125 | export function isTimeoutError(error) { 126 | if ( 127 | error instanceof APICallError && 128 | /** @type {GrokCliErrorMetadata} */ (error.data)?.code === 'TIMEOUT' 129 | ) 130 | return true; 131 | return false; 132 | } 133 | 134 | /** 135 | * Check if an error is an installation error 136 | * @param {unknown} error - Error to check 137 | * @returns {boolean} 138 | */ 139 | export function isInstallationError(error) { 140 | if (error instanceof APICallError && error.url === 'grok-cli://installation') 141 | return true; 142 | return false; 143 | } 144 | 145 | /** 146 | * Get error metadata from an error 147 | * @param {unknown} error - Error to extract metadata from 148 | * @returns {GrokCliErrorMetadata|undefined} 149 | */ 150 | export function getErrorMetadata(error) { 151 | if (error instanceof APICallError && error.data) { 152 | return /** @type {GrokCliErrorMetadata} */ (error.data); 153 | } 154 | return undefined; 155 | } 156 | ``` -------------------------------------------------------------------------------- /packages/tm-core/src/config/services/config-loader.service.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Unit tests for ConfigLoader service 3 | */ 4 | 5 | import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; 6 | import { promises as fs } from 'node:fs'; 7 | import { ConfigLoader } from './config-loader.service.js'; 8 | import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js'; 9 | 10 | vi.mock('node:fs', () => ({ 11 | promises: { 12 | readFile: vi.fn(), 13 | access: vi.fn() 14 | } 15 | })); 16 | 17 | describe('ConfigLoader', () => { 18 | let configLoader: ConfigLoader; 19 | const testProjectRoot = '/test/project'; 20 | 21 | beforeEach(() => { 22 | configLoader = new ConfigLoader(testProjectRoot); 23 | vi.clearAllMocks(); 24 | }); 25 | 26 | afterEach(() => { 27 | vi.restoreAllMocks(); 28 | }); 29 | 30 | describe('getDefaultConfig', () => { 31 | it('should return default configuration values', () => { 32 | const config = configLoader.getDefaultConfig(); 33 | 34 | expect(config.models).toEqual({ 35 | main: DEFAULT_CONFIG_VALUES.MODELS.MAIN, 36 | fallback: DEFAULT_CONFIG_VALUES.MODELS.FALLBACK 37 | }); 38 | 39 | expect(config.storage).toEqual({ 40 | type: DEFAULT_CONFIG_VALUES.STORAGE.TYPE, 41 | encoding: DEFAULT_CONFIG_VALUES.STORAGE.ENCODING, 42 | enableBackup: false, 43 | maxBackups: DEFAULT_CONFIG_VALUES.STORAGE.MAX_BACKUPS, 44 | enableCompression: false, 45 | atomicOperations: true 46 | }); 47 | 48 | expect(config.version).toBe(DEFAULT_CONFIG_VALUES.VERSION); 49 | }); 50 | }); 51 | 52 | describe('loadLocalConfig', () => { 53 | it('should load and parse local configuration file', async () => { 54 | const mockConfig = { 55 | models: { main: 'test-model' }, 56 | storage: { type: 'api' as const } 57 | }; 58 | 59 | vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig)); 60 | 61 | const result = await configLoader.loadLocalConfig(); 62 | 63 | expect(fs.readFile).toHaveBeenCalledWith( 64 | '/test/project/.taskmaster/config.json', 65 | 'utf-8' 66 | ); 67 | expect(result).toEqual(mockConfig); 68 | }); 69 | 70 | it('should return null when config file does not exist', async () => { 71 | const error = new Error('File not found') as any; 72 | error.code = 'ENOENT'; 73 | vi.mocked(fs.readFile).mockRejectedValue(error); 74 | 75 | const result = await configLoader.loadLocalConfig(); 76 | 77 | expect(result).toBeNull(); 78 | }); 79 | 80 | it('should throw TaskMasterError for other file errors', async () => { 81 | const error = new Error('Permission denied'); 82 | vi.mocked(fs.readFile).mockRejectedValue(error); 83 | 84 | await expect(configLoader.loadLocalConfig()).rejects.toThrow( 85 | 'Failed to load local configuration' 86 | ); 87 | }); 88 | 89 | it('should throw error for invalid JSON', async () => { 90 | vi.mocked(fs.readFile).mockResolvedValue('invalid json'); 91 | 92 | await expect(configLoader.loadLocalConfig()).rejects.toThrow(); 93 | }); 94 | }); 95 | 96 | describe('loadGlobalConfig', () => { 97 | it('should return null (not implemented yet)', async () => { 98 | const result = await configLoader.loadGlobalConfig(); 99 | expect(result).toBeNull(); 100 | }); 101 | }); 102 | 103 | describe('hasLocalConfig', () => { 104 | it('should return true when local config exists', async () => { 105 | vi.mocked(fs.access).mockResolvedValue(undefined); 106 | 107 | const result = await configLoader.hasLocalConfig(); 108 | 109 | expect(fs.access).toHaveBeenCalledWith( 110 | '/test/project/.taskmaster/config.json' 111 | ); 112 | expect(result).toBe(true); 113 | }); 114 | 115 | it('should return false when local config does not exist', async () => { 116 | vi.mocked(fs.access).mockRejectedValue(new Error('Not found')); 117 | 118 | const result = await configLoader.hasLocalConfig(); 119 | 120 | expect(result).toBe(false); 121 | }); 122 | }); 123 | 124 | describe('hasGlobalConfig', () => { 125 | it('should check global config path', async () => { 126 | vi.mocked(fs.access).mockResolvedValue(undefined); 127 | 128 | const result = await configLoader.hasGlobalConfig(); 129 | 130 | expect(fs.access).toHaveBeenCalledWith( 131 | expect.stringContaining('.taskmaster/config.json') 132 | ); 133 | expect(result).toBe(true); 134 | }); 135 | 136 | it('should return false when global config does not exist', async () => { 137 | vi.mocked(fs.access).mockRejectedValue(new Error('Not found')); 138 | 139 | const result = await configLoader.hasGlobalConfig(); 140 | 141 | expect(result).toBe(false); 142 | }); 143 | }); 144 | }); 145 | ```