#
tokens: 43601/50000 3/975 files (page 50/69)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 50/69FirstPrevNextLast