This is page 50 of 69. 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
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .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
│ │ └── validate-changesets.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
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── 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
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.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_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── 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
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── 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
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── 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
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── 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_CODE_PLUGIN.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
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── 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
│ │ │ ├── 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
│ │ │ ├── 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
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── 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
│ ├── tool-registry.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
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.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
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.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
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.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
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.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
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.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
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── 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
│ │ └── providers
│ │ └── temperature-support.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
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.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
│ │ └── tool-registration.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
│ │ └── prompt-migration.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
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.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
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/mcp-server/src/tools/utils.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * tools/utils.js
3 | * Utility functions for Task Master CLI integration
4 | */
5 |
6 | import { spawnSync } from 'child_process';
7 | import path from 'path';
8 | import fs from 'fs';
9 | import { contextManager } from '../core/context-manager.js'; // Import the singleton
10 | import { fileURLToPath } from 'url';
11 | import packageJson from '../../../package.json' with { type: 'json' };
12 | import { getCurrentTag } from '../../../scripts/modules/utils.js';
13 |
14 | // Import path utilities to ensure consistent path resolution
15 | import {
16 | lastFoundProjectRoot,
17 | PROJECT_MARKERS
18 | } from '../core/utils/path-utils.js';
19 |
20 | const __filename = fileURLToPath(import.meta.url);
21 |
22 | // Cache for version info to avoid repeated file reads
23 | let cachedVersionInfo = null;
24 |
25 | /**
26 | * Get version information from package.json
27 | * @returns {Object} Version information
28 | */
29 | function getVersionInfo() {
30 | // Return cached version if available
31 | if (cachedVersionInfo) {
32 | return cachedVersionInfo;
33 | }
34 |
35 | // Use the imported packageJson directly
36 | cachedVersionInfo = {
37 | version: packageJson.version || 'unknown',
38 | name: packageJson.name || 'task-master-ai'
39 | };
40 | return cachedVersionInfo;
41 | }
42 |
43 | /**
44 | * Get current tag information for MCP responses
45 | * @param {string} projectRoot - The project root directory
46 | * @param {Object} log - Logger object
47 | * @returns {Object} Tag information object
48 | */
49 | function getTagInfo(projectRoot, log) {
50 | try {
51 | if (!projectRoot) {
52 | log.warn('No project root provided for tag information');
53 | return { currentTag: 'master', availableTags: ['master'] };
54 | }
55 |
56 | const currentTag = getCurrentTag(projectRoot);
57 |
58 | // Read available tags from tasks.json
59 | let availableTags = ['master']; // Default fallback
60 | try {
61 | const tasksJsonPath = path.join(
62 | projectRoot,
63 | '.taskmaster',
64 | 'tasks',
65 | 'tasks.json'
66 | );
67 | if (fs.existsSync(tasksJsonPath)) {
68 | const tasksData = JSON.parse(fs.readFileSync(tasksJsonPath, 'utf-8'));
69 |
70 | // If it's the new tagged format, extract tag keys
71 | if (
72 | tasksData &&
73 | typeof tasksData === 'object' &&
74 | !Array.isArray(tasksData.tasks)
75 | ) {
76 | const tagKeys = Object.keys(tasksData).filter(
77 | (key) =>
78 | tasksData[key] &&
79 | typeof tasksData[key] === 'object' &&
80 | Array.isArray(tasksData[key].tasks)
81 | );
82 | if (tagKeys.length > 0) {
83 | availableTags = tagKeys;
84 | }
85 | }
86 | }
87 | } catch (tagError) {
88 | log.debug(`Could not read available tags: ${tagError.message}`);
89 | }
90 |
91 | return {
92 | currentTag: currentTag || 'master',
93 | availableTags: availableTags
94 | };
95 | } catch (error) {
96 | log.warn(`Error getting tag information: ${error.message}`);
97 | return { currentTag: 'master', availableTags: ['master'] };
98 | }
99 | }
100 |
101 | /**
102 | * Get normalized project root path
103 | * @param {string|undefined} projectRootRaw - Raw project root from arguments
104 | * @param {Object} log - Logger object
105 | * @returns {string} - Normalized absolute path to project root
106 | */
107 | function getProjectRoot(projectRootRaw, log) {
108 | // PRECEDENCE ORDER:
109 | // 1. Environment variable override (TASK_MASTER_PROJECT_ROOT)
110 | // 2. Explicitly provided projectRoot in args
111 | // 3. Previously found/cached project root
112 | // 4. Current directory if it has project markers
113 | // 5. Current directory with warning
114 |
115 | // 1. Check for environment variable override
116 | if (process.env.TASK_MASTER_PROJECT_ROOT) {
117 | const envRoot = process.env.TASK_MASTER_PROJECT_ROOT;
118 | const absolutePath = path.isAbsolute(envRoot)
119 | ? envRoot
120 | : path.resolve(process.cwd(), envRoot);
121 | log.info(
122 | `Using project root from TASK_MASTER_PROJECT_ROOT environment variable: ${absolutePath}`
123 | );
124 | return absolutePath;
125 | }
126 |
127 | // 2. If project root is explicitly provided, use it
128 | if (projectRootRaw) {
129 | const absolutePath = path.isAbsolute(projectRootRaw)
130 | ? projectRootRaw
131 | : path.resolve(process.cwd(), projectRootRaw);
132 |
133 | log.info(`Using explicitly provided project root: ${absolutePath}`);
134 | return absolutePath;
135 | }
136 |
137 | // 3. If we have a last found project root from a tasks.json search, use that for consistency
138 | if (lastFoundProjectRoot) {
139 | log.info(
140 | `Using last known project root where tasks.json was found: ${lastFoundProjectRoot}`
141 | );
142 | return lastFoundProjectRoot;
143 | }
144 |
145 | // 4. Check if the current directory has any indicators of being a task-master project
146 | const currentDir = process.cwd();
147 | if (
148 | PROJECT_MARKERS.some((marker) => {
149 | const markerPath = path.join(currentDir, marker);
150 | return fs.existsSync(markerPath);
151 | })
152 | ) {
153 | log.info(
154 | `Using current directory as project root (found project markers): ${currentDir}`
155 | );
156 | return currentDir;
157 | }
158 |
159 | // 5. Default to current working directory but warn the user
160 | log.warn(
161 | `No task-master project detected in current directory. Using ${currentDir} as project root.`
162 | );
163 | log.warn(
164 | 'Consider using --project-root to specify the correct project location or set TASK_MASTER_PROJECT_ROOT environment variable.'
165 | );
166 | return currentDir;
167 | }
168 |
169 | /**
170 | * Extracts and normalizes the project root path from the MCP session object.
171 | * @param {Object} session - The MCP session object.
172 | * @param {Object} log - The MCP logger object.
173 | * @returns {string|null} - The normalized absolute project root path or null if not found/invalid.
174 | */
175 | function getProjectRootFromSession(session, log) {
176 | try {
177 | // Add detailed logging of session structure
178 | log.info(
179 | `Session object: ${JSON.stringify({
180 | hasSession: !!session,
181 | hasRoots: !!session?.roots,
182 | rootsType: typeof session?.roots,
183 | isRootsArray: Array.isArray(session?.roots),
184 | rootsLength: session?.roots?.length,
185 | firstRoot: session?.roots?.[0],
186 | hasRootsRoots: !!session?.roots?.roots,
187 | rootsRootsType: typeof session?.roots?.roots,
188 | isRootsRootsArray: Array.isArray(session?.roots?.roots),
189 | rootsRootsLength: session?.roots?.roots?.length,
190 | firstRootsRoot: session?.roots?.roots?.[0]
191 | })}`
192 | );
193 |
194 | let rawRootPath = null;
195 | let decodedPath = null;
196 | let finalPath = null;
197 |
198 | // Check primary location
199 | if (session?.roots?.[0]?.uri) {
200 | rawRootPath = session.roots[0].uri;
201 | log.info(`Found raw root URI in session.roots[0].uri: ${rawRootPath}`);
202 | }
203 | // Check alternate location
204 | else if (session?.roots?.roots?.[0]?.uri) {
205 | rawRootPath = session.roots.roots[0].uri;
206 | log.info(
207 | `Found raw root URI in session.roots.roots[0].uri: ${rawRootPath}`
208 | );
209 | }
210 |
211 | if (rawRootPath) {
212 | // Decode URI and strip file:// protocol
213 | decodedPath = rawRootPath.startsWith('file://')
214 | ? decodeURIComponent(rawRootPath.slice(7))
215 | : rawRootPath; // Assume non-file URI is already decoded? Or decode anyway? Let's decode.
216 | if (!rawRootPath.startsWith('file://')) {
217 | decodedPath = decodeURIComponent(rawRootPath); // Decode even if no file://
218 | }
219 |
220 | // Handle potential Windows drive prefix after stripping protocol (e.g., /C:/...)
221 | if (
222 | decodedPath.startsWith('/') &&
223 | /[A-Za-z]:/.test(decodedPath.substring(1, 3))
224 | ) {
225 | decodedPath = decodedPath.substring(1); // Remove leading slash if it's like /C:/...
226 | }
227 |
228 | log.info(`Decoded path: ${decodedPath}`);
229 |
230 | // Normalize slashes and resolve
231 | const normalizedSlashes = decodedPath.replace(/\\/g, '/');
232 | finalPath = path.resolve(normalizedSlashes); // Resolve to absolute path for current OS
233 |
234 | log.info(`Normalized and resolved session path: ${finalPath}`);
235 | return finalPath;
236 | }
237 |
238 | // Fallback Logic (remains the same)
239 | log.warn('No project root URI found in session. Attempting fallbacks...');
240 | const cwd = process.cwd();
241 |
242 | // Fallback 1: Use server path deduction (Cursor IDE)
243 | const serverPath = process.argv[1];
244 | if (serverPath && serverPath.includes('mcp-server')) {
245 | const mcpServerIndex = serverPath.indexOf('mcp-server');
246 | if (mcpServerIndex !== -1) {
247 | const projectRoot = path.dirname(
248 | serverPath.substring(0, mcpServerIndex)
249 | ); // Go up one level
250 |
251 | if (
252 | fs.existsSync(path.join(projectRoot, '.cursor')) ||
253 | fs.existsSync(path.join(projectRoot, 'mcp-server')) ||
254 | fs.existsSync(path.join(projectRoot, 'package.json'))
255 | ) {
256 | log.info(
257 | `Using project root derived from server path: ${projectRoot}`
258 | );
259 | return projectRoot; // Already absolute
260 | }
261 | }
262 | }
263 |
264 | // Fallback 2: Use CWD
265 | log.info(`Using current working directory as ultimate fallback: ${cwd}`);
266 | return cwd; // Already absolute
267 | } catch (e) {
268 | log.error(`Error in getProjectRootFromSession: ${e.message}`);
269 | // Attempt final fallback to CWD on error
270 | const cwd = process.cwd();
271 | log.warn(
272 | `Returning CWD (${cwd}) due to error during session root processing.`
273 | );
274 | return cwd;
275 | }
276 | }
277 |
278 | /**
279 | * Handle API result with standardized error handling and response formatting
280 | * @param {Object} result - Result object from API call with success, data, and error properties
281 | * @param {Object} log - Logger object
282 | * @param {string} errorPrefix - Prefix for error messages
283 | * @param {Function} processFunction - Optional function to process successful result data
284 | * @param {string} [projectRoot] - Optional project root for tag information
285 | * @returns {Object} - Standardized MCP response object
286 | */
287 | async function handleApiResult(
288 | result,
289 | log,
290 | errorPrefix = 'API error',
291 | processFunction = processMCPResponseData,
292 | projectRoot = null
293 | ) {
294 | // Get version info for every response
295 | const versionInfo = getVersionInfo();
296 |
297 | // Get tag info if project root is provided
298 | const tagInfo = projectRoot ? getTagInfo(projectRoot, log) : null;
299 |
300 | if (!result.success) {
301 | const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
302 | log.error(`${errorPrefix}: ${errorMsg}`);
303 | return createErrorResponse(errorMsg, versionInfo, tagInfo);
304 | }
305 |
306 | // Process the result data if needed
307 | const processedData = processFunction
308 | ? processFunction(result.data)
309 | : result.data;
310 |
311 | log.info('Successfully completed operation');
312 |
313 | // Create the response payload including version info and tag info
314 | const responsePayload = {
315 | data: processedData,
316 | version: versionInfo
317 | };
318 |
319 | // Add tag information if available
320 | if (tagInfo) {
321 | responsePayload.tag = tagInfo;
322 | }
323 |
324 | return createContentResponse(responsePayload);
325 | }
326 |
327 | /**
328 | * Executes a task-master CLI command synchronously.
329 | * @param {string} command - The command to execute (e.g., 'add-task')
330 | * @param {Object} log - Logger instance
331 | * @param {Array} args - Arguments for the command
332 | * @param {string|undefined} projectRootRaw - Optional raw project root path (will be normalized internally)
333 | * @param {Object|null} customEnv - Optional object containing environment variables to pass to the child process
334 | * @returns {Object} - The result of the command execution
335 | */
336 | function executeTaskMasterCommand(
337 | command,
338 | log,
339 | args = [],
340 | projectRootRaw = null,
341 | customEnv = null // Changed from session to customEnv
342 | ) {
343 | try {
344 | // Normalize project root internally using the getProjectRoot utility
345 | const cwd = getProjectRoot(projectRootRaw, log);
346 |
347 | log.info(
348 | `Executing task-master ${command} with args: ${JSON.stringify(
349 | args
350 | )} in directory: ${cwd}`
351 | );
352 |
353 | // Prepare full arguments array
354 | const fullArgs = [command, ...args];
355 |
356 | // Common options for spawn
357 | const spawnOptions = {
358 | encoding: 'utf8',
359 | cwd: cwd,
360 | // Merge process.env with customEnv, giving precedence to customEnv
361 | env: { ...process.env, ...(customEnv || {}) }
362 | };
363 |
364 | // Log the environment being passed (optional, for debugging)
365 | // log.info(`Spawn options env: ${JSON.stringify(spawnOptions.env)}`);
366 |
367 | // Execute the command using the global task-master CLI or local script
368 | // Try the global CLI first
369 | let result = spawnSync('task-master', fullArgs, spawnOptions);
370 |
371 | // If global CLI is not available, try fallback to the local script
372 | if (result.error && result.error.code === 'ENOENT') {
373 | log.info('Global task-master not found, falling back to local script');
374 | // Pass the same spawnOptions (including env) to the fallback
375 | result = spawnSync('node', ['scripts/dev.js', ...fullArgs], spawnOptions);
376 | }
377 |
378 | if (result.error) {
379 | throw new Error(`Command execution error: ${result.error.message}`);
380 | }
381 |
382 | if (result.status !== 0) {
383 | // Improve error handling by combining stderr and stdout if stderr is empty
384 | const errorOutput = result.stderr
385 | ? result.stderr.trim()
386 | : result.stdout
387 | ? result.stdout.trim()
388 | : 'Unknown error';
389 | throw new Error(
390 | `Command failed with exit code ${result.status}: ${errorOutput}`
391 | );
392 | }
393 |
394 | return {
395 | success: true,
396 | stdout: result.stdout,
397 | stderr: result.stderr
398 | };
399 | } catch (error) {
400 | log.error(`Error executing task-master command: ${error.message}`);
401 | return {
402 | success: false,
403 | error: error.message
404 | };
405 | }
406 | }
407 |
408 | /**
409 | * Checks cache for a result using the provided key. If not found, executes the action function,
410 | * caches the result upon success, and returns the result.
411 | *
412 | * @param {Object} options - Configuration options.
413 | * @param {string} options.cacheKey - The unique key for caching this operation's result.
414 | * @param {Function} options.actionFn - The async function to execute if the cache misses.
415 | * Should return an object like { success: boolean, data?: any, error?: { code: string, message: string } }.
416 | * @param {Object} options.log - The logger instance.
417 | * @returns {Promise<Object>} - An object containing the result.
418 | * Format: { success: boolean, data?: any, error?: { code: string, message: string } }
419 | */
420 | async function getCachedOrExecute({ cacheKey, actionFn, log }) {
421 | // Check cache first
422 | const cachedResult = contextManager.getCachedData(cacheKey);
423 |
424 | if (cachedResult !== undefined) {
425 | log.info(`Cache hit for key: ${cacheKey}`);
426 | return cachedResult;
427 | }
428 |
429 | log.info(`Cache miss for key: ${cacheKey}. Executing action function.`);
430 |
431 | // Execute the action function if cache missed
432 | const result = await actionFn();
433 |
434 | // If the action was successful, cache the result
435 | if (result.success && result.data !== undefined) {
436 | log.info(`Action successful. Caching result for key: ${cacheKey}`);
437 | contextManager.setCachedData(cacheKey, result);
438 | } else if (!result.success) {
439 | log.warn(
440 | `Action failed for cache key ${cacheKey}. Result not cached. Error: ${result.error?.message}`
441 | );
442 | } else {
443 | log.warn(
444 | `Action for cache key ${cacheKey} succeeded but returned no data. Result not cached.`
445 | );
446 | }
447 |
448 | return result;
449 | }
450 |
451 | /**
452 | * Recursively removes specified fields from task objects, whether single or in an array.
453 | * Handles common data structures returned by task commands.
454 | * @param {Object|Array} taskOrData - A single task object or a data object containing a 'tasks' array.
455 | * @param {string[]} fieldsToRemove - An array of field names to remove.
456 | * @returns {Object|Array} - The processed data with specified fields removed.
457 | */
458 | function processMCPResponseData(
459 | taskOrData,
460 | fieldsToRemove = ['details', 'testStrategy']
461 | ) {
462 | if (!taskOrData) {
463 | return taskOrData;
464 | }
465 |
466 | // Helper function to process a single task object
467 | const processSingleTask = (task) => {
468 | if (typeof task !== 'object' || task === null) {
469 | return task;
470 | }
471 |
472 | const processedTask = { ...task };
473 |
474 | // Remove specified fields from the task
475 | fieldsToRemove.forEach((field) => {
476 | delete processedTask[field];
477 | });
478 |
479 | // Recursively process subtasks if they exist and are an array
480 | if (processedTask.subtasks && Array.isArray(processedTask.subtasks)) {
481 | // Use processArrayOfTasks to handle the subtasks array
482 | processedTask.subtasks = processArrayOfTasks(processedTask.subtasks);
483 | }
484 |
485 | return processedTask;
486 | };
487 |
488 | // Helper function to process an array of tasks
489 | const processArrayOfTasks = (tasks) => {
490 | return tasks.map(processSingleTask);
491 | };
492 |
493 | // Check if the input is a data structure containing a 'tasks' array (like from listTasks)
494 | if (
495 | typeof taskOrData === 'object' &&
496 | taskOrData !== null &&
497 | Array.isArray(taskOrData.tasks)
498 | ) {
499 | return {
500 | ...taskOrData, // Keep other potential fields like 'stats', 'filter'
501 | tasks: processArrayOfTasks(taskOrData.tasks)
502 | };
503 | }
504 | // Check if the input is likely a single task object (add more checks if needed)
505 | else if (
506 | typeof taskOrData === 'object' &&
507 | taskOrData !== null &&
508 | 'id' in taskOrData &&
509 | 'title' in taskOrData
510 | ) {
511 | return processSingleTask(taskOrData);
512 | }
513 | // Check if the input is an array of tasks directly (less common but possible)
514 | else if (Array.isArray(taskOrData)) {
515 | return processArrayOfTasks(taskOrData);
516 | }
517 |
518 | // If it doesn't match known task structures, return it as is
519 | return taskOrData;
520 | }
521 |
522 | /**
523 | * Creates standard content response for tools
524 | * @param {string|Object} content - Content to include in response
525 | * @returns {Object} - Content response object in FastMCP format
526 | */
527 | function createContentResponse(content) {
528 | // FastMCP requires text type, so we format objects as JSON strings
529 | return {
530 | content: [
531 | {
532 | type: 'text',
533 | text:
534 | typeof content === 'object'
535 | ? // Format JSON nicely with indentation
536 | JSON.stringify(content, null, 2)
537 | : // Keep other content types as-is
538 | String(content)
539 | }
540 | ]
541 | };
542 | }
543 |
544 | /**
545 | * Creates error response for tools
546 | * @param {string} errorMessage - Error message to include in response
547 | * @param {Object} [versionInfo] - Optional version information object
548 | * @param {Object} [tagInfo] - Optional tag information object
549 | * @returns {Object} - Error content response object in FastMCP format
550 | */
551 | function createErrorResponse(errorMessage, versionInfo, tagInfo) {
552 | // Provide fallback version info if not provided
553 | if (!versionInfo) {
554 | versionInfo = getVersionInfo();
555 | }
556 |
557 | let responseText = `Error: ${errorMessage}
558 | Version: ${versionInfo.version}
559 | Name: ${versionInfo.name}`;
560 |
561 | // Add tag information if available
562 | if (tagInfo) {
563 | responseText += `
564 | Current Tag: ${tagInfo.currentTag}`;
565 | }
566 |
567 | return {
568 | content: [
569 | {
570 | type: 'text',
571 | text: responseText
572 | }
573 | ],
574 | isError: true
575 | };
576 | }
577 |
578 | /**
579 | * Creates a logger wrapper object compatible with core function expectations.
580 | * Adapts the MCP logger to the { info, warn, error, debug, success } structure.
581 | * @param {Object} log - The MCP logger instance.
582 | * @returns {Object} - The logger wrapper object.
583 | */
584 | function createLogWrapper(log) {
585 | return {
586 | info: (message, ...args) => log.info(message, ...args),
587 | warn: (message, ...args) => log.warn(message, ...args),
588 | error: (message, ...args) => log.error(message, ...args),
589 | // Handle optional debug method
590 | debug: (message, ...args) =>
591 | log.debug ? log.debug(message, ...args) : null,
592 | // Map success to info as a common fallback
593 | success: (message, ...args) => log.info(message, ...args)
594 | };
595 | }
596 |
597 | /**
598 | * Resolves and normalizes a project root path from various formats.
599 | * Handles URI encoding, Windows paths, and file protocols.
600 | * @param {string | undefined | null} rawPath - The raw project root path.
601 | * @param {object} [log] - Optional logger object.
602 | * @returns {string | null} Normalized absolute path or null if input is invalid/empty.
603 | */
604 | function normalizeProjectRoot(rawPath, log) {
605 | if (!rawPath) return null;
606 | try {
607 | let pathString = Array.isArray(rawPath) ? rawPath[0] : String(rawPath);
608 | if (!pathString) return null;
609 |
610 | // 1. Decode URI Encoding
611 | // Use try-catch for decoding as malformed URIs can throw
612 | try {
613 | pathString = decodeURIComponent(pathString);
614 | } catch (decodeError) {
615 | if (log)
616 | log.warn(
617 | `Could not decode URI component for path "${rawPath}": ${decodeError.message}. Proceeding with raw string.`
618 | );
619 | // Proceed with the original string if decoding fails
620 | pathString = Array.isArray(rawPath) ? rawPath[0] : String(rawPath);
621 | }
622 |
623 | // 2. Strip file:// prefix (handle 2 or 3 slashes)
624 | if (pathString.startsWith('file:///')) {
625 | pathString = pathString.slice(7); // Slice 7 for file:///, may leave leading / on Windows
626 | } else if (pathString.startsWith('file://')) {
627 | pathString = pathString.slice(7); // Slice 7 for file://
628 | }
629 |
630 | // 3. Handle potential Windows leading slash after stripping prefix (e.g., /C:/...)
631 | // This checks if it starts with / followed by a drive letter C: D: etc.
632 | if (
633 | pathString.startsWith('/') &&
634 | /[A-Za-z]:/.test(pathString.substring(1, 3))
635 | ) {
636 | pathString = pathString.substring(1); // Remove the leading slash
637 | }
638 |
639 | // 4. Normalize backslashes to forward slashes
640 | pathString = pathString.replace(/\\/g, '/');
641 |
642 | // 5. Resolve to absolute path using server's OS convention
643 | const resolvedPath = path.resolve(pathString);
644 | return resolvedPath;
645 | } catch (error) {
646 | if (log) {
647 | log.error(
648 | `Error normalizing project root path "${rawPath}": ${error.message}`
649 | );
650 | }
651 | return null; // Return null on error
652 | }
653 | }
654 |
655 | /**
656 | * Extracts the raw project root path from the session (without normalization).
657 | * Used as a fallback within the HOF.
658 | * @param {Object} session - The MCP session object.
659 | * @param {Object} log - The MCP logger object.
660 | * @returns {string|null} The raw path string or null.
661 | */
662 | function getRawProjectRootFromSession(session, log) {
663 | try {
664 | // Check primary location
665 | if (session?.roots?.[0]?.uri) {
666 | return session.roots[0].uri;
667 | }
668 | // Check alternate location
669 | else if (session?.roots?.roots?.[0]?.uri) {
670 | return session.roots.roots[0].uri;
671 | }
672 | return null; // Not found in expected session locations
673 | } catch (e) {
674 | log.error(`Error accessing session roots: ${e.message}`);
675 | return null;
676 | }
677 | }
678 |
679 | /**
680 | * Higher-order function to wrap MCP tool execute methods.
681 | * Ensures args.projectRoot is present and normalized before execution.
682 | * Uses TASK_MASTER_PROJECT_ROOT environment variable with proper precedence.
683 | * @param {Function} executeFn - The original async execute(args, context) function.
684 | * @returns {Function} The wrapped async execute function.
685 | */
686 | function withNormalizedProjectRoot(executeFn) {
687 | return async (args, context) => {
688 | const { log, session } = context;
689 | let normalizedRoot = null;
690 | let rootSource = 'unknown';
691 |
692 | try {
693 | // PRECEDENCE ORDER:
694 | // 1. TASK_MASTER_PROJECT_ROOT environment variable (from process.env or session)
695 | // 2. args.projectRoot (explicitly provided)
696 | // 3. Session-based project root resolution
697 | // 4. Current directory fallback
698 |
699 | // 1. Check for TASK_MASTER_PROJECT_ROOT environment variable first
700 | if (process.env.TASK_MASTER_PROJECT_ROOT) {
701 | const envRoot = process.env.TASK_MASTER_PROJECT_ROOT;
702 | normalizedRoot = path.isAbsolute(envRoot)
703 | ? envRoot
704 | : path.resolve(process.cwd(), envRoot);
705 | rootSource = 'TASK_MASTER_PROJECT_ROOT environment variable';
706 | log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
707 | }
708 | // Also check session environment variables for TASK_MASTER_PROJECT_ROOT
709 | else if (session?.env?.TASK_MASTER_PROJECT_ROOT) {
710 | const envRoot = session.env.TASK_MASTER_PROJECT_ROOT;
711 | normalizedRoot = path.isAbsolute(envRoot)
712 | ? envRoot
713 | : path.resolve(process.cwd(), envRoot);
714 | rootSource = 'TASK_MASTER_PROJECT_ROOT session environment variable';
715 | log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
716 | }
717 | // 2. If no environment variable, try args.projectRoot
718 | else if (args.projectRoot) {
719 | normalizedRoot = normalizeProjectRoot(args.projectRoot, log);
720 | rootSource = 'args.projectRoot';
721 | log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
722 | }
723 | // 3. If no args.projectRoot, try session-based resolution
724 | else {
725 | const sessionRoot = getProjectRootFromSession(session, log);
726 | if (sessionRoot) {
727 | normalizedRoot = sessionRoot; // getProjectRootFromSession already normalizes
728 | rootSource = 'session';
729 | log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
730 | }
731 | }
732 |
733 | if (!normalizedRoot) {
734 | log.error(
735 | 'Could not determine project root from environment, args, or session.'
736 | );
737 | return createErrorResponse(
738 | 'Could not determine project root. Please provide projectRoot argument or ensure TASK_MASTER_PROJECT_ROOT environment variable is set.'
739 | );
740 | }
741 |
742 | // Inject the normalized root back into args
743 | const updatedArgs = { ...args, projectRoot: normalizedRoot };
744 |
745 | // Execute the original function with normalized root in args
746 | return await executeFn(updatedArgs, context);
747 | } catch (error) {
748 | log.error(
749 | `Error within withNormalizedProjectRoot HOF (Normalized Root: ${normalizedRoot}): ${error.message}`
750 | );
751 | // Add stack trace if available and debug enabled
752 | if (error.stack && log.debug) {
753 | log.debug(error.stack);
754 | }
755 | // Return a generic error or re-throw depending on desired behavior
756 | return createErrorResponse(`Operation failed: ${error.message}`);
757 | }
758 | };
759 | }
760 |
761 | /**
762 | * Checks progress reporting capability and returns the validated function or undefined.
763 | *
764 | * STANDARD PATTERN for AI-powered, long-running operations (parse-prd, expand-task, expand-all, analyze):
765 | *
766 | * This helper should be used as the first step in any MCP tool that performs long-running
767 | * AI operations. It validates the availability of progress reporting and provides consistent
768 | * logging about the capability status.
769 | *
770 | * Operations that should use this pattern:
771 | * - parse-prd: Parsing PRD documents with AI
772 | * - expand-task: Expanding tasks into subtasks
773 | * - expand-all: Expanding all tasks in batch
774 | * - analyze-complexity: Analyzing task complexity
775 | * - update-task: Updating tasks with AI assistance
776 | * - add-task: Creating new tasks with AI
777 | * - Any operation that makes AI service calls
778 | *
779 | * @example Basic usage in a tool's execute function:
780 | * ```javascript
781 | * import { checkProgressCapability } from './utils.js';
782 | *
783 | * async execute(args, context) {
784 | * const { log, reportProgress, session } = context;
785 | *
786 | * // Always validate progress capability first
787 | * const progressCapability = checkProgressCapability(reportProgress, log);
788 | *
789 | * // Pass to direct function - it handles undefined gracefully
790 | * const result = await expandTask(taskId, numSubtasks, {
791 | * session,
792 | * reportProgress: progressCapability,
793 | * mcpLog: log
794 | * });
795 | * }
796 | * ```
797 | *
798 | * @example With progress reporting available:
799 | * ```javascript
800 | * // When reportProgress is available, users see real-time updates:
801 | * // "Starting PRD analysis (Input: 5432 tokens)..."
802 | * // "Task 1/10 - Implement user authentication"
803 | * // "Task 2/10 - Create database schema"
804 | * // "Task Generation Completed | Tokens: 5432/1234"
805 | * ```
806 | *
807 | * @example Without progress reporting (graceful degradation):
808 | * ```javascript
809 | * // When reportProgress is not available:
810 | * // - Operation runs normally without progress updates
811 | * // - Debug log: "reportProgress not available - operation will run without progress updates"
812 | * // - User gets final result after completion
813 | * ```
814 | *
815 | * @param {Function|undefined} reportProgress - The reportProgress function from MCP context.
816 | * Expected signature: async (progress: {progress: number, total: number, message: string}) => void
817 | * @param {Object} log - Logger instance with debug, info, warn, error methods
818 | * @returns {Function|undefined} The validated reportProgress function or undefined if not available
819 | */
820 | function checkProgressCapability(reportProgress, log) {
821 | // Validate that reportProgress is available for long-running operations
822 | if (typeof reportProgress !== 'function') {
823 | log.debug(
824 | 'reportProgress not available - operation will run without progress updates'
825 | );
826 | return undefined;
827 | }
828 |
829 | return reportProgress;
830 | }
831 |
832 | // Ensure all functions are exported
833 | export {
834 | getProjectRoot,
835 | getProjectRootFromSession,
836 | getTagInfo,
837 | handleApiResult,
838 | executeTaskMasterCommand,
839 | getCachedOrExecute,
840 | processMCPResponseData,
841 | createContentResponse,
842 | createErrorResponse,
843 | createLogWrapper,
844 | normalizeProjectRoot,
845 | getRawProjectRootFromSession,
846 | withNormalizedProjectRoot,
847 | checkProgressCapability
848 | };
849 |
```
--------------------------------------------------------------------------------
/scripts/init.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Task Master
3 | * Copyright (c) 2025 Eyal Toledano, Ralph Khreish
4 | *
5 | * This software is licensed under the MIT License with Commons Clause.
6 | * You may use this software for any purpose, including commercial applications,
7 | * and modify and redistribute it freely, subject to the following restrictions:
8 | *
9 | * 1. You may not sell this software or offer it as a service.
10 | * 2. The origin of this software must not be misrepresented.
11 | * 3. Altered source versions must be plainly marked as such.
12 | *
13 | * For the full license text, see the LICENSE file in the root directory.
14 | */
15 |
16 | import fs from 'fs';
17 | import path from 'path';
18 | import readline from 'readline';
19 | import chalk from 'chalk';
20 | import figlet from 'figlet';
21 | import boxen from 'boxen';
22 | import gradient from 'gradient-string';
23 | import { isSilentMode } from './modules/utils.js';
24 | import { insideGitWorkTree } from './modules/utils/git-utils.js';
25 | import { manageGitignoreFile } from '../src/utils/manage-gitignore.js';
26 | import { RULE_PROFILES } from '../src/constants/profiles.js';
27 | import {
28 | convertAllRulesToProfileRules,
29 | getRulesProfile
30 | } from '../src/utils/rule-transformer.js';
31 | import { updateConfigMaxTokens } from './modules/update-config-tokens.js';
32 |
33 | // Import asset resolver
34 | import { assetExists, readAsset } from '../src/utils/asset-resolver.js';
35 |
36 | import { execSync } from 'child_process';
37 | import {
38 | EXAMPLE_PRD_FILE,
39 | TASKMASTER_CONFIG_FILE,
40 | TASKMASTER_TEMPLATES_DIR,
41 | TASKMASTER_DIR,
42 | TASKMASTER_TASKS_DIR,
43 | TASKMASTER_DOCS_DIR,
44 | TASKMASTER_REPORTS_DIR,
45 | TASKMASTER_STATE_FILE,
46 | ENV_EXAMPLE_FILE,
47 | GITIGNORE_FILE
48 | } from '../src/constants/paths.js';
49 |
50 | // Define log levels
51 | const LOG_LEVELS = {
52 | debug: 0,
53 | info: 1,
54 | warn: 2,
55 | error: 3,
56 | success: 4
57 | };
58 |
59 | // Determine log level from environment variable or default to 'info'
60 | const LOG_LEVEL = process.env.TASKMASTER_LOG_LEVEL
61 | ? LOG_LEVELS[process.env.TASKMASTER_LOG_LEVEL.toLowerCase()]
62 | : LOG_LEVELS.info; // Default to info
63 |
64 | // Create a color gradient for the banner
65 | const coolGradient = gradient(['#00b4d8', '#0077b6', '#03045e']);
66 | const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']);
67 |
68 | // Display a fancy banner
69 | function displayBanner() {
70 | if (isSilentMode()) return;
71 |
72 | console.clear();
73 | const bannerText = figlet.textSync('Task Master AI', {
74 | font: 'Standard',
75 | horizontalLayout: 'default',
76 | verticalLayout: 'default'
77 | });
78 |
79 | console.log(coolGradient(bannerText));
80 |
81 | // Add creator credit line below the banner
82 | console.log(
83 | chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano')
84 | );
85 |
86 | console.log(
87 | boxen(chalk.white(`${chalk.bold('Initializing')} your new project`), {
88 | padding: 1,
89 | margin: { top: 0, bottom: 1 },
90 | borderStyle: 'round',
91 | borderColor: 'cyan'
92 | })
93 | );
94 | }
95 |
96 | // Logging function with icons and colors
97 | function log(level, ...args) {
98 | const icons = {
99 | debug: chalk.gray('🔍'),
100 | info: chalk.blue('ℹ️'),
101 | warn: chalk.yellow('⚠️'),
102 | error: chalk.red('❌'),
103 | success: chalk.green('✅')
104 | };
105 |
106 | if (LOG_LEVELS[level] >= LOG_LEVEL) {
107 | const icon = icons[level] || '';
108 |
109 | // Only output to console if not in silent mode
110 | if (!isSilentMode()) {
111 | if (level === 'error') {
112 | console.error(icon, chalk.red(...args));
113 | } else if (level === 'warn') {
114 | console.warn(icon, chalk.yellow(...args));
115 | } else if (level === 'success') {
116 | console.log(icon, chalk.green(...args));
117 | } else if (level === 'info') {
118 | console.log(icon, chalk.blue(...args));
119 | } else {
120 | console.log(icon, ...args);
121 | }
122 | }
123 | }
124 |
125 | // Write to debug log if DEBUG=true
126 | if (process.env.DEBUG === 'true') {
127 | const logMessage = `[${level.toUpperCase()}] ${args.join(' ')}\n`;
128 | fs.appendFileSync('init-debug.log', logMessage);
129 | }
130 | }
131 |
132 | // Function to create directory if it doesn't exist
133 | function ensureDirectoryExists(dirPath) {
134 | if (!fs.existsSync(dirPath)) {
135 | fs.mkdirSync(dirPath, { recursive: true });
136 | log('info', `Created directory: ${dirPath}`);
137 | }
138 | }
139 |
140 | // Function to add shell aliases to the user's shell configuration
141 | function addShellAliases() {
142 | const homeDir = process.env.HOME || process.env.USERPROFILE;
143 | let shellConfigFile;
144 |
145 | // Determine which shell config file to use
146 | if (process.env.SHELL?.includes('zsh')) {
147 | shellConfigFile = path.join(homeDir, '.zshrc');
148 | } else if (process.env.SHELL?.includes('bash')) {
149 | shellConfigFile = path.join(homeDir, '.bashrc');
150 | } else {
151 | log('warn', 'Could not determine shell type. Aliases not added.');
152 | return false;
153 | }
154 |
155 | try {
156 | // Check if file exists
157 | if (!fs.existsSync(shellConfigFile)) {
158 | log(
159 | 'warn',
160 | `Shell config file ${shellConfigFile} not found. Aliases not added.`
161 | );
162 | return false;
163 | }
164 |
165 | // Check if aliases already exist
166 | const configContent = fs.readFileSync(shellConfigFile, 'utf8');
167 | if (configContent.includes("alias tm='task-master'")) {
168 | log('info', 'Task Master aliases already exist in shell config.');
169 | return true;
170 | }
171 |
172 | // Add aliases to the shell config file
173 | const aliasBlock = `
174 | # Task Master aliases added on ${new Date().toLocaleDateString()}
175 | alias tm='task-master'
176 | alias taskmaster='task-master'
177 | `;
178 |
179 | fs.appendFileSync(shellConfigFile, aliasBlock);
180 | log('success', `Added Task Master aliases to ${shellConfigFile}`);
181 | log(
182 | 'info',
183 | `To use the aliases in your current terminal, run: source ${shellConfigFile}`
184 | );
185 |
186 | return true;
187 | } catch (error) {
188 | log('error', `Failed to add aliases: ${error.message}`);
189 | return false;
190 | }
191 | }
192 |
193 | // Function to create initial state.json file for tag management
194 | function createInitialStateFile(targetDir) {
195 | const stateFilePath = path.join(targetDir, TASKMASTER_STATE_FILE);
196 |
197 | // Check if state.json already exists
198 | if (fs.existsSync(stateFilePath)) {
199 | log('info', 'State file already exists, preserving current configuration');
200 | return;
201 | }
202 |
203 | // Create initial state configuration
204 | const initialState = {
205 | currentTag: 'master',
206 | lastSwitched: new Date().toISOString(),
207 | branchTagMapping: {},
208 | migrationNoticeShown: false
209 | };
210 |
211 | try {
212 | fs.writeFileSync(stateFilePath, JSON.stringify(initialState, null, 2));
213 | log('success', `Created initial state file: ${stateFilePath}`);
214 | log('info', 'Default tag set to "master" for task organization');
215 | } catch (error) {
216 | log('error', `Failed to create state file: ${error.message}`);
217 | }
218 | }
219 |
220 | // Function to copy a file from the package to the target directory
221 | function copyTemplateFile(templateName, targetPath, replacements = {}) {
222 | // Get the file content from the appropriate source directory
223 | // Check if the asset exists
224 | if (!assetExists(templateName)) {
225 | log('error', `Source file not found: ${templateName}`);
226 | return;
227 | }
228 |
229 | // Read the asset content using the resolver
230 | let content = readAsset(templateName, 'utf8');
231 |
232 | // Replace placeholders with actual values
233 | Object.entries(replacements).forEach(([key, value]) => {
234 | const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
235 | content = content.replace(regex, value);
236 | });
237 |
238 | // Handle special files that should be merged instead of overwritten
239 | if (fs.existsSync(targetPath)) {
240 | const filename = path.basename(targetPath);
241 |
242 | // Handle .gitignore - append lines that don't exist
243 | if (filename === '.gitignore') {
244 | log('info', `${targetPath} already exists, merging content...`);
245 | const existingContent = fs.readFileSync(targetPath, 'utf8');
246 | const existingLines = new Set(
247 | existingContent.split('\n').map((line) => line.trim())
248 | );
249 | const newLines = content
250 | .split('\n')
251 | .filter((line) => !existingLines.has(line.trim()));
252 |
253 | if (newLines.length > 0) {
254 | // Add a comment to separate the original content from our additions
255 | const updatedContent = `${existingContent.trim()}\n\n# Added by Task Master AI\n${newLines.join('\n')}`;
256 | fs.writeFileSync(targetPath, updatedContent);
257 | log('success', `Updated ${targetPath} with additional entries`);
258 | } else {
259 | log('info', `No new content to add to ${targetPath}`);
260 | }
261 | return;
262 | }
263 |
264 | // Handle README.md - offer to preserve or create a different file
265 | if (filename === 'README-task-master.md') {
266 | log('info', `${targetPath} already exists`);
267 | // Create a separate README file specifically for this project
268 | const taskMasterReadmePath = path.join(
269 | path.dirname(targetPath),
270 | 'README-task-master.md'
271 | );
272 | fs.writeFileSync(taskMasterReadmePath, content);
273 | log(
274 | 'success',
275 | `Created ${taskMasterReadmePath} (preserved original README-task-master.md)`
276 | );
277 | return;
278 | }
279 |
280 | // For other files, warn and prompt before overwriting
281 | log('warn', `${targetPath} already exists, skipping.`);
282 | return;
283 | }
284 |
285 | // If the file doesn't exist, create it normally
286 | fs.writeFileSync(targetPath, content);
287 | log('info', `Created file: ${targetPath}`);
288 | }
289 |
290 | // Main function to initialize a new project
291 | async function initializeProject(options = {}) {
292 | // Receives options as argument
293 | // Only display banner if not in silent mode
294 | if (!isSilentMode()) {
295 | displayBanner();
296 | }
297 |
298 | // Debug logging only if not in silent mode
299 | // if (!isSilentMode()) {
300 | // console.log('===== DEBUG: INITIALIZE PROJECT OPTIONS RECEIVED =====');
301 | // console.log('Full options object:', JSON.stringify(options));
302 | // console.log('options.yes:', options.yes);
303 | // console.log('==================================================');
304 | // }
305 |
306 | // Handle boolean aliases flags
307 | if (options.aliases === true) {
308 | options.addAliases = true; // --aliases flag provided
309 | } else if (options.aliases === false) {
310 | options.addAliases = false; // --no-aliases flag provided
311 | }
312 | // If options.aliases and options.noAliases are undefined, we'll prompt for it
313 |
314 | // Handle boolean git flags
315 | if (options.git === true) {
316 | options.initGit = true; // --git flag provided
317 | } else if (options.git === false) {
318 | options.initGit = false; // --no-git flag provided
319 | }
320 | // If options.git and options.noGit are undefined, we'll prompt for it
321 |
322 | // Handle boolean gitTasks flags
323 | if (options.gitTasks === true) {
324 | options.storeTasksInGit = true; // --git-tasks flag provided
325 | } else if (options.gitTasks === false) {
326 | options.storeTasksInGit = false; // --no-git-tasks flag provided
327 | }
328 | // If options.gitTasks and options.noGitTasks are undefined, we'll prompt for it
329 |
330 | const skipPrompts = options.yes || (options.name && options.description);
331 |
332 | // if (!isSilentMode()) {
333 | // console.log('Skip prompts determined:', skipPrompts);
334 | // }
335 |
336 | let selectedRuleProfiles;
337 | if (options.rulesExplicitlyProvided) {
338 | // If --rules flag was used, always respect it.
339 | log(
340 | 'info',
341 | `Using rule profiles provided via command line: ${options.rules.join(', ')}`
342 | );
343 | selectedRuleProfiles = options.rules;
344 | } else if (skipPrompts) {
345 | // If non-interactive (e.g., --yes) and no rules specified, default to ALL.
346 | log(
347 | 'info',
348 | `No rules specified in non-interactive mode, defaulting to all profiles.`
349 | );
350 | selectedRuleProfiles = RULE_PROFILES;
351 | } else {
352 | // If interactive and no rules specified, default to NONE.
353 | // The 'rules --setup' wizard will handle selection.
354 | log(
355 | 'info',
356 | 'No rules specified; interactive setup will be launched to select profiles.'
357 | );
358 | selectedRuleProfiles = [];
359 | }
360 |
361 | if (skipPrompts) {
362 | if (!isSilentMode()) {
363 | console.log('SKIPPING PROMPTS - Using defaults or provided values');
364 | }
365 |
366 | // Use provided options or defaults
367 | const projectName = options.name || 'task-master-project';
368 | const projectDescription =
369 | options.description || 'A project managed with Task Master AI';
370 | const projectVersion = options.version || '0.1.0';
371 | const authorName = options.author || 'Vibe coder';
372 | const dryRun = options.dryRun || false;
373 | const addAliases =
374 | options.addAliases !== undefined ? options.addAliases : true; // Default to true if not specified
375 | const initGit = options.initGit !== undefined ? options.initGit : true; // Default to true if not specified
376 | const storeTasksInGit =
377 | options.storeTasksInGit !== undefined ? options.storeTasksInGit : true; // Default to true if not specified
378 |
379 | if (dryRun) {
380 | log('info', 'DRY RUN MODE: No files will be modified');
381 | log('info', 'Would initialize Task Master project');
382 | log('info', 'Would create/update necessary project files');
383 |
384 | // Show flag-specific behavior
385 | log(
386 | 'info',
387 | `${addAliases ? 'Would add shell aliases (tm, taskmaster)' : 'Would skip shell aliases'}`
388 | );
389 | log(
390 | 'info',
391 | `${initGit ? 'Would initialize Git repository' : 'Would skip Git initialization'}`
392 | );
393 | log(
394 | 'info',
395 | `${storeTasksInGit ? 'Would store tasks in Git' : 'Would exclude tasks from Git'}`
396 | );
397 |
398 | return {
399 | dryRun: true
400 | };
401 | }
402 |
403 | createProjectStructure(
404 | addAliases,
405 | initGit,
406 | storeTasksInGit,
407 | dryRun,
408 | options,
409 | selectedRuleProfiles
410 | );
411 | } else {
412 | // Interactive logic
413 | log('info', 'Required options not provided, proceeding with prompts.');
414 |
415 | try {
416 | const rl = readline.createInterface({
417 | input: process.stdin,
418 | output: process.stdout
419 | });
420 | // Prompt for shell aliases (skip if --aliases or --no-aliases flag was provided)
421 | let addAliasesPrompted = true; // Default to true
422 | if (options.addAliases !== undefined) {
423 | addAliasesPrompted = options.addAliases; // Use flag value if provided
424 | } else {
425 | const addAliasesInput = await promptQuestion(
426 | rl,
427 | chalk.cyan(
428 | 'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
429 | )
430 | );
431 | addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
432 | }
433 |
434 | // Prompt for Git initialization (skip if --git or --no-git flag was provided)
435 | let initGitPrompted = true; // Default to true
436 | if (options.initGit !== undefined) {
437 | initGitPrompted = options.initGit; // Use flag value if provided
438 | } else {
439 | const gitInitInput = await promptQuestion(
440 | rl,
441 | chalk.cyan('Initialize a Git repository in project root? (Y/n): ')
442 | );
443 | initGitPrompted = gitInitInput.trim().toLowerCase() !== 'n';
444 | }
445 |
446 | // Prompt for Git tasks storage (skip if --git-tasks or --no-git-tasks flag was provided)
447 | let storeGitPrompted = true; // Default to true
448 | if (options.storeTasksInGit !== undefined) {
449 | storeGitPrompted = options.storeTasksInGit; // Use flag value if provided
450 | } else {
451 | const gitTasksInput = await promptQuestion(
452 | rl,
453 | chalk.cyan(
454 | 'Store tasks in Git (tasks.json and tasks/ directory)? (Y/n): '
455 | )
456 | );
457 | storeGitPrompted = gitTasksInput.trim().toLowerCase() !== 'n';
458 | }
459 |
460 | // Confirm settings...
461 | console.log('\nTask Master Project settings:');
462 | console.log(
463 | chalk.blue(
464 | 'Add shell aliases (so you can use "tm" instead of "task-master"):'
465 | ),
466 | chalk.white(addAliasesPrompted ? 'Yes' : 'No')
467 | );
468 | console.log(
469 | chalk.blue('Initialize Git repository in project root:'),
470 | chalk.white(initGitPrompted ? 'Yes' : 'No')
471 | );
472 | console.log(
473 | chalk.blue('Store tasks in Git (tasks.json and tasks/ directory):'),
474 | chalk.white(storeGitPrompted ? 'Yes' : 'No')
475 | );
476 |
477 | const confirmInput = await promptQuestion(
478 | rl,
479 | chalk.yellow('\nDo you want to continue with these settings? (Y/n): ')
480 | );
481 | const shouldContinue = confirmInput.trim().toLowerCase() !== 'n';
482 |
483 | if (!shouldContinue) {
484 | rl.close();
485 | log('info', 'Project initialization cancelled by user');
486 | process.exit(0);
487 | return;
488 | }
489 |
490 | // Only run interactive rules if rules flag not provided via command line
491 | if (options.rulesExplicitlyProvided) {
492 | log(
493 | 'info',
494 | `Using rule profiles provided via command line: ${selectedRuleProfiles.join(', ')}`
495 | );
496 | }
497 |
498 | const dryRun = options.dryRun || false;
499 |
500 | if (dryRun) {
501 | log('info', 'DRY RUN MODE: No files will be modified');
502 | log('info', 'Would initialize Task Master project');
503 | log('info', 'Would create/update necessary project files');
504 |
505 | // Show flag-specific behavior
506 | log(
507 | 'info',
508 | `${addAliasesPrompted ? 'Would add shell aliases (tm, taskmaster)' : 'Would skip shell aliases'}`
509 | );
510 | log(
511 | 'info',
512 | `${initGitPrompted ? 'Would initialize Git repository' : 'Would skip Git initialization'}`
513 | );
514 | log(
515 | 'info',
516 | `${storeGitPrompted ? 'Would store tasks in Git' : 'Would exclude tasks from Git'}`
517 | );
518 |
519 | return {
520 | dryRun: true
521 | };
522 | }
523 |
524 | // Create structure using only necessary values
525 | createProjectStructure(
526 | addAliasesPrompted,
527 | initGitPrompted,
528 | storeGitPrompted,
529 | dryRun,
530 | options,
531 | selectedRuleProfiles
532 | );
533 | rl.close();
534 | } catch (error) {
535 | if (rl) {
536 | rl.close();
537 | }
538 | log('error', `Error during initialization process: ${error.message}`);
539 | process.exit(1);
540 | }
541 | }
542 | }
543 |
544 | // Helper function to promisify readline question
545 | function promptQuestion(rl, question) {
546 | return new Promise((resolve) => {
547 | rl.question(question, (answer) => {
548 | resolve(answer);
549 | });
550 | });
551 | }
552 |
553 | // Function to create the project structure
554 | function createProjectStructure(
555 | addAliases,
556 | initGit,
557 | storeTasksInGit,
558 | dryRun,
559 | options,
560 | selectedRuleProfiles = RULE_PROFILES
561 | ) {
562 | const targetDir = process.cwd();
563 | log('info', `Initializing project in ${targetDir}`);
564 |
565 | // Create NEW .taskmaster directory structure (using constants)
566 | ensureDirectoryExists(path.join(targetDir, TASKMASTER_DIR));
567 | ensureDirectoryExists(path.join(targetDir, TASKMASTER_TASKS_DIR));
568 | ensureDirectoryExists(path.join(targetDir, TASKMASTER_DOCS_DIR));
569 | ensureDirectoryExists(path.join(targetDir, TASKMASTER_REPORTS_DIR));
570 | ensureDirectoryExists(path.join(targetDir, TASKMASTER_TEMPLATES_DIR));
571 |
572 | // Create initial state.json file for tag management
573 | createInitialStateFile(targetDir);
574 |
575 | // Copy template files with replacements
576 | const replacements = {
577 | year: new Date().getFullYear()
578 | };
579 |
580 | // Helper function to create rule profiles
581 | function _processSingleProfile(profileName) {
582 | const profile = getRulesProfile(profileName);
583 | if (profile) {
584 | convertAllRulesToProfileRules(targetDir, profile);
585 | // Also triggers MCP config setup (if applicable)
586 | } else {
587 | log('warn', `Unknown rule profile: ${profileName}`);
588 | }
589 | }
590 |
591 | // Copy .env.example
592 | copyTemplateFile(
593 | 'env.example',
594 | path.join(targetDir, ENV_EXAMPLE_FILE),
595 | replacements
596 | );
597 |
598 | // Copy config.json with project name to NEW location
599 | copyTemplateFile(
600 | 'config.json',
601 | path.join(targetDir, TASKMASTER_CONFIG_FILE),
602 | {
603 | ...replacements
604 | }
605 | );
606 |
607 | // Update config.json with correct maxTokens values from supported-models.json
608 | const configPath = path.join(targetDir, TASKMASTER_CONFIG_FILE);
609 | if (updateConfigMaxTokens(configPath)) {
610 | log('info', 'Updated config with correct maxTokens values');
611 | } else {
612 | log('warn', 'Could not update maxTokens in config');
613 | }
614 |
615 | // Copy .gitignore with GitTasks preference
616 | try {
617 | const templateContent = readAsset('gitignore', 'utf8');
618 | manageGitignoreFile(
619 | path.join(targetDir, GITIGNORE_FILE),
620 | templateContent,
621 | storeTasksInGit,
622 | log
623 | );
624 | } catch (error) {
625 | log('error', `Failed to create .gitignore: ${error.message}`);
626 | }
627 |
628 | // Copy example_prd.txt to NEW location
629 | copyTemplateFile('example_prd.txt', path.join(targetDir, EXAMPLE_PRD_FILE));
630 |
631 | // Copy example_prd_rpg.txt to templates directory
632 | copyTemplateFile(
633 | 'example_prd_rpg.txt',
634 | path.join(targetDir, TASKMASTER_TEMPLATES_DIR, 'example_prd_rpg.txt')
635 | );
636 |
637 | // Initialize git repository if git is available
638 | try {
639 | if (initGit === false) {
640 | log('info', 'Git initialization skipped due to --no-git flag.');
641 | } else if (initGit === true) {
642 | if (insideGitWorkTree()) {
643 | log(
644 | 'info',
645 | 'Existing Git repository detected – skipping git init despite --git flag.'
646 | );
647 | } else {
648 | log('info', 'Initializing Git repository due to --git flag...');
649 | execSync('git init', { cwd: targetDir, stdio: 'ignore' });
650 | log('success', 'Git repository initialized');
651 | }
652 | } else {
653 | // Default behavior when no flag is provided (from interactive prompt)
654 | if (insideGitWorkTree()) {
655 | log('info', 'Existing Git repository detected – skipping git init.');
656 | } else {
657 | log(
658 | 'info',
659 | 'No Git repository detected. Initializing one in project root...'
660 | );
661 | execSync('git init', { cwd: targetDir, stdio: 'ignore' });
662 | log('success', 'Git repository initialized');
663 | }
664 | }
665 | } catch (error) {
666 | log('warn', 'Git not available, skipping repository initialization');
667 | }
668 |
669 | // Only run the manual transformer if rules were provided via flags.
670 | // The interactive `rules --setup` wizard handles its own installation.
671 | if (options.rulesExplicitlyProvided || options.yes) {
672 | log('info', 'Generating profile rules from command-line flags...');
673 | for (const profileName of selectedRuleProfiles) {
674 | _processSingleProfile(profileName);
675 | }
676 | }
677 |
678 | // Add shell aliases if requested
679 | if (addAliases) {
680 | addShellAliases();
681 | }
682 |
683 | // Run npm install automatically
684 | const npmInstallOptions = {
685 | cwd: targetDir,
686 | // Default to inherit for interactive CLI, change if silent
687 | stdio: 'inherit'
688 | };
689 |
690 | if (isSilentMode()) {
691 | // If silent (MCP mode), suppress npm install output
692 | npmInstallOptions.stdio = 'ignore';
693 | log('info', 'Running npm install silently...'); // Log our own message
694 | } else {
695 | // Interactive mode, show the boxen message
696 | console.log(
697 | boxen(chalk.cyan('Installing dependencies...'), {
698 | padding: 0.5,
699 | margin: 0.5,
700 | borderStyle: 'round',
701 | borderColor: 'blue'
702 | })
703 | );
704 | }
705 |
706 | // === Add Rule Profiles Setup Step ===
707 | if (
708 | !isSilentMode() &&
709 | !dryRun &&
710 | !options?.yes &&
711 | !options.rulesExplicitlyProvided
712 | ) {
713 | console.log(
714 | boxen(chalk.cyan('Configuring Rule Profiles...'), {
715 | padding: 0.5,
716 | margin: { top: 1, bottom: 0.5 },
717 | borderStyle: 'round',
718 | borderColor: 'blue'
719 | })
720 | );
721 | log(
722 | 'info',
723 | 'Running interactive rules setup. Please select which rule profiles to include.'
724 | );
725 | try {
726 | // Correct command confirmed by you.
727 | execSync('npx task-master rules --setup', {
728 | stdio: 'inherit',
729 | cwd: targetDir
730 | });
731 | log('success', 'Rule profiles configured.');
732 | } catch (error) {
733 | log('error', 'Failed to configure rule profiles:', error.message);
734 | log('warn', 'You may need to run "task-master rules --setup" manually.');
735 | }
736 | } else if (isSilentMode() || dryRun || options?.yes) {
737 | // This branch can log why setup was skipped, similar to the model setup logic.
738 | if (options.rulesExplicitlyProvided) {
739 | log(
740 | 'info',
741 | 'Skipping interactive rules setup because --rules flag was used.'
742 | );
743 | } else {
744 | log('info', 'Skipping interactive rules setup in non-interactive mode.');
745 | }
746 | }
747 | // =====================================
748 |
749 | // === Add Response Language Step ===
750 | if (!isSilentMode() && !dryRun && !options?.yes) {
751 | console.log(
752 | boxen(chalk.cyan('Configuring Response Language...'), {
753 | padding: 0.5,
754 | margin: { top: 1, bottom: 0.5 },
755 | borderStyle: 'round',
756 | borderColor: 'blue'
757 | })
758 | );
759 | log(
760 | 'info',
761 | 'Running interactive response language setup. Please input your preferred language.'
762 | );
763 | try {
764 | execSync('npx task-master lang --setup', {
765 | stdio: 'inherit',
766 | cwd: targetDir
767 | });
768 | log('success', 'Response Language configured.');
769 | } catch (error) {
770 | log('error', 'Failed to configure response language:', error.message);
771 | log('warn', 'You may need to run "task-master lang --setup" manually.');
772 | }
773 | } else if (isSilentMode() && !dryRun) {
774 | log(
775 | 'info',
776 | 'Skipping interactive response language setup in silent (MCP) mode.'
777 | );
778 | log(
779 | 'warn',
780 | 'Please configure response language using "task-master models --set-response-language" or the "models" MCP tool.'
781 | );
782 | } else if (dryRun) {
783 | log('info', 'DRY RUN: Skipping interactive response language setup.');
784 | }
785 | // =====================================
786 |
787 | // === Add Model Configuration Step ===
788 | if (!isSilentMode() && !dryRun && !options?.yes) {
789 | console.log(
790 | boxen(chalk.cyan('Configuring AI Models...'), {
791 | padding: 0.5,
792 | margin: { top: 1, bottom: 0.5 },
793 | borderStyle: 'round',
794 | borderColor: 'blue'
795 | })
796 | );
797 | log(
798 | 'info',
799 | 'Running interactive model setup. Please select your preferred AI models.'
800 | );
801 | try {
802 | execSync('npx task-master models --setup', {
803 | stdio: 'inherit',
804 | cwd: targetDir
805 | });
806 | log('success', 'AI Models configured.');
807 | } catch (error) {
808 | log('error', 'Failed to configure AI models:', error.message);
809 | log('warn', 'You may need to run "task-master models --setup" manually.');
810 | }
811 | } else if (isSilentMode() && !dryRun) {
812 | log('info', 'Skipping interactive model setup in silent (MCP) mode.');
813 | log(
814 | 'warn',
815 | 'Please configure AI models using "task-master models --set-..." or the "models" MCP tool.'
816 | );
817 | } else if (dryRun) {
818 | log('info', 'DRY RUN: Skipping interactive model setup.');
819 | } else if (options?.yes) {
820 | log('info', 'Skipping interactive model setup due to --yes flag.');
821 | log(
822 | 'info',
823 | 'Default AI models will be used. You can configure different models later using "task-master models --setup" or "task-master models --set-..." commands.'
824 | );
825 | }
826 | // ====================================
827 |
828 | // Add shell aliases if requested
829 | if (addAliases && !dryRun) {
830 | log('info', 'Adding shell aliases...');
831 | const aliasResult = addShellAliases();
832 | if (aliasResult) {
833 | log('success', 'Shell aliases added successfully');
834 | }
835 | } else if (addAliases && dryRun) {
836 | log('info', 'DRY RUN: Would add shell aliases (tm, taskmaster)');
837 | }
838 |
839 | // Display success message
840 | if (!isSilentMode()) {
841 | console.log(
842 | boxen(
843 | `${warmGradient.multiline(
844 | figlet.textSync('Success!', { font: 'Standard' })
845 | )}\n${chalk.green('Project initialized successfully!')}`,
846 | {
847 | padding: 1,
848 | margin: 1,
849 | borderStyle: 'double',
850 | borderColor: 'green'
851 | }
852 | )
853 | );
854 | }
855 |
856 | // Display next steps in a nice box
857 | if (!isSilentMode()) {
858 | console.log(
859 | boxen(
860 | `${chalk.cyan.bold('Things you should do next:')}\n\n${chalk.white('1. ')}${chalk.yellow(
861 | 'Configure AI models (if needed) and add API keys to `.env`'
862 | )}\n${chalk.white(' ├─ ')}${chalk.dim('Models: Use `task-master models` commands')}\n${chalk.white(' └─ ')}${chalk.dim(
863 | 'Keys: Add provider API keys to .env (or inside the MCP config file i.e. .cursor/mcp.json)'
864 | )}\n${chalk.white('2. ')}${chalk.yellow(
865 | 'Discuss your idea with AI and ask for a PRD, and save it to .taskmaster/docs/prd.txt'
866 | )}\n${chalk.white(' ├─ ')}${chalk.dim('Simple projects: Use ')}${chalk.cyan('example_prd.txt')}${chalk.dim(' template')}\n${chalk.white(' └─ ')}${chalk.dim('Complex systems: Use ')}${chalk.cyan('example_prd_rpg.txt')}${chalk.dim(' template (for dependency-aware task graphs)')}\n${chalk.white('3. ')}${chalk.yellow(
867 | 'Ask Cursor Agent (or run CLI) to parse your PRD and generate initial tasks:'
868 | )}\n${chalk.white(' └─ ')}${chalk.dim('MCP Tool: ')}${chalk.cyan('parse_prd')}${chalk.dim(' | CLI: ')}${chalk.cyan('task-master parse-prd .taskmaster/docs/prd.txt')}\n${chalk.white('4. ')}${chalk.yellow(
869 | 'Ask Cursor to analyze the complexity of the tasks in your PRD using research'
870 | )}\n${chalk.white(' └─ ')}${chalk.dim('MCP Tool: ')}${chalk.cyan('analyze_project_complexity')}${chalk.dim(' | CLI: ')}${chalk.cyan('task-master analyze-complexity')}\n${chalk.white('5. ')}${chalk.yellow(
871 | 'Ask Cursor to expand all of your tasks using the complexity analysis'
872 | )}\n${chalk.white('6. ')}${chalk.yellow('Ask Cursor to begin working on the next task')}\n${chalk.white('7. ')}${chalk.yellow(
873 | 'Add new tasks anytime using the add-task command or MCP tool'
874 | )}\n${chalk.white('8. ')}${chalk.yellow(
875 | 'Ask Cursor to set the status of one or many tasks/subtasks at a time. Use the task id from the task lists.'
876 | )}\n${chalk.white('9. ')}${chalk.yellow(
877 | 'Ask Cursor to update all tasks from a specific task id based on new learnings or pivots in your project.'
878 | )}\n${chalk.white('10. ')}${chalk.green.bold('Ship it!')}\n\n${chalk.dim(
879 | '* Review the README.md file to learn how to use other commands via Cursor Agent.'
880 | )}\n${chalk.dim(
881 | '* Use the task-master command without arguments to see all available commands.'
882 | )}`,
883 | {
884 | padding: 1,
885 | margin: 1,
886 | borderStyle: 'round',
887 | borderColor: 'yellow',
888 | title: 'Getting Started',
889 | titleAlignment: 'center'
890 | }
891 | )
892 | );
893 | }
894 | }
895 |
896 | // Ensure necessary functions are exported
897 | export { initializeProject, log };
898 |
```
--------------------------------------------------------------------------------
/scripts/modules/task-manager/research.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * research.js
3 | * Core research functionality for AI-powered queries with project context
4 | */
5 |
6 | import fs from 'fs';
7 | import path from 'path';
8 | import chalk from 'chalk';
9 | import boxen from 'boxen';
10 | import inquirer from 'inquirer';
11 | import { highlight } from 'cli-highlight';
12 | import { ContextGatherer } from '../utils/contextGatherer.js';
13 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
14 | import { generateTextService } from '../ai-services-unified.js';
15 | import { getPromptManager } from '../prompt-manager.js';
16 | import {
17 | log as consoleLog,
18 | findProjectRoot,
19 | readJSON,
20 | flattenTasksWithSubtasks
21 | } from '../utils.js';
22 | import {
23 | displayAiUsageSummary,
24 | startLoadingIndicator,
25 | stopLoadingIndicator
26 | } from '../ui.js';
27 |
28 | /**
29 | * Perform AI-powered research with project context
30 | * @param {string} query - Research query/prompt
31 | * @param {Object} options - Research options
32 | * @param {Array<string>} [options.taskIds] - Task/subtask IDs for context
33 | * @param {Array<string>} [options.filePaths] - File paths for context
34 | * @param {string} [options.customContext] - Additional custom context
35 | * @param {boolean} [options.includeProjectTree] - Include project file tree
36 | * @param {string} [options.detailLevel] - Detail level: 'low', 'medium', 'high'
37 | * @param {string} [options.projectRoot] - Project root directory
38 | * @param {string} [options.tag] - Tag for the task
39 | * @param {boolean} [options.saveToFile] - Whether to save results to file (MCP mode)
40 | * @param {Object} [context] - Execution context
41 | * @param {Object} [context.session] - MCP session object
42 | * @param {Object} [context.mcpLog] - MCP logger object
43 | * @param {string} [context.commandName] - Command name for telemetry
44 | * @param {string} [context.outputType] - Output type ('cli' or 'mcp')
45 | * @param {string} [outputFormat] - Output format ('text' or 'json')
46 | * @param {boolean} [allowFollowUp] - Whether to allow follow-up questions (default: true)
47 | * @returns {Promise<Object>} Research results with telemetry data
48 | */
49 | async function performResearch(
50 | query,
51 | options = {},
52 | context = {},
53 | outputFormat = 'text',
54 | allowFollowUp = true
55 | ) {
56 | const {
57 | taskIds = [],
58 | filePaths = [],
59 | customContext = '',
60 | includeProjectTree = false,
61 | detailLevel = 'medium',
62 | projectRoot: providedProjectRoot,
63 | tag,
64 | saveToFile = false
65 | } = options;
66 |
67 | const {
68 | session,
69 | mcpLog,
70 | commandName = 'research',
71 | outputType = 'cli'
72 | } = context;
73 | const isMCP = !!mcpLog;
74 |
75 | // Determine project root
76 | const projectRoot = providedProjectRoot || findProjectRoot();
77 | if (!projectRoot) {
78 | throw new Error('Could not determine project root directory');
79 | }
80 |
81 | // Create consistent logger
82 | const logFn = isMCP
83 | ? mcpLog
84 | : {
85 | info: (...args) => consoleLog('info', ...args),
86 | warn: (...args) => consoleLog('warn', ...args),
87 | error: (...args) => consoleLog('error', ...args),
88 | debug: (...args) => consoleLog('debug', ...args),
89 | success: (...args) => consoleLog('success', ...args)
90 | };
91 |
92 | // Show UI banner for CLI mode
93 | if (outputFormat === 'text') {
94 | console.log(
95 | boxen(chalk.cyan.bold(`🔍 AI Research Query`), {
96 | padding: 1,
97 | borderColor: 'cyan',
98 | borderStyle: 'round',
99 | margin: { top: 1, bottom: 1 }
100 | })
101 | );
102 | }
103 |
104 | try {
105 | // Initialize context gatherer
106 | const contextGatherer = new ContextGatherer(projectRoot, tag);
107 |
108 | // Auto-discover relevant tasks using fuzzy search to supplement provided tasks
109 | let finalTaskIds = [...taskIds]; // Start with explicitly provided tasks
110 | let autoDiscoveredIds = [];
111 |
112 | try {
113 | const tasksPath = path.join(
114 | projectRoot,
115 | '.taskmaster',
116 | 'tasks',
117 | 'tasks.json'
118 | );
119 | const tasksData = await readJSON(tasksPath, projectRoot, tag);
120 |
121 | if (tasksData && tasksData.tasks && tasksData.tasks.length > 0) {
122 | // Flatten tasks to include subtasks for fuzzy search
123 | const flattenedTasks = flattenTasksWithSubtasks(tasksData.tasks);
124 | const fuzzySearch = new FuzzyTaskSearch(flattenedTasks, 'research');
125 | const searchResults = fuzzySearch.findRelevantTasks(query, {
126 | maxResults: 8,
127 | includeRecent: true,
128 | includeCategoryMatches: true
129 | });
130 |
131 | autoDiscoveredIds = fuzzySearch.getTaskIds(searchResults);
132 |
133 | // Remove any auto-discovered tasks that were already explicitly provided
134 | const uniqueAutoDiscovered = autoDiscoveredIds.filter(
135 | (id) => !finalTaskIds.includes(id)
136 | );
137 |
138 | // Add unique auto-discovered tasks to the final list
139 | finalTaskIds = [...finalTaskIds, ...uniqueAutoDiscovered];
140 |
141 | if (outputFormat === 'text' && finalTaskIds.length > 0) {
142 | // Sort task IDs numerically for better display
143 | const sortedTaskIds = finalTaskIds
144 | .map((id) => parseInt(id))
145 | .sort((a, b) => a - b)
146 | .map((id) => id.toString());
147 |
148 | // Show different messages based on whether tasks were explicitly provided
149 | if (taskIds.length > 0) {
150 | const sortedProvidedIds = taskIds
151 | .map((id) => parseInt(id))
152 | .sort((a, b) => a - b)
153 | .map((id) => id.toString());
154 |
155 | console.log(
156 | chalk.gray('Provided tasks: ') +
157 | chalk.cyan(sortedProvidedIds.join(', '))
158 | );
159 |
160 | if (uniqueAutoDiscovered.length > 0) {
161 | const sortedAutoIds = uniqueAutoDiscovered
162 | .map((id) => parseInt(id))
163 | .sort((a, b) => a - b)
164 | .map((id) => id.toString());
165 |
166 | console.log(
167 | chalk.gray('+ Auto-discovered related tasks: ') +
168 | chalk.cyan(sortedAutoIds.join(', '))
169 | );
170 | }
171 | } else {
172 | console.log(
173 | chalk.gray('Auto-discovered relevant tasks: ') +
174 | chalk.cyan(sortedTaskIds.join(', '))
175 | );
176 | }
177 | }
178 | }
179 | } catch (error) {
180 | // Silently continue without auto-discovered tasks if there's an error
181 | logFn.debug(`Could not auto-discover tasks: ${error.message}`);
182 | }
183 |
184 | const contextResult = await contextGatherer.gather({
185 | tasks: finalTaskIds,
186 | files: filePaths,
187 | customContext,
188 | includeProjectTree,
189 | format: 'research', // Use research format for AI consumption
190 | includeTokenCounts: true
191 | });
192 |
193 | const gatheredContext = contextResult.context;
194 | const tokenBreakdown = contextResult.tokenBreakdown;
195 |
196 | // Load prompts using PromptManager
197 | const promptManager = getPromptManager();
198 |
199 | const promptParams = {
200 | query: query,
201 | gatheredContext: gatheredContext || '',
202 | detailLevel: detailLevel,
203 | projectInfo: {
204 | root: projectRoot,
205 | taskCount: finalTaskIds.length,
206 | fileCount: filePaths.length
207 | }
208 | };
209 |
210 | // Load prompts - the research template handles detail level internally
211 | const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
212 | 'research',
213 | promptParams
214 | );
215 |
216 | // Count tokens for system and user prompts
217 | const systemPromptTokens = contextGatherer.countTokens(systemPrompt);
218 | const userPromptTokens = contextGatherer.countTokens(userPrompt);
219 | const totalInputTokens = systemPromptTokens + userPromptTokens;
220 |
221 | if (outputFormat === 'text') {
222 | // Display detailed token breakdown in a clean box
223 | displayDetailedTokenBreakdown(
224 | tokenBreakdown,
225 | systemPromptTokens,
226 | userPromptTokens
227 | );
228 | }
229 |
230 | // Only log detailed info in debug mode or MCP
231 | if (outputFormat !== 'text') {
232 | logFn.info(
233 | `Calling AI service with research role, context size: ${tokenBreakdown.total} tokens (${gatheredContext.length} characters)`
234 | );
235 | }
236 |
237 | // Start loading indicator for CLI mode
238 | let loadingIndicator = null;
239 | if (outputFormat === 'text') {
240 | loadingIndicator = startLoadingIndicator('Researching with AI...\n');
241 | }
242 |
243 | let aiResult;
244 | try {
245 | // Call AI service with research role
246 | aiResult = await generateTextService({
247 | role: 'research', // Always use research role for research command
248 | session,
249 | projectRoot,
250 | systemPrompt,
251 | prompt: userPrompt,
252 | commandName,
253 | outputType
254 | });
255 | } catch (error) {
256 | if (loadingIndicator) {
257 | stopLoadingIndicator(loadingIndicator);
258 | }
259 | throw error;
260 | } finally {
261 | if (loadingIndicator) {
262 | stopLoadingIndicator(loadingIndicator);
263 | }
264 | }
265 |
266 | const researchResult = aiResult.mainResult;
267 | const telemetryData = aiResult.telemetryData;
268 | const tagInfo = aiResult.tagInfo;
269 |
270 | // Format and display results
271 | // Initialize interactive save tracking
272 | let interactiveSaveInfo = { interactiveSaveOccurred: false };
273 |
274 | if (outputFormat === 'text') {
275 | displayResearchResults(
276 | researchResult,
277 | query,
278 | detailLevel,
279 | tokenBreakdown
280 | );
281 |
282 | // Display AI usage telemetry for CLI users
283 | if (telemetryData) {
284 | displayAiUsageSummary(telemetryData, 'cli');
285 | }
286 |
287 | // Offer follow-up question option (only for initial CLI queries, not MCP)
288 | if (allowFollowUp && !isMCP) {
289 | interactiveSaveInfo = await handleFollowUpQuestions(
290 | options,
291 | context,
292 | outputFormat,
293 | projectRoot,
294 | logFn,
295 | query,
296 | researchResult
297 | );
298 | }
299 | }
300 |
301 | // Handle MCP save-to-file request
302 | if (saveToFile && isMCP) {
303 | const conversationHistory = [
304 | {
305 | question: query,
306 | answer: researchResult,
307 | type: 'initial',
308 | timestamp: new Date().toISOString()
309 | }
310 | ];
311 |
312 | const savedFilePath = await handleSaveToFile(
313 | conversationHistory,
314 | projectRoot,
315 | context,
316 | logFn
317 | );
318 |
319 | // Add saved file path to return data
320 | return {
321 | query,
322 | result: researchResult,
323 | contextSize: gatheredContext.length,
324 | contextTokens: tokenBreakdown.total,
325 | tokenBreakdown,
326 | systemPromptTokens,
327 | userPromptTokens,
328 | totalInputTokens,
329 | detailLevel,
330 | telemetryData,
331 | tagInfo,
332 | savedFilePath,
333 | interactiveSaveOccurred: false // MCP save-to-file doesn't count as interactive save
334 | };
335 | }
336 |
337 | logFn.success('Research query completed successfully');
338 |
339 | return {
340 | query,
341 | result: researchResult,
342 | contextSize: gatheredContext.length,
343 | contextTokens: tokenBreakdown.total,
344 | tokenBreakdown,
345 | systemPromptTokens,
346 | userPromptTokens,
347 | totalInputTokens,
348 | detailLevel,
349 | telemetryData,
350 | tagInfo,
351 | interactiveSaveOccurred:
352 | interactiveSaveInfo?.interactiveSaveOccurred || false
353 | };
354 | } catch (error) {
355 | logFn.error(`Research query failed: ${error.message}`);
356 |
357 | if (outputFormat === 'text') {
358 | console.error(chalk.red(`\n❌ Research failed: ${error.message}`));
359 | }
360 |
361 | throw error;
362 | }
363 | }
364 |
365 | /**
366 | * Display detailed token breakdown for context and prompts
367 | * @param {Object} tokenBreakdown - Token breakdown from context gatherer
368 | * @param {number} systemPromptTokens - System prompt token count
369 | * @param {number} userPromptTokens - User prompt token count
370 | */
371 | function displayDetailedTokenBreakdown(
372 | tokenBreakdown,
373 | systemPromptTokens,
374 | userPromptTokens
375 | ) {
376 | const parts = [];
377 |
378 | // Custom context
379 | if (tokenBreakdown.customContext) {
380 | parts.push(
381 | chalk.cyan('Custom: ') +
382 | chalk.yellow(tokenBreakdown.customContext.tokens.toLocaleString())
383 | );
384 | }
385 |
386 | // Tasks breakdown
387 | if (tokenBreakdown.tasks && tokenBreakdown.tasks.length > 0) {
388 | const totalTaskTokens = tokenBreakdown.tasks.reduce(
389 | (sum, task) => sum + task.tokens,
390 | 0
391 | );
392 | const taskDetails = tokenBreakdown.tasks
393 | .map((task) => {
394 | const titleDisplay =
395 | task.title.length > 30
396 | ? task.title.substring(0, 30) + '...'
397 | : task.title;
398 | return ` ${chalk.gray(task.id)} ${chalk.white(titleDisplay)} ${chalk.yellow(task.tokens.toLocaleString())} tokens`;
399 | })
400 | .join('\n');
401 |
402 | parts.push(
403 | chalk.cyan('Tasks: ') +
404 | chalk.yellow(totalTaskTokens.toLocaleString()) +
405 | chalk.gray(` (${tokenBreakdown.tasks.length} items)`) +
406 | '\n' +
407 | taskDetails
408 | );
409 | }
410 |
411 | // Files breakdown
412 | if (tokenBreakdown.files && tokenBreakdown.files.length > 0) {
413 | const totalFileTokens = tokenBreakdown.files.reduce(
414 | (sum, file) => sum + file.tokens,
415 | 0
416 | );
417 | const fileDetails = tokenBreakdown.files
418 | .map((file) => {
419 | const pathDisplay =
420 | file.path.length > 40
421 | ? '...' + file.path.substring(file.path.length - 37)
422 | : file.path;
423 | return ` ${chalk.gray(pathDisplay)} ${chalk.yellow(file.tokens.toLocaleString())} tokens ${chalk.gray(`(${file.sizeKB}KB)`)}`;
424 | })
425 | .join('\n');
426 |
427 | parts.push(
428 | chalk.cyan('Files: ') +
429 | chalk.yellow(totalFileTokens.toLocaleString()) +
430 | chalk.gray(` (${tokenBreakdown.files.length} files)`) +
431 | '\n' +
432 | fileDetails
433 | );
434 | }
435 |
436 | // Project tree
437 | if (tokenBreakdown.projectTree) {
438 | parts.push(
439 | chalk.cyan('Project Tree: ') +
440 | chalk.yellow(tokenBreakdown.projectTree.tokens.toLocaleString()) +
441 | chalk.gray(
442 | ` (${tokenBreakdown.projectTree.fileCount} files, ${tokenBreakdown.projectTree.dirCount} dirs)`
443 | )
444 | );
445 | }
446 |
447 | // Prompts breakdown
448 | const totalPromptTokens = systemPromptTokens + userPromptTokens;
449 | const promptDetails = [
450 | ` ${chalk.gray('System:')} ${chalk.yellow(systemPromptTokens.toLocaleString())} tokens`,
451 | ` ${chalk.gray('User:')} ${chalk.yellow(userPromptTokens.toLocaleString())} tokens`
452 | ].join('\n');
453 |
454 | parts.push(
455 | chalk.cyan('Prompts: ') +
456 | chalk.yellow(totalPromptTokens.toLocaleString()) +
457 | chalk.gray(' (generated)') +
458 | '\n' +
459 | promptDetails
460 | );
461 |
462 | // Display the breakdown in a clean box
463 | if (parts.length > 0) {
464 | const content = parts.join('\n\n');
465 | const tokenBox = boxen(content, {
466 | title: chalk.blue.bold('Context Analysis'),
467 | titleAlignment: 'left',
468 | padding: { top: 1, bottom: 1, left: 2, right: 2 },
469 | margin: { top: 0, bottom: 1 },
470 | borderStyle: 'single',
471 | borderColor: 'blue'
472 | });
473 | console.log(tokenBox);
474 | }
475 | }
476 |
477 | /**
478 | * Process research result text to highlight code blocks
479 | * @param {string} text - Raw research result text
480 | * @returns {string} Processed text with highlighted code blocks
481 | */
482 | function processCodeBlocks(text) {
483 | // Regex to match code blocks with optional language specification
484 | const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
485 |
486 | return text.replace(codeBlockRegex, (match, language, code) => {
487 | try {
488 | // Default to javascript if no language specified
489 | const lang = language || 'javascript';
490 |
491 | // Highlight the code using cli-highlight
492 | const highlightedCode = highlight(code.trim(), {
493 | language: lang,
494 | ignoreIllegals: true // Don't fail on unrecognized syntax
495 | });
496 |
497 | // Add a subtle border around code blocks
498 | const codeBox = boxen(highlightedCode, {
499 | padding: { top: 0, bottom: 0, left: 1, right: 1 },
500 | margin: { top: 0, bottom: 0 },
501 | borderStyle: 'single',
502 | borderColor: 'dim'
503 | });
504 |
505 | return '\n' + codeBox + '\n';
506 | } catch (error) {
507 | // If highlighting fails, return the original code block with basic formatting
508 | return (
509 | '\n' +
510 | chalk.gray('```' + (language || '')) +
511 | '\n' +
512 | chalk.white(code.trim()) +
513 | '\n' +
514 | chalk.gray('```') +
515 | '\n'
516 | );
517 | }
518 | });
519 | }
520 |
521 | /**
522 | * Display research results in formatted output
523 | * @param {string} result - AI research result
524 | * @param {string} query - Original query
525 | * @param {string} detailLevel - Detail level used
526 | * @param {Object} tokenBreakdown - Detailed token usage
527 | */
528 | function displayResearchResults(result, query, detailLevel, tokenBreakdown) {
529 | // Header with query info
530 | const header = boxen(
531 | chalk.green.bold('Research Results') +
532 | '\n\n' +
533 | chalk.gray('Query: ') +
534 | chalk.white(query) +
535 | '\n' +
536 | chalk.gray('Detail Level: ') +
537 | chalk.cyan(detailLevel),
538 | {
539 | padding: { top: 1, bottom: 1, left: 2, right: 2 },
540 | margin: { top: 1, bottom: 0 },
541 | borderStyle: 'round',
542 | borderColor: 'green'
543 | }
544 | );
545 | console.log(header);
546 |
547 | // Process the result to highlight code blocks
548 | const processedResult = processCodeBlocks(result);
549 |
550 | // Main research content in a clean box
551 | const contentBox = boxen(processedResult, {
552 | padding: { top: 1, bottom: 1, left: 2, right: 2 },
553 | margin: { top: 0, bottom: 1 },
554 | borderStyle: 'single',
555 | borderColor: 'gray'
556 | });
557 | console.log(contentBox);
558 |
559 | // Success footer
560 | console.log(chalk.green('✅ Research completed'));
561 | }
562 |
563 | /**
564 | * Handle follow-up questions and save functionality in interactive mode
565 | * @param {Object} originalOptions - Original research options
566 | * @param {Object} context - Execution context
567 | * @param {string} outputFormat - Output format
568 | * @param {string} projectRoot - Project root directory
569 | * @param {Object} logFn - Logger function
570 | * @param {string} initialQuery - Initial query for context
571 | * @param {string} initialResult - Initial AI result for context
572 | */
573 | async function handleFollowUpQuestions(
574 | originalOptions,
575 | context,
576 | outputFormat,
577 | projectRoot,
578 | logFn,
579 | initialQuery,
580 | initialResult
581 | ) {
582 | let interactiveSaveOccurred = false;
583 |
584 | try {
585 | // Import required modules for saving
586 | const { readJSON } = await import('../utils.js');
587 | const updateTaskById = (await import('./update-task-by-id.js')).default;
588 | const { updateSubtaskById } = await import('./update-subtask-by-id.js');
589 |
590 | // Initialize conversation history with the initial Q&A
591 | const conversationHistory = [
592 | {
593 | question: initialQuery,
594 | answer: initialResult,
595 | type: 'initial',
596 | timestamp: new Date().toISOString()
597 | }
598 | ];
599 |
600 | while (true) {
601 | // Get user choice
602 | const { action } = await inquirer.prompt([
603 | {
604 | type: 'list',
605 | name: 'action',
606 | message: 'What would you like to do next?',
607 | choices: [
608 | { name: 'Ask a follow-up question', value: 'followup' },
609 | { name: 'Save to file', value: 'savefile' },
610 | { name: 'Save to task/subtask', value: 'save' },
611 | { name: 'Quit', value: 'quit' }
612 | ],
613 | pageSize: 4
614 | }
615 | ]);
616 |
617 | if (action === 'quit') {
618 | break;
619 | }
620 |
621 | if (action === 'savefile') {
622 | // Handle save to file functionality
623 | await handleSaveToFile(
624 | conversationHistory,
625 | projectRoot,
626 | context,
627 | logFn
628 | );
629 | continue;
630 | }
631 |
632 | if (action === 'save') {
633 | // Handle save functionality
634 | const saveResult = await handleSaveToTask(
635 | conversationHistory,
636 | projectRoot,
637 | context,
638 | logFn
639 | );
640 | if (saveResult) {
641 | interactiveSaveOccurred = true;
642 | }
643 | continue;
644 | }
645 |
646 | if (action === 'followup') {
647 | // Get the follow-up question
648 | const { followUpQuery } = await inquirer.prompt([
649 | {
650 | type: 'input',
651 | name: 'followUpQuery',
652 | message: 'Enter your follow-up question:',
653 | validate: (input) => {
654 | if (!input || input.trim().length === 0) {
655 | return 'Please enter a valid question.';
656 | }
657 | return true;
658 | }
659 | }
660 | ]);
661 |
662 | if (!followUpQuery || followUpQuery.trim().length === 0) {
663 | continue;
664 | }
665 |
666 | console.log('\n' + chalk.gray('─'.repeat(60)) + '\n');
667 |
668 | // Build cumulative conversation context from all previous exchanges
669 | const conversationContext =
670 | buildConversationContext(conversationHistory);
671 |
672 | // Create enhanced options for follow-up with full conversation context
673 | const followUpOptions = {
674 | ...originalOptions,
675 | taskIds: [], // Clear task IDs to allow fresh fuzzy search
676 | customContext:
677 | conversationContext +
678 | (originalOptions.customContext
679 | ? `\n\n--- Original Context ---\n${originalOptions.customContext}`
680 | : '')
681 | };
682 |
683 | // Perform follow-up research
684 | const followUpResult = await performResearch(
685 | followUpQuery.trim(),
686 | followUpOptions,
687 | context,
688 | outputFormat,
689 | false // allowFollowUp = false for nested calls
690 | );
691 |
692 | // Add this exchange to the conversation history
693 | conversationHistory.push({
694 | question: followUpQuery.trim(),
695 | answer: followUpResult.result,
696 | type: 'followup',
697 | timestamp: new Date().toISOString()
698 | });
699 | }
700 | }
701 | } catch (error) {
702 | // If there's an error with inquirer (e.g., non-interactive terminal),
703 | // silently continue without follow-up functionality
704 | logFn.debug(`Follow-up questions not available: ${error.message}`);
705 | }
706 |
707 | return { interactiveSaveOccurred };
708 | }
709 |
710 | /**
711 | * Handle saving conversation to a task or subtask
712 | * @param {Array} conversationHistory - Array of conversation exchanges
713 | * @param {string} projectRoot - Project root directory
714 | * @param {Object} context - Execution context
715 | * @param {Object} logFn - Logger function
716 | */
717 | async function handleSaveToTask(
718 | conversationHistory,
719 | projectRoot,
720 | context,
721 | logFn
722 | ) {
723 | try {
724 | // Import required modules
725 | const { readJSON } = await import('../utils.js');
726 | const updateTaskById = (await import('./update-task-by-id.js')).default;
727 | const { updateSubtaskById } = await import('./update-subtask-by-id.js');
728 |
729 | // Get task ID from user
730 | const { taskId } = await inquirer.prompt([
731 | {
732 | type: 'input',
733 | name: 'taskId',
734 | message: 'Enter task ID (e.g., "15" for task or "15.2" for subtask):',
735 | validate: (input) => {
736 | if (!input || input.trim().length === 0) {
737 | return 'Please enter a task ID.';
738 | }
739 |
740 | const trimmedInput = input.trim();
741 | // Validate format: number or number.number
742 | if (!/^\d+(\.\d+)?$/.test(trimmedInput)) {
743 | return 'Invalid format. Use "15" for task or "15.2" for subtask.';
744 | }
745 |
746 | return true;
747 | }
748 | }
749 | ]);
750 |
751 | const trimmedTaskId = taskId.trim();
752 |
753 | // Format conversation thread for saving
754 | const conversationThread = formatConversationForSaving(conversationHistory);
755 |
756 | // Determine if it's a task or subtask
757 | const isSubtask = trimmedTaskId.includes('.');
758 |
759 | // Try to save - first validate the ID exists
760 | const tasksPath = path.join(
761 | projectRoot,
762 | '.taskmaster',
763 | 'tasks',
764 | 'tasks.json'
765 | );
766 |
767 | if (!fs.existsSync(tasksPath)) {
768 | console.log(
769 | chalk.red('❌ Tasks file not found. Please run task-master init first.')
770 | );
771 | return;
772 | }
773 |
774 | const data = readJSON(tasksPath, projectRoot, context.tag);
775 | if (!data || !data.tasks) {
776 | console.log(chalk.red('❌ No valid tasks found.'));
777 | return;
778 | }
779 |
780 | if (isSubtask) {
781 | // Validate subtask exists
782 | const [parentId, subtaskId] = trimmedTaskId
783 | .split('.')
784 | .map((id) => parseInt(id, 10));
785 | const parentTask = data.tasks.find((t) => t.id === parentId);
786 |
787 | if (!parentTask) {
788 | console.log(chalk.red(`❌ Parent task ${parentId} not found.`));
789 | return;
790 | }
791 |
792 | if (
793 | !parentTask.subtasks ||
794 | !parentTask.subtasks.find((st) => st.id === subtaskId)
795 | ) {
796 | console.log(chalk.red(`❌ Subtask ${trimmedTaskId} not found.`));
797 | return;
798 | }
799 |
800 | // Save to subtask using updateSubtaskById
801 | console.log(chalk.blue('💾 Saving research conversation to subtask...'));
802 |
803 | await updateSubtaskById(
804 | tasksPath,
805 | trimmedTaskId,
806 | conversationThread,
807 | false, // useResearch = false for simple append
808 | context,
809 | 'text'
810 | );
811 |
812 | console.log(
813 | chalk.green(
814 | `✅ Research conversation saved to subtask ${trimmedTaskId}`
815 | )
816 | );
817 | } else {
818 | // Validate task exists
819 | const taskIdNum = parseInt(trimmedTaskId, 10);
820 | const task = data.tasks.find((t) => t.id === taskIdNum);
821 |
822 | if (!task) {
823 | console.log(chalk.red(`❌ Task ${trimmedTaskId} not found.`));
824 | return;
825 | }
826 |
827 | // Save to task using updateTaskById with append mode
828 | console.log(chalk.blue('💾 Saving research conversation to task...'));
829 |
830 | await updateTaskById(
831 | tasksPath,
832 | taskIdNum,
833 | conversationThread,
834 | false, // useResearch = false for simple append
835 | context,
836 | 'text',
837 | true // appendMode = true
838 | );
839 |
840 | console.log(
841 | chalk.green(`✅ Research conversation saved to task ${trimmedTaskId}`)
842 | );
843 | }
844 |
845 | return true; // Indicate successful save
846 | } catch (error) {
847 | console.log(chalk.red(`❌ Error saving conversation: ${error.message}`));
848 | logFn.error(`Error saving conversation: ${error.message}`);
849 | return false; // Indicate failed save
850 | }
851 | }
852 |
853 | /**
854 | * Handle saving conversation to a file in .taskmaster/docs/research/
855 | * @param {Array} conversationHistory - Array of conversation exchanges
856 | * @param {string} projectRoot - Project root directory
857 | * @param {Object} context - Execution context
858 | * @param {Object} logFn - Logger function
859 | * @returns {Promise<string>} Path to saved file
860 | */
861 | async function handleSaveToFile(
862 | conversationHistory,
863 | projectRoot,
864 | context,
865 | logFn
866 | ) {
867 | try {
868 | // Create research directory if it doesn't exist
869 | const researchDir = path.join(
870 | projectRoot,
871 | '.taskmaster',
872 | 'docs',
873 | 'research'
874 | );
875 | if (!fs.existsSync(researchDir)) {
876 | fs.mkdirSync(researchDir, { recursive: true });
877 | }
878 |
879 | // Generate filename from first query and timestamp
880 | const firstQuery = conversationHistory[0]?.question || 'research-query';
881 | const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
882 |
883 | // Create a slug from the query (remove special chars, limit length)
884 | const querySlug = firstQuery
885 | .toLowerCase()
886 | .replace(/[^a-z0-9\s-]/g, '') // Remove special characters
887 | .replace(/\s+/g, '-') // Replace spaces with hyphens
888 | .replace(/-+/g, '-') // Replace multiple hyphens with single
889 | .substring(0, 50) // Limit length
890 | .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
891 |
892 | const filename = `${timestamp}_${querySlug}.md`;
893 | const filePath = path.join(researchDir, filename);
894 |
895 | // Format conversation for file
896 | const fileContent = formatConversationForFile(
897 | conversationHistory,
898 | firstQuery
899 | );
900 |
901 | // Write file
902 | fs.writeFileSync(filePath, fileContent, 'utf8');
903 |
904 | const relativePath = path.relative(projectRoot, filePath);
905 | console.log(
906 | chalk.green(`✅ Research saved to: ${chalk.cyan(relativePath)}`)
907 | );
908 |
909 | logFn.success(`Research conversation saved to ${relativePath}`);
910 |
911 | return filePath;
912 | } catch (error) {
913 | console.log(chalk.red(`❌ Error saving research file: ${error.message}`));
914 | logFn.error(`Error saving research file: ${error.message}`);
915 | throw error;
916 | }
917 | }
918 |
919 | /**
920 | * Format conversation history for saving to a file
921 | * @param {Array} conversationHistory - Array of conversation exchanges
922 | * @param {string} initialQuery - The initial query for metadata
923 | * @returns {string} Formatted file content
924 | */
925 | function formatConversationForFile(conversationHistory, initialQuery) {
926 | const timestamp = new Date().toISOString();
927 | const date = new Date().toLocaleDateString();
928 | const time = new Date().toLocaleTimeString();
929 |
930 | // Create metadata header
931 | let content = `---
932 | title: Research Session
933 | query: "${initialQuery}"
934 | date: ${date}
935 | time: ${time}
936 | timestamp: ${timestamp}
937 | exchanges: ${conversationHistory.length}
938 | ---
939 |
940 | # Research Session
941 |
942 | `;
943 |
944 | // Add each conversation exchange
945 | conversationHistory.forEach((exchange, index) => {
946 | if (exchange.type === 'initial') {
947 | content += `## Initial Query\n\n**Question:** ${exchange.question}\n\n**Response:**\n\n${exchange.answer}\n\n`;
948 | } else {
949 | content += `## Follow-up ${index}\n\n**Question:** ${exchange.question}\n\n**Response:**\n\n${exchange.answer}\n\n`;
950 | }
951 |
952 | if (index < conversationHistory.length - 1) {
953 | content += '---\n\n';
954 | }
955 | });
956 |
957 | // Add footer
958 | content += `\n---\n\n*Generated by Task Master Research Command* \n*Timestamp: ${timestamp}*\n`;
959 |
960 | return content;
961 | }
962 |
963 | /**
964 | * Format conversation history for saving to a task/subtask
965 | * @param {Array} conversationHistory - Array of conversation exchanges
966 | * @returns {string} Formatted conversation thread
967 | */
968 | function formatConversationForSaving(conversationHistory) {
969 | const timestamp = new Date().toISOString();
970 | let formatted = `## Research Session - ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}\n\n`;
971 |
972 | conversationHistory.forEach((exchange, index) => {
973 | if (exchange.type === 'initial') {
974 | formatted += `**Initial Query:** ${exchange.question}\n\n`;
975 | formatted += `**Response:** ${exchange.answer}\n\n`;
976 | } else {
977 | formatted += `**Follow-up ${index}:** ${exchange.question}\n\n`;
978 | formatted += `**Response:** ${exchange.answer}\n\n`;
979 | }
980 |
981 | if (index < conversationHistory.length - 1) {
982 | formatted += '---\n\n';
983 | }
984 | });
985 |
986 | return formatted;
987 | }
988 |
989 | /**
990 | * Build conversation context string from conversation history
991 | * @param {Array} conversationHistory - Array of conversation exchanges
992 | * @returns {string} Formatted conversation context
993 | */
994 | function buildConversationContext(conversationHistory) {
995 | if (conversationHistory.length === 0) {
996 | return '';
997 | }
998 |
999 | const contextParts = ['--- Conversation History ---'];
1000 |
1001 | conversationHistory.forEach((exchange, index) => {
1002 | const questionLabel =
1003 | exchange.type === 'initial' ? 'Initial Question' : `Follow-up ${index}`;
1004 | const answerLabel =
1005 | exchange.type === 'initial' ? 'Initial Answer' : `Answer ${index}`;
1006 |
1007 | contextParts.push(`\n${questionLabel}: ${exchange.question}`);
1008 | contextParts.push(`${answerLabel}: ${exchange.answer}`);
1009 | });
1010 |
1011 | return contextParts.join('\n');
1012 | }
1013 |
1014 | export { performResearch };
1015 |
```