#
tokens: 41630/50000 3/975 files (page 49/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 49 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

--------------------------------------------------------------------------------
/scripts/modules/utils/contextGatherer.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * contextGatherer.js
  3 |  * Comprehensive context gathering utility for Task Master AI operations
  4 |  * Supports task context, file context, project tree, and custom context
  5 |  */
  6 | 
  7 | import fs from 'fs';
  8 | import path from 'path';
  9 | import pkg from 'gpt-tokens';
 10 | import Fuse from 'fuse.js';
 11 | import {
 12 | 	readJSON,
 13 | 	findTaskById,
 14 | 	truncate,
 15 | 	flattenTasksWithSubtasks
 16 | } from '../utils.js';
 17 | 
 18 | const { encode } = pkg;
 19 | 
 20 | /**
 21 |  * Context Gatherer class for collecting and formatting context from various sources
 22 |  */
 23 | export class ContextGatherer {
 24 | 	constructor(projectRoot, tag) {
 25 | 		this.projectRoot = projectRoot;
 26 | 		this.tasksPath = path.join(
 27 | 			projectRoot,
 28 | 			'.taskmaster',
 29 | 			'tasks',
 30 | 			'tasks.json'
 31 | 		);
 32 | 		this.tag = tag;
 33 | 		this.allTasks = this._loadAllTasks();
 34 | 	}
 35 | 
 36 | 	_loadAllTasks() {
 37 | 		try {
 38 | 			const data = readJSON(this.tasksPath, this.projectRoot, this.tag);
 39 | 			const tasks = data?.tasks || [];
 40 | 			return tasks;
 41 | 		} catch (error) {
 42 | 			console.warn(
 43 | 				`Warning: Could not load tasks for ContextGatherer: ${error.message}`
 44 | 			);
 45 | 			return [];
 46 | 		}
 47 | 	}
 48 | 
 49 | 	/**
 50 | 	 * Count tokens in a text string using gpt-tokens
 51 | 	 * @param {string} text - Text to count tokens for
 52 | 	 * @returns {number} Token count
 53 | 	 */
 54 | 	countTokens(text) {
 55 | 		if (!text || typeof text !== 'string') {
 56 | 			return 0;
 57 | 		}
 58 | 		try {
 59 | 			return encode(text).length;
 60 | 		} catch (error) {
 61 | 			// Fallback to rough character-based estimation if tokenizer fails
 62 | 			// Rough estimate: ~4 characters per token for English text
 63 | 			return Math.ceil(text.length / 4);
 64 | 		}
 65 | 	}
 66 | 
 67 | 	/**
 68 | 	 * Main method to gather context from multiple sources
 69 | 	 * @param {Object} options - Context gathering options
 70 | 	 * @param {Array<string>} [options.tasks] - Task/subtask IDs to include
 71 | 	 * @param {Array<string>} [options.files] - File paths to include
 72 | 	 * @param {string} [options.customContext] - Additional custom context
 73 | 	 * @param {boolean} [options.includeProjectTree] - Include project file tree
 74 | 	 * @param {string} [options.format] - Output format: 'research', 'chat', 'system-prompt'
 75 | 	 * @param {boolean} [options.includeTokenCounts] - Whether to include token breakdown
 76 | 	 * @param {string} [options.semanticQuery] - A query string for semantic task searching.
 77 | 	 * @param {number} [options.maxSemanticResults] - Max number of semantic results.
 78 | 	 * @param {Array<number>} [options.dependencyTasks] - Array of task IDs to build dependency graphs from.
 79 | 	 * @returns {Promise<Object>} Object with context string and analysis data
 80 | 	 */
 81 | 	async gather(options = {}) {
 82 | 		const {
 83 | 			tasks = [],
 84 | 			files = [],
 85 | 			customContext = '',
 86 | 			includeProjectTree = false,
 87 | 			format = 'research',
 88 | 			includeTokenCounts = false,
 89 | 			semanticQuery,
 90 | 			maxSemanticResults = 10,
 91 | 			dependencyTasks = []
 92 | 		} = options;
 93 | 
 94 | 		const contextSections = [];
 95 | 		const finalTaskIds = new Set(tasks.map(String));
 96 | 		let analysisData = null;
 97 | 		let tokenBreakdown = null;
 98 | 
 99 | 		// Initialize token breakdown if requested
100 | 		if (includeTokenCounts) {
101 | 			tokenBreakdown = {
102 | 				total: 0,
103 | 				customContext: null,
104 | 				tasks: [],
105 | 				files: [],
106 | 				projectTree: null
107 | 			};
108 | 		}
109 | 
110 | 		// Semantic Search
111 | 		if (semanticQuery && this.allTasks.length > 0) {
112 | 			const semanticResults = this._performSemanticSearch(
113 | 				semanticQuery,
114 | 				maxSemanticResults
115 | 			);
116 | 
117 | 			// Store the analysis data for UI display
118 | 			analysisData = semanticResults.analysisData;
119 | 
120 | 			semanticResults.tasks.forEach((task) => {
121 | 				finalTaskIds.add(String(task.id));
122 | 			});
123 | 		}
124 | 
125 | 		// Dependency Graph Analysis
126 | 		if (dependencyTasks.length > 0) {
127 | 			const dependencyResults = this._buildDependencyGraphs(dependencyTasks);
128 | 			dependencyResults.allRelatedTaskIds.forEach((id) =>
129 | 				finalTaskIds.add(String(id))
130 | 			);
131 | 			// We can format and add dependencyResults.graphVisualization later if needed
132 | 		}
133 | 
134 | 		// Add custom context first
135 | 		if (customContext && customContext.trim()) {
136 | 			const formattedCustomContext = this._formatCustomContext(
137 | 				customContext,
138 | 				format
139 | 			);
140 | 			contextSections.push(formattedCustomContext);
141 | 
142 | 			// Calculate tokens for custom context if requested
143 | 			if (includeTokenCounts) {
144 | 				tokenBreakdown.customContext = {
145 | 					tokens: this.countTokens(formattedCustomContext),
146 | 					characters: formattedCustomContext.length
147 | 				};
148 | 				tokenBreakdown.total += tokenBreakdown.customContext.tokens;
149 | 			}
150 | 		}
151 | 
152 | 		// Gather context for the final list of tasks
153 | 		if (finalTaskIds.size > 0) {
154 | 			const taskContextResult = await this._gatherTaskContext(
155 | 				Array.from(finalTaskIds),
156 | 				format,
157 | 				includeTokenCounts
158 | 			);
159 | 			if (taskContextResult.context) {
160 | 				contextSections.push(taskContextResult.context);
161 | 
162 | 				// Add task breakdown if token counting is enabled
163 | 				if (includeTokenCounts && taskContextResult.breakdown) {
164 | 					tokenBreakdown.tasks = taskContextResult.breakdown;
165 | 					const taskTokens = taskContextResult.breakdown.reduce(
166 | 						(sum, task) => sum + task.tokens,
167 | 						0
168 | 					);
169 | 					tokenBreakdown.total += taskTokens;
170 | 				}
171 | 			}
172 | 		}
173 | 
174 | 		// Add file context
175 | 		if (files.length > 0) {
176 | 			const fileContextResult = await this._gatherFileContext(
177 | 				files,
178 | 				format,
179 | 				includeTokenCounts
180 | 			);
181 | 			if (fileContextResult.context) {
182 | 				contextSections.push(fileContextResult.context);
183 | 
184 | 				// Add file breakdown if token counting is enabled
185 | 				if (includeTokenCounts && fileContextResult.breakdown) {
186 | 					tokenBreakdown.files = fileContextResult.breakdown;
187 | 					const fileTokens = fileContextResult.breakdown.reduce(
188 | 						(sum, file) => sum + file.tokens,
189 | 						0
190 | 					);
191 | 					tokenBreakdown.total += fileTokens;
192 | 				}
193 | 			}
194 | 		}
195 | 
196 | 		// Add project tree context
197 | 		if (includeProjectTree) {
198 | 			const treeContextResult = await this._gatherProjectTreeContext(
199 | 				format,
200 | 				includeTokenCounts
201 | 			);
202 | 			if (treeContextResult.context) {
203 | 				contextSections.push(treeContextResult.context);
204 | 
205 | 				// Add tree breakdown if token counting is enabled
206 | 				if (includeTokenCounts && treeContextResult.breakdown) {
207 | 					tokenBreakdown.projectTree = treeContextResult.breakdown;
208 | 					tokenBreakdown.total += treeContextResult.breakdown.tokens;
209 | 				}
210 | 			}
211 | 		}
212 | 
213 | 		const finalContext = this._joinContextSections(contextSections, format);
214 | 
215 | 		const result = {
216 | 			context: finalContext,
217 | 			analysisData: analysisData,
218 | 			contextSections: contextSections.length,
219 | 			finalTaskIds: Array.from(finalTaskIds)
220 | 		};
221 | 
222 | 		// Only include tokenBreakdown if it was requested
223 | 		if (includeTokenCounts) {
224 | 			result.tokenBreakdown = tokenBreakdown;
225 | 		}
226 | 
227 | 		return result;
228 | 	}
229 | 
230 | 	_performSemanticSearch(query, maxResults) {
231 | 		const searchableTasks = this.allTasks.map((task) => {
232 | 			const dependencyTitles =
233 | 				task.dependencies?.length > 0
234 | 					? task.dependencies
235 | 							.map((depId) => this.allTasks.find((t) => t.id === depId)?.title)
236 | 							.filter(Boolean)
237 | 							.join(' ')
238 | 					: '';
239 | 			return { ...task, dependencyTitles };
240 | 		});
241 | 
242 | 		// Use the exact same approach as add-task.js
243 | 		const searchOptions = {
244 | 			includeScore: true, // Return match scores
245 | 			threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
246 | 			keys: [
247 | 				{ name: 'title', weight: 1.5 }, // Title is most important
248 | 				{ name: 'description', weight: 2 }, // Description is very important
249 | 				{ name: 'details', weight: 3 }, // Details is most important
250 | 				// Search dependencies to find tasks that depend on similar things
251 | 				{ name: 'dependencyTitles', weight: 0.5 }
252 | 			],
253 | 			// Sort matches by score (lower is better)
254 | 			shouldSort: true,
255 | 			// Allow searching in nested properties
256 | 			useExtendedSearch: true,
257 | 			// Return up to 50 matches
258 | 			limit: 50
259 | 		};
260 | 
261 | 		// Create search index using Fuse.js
262 | 		const fuse = new Fuse(searchableTasks, searchOptions);
263 | 
264 | 		// Extract significant words and phrases from the prompt (like add-task.js does)
265 | 		const promptWords = query
266 | 			.toLowerCase()
267 | 			.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
268 | 			.split(/\s+/)
269 | 			.filter((word) => word.length > 3); // Words at least 4 chars
270 | 
271 | 		// Use the user's prompt for fuzzy search
272 | 		const fuzzyResults = fuse.search(query);
273 | 
274 | 		// Also search for each significant word to catch different aspects
275 | 		const wordResults = [];
276 | 		for (const word of promptWords) {
277 | 			if (word.length > 5) {
278 | 				// Only use significant words
279 | 				const results = fuse.search(word);
280 | 				if (results.length > 0) {
281 | 					wordResults.push(...results);
282 | 				}
283 | 			}
284 | 		}
285 | 
286 | 		// Merge and deduplicate results
287 | 		const mergedResults = [...fuzzyResults];
288 | 
289 | 		// Add word results that aren't already in fuzzyResults
290 | 		for (const wordResult of wordResults) {
291 | 			if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
292 | 				mergedResults.push(wordResult);
293 | 			}
294 | 		}
295 | 
296 | 		// Group search results by relevance
297 | 		const highRelevance = mergedResults
298 | 			.filter((result) => result.score < 0.25)
299 | 			.map((result) => result.item);
300 | 
301 | 		const mediumRelevance = mergedResults
302 | 			.filter((result) => result.score >= 0.25 && result.score < 0.4)
303 | 			.map((result) => result.item);
304 | 
305 | 		// Get recent tasks (newest first)
306 | 		const recentTasks = [...this.allTasks]
307 | 			.sort((a, b) => b.id - a.id)
308 | 			.slice(0, 5);
309 | 
310 | 		// Combine high relevance, medium relevance, and recent tasks
311 | 		// Prioritize high relevance first
312 | 		const allRelevantTasks = [...highRelevance];
313 | 
314 | 		// Add medium relevance if not already included
315 | 		for (const task of mediumRelevance) {
316 | 			if (!allRelevantTasks.some((t) => t.id === task.id)) {
317 | 				allRelevantTasks.push(task);
318 | 			}
319 | 		}
320 | 
321 | 		// Add recent tasks if not already included
322 | 		for (const task of recentTasks) {
323 | 			if (!allRelevantTasks.some((t) => t.id === task.id)) {
324 | 				allRelevantTasks.push(task);
325 | 			}
326 | 		}
327 | 
328 | 		// Get top N results for context
329 | 		const finalResults = allRelevantTasks.slice(0, maxResults);
330 | 		return {
331 | 			tasks: finalResults,
332 | 			analysisData: {
333 | 				highRelevance: highRelevance,
334 | 				mediumRelevance: mediumRelevance,
335 | 				recentTasks: recentTasks,
336 | 				allRelevantTasks: allRelevantTasks
337 | 			}
338 | 		};
339 | 	}
340 | 
341 | 	_buildDependencyContext(taskIds) {
342 | 		const { allRelatedTaskIds, graphs, depthMap } =
343 | 			this._buildDependencyGraphs(taskIds);
344 | 		if (allRelatedTaskIds.size === 0) return '';
345 | 
346 | 		const dependentTasks = Array.from(allRelatedTaskIds)
347 | 			.map((id) => this.allTasks.find((t) => t.id === id))
348 | 			.filter(Boolean)
349 | 			.sort((a, b) => (depthMap.get(a.id) || 0) - (depthMap.get(b.id) || 0));
350 | 
351 | 		const uniqueDetailedTasks = dependentTasks.slice(0, 8);
352 | 
353 | 		let context = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.`;
354 | 
355 | 		const directDeps = this.allTasks.filter((t) => taskIds.includes(t.id));
356 | 		if (directDeps.length > 0) {
357 | 			context += `\n\nDirect dependencies:\n${directDeps
358 | 				.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
359 | 				.join('\n')}`;
360 | 		}
361 | 
362 | 		const indirectDeps = dependentTasks.filter((t) => !taskIds.includes(t.id));
363 | 		if (indirectDeps.length > 0) {
364 | 			context += `\n\nIndirect dependencies (dependencies of dependencies):\n${indirectDeps
365 | 				.slice(0, 5)
366 | 				.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
367 | 				.join('\n')}`;
368 | 			if (indirectDeps.length > 5)
369 | 				context += `\n- ... and ${
370 | 					indirectDeps.length - 5
371 | 				} more indirect dependencies`;
372 | 		}
373 | 
374 | 		context += `\n\nDetailed information about dependencies:`;
375 | 		for (const depTask of uniqueDetailedTasks) {
376 | 			const isDirect = taskIds.includes(depTask.id)
377 | 				? ' [DIRECT DEPENDENCY]'
378 | 				: '';
379 | 			context += `\n\n------ Task ${depTask.id}${isDirect}: ${depTask.title} ------\n`;
380 | 			context += `Description: ${depTask.description}\n`;
381 | 			if (depTask.dependencies?.length) {
382 | 				context += `Dependencies: ${depTask.dependencies.join(', ')}\n`;
383 | 			}
384 | 			if (depTask.details) {
385 | 				context += `Implementation Details: ${truncate(
386 | 					depTask.details,
387 | 					400
388 | 				)}\n`;
389 | 			}
390 | 		}
391 | 
392 | 		if (graphs.length > 0) {
393 | 			context += '\n\nDependency Chain Visualization:';
394 | 			context += graphs
395 | 				.map((graph) => this._formatDependencyChain(graph))
396 | 				.join('');
397 | 		}
398 | 
399 | 		return context;
400 | 	}
401 | 
402 | 	_buildDependencyGraphs(taskIds) {
403 | 		const visited = new Set();
404 | 		const depthMap = new Map();
405 | 		const graphs = [];
406 | 
407 | 		for (const id of taskIds) {
408 | 			const graph = this._buildDependencyGraph(id, visited, depthMap);
409 | 			if (graph) graphs.push(graph);
410 | 		}
411 | 
412 | 		return { allRelatedTaskIds: visited, graphs, depthMap };
413 | 	}
414 | 
415 | 	_buildDependencyGraph(taskId, visited, depthMap, depth = 0) {
416 | 		if (visited.has(taskId) || depth > 5) return null; // Limit recursion depth
417 | 		const task = this.allTasks.find((t) => t.id === taskId);
418 | 		if (!task) return null;
419 | 
420 | 		visited.add(taskId);
421 | 		if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) {
422 | 			depthMap.set(taskId, depth);
423 | 		}
424 | 
425 | 		const dependencies =
426 | 			task.dependencies
427 | 				?.map((depId) =>
428 | 					this._buildDependencyGraph(depId, visited, depthMap, depth + 1)
429 | 				)
430 | 				.filter(Boolean) || [];
431 | 
432 | 		return { ...task, dependencies };
433 | 	}
434 | 
435 | 	_formatDependencyChain(node, prefix = '', isLast = true, depth = 0) {
436 | 		if (depth > 3) return '';
437 | 		const connector = isLast ? '└── ' : '├── ';
438 | 		let result = `${prefix}${connector}Task ${node.id}: ${node.title}`;
439 | 		if (node.dependencies?.length) {
440 | 			const childPrefix = prefix + (isLast ? '    ' : '│   ');
441 | 			result += node.dependencies
442 | 				.map((dep, index) =>
443 | 					this._formatDependencyChain(
444 | 						dep,
445 | 						childPrefix,
446 | 						index === node.dependencies.length - 1,
447 | 						depth + 1
448 | 					)
449 | 				)
450 | 				.join('');
451 | 		}
452 | 		return '\n' + result;
453 | 	}
454 | 
455 | 	/**
456 | 	 * Parse task ID strings into structured format
457 | 	 * Supports formats: "15", "15.2", "16,17.1"
458 | 	 * @param {Array<string>} taskIds - Array of task ID strings
459 | 	 * @returns {Array<Object>} Parsed task identifiers
460 | 	 */
461 | 	_parseTaskIds(taskIds) {
462 | 		const parsed = [];
463 | 
464 | 		for (const idStr of taskIds) {
465 | 			if (idStr.includes('.')) {
466 | 				// Subtask format: "15.2"
467 | 				const [parentId, subtaskId] = idStr.split('.');
468 | 				parsed.push({
469 | 					type: 'subtask',
470 | 					parentId: parseInt(parentId, 10),
471 | 					subtaskId: parseInt(subtaskId, 10),
472 | 					fullId: idStr
473 | 				});
474 | 			} else {
475 | 				// Task format: "15"
476 | 				parsed.push({
477 | 					type: 'task',
478 | 					taskId: parseInt(idStr, 10),
479 | 					fullId: idStr
480 | 				});
481 | 			}
482 | 		}
483 | 
484 | 		return parsed;
485 | 	}
486 | 
487 | 	/**
488 | 	 * Gather context from tasks and subtasks
489 | 	 * @param {Array<string>} taskIds - Task/subtask IDs
490 | 	 * @param {string} format - Output format
491 | 	 * @param {boolean} includeTokenCounts - Whether to include token breakdown
492 | 	 * @returns {Promise<Object>} Task context result with breakdown
493 | 	 */
494 | 	async _gatherTaskContext(taskIds, format, includeTokenCounts = false) {
495 | 		try {
496 | 			if (!this.allTasks || this.allTasks.length === 0) {
497 | 				return { context: null, breakdown: [] };
498 | 			}
499 | 
500 | 			const parsedIds = this._parseTaskIds(taskIds);
501 | 			const contextItems = [];
502 | 			const breakdown = [];
503 | 
504 | 			for (const parsed of parsedIds) {
505 | 				let formattedItem = null;
506 | 				let itemInfo = null;
507 | 
508 | 				if (parsed.type === 'task') {
509 | 					const result = findTaskById(this.allTasks, parsed.taskId);
510 | 					if (result.task) {
511 | 						formattedItem = this._formatTaskForContext(result.task, format);
512 | 						itemInfo = {
513 | 							id: parsed.fullId,
514 | 							type: 'task',
515 | 							title: result.task.title,
516 | 							tokens: includeTokenCounts ? this.countTokens(formattedItem) : 0,
517 | 							characters: formattedItem.length
518 | 						};
519 | 					}
520 | 				} else if (parsed.type === 'subtask') {
521 | 					const parentResult = findTaskById(this.allTasks, parsed.parentId);
522 | 					if (parentResult.task && parentResult.task.subtasks) {
523 | 						const subtask = parentResult.task.subtasks.find(
524 | 							(st) => st.id === parsed.subtaskId
525 | 						);
526 | 						if (subtask) {
527 | 							formattedItem = this._formatSubtaskForContext(
528 | 								subtask,
529 | 								parentResult.task,
530 | 								format
531 | 							);
532 | 							itemInfo = {
533 | 								id: parsed.fullId,
534 | 								type: 'subtask',
535 | 								title: subtask.title,
536 | 								parentTitle: parentResult.task.title,
537 | 								tokens: includeTokenCounts
538 | 									? this.countTokens(formattedItem)
539 | 									: 0,
540 | 								characters: formattedItem.length
541 | 							};
542 | 						}
543 | 					}
544 | 				}
545 | 
546 | 				if (formattedItem && itemInfo) {
547 | 					contextItems.push(formattedItem);
548 | 					if (includeTokenCounts) {
549 | 						breakdown.push(itemInfo);
550 | 					}
551 | 				}
552 | 			}
553 | 
554 | 			if (contextItems.length === 0) {
555 | 				return { context: null, breakdown: [] };
556 | 			}
557 | 
558 | 			const finalContext = this._formatTaskContextSection(contextItems, format);
559 | 			return {
560 | 				context: finalContext,
561 | 				breakdown: includeTokenCounts ? breakdown : []
562 | 			};
563 | 		} catch (error) {
564 | 			console.warn(`Warning: Could not gather task context: ${error.message}`);
565 | 			return { context: null, breakdown: [] };
566 | 		}
567 | 	}
568 | 
569 | 	/**
570 | 	 * Format a task for context inclusion
571 | 	 * @param {Object} task - Task object
572 | 	 * @param {string} format - Output format
573 | 	 * @returns {string} Formatted task context
574 | 	 */
575 | 	_formatTaskForContext(task, format) {
576 | 		const sections = [];
577 | 
578 | 		sections.push(`**Task ${task.id}: ${task.title}**`);
579 | 		sections.push(`Description: ${task.description}`);
580 | 		sections.push(`Status: ${task.status || 'pending'}`);
581 | 		sections.push(`Priority: ${task.priority || 'medium'}`);
582 | 
583 | 		if (task.dependencies && task.dependencies.length > 0) {
584 | 			sections.push(`Dependencies: ${task.dependencies.join(', ')}`);
585 | 		}
586 | 
587 | 		if (task.details) {
588 | 			const details = truncate(task.details, 500);
589 | 			sections.push(`Implementation Details: ${details}`);
590 | 		}
591 | 
592 | 		if (task.testStrategy) {
593 | 			const testStrategy = truncate(task.testStrategy, 300);
594 | 			sections.push(`Test Strategy: ${testStrategy}`);
595 | 		}
596 | 
597 | 		if (task.subtasks && task.subtasks.length > 0) {
598 | 			sections.push(`Subtasks: ${task.subtasks.length} subtasks defined`);
599 | 		}
600 | 
601 | 		return sections.join('\n');
602 | 	}
603 | 
604 | 	/**
605 | 	 * Format a subtask for context inclusion
606 | 	 * @param {Object} subtask - Subtask object
607 | 	 * @param {Object} parentTask - Parent task object
608 | 	 * @param {string} format - Output format
609 | 	 * @returns {string} Formatted subtask context
610 | 	 */
611 | 	_formatSubtaskForContext(subtask, parentTask, format) {
612 | 		const sections = [];
613 | 
614 | 		sections.push(
615 | 			`**Subtask ${parentTask.id}.${subtask.id}: ${subtask.title}**`
616 | 		);
617 | 		sections.push(`Parent Task: ${parentTask.title}`);
618 | 		sections.push(`Description: ${subtask.description}`);
619 | 		sections.push(`Status: ${subtask.status || 'pending'}`);
620 | 
621 | 		if (subtask.dependencies && subtask.dependencies.length > 0) {
622 | 			sections.push(`Dependencies: ${subtask.dependencies.join(', ')}`);
623 | 		}
624 | 
625 | 		if (subtask.details) {
626 | 			const details = truncate(subtask.details, 500);
627 | 			sections.push(`Implementation Details: ${details}`);
628 | 		}
629 | 
630 | 		return sections.join('\n');
631 | 	}
632 | 
633 | 	/**
634 | 	 * Gather context from files
635 | 	 * @param {Array<string>} filePaths - File paths to read
636 | 	 * @param {string} format - Output format
637 | 	 * @param {boolean} includeTokenCounts - Whether to include token breakdown
638 | 	 * @returns {Promise<Object>} File context result with breakdown
639 | 	 */
640 | 	async _gatherFileContext(filePaths, format, includeTokenCounts = false) {
641 | 		const fileContents = [];
642 | 		const breakdown = [];
643 | 
644 | 		for (const filePath of filePaths) {
645 | 			try {
646 | 				const fullPath = path.isAbsolute(filePath)
647 | 					? filePath
648 | 					: path.join(this.projectRoot, filePath);
649 | 
650 | 				if (!fs.existsSync(fullPath)) {
651 | 					continue;
652 | 				}
653 | 
654 | 				const stats = fs.statSync(fullPath);
655 | 				if (!stats.isFile()) {
656 | 					continue;
657 | 				}
658 | 
659 | 				// Check file size (limit to 50KB for context)
660 | 				if (stats.size > 50 * 1024) {
661 | 					continue;
662 | 				}
663 | 
664 | 				const content = fs.readFileSync(fullPath, 'utf-8');
665 | 				const relativePath = path.relative(this.projectRoot, fullPath);
666 | 
667 | 				const fileData = {
668 | 					path: relativePath,
669 | 					size: stats.size,
670 | 					content: content,
671 | 					lastModified: stats.mtime
672 | 				};
673 | 
674 | 				fileContents.push(fileData);
675 | 
676 | 				// Calculate tokens for this individual file if requested
677 | 				if (includeTokenCounts) {
678 | 					const formattedFile = this._formatSingleFileForContext(
679 | 						fileData,
680 | 						format
681 | 					);
682 | 					breakdown.push({
683 | 						path: relativePath,
684 | 						sizeKB: Math.round(stats.size / 1024),
685 | 						tokens: this.countTokens(formattedFile),
686 | 						characters: formattedFile.length
687 | 					});
688 | 				}
689 | 			} catch (error) {
690 | 				console.warn(
691 | 					`Warning: Could not read file ${filePath}: ${error.message}`
692 | 				);
693 | 			}
694 | 		}
695 | 
696 | 		if (fileContents.length === 0) {
697 | 			return { context: null, breakdown: [] };
698 | 		}
699 | 
700 | 		const finalContext = this._formatFileContextSection(fileContents, format);
701 | 		return {
702 | 			context: finalContext,
703 | 			breakdown: includeTokenCounts ? breakdown : []
704 | 		};
705 | 	}
706 | 
707 | 	/**
708 | 	 * Generate project file tree context
709 | 	 * @param {string} format - Output format
710 | 	 * @param {boolean} includeTokenCounts - Whether to include token breakdown
711 | 	 * @returns {Promise<Object>} Project tree context result with breakdown
712 | 	 */
713 | 	async _gatherProjectTreeContext(format, includeTokenCounts = false) {
714 | 		try {
715 | 			const tree = this._generateFileTree(this.projectRoot, 5); // Max depth 5
716 | 			const finalContext = this._formatProjectTreeSection(tree, format);
717 | 
718 | 			const breakdown = includeTokenCounts
719 | 				? {
720 | 						tokens: this.countTokens(finalContext),
721 | 						characters: finalContext.length,
722 | 						fileCount: tree.fileCount || 0,
723 | 						dirCount: tree.dirCount || 0
724 | 					}
725 | 				: null;
726 | 
727 | 			return {
728 | 				context: finalContext,
729 | 				breakdown: breakdown
730 | 			};
731 | 		} catch (error) {
732 | 			console.warn(
733 | 				`Warning: Could not generate project tree: ${error.message}`
734 | 			);
735 | 			return { context: null, breakdown: null };
736 | 		}
737 | 	}
738 | 
739 | 	/**
740 | 	 * Format a single file for context (used for token counting)
741 | 	 * @param {Object} fileData - File data object
742 | 	 * @param {string} format - Output format
743 | 	 * @returns {string} Formatted file context
744 | 	 */
745 | 	_formatSingleFileForContext(fileData, format) {
746 | 		const header = `**File: ${fileData.path}** (${Math.round(fileData.size / 1024)}KB)`;
747 | 		const content = `\`\`\`\n${fileData.content}\n\`\`\``;
748 | 		return `${header}\n\n${content}`;
749 | 	}
750 | 
751 | 	/**
752 | 	 * Generate file tree structure
753 | 	 * @param {string} dirPath - Directory path
754 | 	 * @param {number} maxDepth - Maximum depth to traverse
755 | 	 * @param {number} currentDepth - Current depth
756 | 	 * @returns {Object} File tree structure
757 | 	 */
758 | 	_generateFileTree(dirPath, maxDepth, currentDepth = 0) {
759 | 		const ignoreDirs = [
760 | 			'.git',
761 | 			'node_modules',
762 | 			'.env',
763 | 			'coverage',
764 | 			'dist',
765 | 			'build'
766 | 		];
767 | 		const ignoreFiles = ['.DS_Store', '.env', '.env.local', '.env.production'];
768 | 
769 | 		if (currentDepth >= maxDepth) {
770 | 			return null;
771 | 		}
772 | 
773 | 		try {
774 | 			const items = fs.readdirSync(dirPath);
775 | 			const tree = {
776 | 				name: path.basename(dirPath),
777 | 				type: 'directory',
778 | 				children: [],
779 | 				fileCount: 0,
780 | 				dirCount: 0
781 | 			};
782 | 
783 | 			for (const item of items) {
784 | 				if (ignoreDirs.includes(item) || ignoreFiles.includes(item)) {
785 | 					continue;
786 | 				}
787 | 
788 | 				const itemPath = path.join(dirPath, item);
789 | 				const stats = fs.statSync(itemPath);
790 | 
791 | 				if (stats.isDirectory()) {
792 | 					tree.dirCount++;
793 | 					if (currentDepth < maxDepth - 1) {
794 | 						const subtree = this._generateFileTree(
795 | 							itemPath,
796 | 							maxDepth,
797 | 							currentDepth + 1
798 | 						);
799 | 						if (subtree) {
800 | 							tree.children.push(subtree);
801 | 						}
802 | 					}
803 | 				} else {
804 | 					tree.fileCount++;
805 | 					tree.children.push({
806 | 						name: item,
807 | 						type: 'file',
808 | 						size: stats.size
809 | 					});
810 | 				}
811 | 			}
812 | 
813 | 			return tree;
814 | 		} catch (error) {
815 | 			return null;
816 | 		}
817 | 	}
818 | 
819 | 	/**
820 | 	 * Format custom context section
821 | 	 * @param {string} customContext - Custom context string
822 | 	 * @param {string} format - Output format
823 | 	 * @returns {string} Formatted custom context
824 | 	 */
825 | 	_formatCustomContext(customContext, format) {
826 | 		switch (format) {
827 | 			case 'research':
828 | 				return `## Additional Context\n\n${customContext}`;
829 | 			case 'chat':
830 | 				return `**Additional Context:**\n${customContext}`;
831 | 			case 'system-prompt':
832 | 				return `Additional context: ${customContext}`;
833 | 			default:
834 | 				return customContext;
835 | 		}
836 | 	}
837 | 
838 | 	/**
839 | 	 * Format task context section
840 | 	 * @param {Array<string>} taskItems - Formatted task items
841 | 	 * @param {string} format - Output format
842 | 	 * @returns {string} Formatted task context section
843 | 	 */
844 | 	_formatTaskContextSection(taskItems, format) {
845 | 		switch (format) {
846 | 			case 'research':
847 | 				return `## Task Context\n\n${taskItems.join('\n\n---\n\n')}`;
848 | 			case 'chat':
849 | 				return `**Task Context:**\n\n${taskItems.join('\n\n')}`;
850 | 			case 'system-prompt':
851 | 				return `Task context: ${taskItems.join(' | ')}`;
852 | 			default:
853 | 				return taskItems.join('\n\n');
854 | 		}
855 | 	}
856 | 
857 | 	/**
858 | 	 * Format file context section
859 | 	 * @param {Array<Object>} fileContents - File content objects
860 | 	 * @param {string} format - Output format
861 | 	 * @returns {string} Formatted file context section
862 | 	 */
863 | 	_formatFileContextSection(fileContents, format) {
864 | 		const fileItems = fileContents.map((file) => {
865 | 			const header = `**File: ${file.path}** (${Math.round(file.size / 1024)}KB)`;
866 | 			const content = `\`\`\`\n${file.content}\n\`\`\``;
867 | 			return `${header}\n\n${content}`;
868 | 		});
869 | 
870 | 		switch (format) {
871 | 			case 'research':
872 | 				return `## File Context\n\n${fileItems.join('\n\n---\n\n')}`;
873 | 			case 'chat':
874 | 				return `**File Context:**\n\n${fileItems.join('\n\n')}`;
875 | 			case 'system-prompt':
876 | 				return `File context: ${fileContents.map((f) => `${f.path} (${f.content.substring(0, 200)}...)`).join(' | ')}`;
877 | 			default:
878 | 				return fileItems.join('\n\n');
879 | 		}
880 | 	}
881 | 
882 | 	/**
883 | 	 * Format project tree section
884 | 	 * @param {Object} tree - File tree structure
885 | 	 * @param {string} format - Output format
886 | 	 * @returns {string} Formatted project tree section
887 | 	 */
888 | 	_formatProjectTreeSection(tree, format) {
889 | 		const treeString = this._renderFileTree(tree);
890 | 
891 | 		switch (format) {
892 | 			case 'research':
893 | 				return `## Project Structure\n\n\`\`\`\n${treeString}\n\`\`\``;
894 | 			case 'chat':
895 | 				return `**Project Structure:**\n\`\`\`\n${treeString}\n\`\`\``;
896 | 			case 'system-prompt':
897 | 				return `Project structure: ${treeString.replace(/\n/g, ' | ')}`;
898 | 			default:
899 | 				return treeString;
900 | 		}
901 | 	}
902 | 
903 | 	/**
904 | 	 * Render file tree as string
905 | 	 * @param {Object} tree - File tree structure
906 | 	 * @param {string} prefix - Current prefix for indentation
907 | 	 * @returns {string} Rendered tree string
908 | 	 */
909 | 	_renderFileTree(tree, prefix = '') {
910 | 		let result = `${prefix}${tree.name}/`;
911 | 
912 | 		if (tree.fileCount > 0 || tree.dirCount > 0) {
913 | 			result += ` (${tree.fileCount} files, ${tree.dirCount} dirs)`;
914 | 		}
915 | 
916 | 		result += '\n';
917 | 
918 | 		if (tree.children) {
919 | 			tree.children.forEach((child, index) => {
920 | 				const isLast = index === tree.children.length - 1;
921 | 				const childPrefix = prefix + (isLast ? '└── ' : '├── ');
922 | 				const nextPrefix = prefix + (isLast ? '    ' : '│   ');
923 | 
924 | 				if (child.type === 'directory') {
925 | 					result += this._renderFileTree(child, childPrefix);
926 | 				} else {
927 | 					result += `${childPrefix}${child.name}\n`;
928 | 				}
929 | 			});
930 | 		}
931 | 
932 | 		return result;
933 | 	}
934 | 
935 | 	/**
936 | 	 * Join context sections based on format
937 | 	 * @param {Array<string>} sections - Context sections
938 | 	 * @param {string} format - Output format
939 | 	 * @returns {string} Joined context string
940 | 	 */
941 | 	_joinContextSections(sections, format) {
942 | 		if (sections.length === 0) {
943 | 			return '';
944 | 		}
945 | 
946 | 		switch (format) {
947 | 			case 'research':
948 | 				return sections.join('\n\n---\n\n');
949 | 			case 'chat':
950 | 				return sections.join('\n\n');
951 | 			case 'system-prompt':
952 | 				return sections.join(' ');
953 | 			default:
954 | 				return sections.join('\n\n');
955 | 		}
956 | 	}
957 | }
958 | 
959 | /**
960 |  * Factory function to create a context gatherer instance
961 |  * @param {string} projectRoot - Project root directory
962 |  * @param {string} tag - Tag for the task
963 |  * @returns {ContextGatherer} Context gatherer instance
964 |  * @throws {Error} If tag is not provided
965 |  */
966 | export function createContextGatherer(projectRoot, tag) {
967 | 	if (!tag) {
968 | 		throw new Error('Tag is required');
969 | 	}
970 | 	return new ContextGatherer(projectRoot, tag);
971 | }
972 | 
973 | export default ContextGatherer;
974 | 
```

--------------------------------------------------------------------------------
/scripts/modules/task-manager/models.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * models.js
  3 |  * Core functionality for managing AI model configurations
  4 |  */
  5 | 
  6 | import https from 'https';
  7 | import http from 'http';
  8 | import {
  9 | 	getMainModelId,
 10 | 	getResearchModelId,
 11 | 	getFallbackModelId,
 12 | 	getAvailableModels,
 13 | 	getMainProvider,
 14 | 	getResearchProvider,
 15 | 	getFallbackProvider,
 16 | 	isApiKeySet,
 17 | 	getMcpApiKeyStatus,
 18 | 	getConfig,
 19 | 	writeConfig,
 20 | 	isConfigFilePresent,
 21 | 	getAllProviders,
 22 | 	getBaseUrlForRole
 23 | } from '../config-manager.js';
 24 | import { findConfigPath } from '../../../src/utils/path-utils.js';
 25 | import { log } from '../utils.js';
 26 | import { CUSTOM_PROVIDERS } from '@tm/core';
 27 | 
 28 | // Constants
 29 | const CONFIG_MISSING_ERROR =
 30 | 	'The configuration file is missing. Run "task-master init" to create it.';
 31 | 
 32 | /**
 33 |  * Fetches the list of models from OpenRouter API.
 34 |  * @returns {Promise<Array|null>} A promise that resolves with the list of model IDs or null if fetch fails.
 35 |  */
 36 | function fetchOpenRouterModels() {
 37 | 	return new Promise((resolve) => {
 38 | 		const options = {
 39 | 			hostname: 'openrouter.ai',
 40 | 			path: '/api/v1/models',
 41 | 			method: 'GET',
 42 | 			headers: {
 43 | 				Accept: 'application/json'
 44 | 			}
 45 | 		};
 46 | 
 47 | 		const req = https.request(options, (res) => {
 48 | 			let data = '';
 49 | 			res.on('data', (chunk) => {
 50 | 				data += chunk;
 51 | 			});
 52 | 			res.on('end', () => {
 53 | 				if (res.statusCode === 200) {
 54 | 					try {
 55 | 						const parsedData = JSON.parse(data);
 56 | 						resolve(parsedData.data || []); // Return the array of models
 57 | 					} catch (e) {
 58 | 						console.error('Error parsing OpenRouter response:', e);
 59 | 						resolve(null); // Indicate failure
 60 | 					}
 61 | 				} else {
 62 | 					console.error(
 63 | 						`OpenRouter API request failed with status code: ${res.statusCode}`
 64 | 					);
 65 | 					resolve(null); // Indicate failure
 66 | 				}
 67 | 			});
 68 | 		});
 69 | 
 70 | 		req.on('error', (e) => {
 71 | 			console.error('Error fetching OpenRouter models:', e);
 72 | 			resolve(null); // Indicate failure
 73 | 		});
 74 | 		req.end();
 75 | 	});
 76 | }
 77 | 
 78 | /**
 79 |  * Fetches the list of models from Ollama instance.
 80 |  * @param {string} baseURL - The base URL for the Ollama API (e.g., "http://localhost:11434/api")
 81 |  * @returns {Promise<Array|null>} A promise that resolves with the list of model objects or null if fetch fails.
 82 |  */
 83 | function fetchOllamaModels(baseURL = 'http://localhost:11434/api') {
 84 | 	return new Promise((resolve) => {
 85 | 		try {
 86 | 			// Parse the base URL to extract hostname, port, and base path
 87 | 			const url = new URL(baseURL);
 88 | 			const isHttps = url.protocol === 'https:';
 89 | 			const port = url.port || (isHttps ? 443 : 80);
 90 | 			const basePath = url.pathname.endsWith('/')
 91 | 				? url.pathname.slice(0, -1)
 92 | 				: url.pathname;
 93 | 
 94 | 			const options = {
 95 | 				hostname: url.hostname,
 96 | 				port: parseInt(port, 10),
 97 | 				path: `${basePath}/tags`,
 98 | 				method: 'GET',
 99 | 				headers: {
100 | 					Accept: 'application/json'
101 | 				}
102 | 			};
103 | 
104 | 			const requestLib = isHttps ? https : http;
105 | 			const req = requestLib.request(options, (res) => {
106 | 				let data = '';
107 | 				res.on('data', (chunk) => {
108 | 					data += chunk;
109 | 				});
110 | 				res.on('end', () => {
111 | 					if (res.statusCode === 200) {
112 | 						try {
113 | 							const parsedData = JSON.parse(data);
114 | 							resolve(parsedData.models || []); // Return the array of models
115 | 						} catch (e) {
116 | 							console.error('Error parsing Ollama response:', e);
117 | 							resolve(null); // Indicate failure
118 | 						}
119 | 					} else {
120 | 						console.error(
121 | 							`Ollama API request failed with status code: ${res.statusCode}`
122 | 						);
123 | 						resolve(null); // Indicate failure
124 | 					}
125 | 				});
126 | 			});
127 | 
128 | 			req.on('error', (e) => {
129 | 				console.error('Error fetching Ollama models:', e);
130 | 				resolve(null); // Indicate failure
131 | 			});
132 | 			req.end();
133 | 		} catch (e) {
134 | 			console.error('Error parsing Ollama base URL:', e);
135 | 			resolve(null); // Indicate failure
136 | 		}
137 | 	});
138 | }
139 | 
140 | /**
141 |  * Get the current model configuration
142 |  * @param {Object} [options] - Options for the operation
143 |  * @param {Object} [options.session] - Session object containing environment variables (for MCP)
144 |  * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
145 |  * @param {string} [options.projectRoot] - Project root directory
146 |  * @returns {Object} RESTful response with current model configuration
147 |  */
148 | async function getModelConfiguration(options = {}) {
149 | 	const { mcpLog, projectRoot, session } = options;
150 | 
151 | 	const report = (level, ...args) => {
152 | 		if (mcpLog && typeof mcpLog[level] === 'function') {
153 | 			mcpLog[level](...args);
154 | 		}
155 | 	};
156 | 
157 | 	if (!projectRoot) {
158 | 		throw new Error('Project root is required but not found.');
159 | 	}
160 | 
161 | 	// Use centralized config path finding instead of hardcoded path
162 | 	const configPath = findConfigPath(null, { projectRoot });
163 | 	const configExists = isConfigFilePresent(projectRoot);
164 | 
165 | 	log(
166 | 		'debug',
167 | 		`Checking for config file using findConfigPath, found: ${configPath}`
168 | 	);
169 | 	log(
170 | 		'debug',
171 | 		`Checking config file using isConfigFilePresent(), exists: ${configExists}`
172 | 	);
173 | 
174 | 	if (!configExists) {
175 | 		throw new Error(CONFIG_MISSING_ERROR);
176 | 	}
177 | 
178 | 	try {
179 | 		// Get current settings - these should use the config from the found path automatically
180 | 		const mainProvider = getMainProvider(projectRoot);
181 | 		const mainModelId = getMainModelId(projectRoot);
182 | 		const mainBaseURL = getBaseUrlForRole('main', projectRoot);
183 | 		const researchProvider = getResearchProvider(projectRoot);
184 | 		const researchModelId = getResearchModelId(projectRoot);
185 | 		const researchBaseURL = getBaseUrlForRole('research', projectRoot);
186 | 		const fallbackProvider = getFallbackProvider(projectRoot);
187 | 		const fallbackModelId = getFallbackModelId(projectRoot);
188 | 		const fallbackBaseURL = getBaseUrlForRole('fallback', projectRoot);
189 | 
190 | 		// Check API keys
191 | 		const mainCliKeyOk = isApiKeySet(mainProvider, session, projectRoot);
192 | 		const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider, projectRoot);
193 | 		const researchCliKeyOk = isApiKeySet(
194 | 			researchProvider,
195 | 			session,
196 | 			projectRoot
197 | 		);
198 | 		const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider, projectRoot);
199 | 		const fallbackCliKeyOk = fallbackProvider
200 | 			? isApiKeySet(fallbackProvider, session, projectRoot)
201 | 			: true;
202 | 		const fallbackMcpKeyOk = fallbackProvider
203 | 			? getMcpApiKeyStatus(fallbackProvider, projectRoot)
204 | 			: true;
205 | 
206 | 		// Get available models to find detailed info
207 | 		const availableModels = getAvailableModels(projectRoot);
208 | 
209 | 		// Find model details
210 | 		const mainModelData = availableModels.find((m) => m.id === mainModelId);
211 | 		const researchModelData = availableModels.find(
212 | 			(m) => m.id === researchModelId
213 | 		);
214 | 		const fallbackModelData = fallbackModelId
215 | 			? availableModels.find((m) => m.id === fallbackModelId)
216 | 			: null;
217 | 
218 | 		// Return structured configuration data
219 | 		return {
220 | 			success: true,
221 | 			data: {
222 | 				activeModels: {
223 | 					main: {
224 | 						provider: mainProvider,
225 | 						modelId: mainModelId,
226 | 						baseURL: mainBaseURL,
227 | 						sweScore: mainModelData?.swe_score || null,
228 | 						cost: mainModelData?.cost_per_1m_tokens || null,
229 | 						keyStatus: {
230 | 							cli: mainCliKeyOk,
231 | 							mcp: mainMcpKeyOk
232 | 						}
233 | 					},
234 | 					research: {
235 | 						provider: researchProvider,
236 | 						modelId: researchModelId,
237 | 						baseURL: researchBaseURL,
238 | 						sweScore: researchModelData?.swe_score || null,
239 | 						cost: researchModelData?.cost_per_1m_tokens || null,
240 | 						keyStatus: {
241 | 							cli: researchCliKeyOk,
242 | 							mcp: researchMcpKeyOk
243 | 						}
244 | 					},
245 | 					fallback: fallbackProvider
246 | 						? {
247 | 								provider: fallbackProvider,
248 | 								modelId: fallbackModelId,
249 | 								baseURL: fallbackBaseURL,
250 | 								sweScore: fallbackModelData?.swe_score || null,
251 | 								cost: fallbackModelData?.cost_per_1m_tokens || null,
252 | 								keyStatus: {
253 | 									cli: fallbackCliKeyOk,
254 | 									mcp: fallbackMcpKeyOk
255 | 								}
256 | 							}
257 | 						: null
258 | 				},
259 | 				message: 'Successfully retrieved current model configuration'
260 | 			}
261 | 		};
262 | 	} catch (error) {
263 | 		report('error', `Error getting model configuration: ${error.message}`);
264 | 		return {
265 | 			success: false,
266 | 			error: {
267 | 				code: 'CONFIG_ERROR',
268 | 				message: error.message
269 | 			}
270 | 		};
271 | 	}
272 | }
273 | 
274 | /**
275 |  * Get all available models not currently in use
276 |  * @param {Object} [options] - Options for the operation
277 |  * @param {Object} [options.session] - Session object containing environment variables (for MCP)
278 |  * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
279 |  * @param {string} [options.projectRoot] - Project root directory
280 |  * @returns {Object} RESTful response with available models
281 |  */
282 | async function getAvailableModelsList(options = {}) {
283 | 	const { mcpLog, projectRoot } = options;
284 | 
285 | 	const report = (level, ...args) => {
286 | 		if (mcpLog && typeof mcpLog[level] === 'function') {
287 | 			mcpLog[level](...args);
288 | 		}
289 | 	};
290 | 
291 | 	if (!projectRoot) {
292 | 		throw new Error('Project root is required but not found.');
293 | 	}
294 | 
295 | 	// Use centralized config path finding instead of hardcoded path
296 | 	const configPath = findConfigPath(null, { projectRoot });
297 | 	const configExists = isConfigFilePresent(projectRoot);
298 | 
299 | 	log(
300 | 		'debug',
301 | 		`Checking for config file using findConfigPath, found: ${configPath}`
302 | 	);
303 | 	log(
304 | 		'debug',
305 | 		`Checking config file using isConfigFilePresent(), exists: ${configExists}`
306 | 	);
307 | 
308 | 	if (!configExists) {
309 | 		throw new Error(CONFIG_MISSING_ERROR);
310 | 	}
311 | 
312 | 	try {
313 | 		// Get all available models
314 | 		const allAvailableModels = getAvailableModels(projectRoot);
315 | 
316 | 		if (!allAvailableModels || allAvailableModels.length === 0) {
317 | 			return {
318 | 				success: true,
319 | 				data: {
320 | 					models: [],
321 | 					message: 'No available models found'
322 | 				}
323 | 			};
324 | 		}
325 | 
326 | 		// Get currently used model IDs
327 | 		const mainModelId = getMainModelId(projectRoot);
328 | 		const researchModelId = getResearchModelId(projectRoot);
329 | 		const fallbackModelId = getFallbackModelId(projectRoot);
330 | 
331 | 		// Filter out placeholder models and active models
332 | 		const activeIds = [mainModelId, researchModelId, fallbackModelId].filter(
333 | 			Boolean
334 | 		);
335 | 		const otherAvailableModels = allAvailableModels.map((model) => ({
336 | 			provider: model.provider || 'N/A',
337 | 			modelId: model.id,
338 | 			sweScore: model.swe_score || null,
339 | 			cost: model.cost_per_1m_tokens || null,
340 | 			allowedRoles: model.allowed_roles || []
341 | 		}));
342 | 
343 | 		return {
344 | 			success: true,
345 | 			data: {
346 | 				models: otherAvailableModels,
347 | 				message: `Successfully retrieved ${otherAvailableModels.length} available models`
348 | 			}
349 | 		};
350 | 	} catch (error) {
351 | 		report('error', `Error getting available models: ${error.message}`);
352 | 		return {
353 | 			success: false,
354 | 			error: {
355 | 				code: 'MODELS_LIST_ERROR',
356 | 				message: error.message
357 | 			}
358 | 		};
359 | 	}
360 | }
361 | 
362 | /**
363 |  * Update a specific model in the configuration
364 |  * @param {string} role - The model role to update ('main', 'research', 'fallback')
365 |  * @param {string} modelId - The model ID to set for the role
366 |  * @param {Object} [options] - Options for the operation
367 |  * @param {string} [options.providerHint] - Provider hint if already determined ('openrouter' or 'ollama')
368 |  * @param {Object} [options.session] - Session object containing environment variables (for MCP)
369 |  * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
370 |  * @param {string} [options.projectRoot] - Project root directory
371 |  * @returns {Object} RESTful response with result of update operation
372 |  */
373 | async function setModel(role, modelId, options = {}) {
374 | 	const { mcpLog, projectRoot, providerHint, baseURL } = options;
375 | 	let computedBaseURL = baseURL; // Track the computed baseURL separately
376 | 
377 | 	const report = (level, ...args) => {
378 | 		if (mcpLog && typeof mcpLog[level] === 'function') {
379 | 			mcpLog[level](...args);
380 | 		}
381 | 	};
382 | 
383 | 	if (!projectRoot) {
384 | 		throw new Error('Project root is required but not found.');
385 | 	}
386 | 
387 | 	// Use centralized config path finding instead of hardcoded path
388 | 	const configPath = findConfigPath(null, { projectRoot });
389 | 	const configExists = isConfigFilePresent(projectRoot);
390 | 
391 | 	log(
392 | 		'debug',
393 | 		`Checking for config file using findConfigPath, found: ${configPath}`
394 | 	);
395 | 	log(
396 | 		'debug',
397 | 		`Checking config file using isConfigFilePresent(), exists: ${configExists}`
398 | 	);
399 | 
400 | 	if (!configExists) {
401 | 		throw new Error(CONFIG_MISSING_ERROR);
402 | 	}
403 | 
404 | 	// Validate role
405 | 	if (!['main', 'research', 'fallback'].includes(role)) {
406 | 		return {
407 | 			success: false,
408 | 			error: {
409 | 				code: 'INVALID_ROLE',
410 | 				message: `Invalid role: ${role}. Must be one of: main, research, fallback.`
411 | 			}
412 | 		};
413 | 	}
414 | 
415 | 	// Validate model ID
416 | 	if (typeof modelId !== 'string' || modelId.trim() === '') {
417 | 		return {
418 | 			success: false,
419 | 			error: {
420 | 				code: 'INVALID_MODEL_ID',
421 | 				message: `Invalid model ID: ${modelId}. Must be a non-empty string.`
422 | 			}
423 | 		};
424 | 	}
425 | 
426 | 	try {
427 | 		const availableModels = getAvailableModels(projectRoot);
428 | 		const currentConfig = getConfig(projectRoot);
429 | 		let determinedProvider = null; // Initialize provider
430 | 		let warningMessage = null;
431 | 
432 | 		// Find the model data in internal list
433 | 		// If we have a provider hint, search for exact provider+model match
434 | 		// Otherwise, just search by model ID (will get first match)
435 | 		let modelData;
436 | 		if (providerHint) {
437 | 			// Search for model with specific provider
438 | 			modelData = availableModels.find(
439 | 				(m) => m.id === modelId && m.provider === providerHint
440 | 			);
441 | 		} else {
442 | 			// Search by ID only
443 | 			modelData = availableModels.find((m) => m.id === modelId);
444 | 		}
445 | 
446 | 		// --- Revised Logic: Prioritize providerHint --- //
447 | 
448 | 		if (providerHint) {
449 | 			// Hint provided (from interactive setup or flag)
450 | 			if (modelData && modelData.provider === providerHint) {
451 | 				// Found internally with matching provider
452 | 				determinedProvider = providerHint;
453 | 				report(
454 | 					'info',
455 | 					`Model ${modelId} found internally with provider ${determinedProvider}.`
456 | 				);
457 | 			} else {
458 | 				// Either not found internally, OR found but under a DIFFERENT provider than hinted.
459 | 				// Proceed with custom logic based ONLY on the hint.
460 | 				if (providerHint === CUSTOM_PROVIDERS.OPENROUTER) {
461 | 					// Check OpenRouter ONLY because hint was openrouter
462 | 					report('info', `Checking OpenRouter for ${modelId} (as hinted)...`);
463 | 					const openRouterModels = await fetchOpenRouterModels();
464 | 
465 | 					if (
466 | 						openRouterModels &&
467 | 						openRouterModels.some((m) => m.id === modelId)
468 | 					) {
469 | 						determinedProvider = CUSTOM_PROVIDERS.OPENROUTER;
470 | 
471 | 						// Check if this is a free model (ends with :free)
472 | 						if (modelId.endsWith(':free')) {
473 | 							warningMessage = `Warning: OpenRouter free model '${modelId}' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version '${modelId.replace(':free', '')}' for full functionality.`;
474 | 						} else {
475 | 							warningMessage = `Warning: Custom OpenRouter model '${modelId}' set. This model is not officially validated by Taskmaster and may not function as expected.`;
476 | 						}
477 | 
478 | 						report('warn', warningMessage);
479 | 					} else {
480 | 						// Hinted as OpenRouter but not found in live check
481 | 						throw new Error(
482 | 							`Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.`
483 | 						);
484 | 					}
485 | 				} else if (providerHint === CUSTOM_PROVIDERS.OLLAMA) {
486 | 					// Check Ollama ONLY because hint was ollama
487 | 					report('info', `Checking Ollama for ${modelId} (as hinted)...`);
488 | 
489 | 					// Get current provider for this role to check if we should preserve baseURL
490 | 					let currentProvider;
491 | 					if (role === 'main') {
492 | 						currentProvider = getMainProvider(projectRoot);
493 | 					} else if (role === 'research') {
494 | 						currentProvider = getResearchProvider(projectRoot);
495 | 					} else if (role === 'fallback') {
496 | 						currentProvider = getFallbackProvider(projectRoot);
497 | 					}
498 | 
499 | 					// Only preserve baseURL if we're already using OLLAMA
500 | 					const existingBaseURL =
501 | 						currentProvider === CUSTOM_PROVIDERS.OLLAMA
502 | 							? getBaseUrlForRole(role, projectRoot)
503 | 							: null;
504 | 
505 | 					// Get the Ollama base URL - use provided, existing, or default
506 | 					const ollamaBaseURL =
507 | 						baseURL || existingBaseURL || 'http://localhost:11434/api';
508 | 					const ollamaModels = await fetchOllamaModels(ollamaBaseURL);
509 | 
510 | 					if (ollamaModels === null) {
511 | 						// Connection failed - server probably not running
512 | 						throw new Error(
513 | 							`Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.`
514 | 						);
515 | 					} else if (ollamaModels.some((m) => m.model === modelId)) {
516 | 						determinedProvider = CUSTOM_PROVIDERS.OLLAMA;
517 | 						warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`;
518 | 						report('warn', warningMessage);
519 | 						// Store the computed baseURL so it gets saved in config
520 | 						computedBaseURL = ollamaBaseURL;
521 | 					} else {
522 | 						// Server is running but model not found
523 | 						const tagsUrl = `${ollamaBaseURL}/tags`;
524 | 						throw new Error(
525 | 							`Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}`
526 | 						);
527 | 					}
528 | 				} else if (providerHint === CUSTOM_PROVIDERS.BEDROCK) {
529 | 					// Set provider without model validation since Bedrock models are managed by AWS
530 | 					determinedProvider = CUSTOM_PROVIDERS.BEDROCK;
531 | 					warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`;
532 | 					report('warn', warningMessage);
533 | 				} else if (providerHint === CUSTOM_PROVIDERS.CLAUDE_CODE) {
534 | 					// Claude Code provider - check if model exists in our list
535 | 					determinedProvider = CUSTOM_PROVIDERS.CLAUDE_CODE;
536 | 					// Re-find modelData specifically for claude-code provider
537 | 					const claudeCodeModels = availableModels.filter(
538 | 						(m) => m.provider === 'claude-code'
539 | 					);
540 | 					const claudeCodeModelData = claudeCodeModels.find(
541 | 						(m) => m.id === modelId
542 | 					);
543 | 					if (claudeCodeModelData) {
544 | 						// Update modelData to the found claude-code model
545 | 						modelData = claudeCodeModelData;
546 | 						report('info', `Setting Claude Code model '${modelId}'.`);
547 | 					} else {
548 | 						warningMessage = `Warning: Claude Code model '${modelId}' not found in supported models. Setting without validation.`;
549 | 						report('warn', warningMessage);
550 | 					}
551 | 				} else if (providerHint === CUSTOM_PROVIDERS.AZURE) {
552 | 					// Set provider without model validation since Azure models are managed by Azure
553 | 					determinedProvider = CUSTOM_PROVIDERS.AZURE;
554 | 					warningMessage = `Warning: Custom Azure model '${modelId}' set. Please ensure the model deployment is valid and accessible in your Azure account.`;
555 | 					report('warn', warningMessage);
556 | 				} else if (providerHint === CUSTOM_PROVIDERS.VERTEX) {
557 | 					// Set provider without model validation since Vertex models are managed by Google Cloud
558 | 					determinedProvider = CUSTOM_PROVIDERS.VERTEX;
559 | 					warningMessage = `Warning: Custom Vertex AI model '${modelId}' set. Please ensure the model is valid and accessible in your Google Cloud project.`;
560 | 					report('warn', warningMessage);
561 | 				} else if (providerHint === CUSTOM_PROVIDERS.GEMINI_CLI) {
562 | 					// Gemini CLI provider - check if model exists in our list
563 | 					determinedProvider = CUSTOM_PROVIDERS.GEMINI_CLI;
564 | 					// Re-find modelData specifically for gemini-cli provider
565 | 					const geminiCliModels = availableModels.filter(
566 | 						(m) => m.provider === 'gemini-cli'
567 | 					);
568 | 					const geminiCliModelData = geminiCliModels.find(
569 | 						(m) => m.id === modelId
570 | 					);
571 | 					if (geminiCliModelData) {
572 | 						// Update modelData to the found gemini-cli model
573 | 						modelData = geminiCliModelData;
574 | 						report('info', `Setting Gemini CLI model '${modelId}'.`);
575 | 					} else {
576 | 						warningMessage = `Warning: Gemini CLI model '${modelId}' not found in supported models. Setting without validation.`;
577 | 						report('warn', warningMessage);
578 | 					}
579 | 				} else if (providerHint === CUSTOM_PROVIDERS.CODEX_CLI) {
580 | 					// Codex CLI provider - enforce supported model list
581 | 					determinedProvider = CUSTOM_PROVIDERS.CODEX_CLI;
582 | 					const codexCliModels = availableModels.filter(
583 | 						(m) => m.provider === 'codex-cli'
584 | 					);
585 | 					const codexCliModelData = codexCliModels.find(
586 | 						(m) => m.id === modelId
587 | 					);
588 | 					if (codexCliModelData) {
589 | 						modelData = codexCliModelData;
590 | 						report('info', `Setting Codex CLI model '${modelId}'.`);
591 | 					} else {
592 | 						warningMessage = `Warning: Codex CLI model '${modelId}' not found in supported models. Setting without validation.`;
593 | 						report('warn', warningMessage);
594 | 					}
595 | 				} else if (providerHint === CUSTOM_PROVIDERS.LMSTUDIO) {
596 | 					// LM Studio provider - set without validation since it's a local server
597 | 					determinedProvider = CUSTOM_PROVIDERS.LMSTUDIO;
598 | 
599 | 					// Get current provider for this role to check if we should preserve baseURL
600 | 					let currentProvider;
601 | 					if (role === 'main') {
602 | 						currentProvider = getMainProvider(projectRoot);
603 | 					} else if (role === 'research') {
604 | 						currentProvider = getResearchProvider(projectRoot);
605 | 					} else if (role === 'fallback') {
606 | 						currentProvider = getFallbackProvider(projectRoot);
607 | 					}
608 | 
609 | 					// Only preserve baseURL if we're already using LMSTUDIO
610 | 					const existingBaseURL =
611 | 						currentProvider === CUSTOM_PROVIDERS.LMSTUDIO
612 | 							? getBaseUrlForRole(role, projectRoot)
613 | 							: null;
614 | 
615 | 					const lmStudioBaseURL =
616 | 						baseURL || existingBaseURL || 'http://localhost:1234/v1';
617 | 					warningMessage = `Warning: Custom LM Studio model '${modelId}' set with base URL '${lmStudioBaseURL}'. Please ensure LM Studio server is running and has loaded this model. Taskmaster cannot guarantee compatibility.`;
618 | 					report('warn', warningMessage);
619 | 					// Store the computed baseURL so it gets saved in config
620 | 					computedBaseURL = lmStudioBaseURL;
621 | 				} else if (providerHint === CUSTOM_PROVIDERS.OPENAI_COMPATIBLE) {
622 | 					// OpenAI-compatible provider - set without validation, requires baseURL
623 | 					determinedProvider = CUSTOM_PROVIDERS.OPENAI_COMPATIBLE;
624 | 
625 | 					// Get current provider for this role to check if we should preserve baseURL
626 | 					let currentProvider;
627 | 					if (role === 'main') {
628 | 						currentProvider = getMainProvider(projectRoot);
629 | 					} else if (role === 'research') {
630 | 						currentProvider = getResearchProvider(projectRoot);
631 | 					} else if (role === 'fallback') {
632 | 						currentProvider = getFallbackProvider(projectRoot);
633 | 					}
634 | 
635 | 					// Only preserve baseURL if we're already using OPENAI_COMPATIBLE
636 | 					const existingBaseURL =
637 | 						currentProvider === CUSTOM_PROVIDERS.OPENAI_COMPATIBLE
638 | 							? getBaseUrlForRole(role, projectRoot)
639 | 							: null;
640 | 
641 | 					const resolvedBaseURL = baseURL || existingBaseURL;
642 | 					if (!resolvedBaseURL) {
643 | 						throw new Error(
644 | 							`Base URL is required for OpenAI-compatible providers. Please provide a baseURL.`
645 | 						);
646 | 					}
647 | 					warningMessage = `Warning: Custom OpenAI-compatible model '${modelId}' set with base URL '${resolvedBaseURL}'. Taskmaster cannot guarantee compatibility. Ensure your API endpoint follows the OpenAI API specification.`;
648 | 					report('warn', warningMessage);
649 | 					// Store the computed baseURL so it gets saved in config
650 | 					computedBaseURL = resolvedBaseURL;
651 | 				} else {
652 | 					// Invalid provider hint - should not happen with our constants
653 | 					throw new Error(`Invalid provider hint received: ${providerHint}`);
654 | 				}
655 | 			}
656 | 		} else {
657 | 			// No hint provided (flags not used)
658 | 			if (modelData) {
659 | 				// Found internally, use the provider from the internal list
660 | 				determinedProvider = modelData.provider;
661 | 				report(
662 | 					'info',
663 | 					`Model ${modelId} found internally with provider ${determinedProvider}.`
664 | 				);
665 | 			} else {
666 | 				// Model not found and no provider hint was given
667 | 				return {
668 | 					success: false,
669 | 					error: {
670 | 						code: 'MODEL_NOT_FOUND_NO_HINT',
671 | 						message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, --vertex, --lmstudio, --openai-compatible, --gemini-cli, or --codex-cli.`
672 | 					}
673 | 				};
674 | 			}
675 | 		}
676 | 
677 | 		// --- End of Revised Logic --- //
678 | 
679 | 		// At this point, we should have a determinedProvider if the model is valid (internally or custom)
680 | 		if (!determinedProvider) {
681 | 			// This case acts as a safeguard
682 | 			return {
683 | 				success: false,
684 | 				error: {
685 | 					code: 'PROVIDER_UNDETERMINED',
686 | 					message: `Could not determine the provider for model ID "${modelId}".`
687 | 				}
688 | 			};
689 | 		}
690 | 
691 | 		// Update configuration
692 | 		currentConfig.models[role] = {
693 | 			...currentConfig.models[role], // Keep existing params like temperature
694 | 			provider: determinedProvider,
695 | 			modelId: modelId
696 | 		};
697 | 
698 | 		// Handle baseURL for providers that support it
699 | 		if (
700 | 			computedBaseURL &&
701 | 			(determinedProvider === CUSTOM_PROVIDERS.OPENAI_COMPATIBLE ||
702 | 				determinedProvider === CUSTOM_PROVIDERS.LMSTUDIO ||
703 | 				determinedProvider === CUSTOM_PROVIDERS.OLLAMA)
704 | 		) {
705 | 			currentConfig.models[role].baseURL = computedBaseURL;
706 | 		} else {
707 | 			// Remove baseURL when switching to a provider that doesn't need it
708 | 			delete currentConfig.models[role].baseURL;
709 | 		}
710 | 
711 | 		// If model data is available, update maxTokens from supported-models.json
712 | 		if (modelData && modelData.max_tokens) {
713 | 			currentConfig.models[role].maxTokens = modelData.max_tokens;
714 | 		}
715 | 
716 | 		// Write updated configuration
717 | 		const writeResult = writeConfig(currentConfig, projectRoot);
718 | 		if (!writeResult) {
719 | 			return {
720 | 				success: false,
721 | 				error: {
722 | 					code: 'CONFIG_WRITE_ERROR',
723 | 					message: 'Error writing updated configuration to configuration file'
724 | 				}
725 | 			};
726 | 		}
727 | 
728 | 		const successMessage = `Successfully set ${role} model to ${modelId} (Provider: ${determinedProvider})`;
729 | 		report('info', successMessage);
730 | 
731 | 		return {
732 | 			success: true,
733 | 			data: {
734 | 				role,
735 | 				provider: determinedProvider,
736 | 				modelId,
737 | 				message: successMessage,
738 | 				warning: warningMessage // Include warning in the response data
739 | 			}
740 | 		};
741 | 	} catch (error) {
742 | 		report('error', `Error setting ${role} model: ${error.message}`);
743 | 		return {
744 | 			success: false,
745 | 			error: {
746 | 				code: 'SET_MODEL_ERROR',
747 | 				message: error.message
748 | 			}
749 | 		};
750 | 	}
751 | }
752 | 
753 | /**
754 |  * Get API key status for all known providers.
755 |  * @param {Object} [options] - Options for the operation
756 |  * @param {Object} [options.session] - Session object containing environment variables (for MCP)
757 |  * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
758 |  * @param {string} [options.projectRoot] - Project root directory
759 |  * @returns {Object} RESTful response with API key status report
760 |  */
761 | async function getApiKeyStatusReport(options = {}) {
762 | 	const { mcpLog, projectRoot, session } = options;
763 | 	const report = (level, ...args) => {
764 | 		if (mcpLog && typeof mcpLog[level] === 'function') {
765 | 			mcpLog[level](...args);
766 | 		}
767 | 	};
768 | 
769 | 	try {
770 | 		const providers = getAllProviders();
771 | 		const providersToCheck = providers.filter(
772 | 			(p) => p.toLowerCase() !== 'ollama'
773 | 		); // Ollama is not a provider, it's a service, doesn't need an api key usually
774 | 		const statusReport = providersToCheck.map((provider) => {
775 | 			// Use provided projectRoot for MCP status check
776 | 			const cliOk = isApiKeySet(provider, session, projectRoot); // Pass session and projectRoot for CLI check
777 | 			const mcpOk = getMcpApiKeyStatus(provider, projectRoot);
778 | 			return {
779 | 				provider,
780 | 				cli: cliOk,
781 | 				mcp: mcpOk
782 | 			};
783 | 		});
784 | 
785 | 		report('info', 'Successfully generated API key status report.');
786 | 		return {
787 | 			success: true,
788 | 			data: {
789 | 				report: statusReport,
790 | 				message: 'API key status report generated.'
791 | 			}
792 | 		};
793 | 	} catch (error) {
794 | 		report('error', `Error generating API key status report: ${error.message}`);
795 | 		return {
796 | 			success: false,
797 | 			error: {
798 | 				code: 'API_KEY_STATUS_ERROR',
799 | 				message: error.message
800 | 			}
801 | 		};
802 | 	}
803 | }
804 | 
805 | export {
806 | 	getModelConfiguration,
807 | 	getAvailableModelsList,
808 | 	setModel,
809 | 	getApiKeyStatusReport
810 | };
811 | 
```

--------------------------------------------------------------------------------
/tests/unit/config-manager.test.mjs:
--------------------------------------------------------------------------------

```
  1 | // @ts-check
  2 | /**
  3 |  * Module to test the config-manager.js functionality
  4 |  * This file uses ES module syntax (.mjs) to properly handle imports
  5 |  */
  6 | 
  7 | import fs from 'fs';
  8 | import path from 'path';
  9 | import { jest } from '@jest/globals';
 10 | import { fileURLToPath } from 'url';
 11 | import { sampleTasks } from '../fixtures/sample-tasks.js';
 12 | 
 13 | // Disable chalk's color detection which can cause fs.readFileSync calls
 14 | process.env.FORCE_COLOR = '0';
 15 | 
 16 | // --- Read REAL supported-models.json data BEFORE mocks ---
 17 | const __filename = fileURLToPath(import.meta.url); // Get current file path
 18 | const __dirname = path.dirname(__filename); // Get current directory
 19 | const realSupportedModelsPath = path.resolve(
 20 | 	__dirname,
 21 | 	'../../scripts/modules/supported-models.json'
 22 | );
 23 | let REAL_SUPPORTED_MODELS_CONTENT;
 24 | let REAL_SUPPORTED_MODELS_DATA;
 25 | try {
 26 | 	REAL_SUPPORTED_MODELS_CONTENT = fs.readFileSync(
 27 | 		realSupportedModelsPath,
 28 | 		'utf-8'
 29 | 	);
 30 | 	REAL_SUPPORTED_MODELS_DATA = JSON.parse(REAL_SUPPORTED_MODELS_CONTENT);
 31 | } catch (err) {
 32 | 	console.error(
 33 | 		'FATAL TEST SETUP ERROR: Could not read or parse real supported-models.json',
 34 | 		err
 35 | 	);
 36 | 	REAL_SUPPORTED_MODELS_CONTENT = '{}'; // Default to empty object on error
 37 | 	REAL_SUPPORTED_MODELS_DATA = {};
 38 | 	process.exit(1); // Exit if essential test data can't be loaded
 39 | }
 40 | 
 41 | // --- Define Mock Function Instances ---
 42 | const mockFindProjectRoot = jest.fn();
 43 | const mockLog = jest.fn();
 44 | const mockResolveEnvVariable = jest.fn();
 45 | 
 46 | // --- Mock fs functions directly instead of the whole module ---
 47 | const mockExistsSync = jest.fn();
 48 | const mockReadFileSync = jest.fn();
 49 | const mockWriteFileSync = jest.fn();
 50 | 
 51 | // Instead of mocking the entire fs module, mock just the functions we need
 52 | fs.existsSync = mockExistsSync;
 53 | fs.readFileSync = mockReadFileSync;
 54 | fs.writeFileSync = mockWriteFileSync;
 55 | 
 56 | // --- Test Data (Keep as is, ensure DEFAULT_CONFIG is accurate) ---
 57 | const MOCK_PROJECT_ROOT = '/mock/project';
 58 | const MOCK_CONFIG_PATH = path.join(MOCK_PROJECT_ROOT, '.taskmasterconfig');
 59 | 
 60 | // Updated DEFAULT_CONFIG reflecting the implementation
 61 | const DEFAULT_CONFIG = {
 62 | 	models: {
 63 | 		main: {
 64 | 			provider: 'anthropic',
 65 | 			modelId: 'claude-3-7-sonnet-20250219',
 66 | 			maxTokens: 64000,
 67 | 			temperature: 0.2
 68 | 		},
 69 | 		research: {
 70 | 			provider: 'perplexity',
 71 | 			modelId: 'sonar-pro',
 72 | 			maxTokens: 8700,
 73 | 			temperature: 0.1
 74 | 		},
 75 | 		fallback: {
 76 | 			provider: 'anthropic',
 77 | 			modelId: 'claude-3-5-sonnet',
 78 | 			maxTokens: 8192,
 79 | 			temperature: 0.2
 80 | 		}
 81 | 	},
 82 | 	global: {
 83 | 		logLevel: 'info',
 84 | 		debug: false,
 85 | 		defaultSubtasks: 5,
 86 | 		defaultPriority: 'medium',
 87 | 		projectName: 'Task Master',
 88 | 		ollamaBaseURL: 'http://localhost:11434/api'
 89 | 	}
 90 | };
 91 | 
 92 | // Other test data (VALID_CUSTOM_CONFIG, PARTIAL_CONFIG, INVALID_PROVIDER_CONFIG)
 93 | const VALID_CUSTOM_CONFIG = {
 94 | 	models: {
 95 | 		main: {
 96 | 			provider: 'openai',
 97 | 			modelId: 'gpt-4o',
 98 | 			maxTokens: 4096,
 99 | 			temperature: 0.5
100 | 		},
101 | 		research: {
102 | 			provider: 'google',
103 | 			modelId: 'gemini-1.5-pro-latest',
104 | 			maxTokens: 8192,
105 | 			temperature: 0.3
106 | 		},
107 | 		fallback: {
108 | 			provider: 'anthropic',
109 | 			modelId: 'claude-3-opus-20240229',
110 | 			maxTokens: 100000,
111 | 			temperature: 0.4
112 | 		}
113 | 	},
114 | 	global: {
115 | 		logLevel: 'debug',
116 | 		defaultPriority: 'high',
117 | 		projectName: 'My Custom Project'
118 | 	}
119 | };
120 | 
121 | const PARTIAL_CONFIG = {
122 | 	models: {
123 | 		main: { provider: 'openai', modelId: 'gpt-4-turbo' }
124 | 	},
125 | 	global: {
126 | 		projectName: 'Partial Project'
127 | 	}
128 | };
129 | 
130 | const INVALID_PROVIDER_CONFIG = {
131 | 	models: {
132 | 		main: { provider: 'invalid-provider', modelId: 'some-model' },
133 | 		research: {
134 | 			provider: 'perplexity',
135 | 			modelId: 'llama-3-sonar-large-32k-online'
136 | 		}
137 | 	},
138 | 	global: {
139 | 		logLevel: 'warn'
140 | 	}
141 | };
142 | 
143 | // Define spies globally to be restored in afterAll
144 | let consoleErrorSpy;
145 | let consoleWarnSpy;
146 | 
147 | beforeAll(() => {
148 | 	// Set up console spies
149 | 	consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
150 | 	consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
151 | });
152 | 
153 | afterAll(() => {
154 | 	// Restore all spies
155 | 	jest.restoreAllMocks();
156 | });
157 | 
158 | describe('Config Manager Module', () => {
159 | 	// Declare variables for imported module
160 | 	let configManager;
161 | 
162 | 	// Reset mocks before each test for isolation
163 | 	beforeEach(async () => {
164 | 		// Clear all mock calls and reset implementations between tests
165 | 		jest.clearAllMocks();
166 | 		// Reset the external mock instances for utils
167 | 		mockFindProjectRoot.mockReset();
168 | 		mockLog.mockReset();
169 | 		mockResolveEnvVariable.mockReset();
170 | 		mockExistsSync.mockReset();
171 | 		mockReadFileSync.mockReset();
172 | 		mockWriteFileSync.mockReset();
173 | 
174 | 		// --- Mock Dependencies BEFORE importing the module under test ---
175 | 		// Mock the 'utils.js' module using doMock (applied at runtime)
176 | 		jest.doMock('../../scripts/modules/utils.js', () => ({
177 | 			__esModule: true, // Indicate it's an ES module mock
178 | 			findProjectRoot: mockFindProjectRoot, // Use the mock function instance
179 | 			log: mockLog, // Use the mock function instance
180 | 			resolveEnvVariable: mockResolveEnvVariable // Use the mock function instance
181 | 		}));
182 | 
183 | 		// Dynamically import the module under test AFTER mocking dependencies
184 | 		configManager = await import('../../scripts/modules/config-manager.js');
185 | 
186 | 		// --- Default Mock Implementations ---
187 | 		mockFindProjectRoot.mockReturnValue(MOCK_PROJECT_ROOT); // Default for utils.findProjectRoot
188 | 		mockExistsSync.mockReturnValue(true); // Assume files exist by default
189 | 
190 | 		// Default readFileSync: Return REAL models content, mocked config, or throw error
191 | 		mockReadFileSync.mockImplementation((filePath) => {
192 | 			const baseName = path.basename(filePath);
193 | 			if (baseName === 'supported-models.json') {
194 | 				// Return the REAL file content stringified
195 | 				return REAL_SUPPORTED_MODELS_CONTENT;
196 | 			} else if (filePath === MOCK_CONFIG_PATH) {
197 | 				// Still mock the .taskmasterconfig reads
198 | 				return JSON.stringify(DEFAULT_CONFIG); // Default behavior
199 | 			}
200 | 			// Throw for unexpected reads - helps catch errors
201 | 			throw new Error(`Unexpected fs.readFileSync call in test: ${filePath}`);
202 | 		});
203 | 
204 | 		// Default writeFileSync: Do nothing, just allow calls
205 | 		mockWriteFileSync.mockImplementation(() => {});
206 | 	});
207 | 
208 | 	// --- Validation Functions ---
209 | 	describe('Validation Functions', () => {
210 | 		// Tests for validateProvider and validateProviderModelCombination
211 | 		test('validateProvider should return true for valid providers', () => {
212 | 			expect(configManager.validateProvider('openai')).toBe(true);
213 | 			expect(configManager.validateProvider('anthropic')).toBe(true);
214 | 			expect(configManager.validateProvider('google')).toBe(true);
215 | 			expect(configManager.validateProvider('perplexity')).toBe(true);
216 | 			expect(configManager.validateProvider('ollama')).toBe(true);
217 | 			expect(configManager.validateProvider('openrouter')).toBe(true);
218 | 		});
219 | 
220 | 		test('validateProvider should return false for invalid providers', () => {
221 | 			expect(configManager.validateProvider('invalid-provider')).toBe(false);
222 | 			expect(configManager.validateProvider('grok')).toBe(false); // Not in mock map
223 | 			expect(configManager.validateProvider('')).toBe(false);
224 | 			expect(configManager.validateProvider(null)).toBe(false);
225 | 		});
226 | 
227 | 		test('validateProviderModelCombination should validate known good combinations', () => {
228 | 			// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
229 | 			configManager.getConfig(MOCK_PROJECT_ROOT, true);
230 | 			expect(
231 | 				configManager.validateProviderModelCombination('openai', 'gpt-4o')
232 | 			).toBe(true);
233 | 			expect(
234 | 				configManager.validateProviderModelCombination(
235 | 					'anthropic',
236 | 					'claude-3-5-sonnet-20241022'
237 | 				)
238 | 			).toBe(true);
239 | 		});
240 | 
241 | 		test('validateProviderModelCombination should return false for known bad combinations', () => {
242 | 			// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
243 | 			configManager.getConfig(MOCK_PROJECT_ROOT, true);
244 | 			expect(
245 | 				configManager.validateProviderModelCombination(
246 | 					'openai',
247 | 					'claude-3-opus-20240229'
248 | 				)
249 | 			).toBe(false);
250 | 		});
251 | 
252 | 		test('validateProviderModelCombination should return true for ollama/openrouter (empty lists in map)', () => {
253 | 			// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
254 | 			configManager.getConfig(MOCK_PROJECT_ROOT, true);
255 | 			expect(
256 | 				configManager.validateProviderModelCombination('ollama', 'any-model')
257 | 			).toBe(false);
258 | 			expect(
259 | 				configManager.validateProviderModelCombination(
260 | 					'openrouter',
261 | 					'any/model'
262 | 				)
263 | 			).toBe(false);
264 | 		});
265 | 
266 | 		test('validateProviderModelCombination should return true for providers not in map', () => {
267 | 			// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
268 | 			configManager.getConfig(MOCK_PROJECT_ROOT, true);
269 | 			// The implementation returns true if the provider isn't in the map
270 | 			expect(
271 | 				configManager.validateProviderModelCombination(
272 | 					'unknown-provider',
273 | 					'some-model'
274 | 				)
275 | 			).toBe(true);
276 | 		});
277 | 	});
278 | 
279 | 	// --- getConfig Tests ---
280 | 	describe('getConfig Tests', () => {
281 | 		test('should return default config if .taskmasterconfig does not exist', () => {
282 | 			// Arrange
283 | 			mockExistsSync.mockReturnValue(false);
284 | 			// findProjectRoot mock is set in beforeEach
285 | 
286 | 			// Act: Call getConfig with explicit root
287 | 			const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Force reload
288 | 
289 | 			// Assert
290 | 			expect(config).toEqual(DEFAULT_CONFIG);
291 | 			expect(mockFindProjectRoot).not.toHaveBeenCalled(); // Explicit root provided
292 | 			expect(mockExistsSync).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
293 | 			expect(mockReadFileSync).not.toHaveBeenCalled(); // No read if file doesn't exist
294 | 			expect(consoleWarnSpy).toHaveBeenCalledWith(
295 | 				expect.stringContaining('not found at provided project root')
296 | 			);
297 | 		});
298 | 
299 | 		test.skip('should use findProjectRoot and return defaults if file not found', () => {
300 | 			// TODO: Fix mock interaction, findProjectRoot isn't being registered as called
301 | 			// Arrange
302 | 			mockExistsSync.mockReturnValue(false);
303 | 			// findProjectRoot mock is set in beforeEach
304 | 
305 | 			// Act: Call getConfig without explicit root
306 | 			const config = configManager.getConfig(null, true); // Force reload
307 | 
308 | 			// Assert
309 | 			expect(mockFindProjectRoot).toHaveBeenCalled(); // Should be called now
310 | 			expect(mockExistsSync).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
311 | 			expect(config).toEqual(DEFAULT_CONFIG);
312 | 			expect(mockReadFileSync).not.toHaveBeenCalled();
313 | 			expect(consoleWarnSpy).toHaveBeenCalledWith(
314 | 				expect.stringContaining('not found at derived root')
315 | 			); // Adjusted expected warning
316 | 		});
317 | 
318 | 		test('should read and merge valid config file with defaults', () => {
319 | 			// Arrange: Override readFileSync for this test
320 | 			mockReadFileSync.mockImplementation((filePath) => {
321 | 				if (filePath === MOCK_CONFIG_PATH)
322 | 					return JSON.stringify(VALID_CUSTOM_CONFIG);
323 | 				if (path.basename(filePath) === 'supported-models.json') {
324 | 					// Provide necessary models for validation within getConfig
325 | 					return JSON.stringify({
326 | 						openai: [{ id: 'gpt-4o' }],
327 | 						google: [{ id: 'gemini-1.5-pro-latest' }],
328 | 						perplexity: [{ id: 'sonar-pro' }],
329 | 						anthropic: [
330 | 							{ id: 'claude-3-opus-20240229' },
331 | 							{ id: 'claude-3-5-sonnet' },
332 | 							{ id: 'claude-3-7-sonnet-20250219' },
333 | 							{ id: 'claude-3-5-sonnet' }
334 | 						],
335 | 						ollama: [],
336 | 						openrouter: []
337 | 					});
338 | 				}
339 | 				throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
340 | 			});
341 | 			mockExistsSync.mockReturnValue(true);
342 | 			// findProjectRoot mock set in beforeEach
343 | 
344 | 			// Act
345 | 			const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Force reload
346 | 
347 | 			// Assert: Construct expected merged config
348 | 			const expectedMergedConfig = {
349 | 				models: {
350 | 					main: {
351 | 						...DEFAULT_CONFIG.models.main,
352 | 						...VALID_CUSTOM_CONFIG.models.main
353 | 					},
354 | 					research: {
355 | 						...DEFAULT_CONFIG.models.research,
356 | 						...VALID_CUSTOM_CONFIG.models.research
357 | 					},
358 | 					fallback: {
359 | 						...DEFAULT_CONFIG.models.fallback,
360 | 						...VALID_CUSTOM_CONFIG.models.fallback
361 | 					}
362 | 				},
363 | 				global: { ...DEFAULT_CONFIG.global, ...VALID_CUSTOM_CONFIG.global }
364 | 			};
365 | 			expect(config).toEqual(expectedMergedConfig);
366 | 			expect(mockExistsSync).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
367 | 			expect(mockReadFileSync).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8');
368 | 		});
369 | 
370 | 		test('should merge defaults for partial config file', () => {
371 | 			// Arrange
372 | 			mockReadFileSync.mockImplementation((filePath) => {
373 | 				if (filePath === MOCK_CONFIG_PATH)
374 | 					return JSON.stringify(PARTIAL_CONFIG);
375 | 				if (path.basename(filePath) === 'supported-models.json') {
376 | 					return JSON.stringify({
377 | 						openai: [{ id: 'gpt-4-turbo' }],
378 | 						perplexity: [{ id: 'sonar-pro' }],
379 | 						anthropic: [
380 | 							{ id: 'claude-3-7-sonnet-20250219' },
381 | 							{ id: 'claude-3-5-sonnet' }
382 | 						],
383 | 						ollama: [],
384 | 						openrouter: []
385 | 					});
386 | 				}
387 | 				throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
388 | 			});
389 | 			mockExistsSync.mockReturnValue(true);
390 | 			// findProjectRoot mock set in beforeEach
391 | 
392 | 			// Act
393 | 			const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
394 | 
395 | 			// Assert: Construct expected merged config
396 | 			const expectedMergedConfig = {
397 | 				models: {
398 | 					main: {
399 | 						...DEFAULT_CONFIG.models.main,
400 | 						...PARTIAL_CONFIG.models.main
401 | 					},
402 | 					research: { ...DEFAULT_CONFIG.models.research },
403 | 					fallback: { ...DEFAULT_CONFIG.models.fallback }
404 | 				},
405 | 				global: { ...DEFAULT_CONFIG.global, ...PARTIAL_CONFIG.global }
406 | 			};
407 | 			expect(config).toEqual(expectedMergedConfig);
408 | 			expect(mockReadFileSync).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8');
409 | 		});
410 | 
411 | 		test('should handle JSON parsing error and return defaults', () => {
412 | 			// Arrange
413 | 			mockReadFileSync.mockImplementation((filePath) => {
414 | 				if (filePath === MOCK_CONFIG_PATH) return 'invalid json';
415 | 				// Mock models read needed for initial load before parse error
416 | 				if (path.basename(filePath) === 'supported-models.json') {
417 | 					return JSON.stringify({
418 | 						anthropic: [{ id: 'claude-3-7-sonnet-20250219' }],
419 | 						perplexity: [{ id: 'sonar-pro' }],
420 | 						fallback: [{ id: 'claude-3-5-sonnet' }],
421 | 						ollama: [],
422 | 						openrouter: []
423 | 					});
424 | 				}
425 | 				throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
426 | 			});
427 | 			mockExistsSync.mockReturnValue(true);
428 | 			// findProjectRoot mock set in beforeEach
429 | 
430 | 			// Act
431 | 			const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
432 | 
433 | 			// Assert
434 | 			expect(config).toEqual(DEFAULT_CONFIG);
435 | 			expect(consoleErrorSpy).toHaveBeenCalledWith(
436 | 				expect.stringContaining('Error reading or parsing')
437 | 			);
438 | 		});
439 | 
440 | 		test('should handle file read error and return defaults', () => {
441 | 			// Arrange
442 | 			const readError = new Error('Permission denied');
443 | 			mockReadFileSync.mockImplementation((filePath) => {
444 | 				if (filePath === MOCK_CONFIG_PATH) throw readError;
445 | 				// Mock models read needed for initial load before read error
446 | 				if (path.basename(filePath) === 'supported-models.json') {
447 | 					return JSON.stringify({
448 | 						anthropic: [{ id: 'claude-3-7-sonnet-20250219' }],
449 | 						perplexity: [{ id: 'sonar-pro' }],
450 | 						fallback: [{ id: 'claude-3-5-sonnet' }],
451 | 						ollama: [],
452 | 						openrouter: []
453 | 					});
454 | 				}
455 | 				throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
456 | 			});
457 | 			mockExistsSync.mockReturnValue(true);
458 | 			// findProjectRoot mock set in beforeEach
459 | 
460 | 			// Act
461 | 			const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
462 | 
463 | 			// Assert
464 | 			expect(config).toEqual(DEFAULT_CONFIG);
465 | 			expect(consoleErrorSpy).toHaveBeenCalledWith(
466 | 				expect.stringContaining(
467 | 					`Permission denied. Using default configuration.`
468 | 				)
469 | 			);
470 | 		});
471 | 
472 | 		test('should validate provider and fallback to default if invalid', () => {
473 | 			// Arrange
474 | 			mockReadFileSync.mockImplementation((filePath) => {
475 | 				if (filePath === MOCK_CONFIG_PATH)
476 | 					return JSON.stringify(INVALID_PROVIDER_CONFIG);
477 | 				if (path.basename(filePath) === 'supported-models.json') {
478 | 					return JSON.stringify({
479 | 						perplexity: [{ id: 'llama-3-sonar-large-32k-online' }],
480 | 						anthropic: [
481 | 							{ id: 'claude-3-7-sonnet-20250219' },
482 | 							{ id: 'claude-3-5-sonnet' }
483 | 						],
484 | 						ollama: [],
485 | 						openrouter: []
486 | 					});
487 | 				}
488 | 				throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
489 | 			});
490 | 			mockExistsSync.mockReturnValue(true);
491 | 			// findProjectRoot mock set in beforeEach
492 | 
493 | 			// Act
494 | 			const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
495 | 
496 | 			// Assert
497 | 			expect(consoleWarnSpy).toHaveBeenCalledWith(
498 | 				expect.stringContaining(
499 | 					'Warning: Invalid main provider "invalid-provider"'
500 | 				)
501 | 			);
502 | 			const expectedMergedConfig = {
503 | 				models: {
504 | 					main: { ...DEFAULT_CONFIG.models.main },
505 | 					research: {
506 | 						...DEFAULT_CONFIG.models.research,
507 | 						...INVALID_PROVIDER_CONFIG.models.research
508 | 					},
509 | 					fallback: { ...DEFAULT_CONFIG.models.fallback }
510 | 				},
511 | 				global: { ...DEFAULT_CONFIG.global, ...INVALID_PROVIDER_CONFIG.global }
512 | 			};
513 | 			expect(config).toEqual(expectedMergedConfig);
514 | 		});
515 | 	});
516 | 
517 | 	// --- writeConfig Tests ---
518 | 	describe('writeConfig', () => {
519 | 		test('should write valid config to file', () => {
520 | 			// Arrange (Default mocks are sufficient)
521 | 			// findProjectRoot mock set in beforeEach
522 | 			mockWriteFileSync.mockImplementation(() => {}); // Ensure it doesn't throw
523 | 
524 | 			// Act
525 | 			const success = configManager.writeConfig(
526 | 				VALID_CUSTOM_CONFIG,
527 | 				MOCK_PROJECT_ROOT
528 | 			);
529 | 
530 | 			// Assert
531 | 			expect(success).toBe(true);
532 | 			expect(mockWriteFileSync).toHaveBeenCalledWith(
533 | 				MOCK_CONFIG_PATH,
534 | 				JSON.stringify(VALID_CUSTOM_CONFIG, null, 2) // writeConfig stringifies
535 | 			);
536 | 			expect(consoleErrorSpy).not.toHaveBeenCalled();
537 | 		});
538 | 
539 | 		test('should return false and log error if write fails', () => {
540 | 			// Arrange
541 | 			const mockWriteError = new Error('Disk full');
542 | 			mockWriteFileSync.mockImplementation(() => {
543 | 				throw mockWriteError;
544 | 			});
545 | 			// findProjectRoot mock set in beforeEach
546 | 
547 | 			// Act
548 | 			const success = configManager.writeConfig(
549 | 				VALID_CUSTOM_CONFIG,
550 | 				MOCK_PROJECT_ROOT
551 | 			);
552 | 
553 | 			// Assert
554 | 			expect(success).toBe(false);
555 | 			expect(mockWriteFileSync).toHaveBeenCalled();
556 | 			expect(consoleErrorSpy).toHaveBeenCalledWith(
557 | 				expect.stringContaining(`Disk full`)
558 | 			);
559 | 		});
560 | 
561 | 		test.skip('should return false if project root cannot be determined', () => {
562 | 			// TODO: Fix mock interaction or function logic, returns true unexpectedly in test
563 | 			// Arrange: Override mock for this specific test
564 | 			mockFindProjectRoot.mockReturnValue(null);
565 | 
566 | 			// Act: Call without explicit root
567 | 			const success = configManager.writeConfig(VALID_CUSTOM_CONFIG);
568 | 
569 | 			// Assert
570 | 			expect(success).toBe(false); // Function should return false if root is null
571 | 			expect(mockFindProjectRoot).toHaveBeenCalled();
572 | 			expect(mockWriteFileSync).not.toHaveBeenCalled();
573 | 			expect(consoleErrorSpy).toHaveBeenCalledWith(
574 | 				expect.stringContaining('Could not determine project root')
575 | 			);
576 | 		});
577 | 	});
578 | 
579 | 	// --- Getter Functions ---
580 | 	describe('Getter Functions', () => {
581 | 		test('getMainProvider should return provider from config', () => {
582 | 			// Arrange: Set up readFileSync to return VALID_CUSTOM_CONFIG
583 | 			mockReadFileSync.mockImplementation((filePath) => {
584 | 				if (filePath === MOCK_CONFIG_PATH)
585 | 					return JSON.stringify(VALID_CUSTOM_CONFIG);
586 | 				if (path.basename(filePath) === 'supported-models.json') {
587 | 					return JSON.stringify({
588 | 						openai: [{ id: 'gpt-4o' }],
589 | 						google: [{ id: 'gemini-1.5-pro-latest' }],
590 | 						anthropic: [
591 | 							{ id: 'claude-3-opus-20240229' },
592 | 							{ id: 'claude-3-7-sonnet-20250219' },
593 | 							{ id: 'claude-3-5-sonnet' }
594 | 						],
595 | 						perplexity: [{ id: 'sonar-pro' }],
596 | 						ollama: [],
597 | 						openrouter: []
598 | 					}); // Added perplexity
599 | 				}
600 | 				throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
601 | 			});
602 | 			mockExistsSync.mockReturnValue(true);
603 | 			// findProjectRoot mock set in beforeEach
604 | 
605 | 			// Act
606 | 			const provider = configManager.getMainProvider(MOCK_PROJECT_ROOT);
607 | 
608 | 			// Assert
609 | 			expect(provider).toBe(VALID_CUSTOM_CONFIG.models.main.provider);
610 | 		});
611 | 
612 | 		test('getLogLevel should return logLevel from config', () => {
613 | 			// Arrange: Set up readFileSync to return VALID_CUSTOM_CONFIG
614 | 			mockReadFileSync.mockImplementation((filePath) => {
615 | 				if (filePath === MOCK_CONFIG_PATH)
616 | 					return JSON.stringify(VALID_CUSTOM_CONFIG);
617 | 				if (path.basename(filePath) === 'supported-models.json') {
618 | 					// Provide enough mock model data for validation within getConfig
619 | 					return JSON.stringify({
620 | 						openai: [{ id: 'gpt-4o' }],
621 | 						google: [{ id: 'gemini-1.5-pro-latest' }],
622 | 						anthropic: [
623 | 							{ id: 'claude-3-opus-20240229' },
624 | 							{ id: 'claude-3-7-sonnet-20250219' },
625 | 							{ id: 'claude-3-5-sonnet' }
626 | 						],
627 | 						perplexity: [{ id: 'sonar-pro' }],
628 | 						ollama: [],
629 | 						openrouter: []
630 | 					});
631 | 				}
632 | 				throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
633 | 			});
634 | 			mockExistsSync.mockReturnValue(true);
635 | 			// findProjectRoot mock set in beforeEach
636 | 
637 | 			// Act
638 | 			const logLevel = configManager.getLogLevel(MOCK_PROJECT_ROOT);
639 | 
640 | 			// Assert
641 | 			expect(logLevel).toBe(VALID_CUSTOM_CONFIG.global.logLevel);
642 | 		});
643 | 
644 | 		// Add more tests for other getters (getResearchProvider, getProjectName, etc.)
645 | 	});
646 | 
647 | 	// --- isConfigFilePresent Tests ---
648 | 	describe('isConfigFilePresent', () => {
649 | 		test('should return true if config file exists', () => {
650 | 			mockExistsSync.mockReturnValue(true);
651 | 			// findProjectRoot mock set in beforeEach
652 | 			expect(configManager.isConfigFilePresent(MOCK_PROJECT_ROOT)).toBe(true);
653 | 			expect(mockExistsSync).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
654 | 		});
655 | 
656 | 		test('should return false if config file does not exist', () => {
657 | 			mockExistsSync.mockReturnValue(false);
658 | 			// findProjectRoot mock set in beforeEach
659 | 			expect(configManager.isConfigFilePresent(MOCK_PROJECT_ROOT)).toBe(false);
660 | 			expect(mockExistsSync).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
661 | 		});
662 | 
663 | 		test.skip('should use findProjectRoot if explicitRoot is not provided', () => {
664 | 			// TODO: Fix mock interaction, findProjectRoot isn't being registered as called
665 | 			mockExistsSync.mockReturnValue(true);
666 | 			// findProjectRoot mock set in beforeEach
667 | 			expect(configManager.isConfigFilePresent()).toBe(true);
668 | 			expect(mockFindProjectRoot).toHaveBeenCalled(); // Should be called now
669 | 		});
670 | 	});
671 | 
672 | 	// --- getAllProviders Tests ---
673 | 	describe('getAllProviders', () => {
674 | 		test('should return list of providers from supported-models.json', () => {
675 | 			// Arrange: Ensure config is loaded with real data
676 | 			configManager.getConfig(null, true); // Force load using the mock that returns real data
677 | 
678 | 			// Act
679 | 			const providers = configManager.getAllProviders();
680 | 			// Assert
681 | 			// Assert against the actual keys in the REAL loaded data
682 | 			const expectedProviders = Object.keys(REAL_SUPPORTED_MODELS_DATA);
683 | 			expect(providers).toEqual(expect.arrayContaining(expectedProviders));
684 | 			expect(providers.length).toBe(expectedProviders.length);
685 | 		});
686 | 	});
687 | 
688 | 	// Add tests for getParametersForRole if needed
689 | 
690 | 	// Note: Tests for setMainModel, setResearchModel were removed as the functions were removed in the implementation.
691 | 	// If similar setter functions exist, add tests for them following the writeConfig pattern.
692 | 
693 | 	// --- isApiKeySet Tests ---
694 | 	describe('isApiKeySet', () => {
695 | 		const mockSession = { env: {} }; // Mock session for MCP context
696 | 
697 | 		// Test cases: [providerName, envVarName, keyValue, expectedResult, testName]
698 | 		const testCases = [
699 | 			// Valid Keys
700 | 			[
701 | 				'anthropic',
702 | 				'ANTHROPIC_API_KEY',
703 | 				'sk-valid-key',
704 | 				true,
705 | 				'valid Anthropic key'
706 | 			],
707 | 			[
708 | 				'openai',
709 | 				'OPENAI_API_KEY',
710 | 				'sk-another-valid-key',
711 | 				true,
712 | 				'valid OpenAI key'
713 | 			],
714 | 			[
715 | 				'perplexity',
716 | 				'PERPLEXITY_API_KEY',
717 | 				'pplx-valid',
718 | 				true,
719 | 				'valid Perplexity key'
720 | 			],
721 | 			[
722 | 				'google',
723 | 				'GOOGLE_API_KEY',
724 | 				'google-valid-key',
725 | 				true,
726 | 				'valid Google key'
727 | 			],
728 | 			[
729 | 				'mistral',
730 | 				'MISTRAL_API_KEY',
731 | 				'mistral-valid-key',
732 | 				true,
733 | 				'valid Mistral key'
734 | 			],
735 | 			[
736 | 				'openrouter',
737 | 				'OPENROUTER_API_KEY',
738 | 				'or-valid-key',
739 | 				true,
740 | 				'valid OpenRouter key'
741 | 			],
742 | 			['xai', 'XAI_API_KEY', 'xai-valid-key', true, 'valid XAI key'],
743 | 			[
744 | 				'azure',
745 | 				'AZURE_OPENAI_API_KEY',
746 | 				'azure-valid-key',
747 | 				true,
748 | 				'valid Azure key'
749 | 			],
750 | 
751 | 			// Ollama (special case - no key needed)
752 | 			[
753 | 				'ollama',
754 | 				'OLLAMA_API_KEY',
755 | 				undefined,
756 | 				true,
757 | 				'Ollama provider (no key needed)'
758 | 			], // OLLAMA_API_KEY might not be in keyMap
759 | 
760 | 			// Invalid / Missing Keys
761 | 			[
762 | 				'anthropic',
763 | 				'ANTHROPIC_API_KEY',
764 | 				undefined,
765 | 				false,
766 | 				'missing Anthropic key'
767 | 			],
768 | 			['anthropic', 'ANTHROPIC_API_KEY', null, false, 'null Anthropic key'],
769 | 			['openai', 'OPENAI_API_KEY', '', false, 'empty OpenAI key'],
770 | 			[
771 | 				'perplexity',
772 | 				'PERPLEXITY_API_KEY',
773 | 				'  ',
774 | 				false,
775 | 				'whitespace Perplexity key'
776 | 			],
777 | 
778 | 			// Placeholder Keys
779 | 			[
780 | 				'google',
781 | 				'GOOGLE_API_KEY',
782 | 				'YOUR_GOOGLE_API_KEY_HERE',
783 | 				false,
784 | 				'placeholder Google key (YOUR_..._HERE)'
785 | 			],
786 | 			[
787 | 				'mistral',
788 | 				'MISTRAL_API_KEY',
789 | 				'MISTRAL_KEY_HERE',
790 | 				false,
791 | 				'placeholder Mistral key (..._KEY_HERE)'
792 | 			],
793 | 			[
794 | 				'openrouter',
795 | 				'OPENROUTER_API_KEY',
796 | 				'ENTER_OPENROUTER_KEY_HERE',
797 | 				false,
798 | 				'placeholder OpenRouter key (general ...KEY_HERE)'
799 | 			],
800 | 
801 | 			// Unknown provider
802 | 			['unknownprovider', 'UNKNOWN_KEY', 'any-key', false, 'unknown provider']
803 | 		];
804 | 
805 | 		testCases.forEach(
806 | 			([providerName, envVarName, keyValue, expectedResult, testName]) => {
807 | 				test(`should return ${expectedResult} for ${testName} (CLI context)`, () => {
808 | 					// CLI context (resolveEnvVariable uses process.env or .env via projectRoot)
809 | 					mockResolveEnvVariable.mockImplementation((key) => {
810 | 						return key === envVarName ? keyValue : undefined;
811 | 					});
812 | 					expect(
813 | 						configManager.isApiKeySet(providerName, null, MOCK_PROJECT_ROOT)
814 | 					).toBe(expectedResult);
815 | 					if (providerName !== 'ollama' && providerName !== 'unknownprovider') {
816 | 						// Ollama and unknown don't try to resolve
817 | 						expect(mockResolveEnvVariable).toHaveBeenCalledWith(
818 | 							envVarName,
819 | 							null,
820 | 							MOCK_PROJECT_ROOT
821 | 						);
822 | 					}
823 | 				});
824 | 
825 | 				test(`should return ${expectedResult} for ${testName} (MCP context)`, () => {
826 | 					// MCP context (resolveEnvVariable uses session.env)
827 | 					const mcpSession = { env: { [envVarName]: keyValue } };
828 | 					mockResolveEnvVariable.mockImplementation((key, sessionArg) => {
829 | 						return sessionArg && sessionArg.env
830 | 							? sessionArg.env[key]
831 | 							: undefined;
832 | 					});
833 | 					expect(
834 | 						configManager.isApiKeySet(providerName, mcpSession, null)
835 | 					).toBe(expectedResult);
836 | 					if (providerName !== 'ollama' && providerName !== 'unknownprovider') {
837 | 						expect(mockResolveEnvVariable).toHaveBeenCalledWith(
838 | 							envVarName,
839 | 							mcpSession,
840 | 							null
841 | 						);
842 | 					}
843 | 				});
844 | 			}
845 | 		);
846 | 
847 | 		test('isApiKeySet should log a warning for an unknown provider', () => {
848 | 			mockLog.mockClear(); // Clear previous log calls
849 | 			configManager.isApiKeySet('nonexistentprovider');
850 | 			expect(mockLog).toHaveBeenCalledWith(
851 | 				'warn',
852 | 				expect.stringContaining('Unknown provider name: nonexistentprovider')
853 | 			);
854 | 		});
855 | 
856 | 		test('isApiKeySet should handle provider names case-insensitively for keyMap lookup', () => {
857 | 			mockResolveEnvVariable.mockReturnValue('a-valid-key');
858 | 			expect(
859 | 				configManager.isApiKeySet('Anthropic', null, MOCK_PROJECT_ROOT)
860 | 			).toBe(true);
861 | 			expect(mockResolveEnvVariable).toHaveBeenCalledWith(
862 | 				'ANTHROPIC_API_KEY',
863 | 				null,
864 | 				MOCK_PROJECT_ROOT
865 | 			);
866 | 
867 | 			mockResolveEnvVariable.mockReturnValue('another-valid-key');
868 | 			expect(configManager.isApiKeySet('OPENAI', null, MOCK_PROJECT_ROOT)).toBe(
869 | 				true
870 | 			);
871 | 			expect(mockResolveEnvVariable).toHaveBeenCalledWith(
872 | 				'OPENAI_API_KEY',
873 | 				null,
874 | 				MOCK_PROJECT_ROOT
875 | 			);
876 | 		});
877 | 	});
878 | });
879 | 
```
Page 49/69FirstPrevNextLast