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

--------------------------------------------------------------------------------
/packages/tm-core/tests/integration/auth-token-refresh.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Integration tests for JWT token auto-refresh functionality
  3 |  *
  4 |  * These tests verify that expired tokens are automatically refreshed
  5 |  * when making API calls through AuthManager.
  6 |  */
  7 | 
  8 | import fs from 'fs';
  9 | import os from 'os';
 10 | import path from 'path';
 11 | import type { Session } from '@supabase/supabase-js';
 12 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 13 | import { AuthManager } from '../../src/modules/auth/managers/auth-manager.js';
 14 | import { CredentialStore } from '../../src/modules/auth/services/credential-store.js';
 15 | import type { AuthCredentials } from '../../src/modules/auth/types.js';
 16 | 
 17 | describe('AuthManager - Token Auto-Refresh Integration', () => {
 18 | 	let authManager: AuthManager;
 19 | 	let credentialStore: CredentialStore;
 20 | 	let tmpDir: string;
 21 | 	let authFile: string;
 22 | 
 23 | 	// Mock Supabase session that will be returned on refresh
 24 | 	const mockRefreshedSession: Session = {
 25 | 		access_token: 'new-access-token-xyz',
 26 | 		refresh_token: 'new-refresh-token-xyz',
 27 | 		token_type: 'bearer',
 28 | 		expires_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
 29 | 		expires_in: 3600,
 30 | 		user: {
 31 | 			id: 'test-user-id',
 32 | 			email: '[email protected]',
 33 | 			aud: 'authenticated',
 34 | 			role: 'authenticated',
 35 | 			app_metadata: {},
 36 | 			user_metadata: {},
 37 | 			created_at: new Date().toISOString()
 38 | 		}
 39 | 	};
 40 | 
 41 | 	beforeEach(() => {
 42 | 		// Reset singletons
 43 | 		AuthManager.resetInstance();
 44 | 		CredentialStore.resetInstance();
 45 | 
 46 | 		// Create temporary directory for test isolation
 47 | 		tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-auth-integration-'));
 48 | 		authFile = path.join(tmpDir, 'auth.json');
 49 | 
 50 | 		// Initialize AuthManager with test config (this will create CredentialStore internally)
 51 | 		authManager = AuthManager.getInstance({
 52 | 			configDir: tmpDir,
 53 | 			configFile: authFile
 54 | 		});
 55 | 
 56 | 		// Get the CredentialStore instance that AuthManager created
 57 | 		credentialStore = CredentialStore.getInstance();
 58 | 		credentialStore.clearCredentials();
 59 | 	});
 60 | 
 61 | 	afterEach(() => {
 62 | 		// Clean up
 63 | 		try {
 64 | 			credentialStore.clearCredentials();
 65 | 		} catch {
 66 | 			// Ignore cleanup errors
 67 | 		}
 68 | 		AuthManager.resetInstance();
 69 | 		CredentialStore.resetInstance();
 70 | 		vi.restoreAllMocks();
 71 | 
 72 | 		// Remove temporary directory
 73 | 		if (tmpDir && fs.existsSync(tmpDir)) {
 74 | 			fs.rmSync(tmpDir, { recursive: true, force: true });
 75 | 		}
 76 | 	});
 77 | 
 78 | 	describe('Expired Token Detection', () => {
 79 | 		it('should return expired token for Supabase to refresh', () => {
 80 | 			// Set up expired credentials
 81 | 			const expiredCredentials: AuthCredentials = {
 82 | 				token: 'expired-token',
 83 | 				refreshToken: 'valid-refresh-token',
 84 | 				userId: 'test-user-id',
 85 | 				email: '[email protected]',
 86 | 				expiresAt: new Date(Date.now() - 60000).toISOString(), // 1 minute ago
 87 | 				savedAt: new Date().toISOString()
 88 | 			};
 89 | 
 90 | 			credentialStore.saveCredentials(expiredCredentials);
 91 | 
 92 | 			authManager = AuthManager.getInstance();
 93 | 
 94 | 			// Get credentials returns them even if expired
 95 | 			const credentials = authManager.getCredentials();
 96 | 
 97 | 			expect(credentials).not.toBeNull();
 98 | 			expect(credentials?.token).toBe('expired-token');
 99 | 			expect(credentials?.refreshToken).toBe('valid-refresh-token');
100 | 		});
101 | 
102 | 		it('should return valid token', () => {
103 | 			// Set up valid credentials
104 | 			const validCredentials: AuthCredentials = {
105 | 				token: 'valid-token',
106 | 				refreshToken: 'valid-refresh-token',
107 | 				userId: 'test-user-id',
108 | 				email: '[email protected]',
109 | 				expiresAt: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
110 | 				savedAt: new Date().toISOString()
111 | 			};
112 | 
113 | 			credentialStore.saveCredentials(validCredentials);
114 | 
115 | 			authManager = AuthManager.getInstance();
116 | 
117 | 			const credentials = authManager.getCredentials();
118 | 
119 | 			expect(credentials?.token).toBe('valid-token');
120 | 		});
121 | 	});
122 | 
123 | 	describe('Token Refresh Flow', () => {
124 | 		it('should manually refresh expired token and save new credentials', async () => {
125 | 			const expiredCredentials: AuthCredentials = {
126 | 				token: 'old-token',
127 | 				refreshToken: 'old-refresh-token',
128 | 				userId: 'test-user-id',
129 | 				email: '[email protected]',
130 | 				expiresAt: new Date(Date.now() - 60000).toISOString(),
131 | 				savedAt: new Date(Date.now() - 3600000).toISOString(),
132 | 				selectedContext: {
133 | 					orgId: 'test-org',
134 | 					briefId: 'test-brief',
135 | 					updatedAt: new Date().toISOString()
136 | 				}
137 | 			};
138 | 
139 | 			credentialStore.saveCredentials(expiredCredentials);
140 | 
141 | 			authManager = AuthManager.getInstance();
142 | 
143 | 			vi.spyOn(
144 | 				authManager['supabaseClient'],
145 | 				'refreshSession'
146 | 			).mockResolvedValue(mockRefreshedSession);
147 | 
148 | 			// Explicitly call refreshToken() method
149 | 			const refreshedCredentials = await authManager.refreshToken();
150 | 
151 | 			expect(refreshedCredentials).not.toBeNull();
152 | 			expect(refreshedCredentials.token).toBe('new-access-token-xyz');
153 | 			expect(refreshedCredentials.refreshToken).toBe('new-refresh-token-xyz');
154 | 
155 | 			// Verify context was preserved
156 | 			expect(refreshedCredentials.selectedContext?.orgId).toBe('test-org');
157 | 			expect(refreshedCredentials.selectedContext?.briefId).toBe('test-brief');
158 | 
159 | 			// Verify new expiration is in the future
160 | 			const newExpiry = new Date(refreshedCredentials.expiresAt!).getTime();
161 | 			const now = Date.now();
162 | 			expect(newExpiry).toBeGreaterThan(now);
163 | 		});
164 | 
165 | 		it('should throw error if manual refresh fails', async () => {
166 | 			const expiredCredentials: AuthCredentials = {
167 | 				token: 'expired-token',
168 | 				refreshToken: 'invalid-refresh-token',
169 | 				userId: 'test-user-id',
170 | 				email: '[email protected]',
171 | 				expiresAt: new Date(Date.now() - 60000).toISOString(),
172 | 				savedAt: new Date().toISOString()
173 | 			};
174 | 
175 | 			credentialStore.saveCredentials(expiredCredentials);
176 | 
177 | 			authManager = AuthManager.getInstance();
178 | 
179 | 			// Mock refresh to fail
180 | 			vi.spyOn(
181 | 				authManager['supabaseClient'],
182 | 				'refreshSession'
183 | 			).mockRejectedValue(new Error('Refresh token expired'));
184 | 
185 | 			// Explicit refreshToken() call should throw
186 | 			await expect(authManager.refreshToken()).rejects.toThrow();
187 | 		});
188 | 
189 | 		it('should return expired credentials even without refresh token', () => {
190 | 			const expiredCredentials: AuthCredentials = {
191 | 				token: 'expired-token',
192 | 				// No refresh token
193 | 				userId: 'test-user-id',
194 | 				email: '[email protected]',
195 | 				expiresAt: new Date(Date.now() - 60000).toISOString(),
196 | 				savedAt: new Date().toISOString()
197 | 			};
198 | 
199 | 			credentialStore.saveCredentials(expiredCredentials);
200 | 
201 | 			authManager = AuthManager.getInstance();
202 | 
203 | 			const credentials = authManager.getCredentials();
204 | 
205 | 			// Credentials are returned even without refresh token
206 | 			expect(credentials).not.toBeNull();
207 | 			expect(credentials?.token).toBe('expired-token');
208 | 			expect(credentials?.refreshToken).toBeUndefined();
209 | 		});
210 | 
211 | 		it('should return null if credentials missing expiresAt', () => {
212 | 			const credentialsWithoutExpiry: AuthCredentials = {
213 | 				token: 'test-token',
214 | 				refreshToken: 'refresh-token',
215 | 				userId: 'test-user-id',
216 | 				email: '[email protected]',
217 | 				// Missing expiresAt - invalid token
218 | 				savedAt: new Date().toISOString()
219 | 			} as any;
220 | 
221 | 			credentialStore.saveCredentials(credentialsWithoutExpiry);
222 | 
223 | 			authManager = AuthManager.getInstance();
224 | 
225 | 			const credentials = authManager.getCredentials();
226 | 
227 | 			// Tokens without valid expiration are considered invalid
228 | 			expect(credentials).toBeNull();
229 | 		});
230 | 	});
231 | 
232 | 	describe('Clock Skew Tolerance', () => {
233 | 		it('should return credentials within 30-second expiry window', () => {
234 | 			// Token expires in 15 seconds (within 30-second buffer)
235 | 			// Supabase will handle refresh automatically
236 | 			const almostExpiredCredentials: AuthCredentials = {
237 | 				token: 'almost-expired-token',
238 | 				refreshToken: 'valid-refresh-token',
239 | 				userId: 'test-user-id',
240 | 				email: '[email protected]',
241 | 				expiresAt: new Date(Date.now() + 15000).toISOString(), // 15 seconds from now
242 | 				savedAt: new Date().toISOString()
243 | 			};
244 | 
245 | 			credentialStore.saveCredentials(almostExpiredCredentials);
246 | 
247 | 			authManager = AuthManager.getInstance();
248 | 
249 | 			const credentials = authManager.getCredentials();
250 | 
251 | 			// Credentials are returned (Supabase handles auto-refresh in background)
252 | 			expect(credentials).not.toBeNull();
253 | 			expect(credentials?.token).toBe('almost-expired-token');
254 | 			expect(credentials?.refreshToken).toBe('valid-refresh-token');
255 | 		});
256 | 
257 | 		it('should return valid token well before expiry', () => {
258 | 			// Token expires in 5 minutes
259 | 			const validCredentials: AuthCredentials = {
260 | 				token: 'valid-token',
261 | 				refreshToken: 'valid-refresh-token',
262 | 				userId: 'test-user-id',
263 | 				email: '[email protected]',
264 | 				expiresAt: new Date(Date.now() + 300000).toISOString(), // 5 minutes
265 | 				savedAt: new Date().toISOString()
266 | 			};
267 | 
268 | 			credentialStore.saveCredentials(validCredentials);
269 | 
270 | 			authManager = AuthManager.getInstance();
271 | 
272 | 			const credentials = authManager.getCredentials();
273 | 
274 | 			// Valid credentials are returned as-is
275 | 			expect(credentials).not.toBeNull();
276 | 			expect(credentials?.token).toBe('valid-token');
277 | 			expect(credentials?.refreshToken).toBe('valid-refresh-token');
278 | 		});
279 | 	});
280 | 
281 | 	describe('Synchronous vs Async Methods', () => {
282 | 		it('getCredentials should return expired credentials', () => {
283 | 			const expiredCredentials: AuthCredentials = {
284 | 				token: 'expired-token',
285 | 				refreshToken: 'valid-refresh-token',
286 | 				userId: 'test-user-id',
287 | 				email: '[email protected]',
288 | 				expiresAt: new Date(Date.now() - 60000).toISOString(),
289 | 				savedAt: new Date().toISOString()
290 | 			};
291 | 
292 | 			credentialStore.saveCredentials(expiredCredentials);
293 | 
294 | 			authManager = AuthManager.getInstance();
295 | 
296 | 			// Returns credentials even if expired - Supabase will handle refresh
297 | 			const credentials = authManager.getCredentials();
298 | 
299 | 			expect(credentials).not.toBeNull();
300 | 			expect(credentials?.token).toBe('expired-token');
301 | 			expect(credentials?.refreshToken).toBe('valid-refresh-token');
302 | 		});
303 | 	});
304 | 
305 | 	describe('Multiple Concurrent Calls', () => {
306 | 		it('should handle concurrent getCredentials calls gracefully', () => {
307 | 			const expiredCredentials: AuthCredentials = {
308 | 				token: 'expired-token',
309 | 				refreshToken: 'valid-refresh-token',
310 | 				userId: 'test-user-id',
311 | 				email: '[email protected]',
312 | 				expiresAt: new Date(Date.now() - 60000).toISOString(),
313 | 				savedAt: new Date().toISOString()
314 | 			};
315 | 
316 | 			credentialStore.saveCredentials(expiredCredentials);
317 | 
318 | 			authManager = AuthManager.getInstance();
319 | 
320 | 			// Make multiple concurrent calls (synchronous now)
321 | 			const creds1 = authManager.getCredentials();
322 | 			const creds2 = authManager.getCredentials();
323 | 			const creds3 = authManager.getCredentials();
324 | 
325 | 			// All should get the same credentials (even if expired)
326 | 			expect(creds1?.token).toBe('expired-token');
327 | 			expect(creds2?.token).toBe('expired-token');
328 | 			expect(creds3?.token).toBe('expired-token');
329 | 
330 | 			// All include refresh token for Supabase to use
331 | 			expect(creds1?.refreshToken).toBe('valid-refresh-token');
332 | 			expect(creds2?.refreshToken).toBe('valid-refresh-token');
333 | 			expect(creds3?.refreshToken).toBe('valid-refresh-token');
334 | 		});
335 | 	});
336 | });
337 | 
```

--------------------------------------------------------------------------------
/apps/extension/src/components/TaskDetails/TaskMetadataSidebar.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import type React from 'react';
  2 | import { useState, useEffect } from 'react';
  3 | import { Button } from '@/components/ui/button';
  4 | import { Loader2, Play } from 'lucide-react';
  5 | import { PriorityBadge } from './PriorityBadge';
  6 | import type { TaskMasterTask } from '../../webview/types';
  7 | import { useVSCodeContext } from '../../webview/contexts/VSCodeContext';
  8 | 
  9 | interface TaskMetadataSidebarProps {
 10 | 	currentTask: TaskMasterTask;
 11 | 	tasks: TaskMasterTask[];
 12 | 	complexity: any;
 13 | 	isSubtask: boolean;
 14 | 	onStatusChange: (status: TaskMasterTask['status']) => void;
 15 | 	onDependencyClick: (depId: string) => void;
 16 | 	isRegenerating?: boolean;
 17 | 	isAppending?: boolean;
 18 | }
 19 | 
 20 | export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
 21 | 	currentTask,
 22 | 	tasks,
 23 | 	complexity,
 24 | 	isSubtask,
 25 | 	onStatusChange,
 26 | 	onDependencyClick,
 27 | 	isRegenerating = false,
 28 | 	isAppending = false
 29 | }) => {
 30 | 	const { sendMessage } = useVSCodeContext();
 31 | 	const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
 32 | 	const [mcpComplexityScore, setMcpComplexityScore] = useState<
 33 | 		number | undefined
 34 | 	>(undefined);
 35 | 	const [isStartingTask, setIsStartingTask] = useState(false);
 36 | 
 37 | 	// Get complexity score from task
 38 | 	const currentComplexityScore = complexity?.score;
 39 | 
 40 | 	// Display logic - use MCP score if available, otherwise use current score
 41 | 	const displayComplexityScore =
 42 | 		mcpComplexityScore !== undefined
 43 | 			? mcpComplexityScore
 44 | 			: currentComplexityScore;
 45 | 
 46 | 	// Fetch complexity from MCP when needed
 47 | 	const fetchComplexityFromMCP = async (force = false) => {
 48 | 		if (!currentTask || (!force && currentComplexityScore !== undefined)) {
 49 | 			return;
 50 | 		}
 51 | 		setIsLoadingComplexity(true);
 52 | 		try {
 53 | 			const complexityResult = await sendMessage({
 54 | 				type: 'mcpRequest',
 55 | 				tool: 'complexity_report',
 56 | 				params: {}
 57 | 			});
 58 | 			if (complexityResult?.data?.report?.complexityAnalysis) {
 59 | 				const taskComplexity =
 60 | 					complexityResult.data.report.complexityAnalysis.tasks?.find(
 61 | 						(t: any) => t.id === currentTask.id
 62 | 					);
 63 | 				if (taskComplexity) {
 64 | 					setMcpComplexityScore(taskComplexity.complexityScore);
 65 | 				}
 66 | 			}
 67 | 		} catch (error) {
 68 | 			console.error('Failed to fetch complexity from MCP:', error);
 69 | 		} finally {
 70 | 			setIsLoadingComplexity(false);
 71 | 		}
 72 | 	};
 73 | 
 74 | 	// Handle running complexity analysis for a task
 75 | 	const handleRunComplexityAnalysis = async () => {
 76 | 		if (!currentTask) {
 77 | 			return;
 78 | 		}
 79 | 		setIsLoadingComplexity(true);
 80 | 		try {
 81 | 			// Run complexity analysis on this specific task
 82 | 			await sendMessage({
 83 | 				type: 'mcpRequest',
 84 | 				tool: 'analyze_project_complexity',
 85 | 				params: {
 86 | 					ids: currentTask.id.toString(),
 87 | 					research: false
 88 | 				}
 89 | 			});
 90 | 			// After analysis, fetch the updated complexity report
 91 | 			setTimeout(() => {
 92 | 				fetchComplexityFromMCP(true);
 93 | 			}, 1000);
 94 | 		} catch (error) {
 95 | 			console.error('Failed to run complexity analysis:', error);
 96 | 		} finally {
 97 | 			setIsLoadingComplexity(false);
 98 | 		}
 99 | 	};
100 | 
101 | 	// Handle starting a task
102 | 	const handleStartTask = async () => {
103 | 		if (!currentTask || isStartingTask) {
104 | 			return;
105 | 		}
106 | 
107 | 		setIsStartingTask(true);
108 | 
109 | 		try {
110 | 			// Send message to extension to open terminal
111 | 			const result = await sendMessage({
112 | 				type: 'openTerminal',
113 | 				data: {
114 | 					taskId: currentTask.id,
115 | 					taskTitle: currentTask.title
116 | 				}
117 | 			});
118 | 
119 | 			// Handle the response
120 | 			if (result && !result.success) {
121 | 				console.error('Terminal execution failed:', result.error);
122 | 				// The extension will show VS Code error notification and webview toast
123 | 			} else if (result && result.success) {
124 | 				console.log('Terminal started successfully:', result.terminalName);
125 | 			}
126 | 		} catch (error) {
127 | 			console.error('Failed to start task:', error);
128 | 			// This handles network/communication errors
129 | 		} finally {
130 | 			// Reset loading state
131 | 			setIsStartingTask(false);
132 | 		}
133 | 	};
134 | 
135 | 	// Effect to handle complexity on task change
136 | 	useEffect(() => {
137 | 		if (currentTask?.id) {
138 | 			setMcpComplexityScore(undefined);
139 | 			if (currentComplexityScore === undefined) {
140 | 				fetchComplexityFromMCP();
141 | 			}
142 | 		}
143 | 	}, [currentTask?.id, currentComplexityScore]);
144 | 
145 | 	return (
146 | 		<div className="md:col-span-1 border-l border-textSeparator-foreground">
147 | 			<div className="p-6">
148 | 				<div className="space-y-6">
149 | 					<div>
150 | 						<h3 className="text-sm font-medium text-vscode-foreground/70 mb-3">
151 | 							Properties
152 | 						</h3>
153 | 					</div>
154 | 
155 | 					<div className="space-y-4">
156 | 						{/* Status */}
157 | 						<div className="flex items-center justify-between">
158 | 							<span className="text-sm text-vscode-foreground/70">Status</span>
159 | 							<select
160 | 								value={currentTask.status}
161 | 								onChange={(e) =>
162 | 									onStatusChange(e.target.value as TaskMasterTask['status'])
163 | 								}
164 | 								className="border rounded-md px-3 py-1 text-sm font-medium focus:ring-1 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
165 | 								style={{
166 | 									backgroundColor:
167 | 										currentTask.status === 'pending'
168 | 											? 'rgba(156, 163, 175, 0.2)'
169 | 											: currentTask.status === 'in-progress'
170 | 												? 'rgba(245, 158, 11, 0.2)'
171 | 												: currentTask.status === 'review'
172 | 													? 'rgba(59, 130, 246, 0.2)'
173 | 													: currentTask.status === 'done'
174 | 														? 'rgba(34, 197, 94, 0.2)'
175 | 														: currentTask.status === 'deferred'
176 | 															? 'rgba(239, 68, 68, 0.2)'
177 | 															: 'var(--vscode-input-background)',
178 | 									color:
179 | 										currentTask.status === 'pending'
180 | 											? 'var(--vscode-foreground)'
181 | 											: currentTask.status === 'in-progress'
182 | 												? '#d97706'
183 | 												: currentTask.status === 'review'
184 | 													? '#2563eb'
185 | 													: currentTask.status === 'done'
186 | 														? '#16a34a'
187 | 														: currentTask.status === 'deferred'
188 | 															? '#dc2626'
189 | 															: 'var(--vscode-foreground)',
190 | 									borderColor:
191 | 										currentTask.status === 'pending'
192 | 											? 'rgba(156, 163, 175, 0.4)'
193 | 											: currentTask.status === 'in-progress'
194 | 												? 'rgba(245, 158, 11, 0.4)'
195 | 												: currentTask.status === 'review'
196 | 													? 'rgba(59, 130, 246, 0.4)'
197 | 													: currentTask.status === 'done'
198 | 														? 'rgba(34, 197, 94, 0.4)'
199 | 														: currentTask.status === 'deferred'
200 | 															? 'rgba(239, 68, 68, 0.4)'
201 | 															: 'var(--vscode-input-border)'
202 | 								}}
203 | 							>
204 | 								<option value="pending">To do</option>
205 | 								<option value="in-progress">In Progress</option>
206 | 								<option value="review">Review</option>
207 | 								<option value="done">Done</option>
208 | 								<option value="deferred">Deferred</option>
209 | 							</select>
210 | 						</div>
211 | 
212 | 						{/* Priority */}
213 | 						<div className="flex items-center justify-between">
214 | 							<span className="text-sm text-muted-foreground">Priority</span>
215 | 							<PriorityBadge priority={currentTask.priority} />
216 | 						</div>
217 | 
218 | 						{/* Complexity Score */}
219 | 						<div className="space-y-2">
220 | 							<label className="text-sm font-medium text-[var(--vscode-foreground)]">
221 | 								Complexity Score
222 | 							</label>
223 | 							{isLoadingComplexity ? (
224 | 								<div className="flex items-center gap-2">
225 | 									<Loader2 className="w-4 h-4 animate-spin text-[var(--vscode-descriptionForeground)]" />
226 | 									<span className="text-sm text-[var(--vscode-descriptionForeground)]">
227 | 										Loading...
228 | 									</span>
229 | 								</div>
230 | 							) : displayComplexityScore !== undefined ? (
231 | 								<div className="flex items-center gap-2">
232 | 									<span className="text-sm font-medium text-[var(--vscode-foreground)]">
233 | 										{displayComplexityScore}/10
234 | 									</span>
235 | 									<div
236 | 										className={`flex-1 rounded-full h-2 ${
237 | 											displayComplexityScore >= 7
238 | 												? 'bg-red-500/20'
239 | 												: displayComplexityScore >= 4
240 | 													? 'bg-yellow-500/20'
241 | 													: 'bg-green-500/20'
242 | 										}`}
243 | 									>
244 | 										<div
245 | 											className={`h-2 rounded-full transition-all duration-300 ${
246 | 												displayComplexityScore >= 7
247 | 													? 'bg-red-500'
248 | 													: displayComplexityScore >= 4
249 | 														? 'bg-yellow-500'
250 | 														: 'bg-green-500'
251 | 											}`}
252 | 											style={{
253 | 												width: `${(displayComplexityScore || 0) * 10}%`
254 | 											}}
255 | 										/>
256 | 									</div>
257 | 								</div>
258 | 							) : currentTask?.status === 'done' ||
259 | 								currentTask?.status === 'deferred' ||
260 | 								currentTask?.status === 'review' ? (
261 | 								<div className="text-sm text-[var(--vscode-descriptionForeground)]">
262 | 									N/A
263 | 								</div>
264 | 							) : (
265 | 								<>
266 | 									<div className="text-sm text-[var(--vscode-descriptionForeground)]">
267 | 										No complexity score available
268 | 									</div>
269 | 									<div className="mt-3">
270 | 										<Button
271 | 											onClick={() => handleRunComplexityAnalysis()}
272 | 											variant="outline"
273 | 											size="sm"
274 | 											className="text-xs"
275 | 											disabled={isRegenerating || isAppending}
276 | 										>
277 | 											Run Complexity Analysis
278 | 										</Button>
279 | 									</div>
280 | 								</>
281 | 							)}
282 | 						</div>
283 | 					</div>
284 | 					<div className="border-b border-textSeparator-foreground" />
285 | 
286 | 					{/* Dependencies */}
287 | 					{currentTask.dependencies && currentTask.dependencies.length > 0 && (
288 | 						<div>
289 | 							<h4 className="text-sm font-medium text-vscode-foreground/70 mb-3">
290 | 								Dependencies
291 | 							</h4>
292 | 							<div className="space-y-2">
293 | 								{currentTask.dependencies.map((depId) => {
294 | 									// Convert both to string for comparison since depId might be string or number
295 | 									const depTask = tasks.find(
296 | 										(t) => String(t.id) === String(depId)
297 | 									);
298 | 									const fullTitle = `Task ${depId}: ${depTask?.title || 'Unknown Task'}`;
299 | 									const truncatedTitle =
300 | 										fullTitle.length > 40
301 | 											? fullTitle.substring(0, 37) + '...'
302 | 											: fullTitle;
303 | 									return (
304 | 										<div
305 | 											key={depId}
306 | 											className="text-sm text-link cursor-pointer hover:text-link-hover"
307 | 											onClick={() => onDependencyClick(depId)}
308 | 											title={fullTitle}
309 | 										>
310 | 											{truncatedTitle}
311 | 										</div>
312 | 									);
313 | 								})}
314 | 							</div>
315 | 						</div>
316 | 					)}
317 | 
318 | 					{/* Divider after Dependencies */}
319 | 					{currentTask.dependencies && currentTask.dependencies.length > 0 && (
320 | 						<div className="border-b border-textSeparator-foreground" />
321 | 					)}
322 | 
323 | 					{/* Start Task Button */}
324 | 					<div className="mt-4">
325 | 						<Button
326 | 							onClick={handleStartTask}
327 | 							variant="default"
328 | 							size="sm"
329 | 							className="w-full text-xs"
330 | 							disabled={
331 | 								isRegenerating ||
332 | 								isAppending ||
333 | 								isStartingTask ||
334 | 								currentTask?.status === 'done' ||
335 | 								currentTask?.status === 'in-progress'
336 | 							}
337 | 						>
338 | 							{isStartingTask ? (
339 | 								<Loader2 className="w-4 h-4 mr-2 animate-spin" />
340 | 							) : (
341 | 								<Play className="w-4 h-4 mr-2" />
342 | 							)}
343 | 							{isStartingTask ? 'Starting...' : 'Start Task'}
344 | 						</Button>
345 | 					</div>
346 | 				</div>
347 | 			</div>
348 | 		</div>
349 | 	);
350 | };
351 | 
```

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

```javascript
  1 | /**
  2 |  * fuzzyTaskSearch.js
  3 |  * Reusable fuzzy search utility for finding relevant tasks based on semantic similarity
  4 |  */
  5 | 
  6 | import Fuse from 'fuse.js';
  7 | 
  8 | /**
  9 |  * Configuration for different search contexts
 10 |  */
 11 | const SEARCH_CONFIGS = {
 12 | 	research: {
 13 | 		threshold: 0.5, // More lenient for research (broader context)
 14 | 		limit: 20,
 15 | 		keys: [
 16 | 			{ name: 'title', weight: 2.0 },
 17 | 			{ name: 'description', weight: 1.0 },
 18 | 			{ name: 'details', weight: 0.5 },
 19 | 			{ name: 'dependencyTitles', weight: 0.5 }
 20 | 		]
 21 | 	},
 22 | 	addTask: {
 23 | 		threshold: 0.4, // Stricter for add-task (more precise context)
 24 | 		limit: 15,
 25 | 		keys: [
 26 | 			{ name: 'title', weight: 2.0 },
 27 | 			{ name: 'description', weight: 1.5 },
 28 | 			{ name: 'details', weight: 0.8 },
 29 | 			{ name: 'dependencyTitles', weight: 0.5 }
 30 | 		]
 31 | 	},
 32 | 	default: {
 33 | 		threshold: 0.4,
 34 | 		limit: 15,
 35 | 		keys: [
 36 | 			{ name: 'title', weight: 2.0 },
 37 | 			{ name: 'description', weight: 1.5 },
 38 | 			{ name: 'details', weight: 1.0 },
 39 | 			{ name: 'dependencyTitles', weight: 0.5 }
 40 | 		]
 41 | 	}
 42 | };
 43 | 
 44 | /**
 45 |  * Purpose categories for pattern-based task matching
 46 |  */
 47 | const PURPOSE_CATEGORIES = [
 48 | 	{ pattern: /(command|cli|flag)/i, label: 'CLI commands' },
 49 | 	{ pattern: /(task|subtask|add)/i, label: 'Task management' },
 50 | 	{ pattern: /(dependency|depend)/i, label: 'Dependency handling' },
 51 | 	{ pattern: /(AI|model|prompt|research)/i, label: 'AI integration' },
 52 | 	{ pattern: /(UI|display|show|interface)/i, label: 'User interface' },
 53 | 	{ pattern: /(schedule|time|cron)/i, label: 'Scheduling' },
 54 | 	{ pattern: /(config|setting|option)/i, label: 'Configuration' },
 55 | 	{ pattern: /(test|testing|spec)/i, label: 'Testing' },
 56 | 	{ pattern: /(auth|login|user)/i, label: 'Authentication' },
 57 | 	{ pattern: /(database|db|data)/i, label: 'Data management' },
 58 | 	{ pattern: /(api|endpoint|route)/i, label: 'API development' },
 59 | 	{ pattern: /(deploy|build|release)/i, label: 'Deployment' },
 60 | 	{ pattern: /(security|auth|login|user)/i, label: 'Security' },
 61 | 	{ pattern: /.*/, label: 'Other' }
 62 | ];
 63 | 
 64 | /**
 65 |  * Relevance score thresholds
 66 |  */
 67 | const RELEVANCE_THRESHOLDS = {
 68 | 	high: 0.25,
 69 | 	medium: 0.4,
 70 | 	low: 0.6
 71 | };
 72 | 
 73 | /**
 74 |  * Fuzzy search utility class for finding relevant tasks
 75 |  */
 76 | export class FuzzyTaskSearch {
 77 | 	constructor(tasks, searchType = 'default') {
 78 | 		this.tasks = tasks;
 79 | 		this.config = SEARCH_CONFIGS[searchType] || SEARCH_CONFIGS.default;
 80 | 		this.searchableTasks = this._prepareSearchableTasks(tasks);
 81 | 		this.fuse = new Fuse(this.searchableTasks, {
 82 | 			includeScore: true,
 83 | 			threshold: this.config.threshold,
 84 | 			keys: this.config.keys,
 85 | 			shouldSort: true,
 86 | 			useExtendedSearch: true,
 87 | 			limit: this.config.limit
 88 | 		});
 89 | 	}
 90 | 
 91 | 	/**
 92 | 	 * Prepare tasks for searching by expanding dependency titles
 93 | 	 * @param {Array} tasks - Array of task objects
 94 | 	 * @returns {Array} Tasks with expanded dependency information
 95 | 	 */
 96 | 	_prepareSearchableTasks(tasks) {
 97 | 		return tasks.map((task) => {
 98 | 			// Get titles of this task's dependencies if they exist
 99 | 			const dependencyTitles =
100 | 				task.dependencies?.length > 0
101 | 					? task.dependencies
102 | 							.map((depId) => {
103 | 								const depTask = tasks.find((t) => t.id === depId);
104 | 								return depTask ? depTask.title : '';
105 | 							})
106 | 							.filter((title) => title)
107 | 							.join(' ')
108 | 					: '';
109 | 
110 | 			return {
111 | 				...task,
112 | 				dependencyTitles
113 | 			};
114 | 		});
115 | 	}
116 | 
117 | 	/**
118 | 	 * Extract significant words from a prompt
119 | 	 * @param {string} prompt - The search prompt
120 | 	 * @returns {Array<string>} Array of significant words
121 | 	 */
122 | 	_extractPromptWords(prompt) {
123 | 		return prompt
124 | 			.toLowerCase()
125 | 			.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
126 | 			.split(/\s+/)
127 | 			.filter((word) => word.length > 3); // Words at least 4 chars
128 | 	}
129 | 
130 | 	/**
131 | 	 * Find tasks related to a prompt using fuzzy search
132 | 	 * @param {string} prompt - The search prompt
133 | 	 * @param {Object} options - Search options
134 | 	 * @param {number} [options.maxResults=8] - Maximum number of results to return
135 | 	 * @param {boolean} [options.includeRecent=true] - Include recent tasks in results
136 | 	 * @param {boolean} [options.includeCategoryMatches=true] - Include category-based matches
137 | 	 * @returns {Object} Search results with relevance breakdown
138 | 	 */
139 | 	findRelevantTasks(prompt, options = {}) {
140 | 		const {
141 | 			maxResults = 8,
142 | 			includeRecent = true,
143 | 			includeCategoryMatches = true
144 | 		} = options;
145 | 
146 | 		// Extract significant words from prompt
147 | 		const promptWords = this._extractPromptWords(prompt);
148 | 
149 | 		// Perform fuzzy search with full prompt
150 | 		const fuzzyResults = this.fuse.search(prompt);
151 | 
152 | 		// Also search for each significant word to catch different aspects
153 | 		let wordResults = [];
154 | 		for (const word of promptWords) {
155 | 			if (word.length > 5) {
156 | 				// Only use significant words
157 | 				const results = this.fuse.search(word);
158 | 				if (results.length > 0) {
159 | 					wordResults.push(...results);
160 | 				}
161 | 			}
162 | 		}
163 | 
164 | 		// Merge and deduplicate results
165 | 		const mergedResults = [...fuzzyResults];
166 | 
167 | 		// Add word results that aren't already in fuzzyResults
168 | 		for (const wordResult of wordResults) {
169 | 			if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
170 | 				mergedResults.push(wordResult);
171 | 			}
172 | 		}
173 | 
174 | 		// Group search results by relevance
175 | 		const highRelevance = mergedResults
176 | 			.filter((result) => result.score < RELEVANCE_THRESHOLDS.high)
177 | 			.map((result) => ({ ...result.item, score: result.score }));
178 | 
179 | 		const mediumRelevance = mergedResults
180 | 			.filter(
181 | 				(result) =>
182 | 					result.score >= RELEVANCE_THRESHOLDS.high &&
183 | 					result.score < RELEVANCE_THRESHOLDS.medium
184 | 			)
185 | 			.map((result) => ({ ...result.item, score: result.score }));
186 | 
187 | 		const lowRelevance = mergedResults
188 | 			.filter(
189 | 				(result) =>
190 | 					result.score >= RELEVANCE_THRESHOLDS.medium &&
191 | 					result.score < RELEVANCE_THRESHOLDS.low
192 | 			)
193 | 			.map((result) => ({ ...result.item, score: result.score }));
194 | 
195 | 		// Get recent tasks (newest first) if requested
196 | 		const recentTasks = includeRecent
197 | 			? [...this.tasks].sort((a, b) => b.id - a.id).slice(0, 5)
198 | 			: [];
199 | 
200 | 		// Find category-based matches if requested
201 | 		let categoryTasks = [];
202 | 		let promptCategory = null;
203 | 		if (includeCategoryMatches) {
204 | 			promptCategory = PURPOSE_CATEGORIES.find((cat) =>
205 | 				cat.pattern.test(prompt)
206 | 			);
207 | 			categoryTasks = promptCategory
208 | 				? this.tasks
209 | 						.filter(
210 | 							(t) =>
211 | 								promptCategory.pattern.test(t.title) ||
212 | 								promptCategory.pattern.test(t.description) ||
213 | 								(t.details && promptCategory.pattern.test(t.details))
214 | 						)
215 | 						.slice(0, 3)
216 | 				: [];
217 | 		}
218 | 
219 | 		// Combine all relevant tasks, prioritizing by relevance
220 | 		const allRelevantTasks = [...highRelevance];
221 | 
222 | 		// Add medium relevance if not already included
223 | 		for (const task of mediumRelevance) {
224 | 			if (!allRelevantTasks.some((t) => t.id === task.id)) {
225 | 				allRelevantTasks.push(task);
226 | 			}
227 | 		}
228 | 
229 | 		// Add low relevance if not already included
230 | 		for (const task of lowRelevance) {
231 | 			if (!allRelevantTasks.some((t) => t.id === task.id)) {
232 | 				allRelevantTasks.push(task);
233 | 			}
234 | 		}
235 | 
236 | 		// Add category tasks if not already included
237 | 		for (const task of categoryTasks) {
238 | 			if (!allRelevantTasks.some((t) => t.id === task.id)) {
239 | 				allRelevantTasks.push(task);
240 | 			}
241 | 		}
242 | 
243 | 		// Add recent tasks if not already included
244 | 		for (const task of recentTasks) {
245 | 			if (!allRelevantTasks.some((t) => t.id === task.id)) {
246 | 				allRelevantTasks.push(task);
247 | 			}
248 | 		}
249 | 
250 | 		// Get top N results for final output
251 | 		const finalResults = allRelevantTasks.slice(0, maxResults);
252 | 
253 | 		return {
254 | 			results: finalResults,
255 | 			breakdown: {
256 | 				highRelevance,
257 | 				mediumRelevance,
258 | 				lowRelevance,
259 | 				categoryTasks,
260 | 				recentTasks,
261 | 				promptCategory,
262 | 				promptWords
263 | 			},
264 | 			metadata: {
265 | 				totalSearched: this.tasks.length,
266 | 				fuzzyMatches: fuzzyResults.length,
267 | 				wordMatches: wordResults.length,
268 | 				finalCount: finalResults.length
269 | 			}
270 | 		};
271 | 	}
272 | 
273 | 	/**
274 | 	 * Get task IDs from search results
275 | 	 * @param {Object} searchResults - Results from findRelevantTasks
276 | 	 * @returns {Array<string>} Array of task ID strings
277 | 	 */
278 | 	getTaskIds(searchResults) {
279 | 		return searchResults.results.map((task) => task.id.toString());
280 | 	}
281 | 
282 | 	/**
283 | 	 * Get task IDs including subtasks from search results
284 | 	 * @param {Object} searchResults - Results from findRelevantTasks
285 | 	 * @param {boolean} [includeSubtasks=false] - Whether to include subtask IDs
286 | 	 * @returns {Array<string>} Array of task and subtask ID strings
287 | 	 */
288 | 	getTaskIdsWithSubtasks(searchResults, includeSubtasks = false) {
289 | 		const taskIds = [];
290 | 
291 | 		for (const task of searchResults.results) {
292 | 			taskIds.push(task.id.toString());
293 | 
294 | 			if (includeSubtasks && task.subtasks && task.subtasks.length > 0) {
295 | 				for (const subtask of task.subtasks) {
296 | 					taskIds.push(`${task.id}.${subtask.id}`);
297 | 				}
298 | 			}
299 | 		}
300 | 
301 | 		return taskIds;
302 | 	}
303 | 
304 | 	/**
305 | 	 * Format search results for display
306 | 	 * @param {Object} searchResults - Results from findRelevantTasks
307 | 	 * @param {Object} options - Formatting options
308 | 	 * @returns {string} Formatted search results summary
309 | 	 */
310 | 	formatSearchSummary(searchResults, options = {}) {
311 | 		const { includeScores = false, includeBreakdown = false } = options;
312 | 		const { results, breakdown, metadata } = searchResults;
313 | 
314 | 		let summary = `Found ${results.length} relevant tasks from ${metadata.totalSearched} total tasks`;
315 | 
316 | 		if (includeBreakdown && breakdown) {
317 | 			const parts = [];
318 | 			if (breakdown.highRelevance.length > 0)
319 | 				parts.push(`${breakdown.highRelevance.length} high relevance`);
320 | 			if (breakdown.mediumRelevance.length > 0)
321 | 				parts.push(`${breakdown.mediumRelevance.length} medium relevance`);
322 | 			if (breakdown.lowRelevance.length > 0)
323 | 				parts.push(`${breakdown.lowRelevance.length} low relevance`);
324 | 			if (breakdown.categoryTasks.length > 0)
325 | 				parts.push(`${breakdown.categoryTasks.length} category matches`);
326 | 
327 | 			if (parts.length > 0) {
328 | 				summary += ` (${parts.join(', ')})`;
329 | 			}
330 | 
331 | 			if (breakdown.promptCategory) {
332 | 				summary += `\nCategory detected: ${breakdown.promptCategory.label}`;
333 | 			}
334 | 		}
335 | 
336 | 		return summary;
337 | 	}
338 | }
339 | 
340 | /**
341 |  * Factory function to create a fuzzy search instance
342 |  * @param {Array} tasks - Array of task objects
343 |  * @param {string} [searchType='default'] - Type of search configuration to use
344 |  * @returns {FuzzyTaskSearch} Fuzzy search instance
345 |  */
346 | export function createFuzzyTaskSearch(tasks, searchType = 'default') {
347 | 	return new FuzzyTaskSearch(tasks, searchType);
348 | }
349 | 
350 | /**
351 |  * Quick utility function to find relevant task IDs for a prompt
352 |  * @param {Array} tasks - Array of task objects
353 |  * @param {string} prompt - Search prompt
354 |  * @param {Object} options - Search options
355 |  * @returns {Array<string>} Array of relevant task ID strings
356 |  */
357 | export function findRelevantTaskIds(tasks, prompt, options = {}) {
358 | 	const {
359 | 		searchType = 'default',
360 | 		maxResults = 8,
361 | 		includeSubtasks = false
362 | 	} = options;
363 | 
364 | 	const fuzzySearch = new FuzzyTaskSearch(tasks, searchType);
365 | 	const results = fuzzySearch.findRelevantTasks(prompt, { maxResults });
366 | 
367 | 	return includeSubtasks
368 | 		? fuzzySearch.getTaskIdsWithSubtasks(results, true)
369 | 		: fuzzySearch.getTaskIds(results);
370 | }
371 | 
372 | export default FuzzyTaskSearch;
373 | 
```

--------------------------------------------------------------------------------
/src/ai-providers/base-provider.js:
--------------------------------------------------------------------------------

```javascript
  1 | import {
  2 | 	generateObject,
  3 | 	generateText,
  4 | 	streamText,
  5 | 	streamObject,
  6 | 	zodSchema,
  7 | 	JSONParseError,
  8 | 	NoObjectGeneratedError
  9 | } from 'ai';
 10 | import { jsonrepair } from 'jsonrepair';
 11 | import { log, findProjectRoot } from '../../scripts/modules/utils.js';
 12 | import { isProxyEnabled } from '../../scripts/modules/config-manager.js';
 13 | import { EnvHttpProxyAgent } from 'undici';
 14 | 
 15 | /**
 16 |  * Base class for all AI providers
 17 |  */
 18 | export class BaseAIProvider {
 19 | 	constructor() {
 20 | 		if (this.constructor === BaseAIProvider) {
 21 | 			throw new Error('BaseAIProvider cannot be instantiated directly');
 22 | 		}
 23 | 
 24 | 		// Each provider must set their name
 25 | 		this.name = this.constructor.name;
 26 | 
 27 | 		// Cache proxy agent to avoid creating multiple instances
 28 | 		this._proxyAgent = null;
 29 | 
 30 | 		/**
 31 | 		 * Whether this provider needs explicit schema in JSON mode
 32 | 		 * Can be overridden by subclasses
 33 | 		 * @type {boolean}
 34 | 		 */
 35 | 		this.needsExplicitJsonSchema = false;
 36 | 
 37 | 		/**
 38 | 		 * Whether this provider supports temperature parameter
 39 | 		 * Can be overridden by subclasses
 40 | 		 * @type {boolean}
 41 | 		 */
 42 | 		this.supportsTemperature = true;
 43 | 	}
 44 | 
 45 | 	/**
 46 | 	 * Validates authentication parameters - can be overridden by providers
 47 | 	 * @param {object} params - Parameters to validate
 48 | 	 */
 49 | 	validateAuth(params) {
 50 | 		// Default: require API key (most providers need this)
 51 | 		if (!params.apiKey) {
 52 | 			throw new Error(`${this.name} API key is required`);
 53 | 		}
 54 | 	}
 55 | 
 56 | 	/**
 57 | 	 * Creates a custom fetch function with proxy support.
 58 | 	 * Only enables proxy when TASKMASTER_ENABLE_PROXY environment variable is set to 'true'
 59 | 	 * or enableProxy is set to true in config.json.
 60 | 	 * Automatically reads http_proxy/https_proxy environment variables when enabled.
 61 | 	 * @returns {Function} Custom fetch function with proxy support, or undefined if proxy is disabled
 62 | 	 */
 63 | 	createProxyFetch() {
 64 | 		// Cache project root to avoid repeated lookups
 65 | 		if (!this._projectRoot) {
 66 | 			this._projectRoot = findProjectRoot();
 67 | 		}
 68 | 		const projectRoot = this._projectRoot;
 69 | 
 70 | 		if (!isProxyEnabled(null, projectRoot)) {
 71 | 			// Return undefined to use default fetch without proxy
 72 | 			return undefined;
 73 | 		}
 74 | 
 75 | 		// Proxy is enabled, create and return proxy fetch
 76 | 		if (!this._proxyAgent) {
 77 | 			this._proxyAgent = new EnvHttpProxyAgent();
 78 | 		}
 79 | 		return (url, options = {}) => {
 80 | 			return fetch(url, {
 81 | 				...options,
 82 | 				dispatcher: this._proxyAgent
 83 | 			});
 84 | 		};
 85 | 	}
 86 | 
 87 | 	/**
 88 | 	 * Validates common parameters across all methods
 89 | 	 * @param {object} params - Parameters to validate
 90 | 	 */
 91 | 	validateParams(params) {
 92 | 		// Validate authentication (can be overridden by providers)
 93 | 		this.validateAuth(params);
 94 | 
 95 | 		// Validate required model ID
 96 | 		if (!params.modelId) {
 97 | 			throw new Error(`${this.name} Model ID is required`);
 98 | 		}
 99 | 
100 | 		// Validate optional parameters
101 | 		this.validateOptionalParams(params);
102 | 	}
103 | 
104 | 	/**
105 | 	 * Validates optional parameters like temperature and maxTokens
106 | 	 * @param {object} params - Parameters to validate
107 | 	 */
108 | 	validateOptionalParams(params) {
109 | 		if (
110 | 			params.temperature !== undefined &&
111 | 			(params.temperature < 0 || params.temperature > 1)
112 | 		) {
113 | 			throw new Error('Temperature must be between 0 and 1');
114 | 		}
115 | 		if (params.maxTokens !== undefined) {
116 | 			const maxTokens = Number(params.maxTokens);
117 | 			if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
118 | 				throw new Error('maxTokens must be a finite number greater than 0');
119 | 			}
120 | 		}
121 | 	}
122 | 
123 | 	/**
124 | 	 * Validates message array structure
125 | 	 */
126 | 	validateMessages(messages) {
127 | 		if (!messages || !Array.isArray(messages) || messages.length === 0) {
128 | 			throw new Error('Invalid or empty messages array provided');
129 | 		}
130 | 
131 | 		for (const msg of messages) {
132 | 			if (!msg.role || !msg.content) {
133 | 				throw new Error(
134 | 					'Invalid message format. Each message must have role and content'
135 | 				);
136 | 			}
137 | 		}
138 | 	}
139 | 
140 | 	/**
141 | 	 * Common error handler
142 | 	 */
143 | 	handleError(operation, error) {
144 | 		const errorMessage = error.message || 'Unknown error occurred';
145 | 		log('error', `${this.name} ${operation} failed: ${errorMessage}`, {
146 | 			error
147 | 		});
148 | 		throw new Error(
149 | 			`${this.name} API error during ${operation}: ${errorMessage}`
150 | 		);
151 | 	}
152 | 
153 | 	/**
154 | 	 * Creates and returns a client instance for the provider
155 | 	 * @abstract
156 | 	 */
157 | 	getClient(params) {
158 | 		throw new Error('getClient must be implemented by provider');
159 | 	}
160 | 
161 | 	/**
162 | 	 * Returns if the API key is required
163 | 	 * @abstract
164 | 	 * @returns {boolean} if the API key is required, defaults to true
165 | 	 */
166 | 	isRequiredApiKey() {
167 | 		return true;
168 | 	}
169 | 
170 | 	/**
171 | 	 * Returns the required API key environment variable name
172 | 	 * @abstract
173 | 	 * @returns {string|null} The environment variable name, or null if no API key is required
174 | 	 */
175 | 	getRequiredApiKeyName() {
176 | 		throw new Error('getRequiredApiKeyName must be implemented by provider');
177 | 	}
178 | 
179 | 	/**
180 | 	 * Prepares token limit parameter based on model requirements
181 | 	 * @param {string} modelId - The model ID
182 | 	 * @param {number} maxTokens - The maximum tokens value
183 | 	 * @returns {object} Object with either maxTokens or max_completion_tokens
184 | 	 */
185 | 	prepareTokenParam(modelId, maxTokens) {
186 | 		if (maxTokens === undefined) {
187 | 			return {};
188 | 		}
189 | 
190 | 		// Ensure maxTokens is an integer
191 | 		const tokenValue = Math.floor(Number(maxTokens));
192 | 
193 | 		return { maxOutputTokens: tokenValue };
194 | 	}
195 | 
196 | 	/**
197 | 	 * Generates text using the provider's model
198 | 	 */
199 | 	async generateText(params) {
200 | 		try {
201 | 			this.validateParams(params);
202 | 			this.validateMessages(params.messages);
203 | 
204 | 			log(
205 | 				'debug',
206 | 				`Generating ${this.name} text with model: ${params.modelId}`
207 | 			);
208 | 
209 | 			const client = await this.getClient(params);
210 | 			const result = await generateText({
211 | 				model: client(params.modelId),
212 | 				messages: params.messages,
213 | 				...this.prepareTokenParam(params.modelId, params.maxTokens),
214 | 				...(this.supportsTemperature && params.temperature !== undefined
215 | 					? { temperature: params.temperature }
216 | 					: {})
217 | 			});
218 | 
219 | 			log(
220 | 				'debug',
221 | 				`${this.name} generateText completed successfully for model: ${params.modelId}`
222 | 			);
223 | 
224 | 			const inputTokens =
225 | 				result.usage?.inputTokens ?? result.usage?.promptTokens ?? 0;
226 | 			const outputTokens =
227 | 				result.usage?.outputTokens ?? result.usage?.completionTokens ?? 0;
228 | 			const totalTokens =
229 | 				result.usage?.totalTokens ?? inputTokens + outputTokens;
230 | 
231 | 			return {
232 | 				text: result.text,
233 | 				usage: {
234 | 					inputTokens,
235 | 					outputTokens,
236 | 					totalTokens
237 | 				}
238 | 			};
239 | 		} catch (error) {
240 | 			this.handleError('text generation', error);
241 | 		}
242 | 	}
243 | 
244 | 	/**
245 | 	 * Streams text using the provider's model
246 | 	 */
247 | 	async streamText(params) {
248 | 		try {
249 | 			this.validateParams(params);
250 | 			this.validateMessages(params.messages);
251 | 
252 | 			log('debug', `Streaming ${this.name} text with model: ${params.modelId}`);
253 | 
254 | 			const client = await this.getClient(params);
255 | 			const stream = await streamText({
256 | 				model: client(params.modelId),
257 | 				messages: params.messages,
258 | 				...this.prepareTokenParam(params.modelId, params.maxTokens),
259 | 				...(this.supportsTemperature && params.temperature !== undefined
260 | 					? { temperature: params.temperature }
261 | 					: {})
262 | 			});
263 | 
264 | 			log(
265 | 				'debug',
266 | 				`${this.name} streamText initiated successfully for model: ${params.modelId}`
267 | 			);
268 | 
269 | 			return stream;
270 | 		} catch (error) {
271 | 			this.handleError('text streaming', error);
272 | 		}
273 | 	}
274 | 
275 | 	/**
276 | 	 * Streams a structured object using the provider's model
277 | 	 */
278 | 	async streamObject(params) {
279 | 		try {
280 | 			this.validateParams(params);
281 | 			this.validateMessages(params.messages);
282 | 
283 | 			if (!params.schema) {
284 | 				throw new Error('Schema is required for object streaming');
285 | 			}
286 | 
287 | 			log(
288 | 				'debug',
289 | 				`Streaming ${this.name} object with model: ${params.modelId}`
290 | 			);
291 | 
292 | 			const client = await this.getClient(params);
293 | 			const result = await streamObject({
294 | 				model: client(params.modelId),
295 | 				messages: params.messages,
296 | 				schema: zodSchema(params.schema),
297 | 				mode: params.mode || 'auto',
298 | 				maxOutputTokens: params.maxTokens,
299 | 				...(this.supportsTemperature && params.temperature !== undefined
300 | 					? { temperature: params.temperature }
301 | 					: {})
302 | 			});
303 | 
304 | 			log(
305 | 				'debug',
306 | 				`${this.name} streamObject initiated successfully for model: ${params.modelId}`
307 | 			);
308 | 
309 | 			// Return the stream result directly
310 | 			// The stream result contains partialObjectStream and other properties
311 | 			return result;
312 | 		} catch (error) {
313 | 			this.handleError('object streaming', error);
314 | 		}
315 | 	}
316 | 
317 | 	/**
318 | 	 * Generates a structured object using the provider's model
319 | 	 */
320 | 	async generateObject(params) {
321 | 		try {
322 | 			this.validateParams(params);
323 | 			this.validateMessages(params.messages);
324 | 
325 | 			if (!params.schema) {
326 | 				throw new Error('Schema is required for object generation');
327 | 			}
328 | 			if (!params.objectName) {
329 | 				throw new Error('Object name is required for object generation');
330 | 			}
331 | 
332 | 			log(
333 | 				'debug',
334 | 				`Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}`
335 | 			);
336 | 
337 | 			const client = await this.getClient(params);
338 | 
339 | 			const result = await generateObject({
340 | 				model: client(params.modelId),
341 | 				messages: params.messages,
342 | 				schema: params.schema,
343 | 				mode: this.needsExplicitJsonSchema ? 'json' : 'auto',
344 | 				schemaName: params.objectName,
345 | 				schemaDescription: `Generate a valid JSON object for ${params.objectName}`,
346 | 				maxTokens: params.maxTokens,
347 | 				...(this.supportsTemperature && params.temperature !== undefined
348 | 					? { temperature: params.temperature }
349 | 					: {})
350 | 			});
351 | 
352 | 			log(
353 | 				'debug',
354 | 				`${this.name} generateObject completed successfully for model: ${params.modelId}`
355 | 			);
356 | 
357 | 			const inputTokens =
358 | 				result.usage?.inputTokens ?? result.usage?.promptTokens ?? 0;
359 | 			const outputTokens =
360 | 				result.usage?.outputTokens ?? result.usage?.completionTokens ?? 0;
361 | 			const totalTokens =
362 | 				result.usage?.totalTokens ?? inputTokens + outputTokens;
363 | 
364 | 			return {
365 | 				object: result.object,
366 | 				usage: {
367 | 					inputTokens,
368 | 					outputTokens,
369 | 					totalTokens
370 | 				}
371 | 			};
372 | 		} catch (error) {
373 | 			// Check if this is a JSON parsing error that we can potentially fix
374 | 			if (
375 | 				NoObjectGeneratedError.isInstance(error) &&
376 | 				error.cause instanceof JSONParseError &&
377 | 				error.cause.text
378 | 			) {
379 | 				log(
380 | 					'warn',
381 | 					`${this.name} generated malformed JSON, attempting to repair...`
382 | 				);
383 | 
384 | 				try {
385 | 					// Use jsonrepair to fix the malformed JSON
386 | 					const repairedJson = jsonrepair(error.cause.text);
387 | 					const parsed = JSON.parse(repairedJson);
388 | 
389 | 					log('info', `Successfully repaired ${this.name} JSON output`);
390 | 
391 | 					// Return in the expected format
392 | 					return {
393 | 						object: parsed,
394 | 						usage: {
395 | 							// Extract usage information from the error if available
396 | 							inputTokens:
397 | 								error.usage?.promptTokens || error.usage?.inputTokens || 0,
398 | 							outputTokens:
399 | 								error.usage?.completionTokens || error.usage?.outputTokens || 0,
400 | 							totalTokens: error.usage?.totalTokens || 0
401 | 						}
402 | 					};
403 | 				} catch (repairError) {
404 | 					log(
405 | 						'error',
406 | 						`Failed to repair ${this.name} JSON: ${repairError.message}`
407 | 					);
408 | 					// Fall through to handleError with original error
409 | 				}
410 | 			}
411 | 
412 | 			this.handleError('object generation', error);
413 | 		}
414 | 	}
415 | }
416 | 
```

--------------------------------------------------------------------------------
/tests/integration/cli/commands.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | 
  3 | // --- Define mock functions ---
  4 | const mockGetMainModelId = jest.fn().mockReturnValue('claude-3-opus');
  5 | const mockGetResearchModelId = jest.fn().mockReturnValue('gpt-4-turbo');
  6 | const mockGetFallbackModelId = jest.fn().mockReturnValue('claude-3-haiku');
  7 | const mockSetMainModel = jest.fn().mockResolvedValue(true);
  8 | const mockSetResearchModel = jest.fn().mockResolvedValue(true);
  9 | const mockSetFallbackModel = jest.fn().mockResolvedValue(true);
 10 | const mockGetAvailableModels = jest.fn().mockReturnValue([
 11 | 	{ id: 'claude-3-opus', name: 'Claude 3 Opus', provider: 'anthropic' },
 12 | 	{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'openai' },
 13 | 	{ id: 'claude-3-haiku', name: 'Claude 3 Haiku', provider: 'anthropic' },
 14 | 	{ id: 'claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'anthropic' }
 15 | ]);
 16 | 
 17 | // Mock UI related functions
 18 | const mockDisplayHelp = jest.fn();
 19 | const mockDisplayBanner = jest.fn();
 20 | const mockLog = jest.fn();
 21 | const mockStartLoadingIndicator = jest.fn(() => ({ stop: jest.fn() }));
 22 | const mockStopLoadingIndicator = jest.fn();
 23 | 
 24 | // --- Setup mocks using unstable_mockModule (recommended for ES modules) ---
 25 | jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
 26 | 	getMainModelId: mockGetMainModelId,
 27 | 	getResearchModelId: mockGetResearchModelId,
 28 | 	getFallbackModelId: mockGetFallbackModelId,
 29 | 	setMainModel: mockSetMainModel,
 30 | 	setResearchModel: mockSetResearchModel,
 31 | 	setFallbackModel: mockSetFallbackModel,
 32 | 	getAvailableModels: mockGetAvailableModels,
 33 | 	VALID_PROVIDERS: ['anthropic', 'openai']
 34 | }));
 35 | 
 36 | jest.unstable_mockModule('../../../scripts/modules/ui.js', () => ({
 37 | 	displayHelp: mockDisplayHelp,
 38 | 	displayBanner: mockDisplayBanner,
 39 | 	log: mockLog,
 40 | 	startLoadingIndicator: mockStartLoadingIndicator,
 41 | 	stopLoadingIndicator: mockStopLoadingIndicator
 42 | }));
 43 | 
 44 | // --- Mock chalk for consistent output formatting ---
 45 | const mockChalk = {
 46 | 	red: jest.fn((text) => text),
 47 | 	yellow: jest.fn((text) => text),
 48 | 	blue: jest.fn((text) => text),
 49 | 	green: jest.fn((text) => text),
 50 | 	gray: jest.fn((text) => text),
 51 | 	dim: jest.fn((text) => text),
 52 | 	bold: {
 53 | 		cyan: jest.fn((text) => text),
 54 | 		white: jest.fn((text) => text),
 55 | 		red: jest.fn((text) => text)
 56 | 	},
 57 | 	cyan: {
 58 | 		bold: jest.fn((text) => text)
 59 | 	},
 60 | 	white: {
 61 | 		bold: jest.fn((text) => text)
 62 | 	}
 63 | };
 64 | // Default function for chalk itself
 65 | mockChalk.default = jest.fn((text) => text);
 66 | // Add the methods to the function itself for dual usage
 67 | Object.keys(mockChalk).forEach((key) => {
 68 | 	if (key !== 'default') mockChalk.default[key] = mockChalk[key];
 69 | });
 70 | 
 71 | jest.unstable_mockModule('chalk', () => ({
 72 | 	default: mockChalk.default
 73 | }));
 74 | 
 75 | // --- Import modules (AFTER mock setup) ---
 76 | let configManager, ui, chalk;
 77 | 
 78 | describe('CLI Models Command (Action Handler Test)', () => {
 79 | 	// Setup dynamic imports before tests run
 80 | 	beforeAll(async () => {
 81 | 		configManager = await import('../../../scripts/modules/config-manager.js');
 82 | 		ui = await import('../../../scripts/modules/ui.js');
 83 | 		chalk = (await import('chalk')).default;
 84 | 	});
 85 | 
 86 | 	// --- Replicate the action handler logic from commands.js ---
 87 | 	async function modelsAction(options) {
 88 | 		options = options || {}; // Ensure options object exists
 89 | 		const availableModels = configManager.getAvailableModels();
 90 | 
 91 | 		const findProvider = (modelId) => {
 92 | 			const modelInfo = availableModels.find((m) => m.id === modelId);
 93 | 			return modelInfo?.provider;
 94 | 		};
 95 | 
 96 | 		let modelSetAction = false;
 97 | 
 98 | 		try {
 99 | 			if (options.setMain) {
100 | 				const modelId = options.setMain;
101 | 				if (typeof modelId !== 'string' || modelId.trim() === '') {
102 | 					console.error(
103 | 						chalk.red('Error: --set-main flag requires a valid model ID.')
104 | 					);
105 | 					process.exit(1);
106 | 				}
107 | 				const provider = findProvider(modelId);
108 | 				if (!provider) {
109 | 					console.error(
110 | 						chalk.red(
111 | 							`Error: Model ID "${modelId}" not found in available models.`
112 | 						)
113 | 					);
114 | 					process.exit(1);
115 | 				}
116 | 				if (await configManager.setMainModel(provider, modelId)) {
117 | 					console.log(
118 | 						chalk.green(`Main model set to: ${modelId} (Provider: ${provider})`)
119 | 					);
120 | 					modelSetAction = true;
121 | 				} else {
122 | 					console.error(chalk.red(`Failed to set main model.`));
123 | 					process.exit(1);
124 | 				}
125 | 			}
126 | 
127 | 			if (options.setResearch) {
128 | 				const modelId = options.setResearch;
129 | 				if (typeof modelId !== 'string' || modelId.trim() === '') {
130 | 					console.error(
131 | 						chalk.red('Error: --set-research flag requires a valid model ID.')
132 | 					);
133 | 					process.exit(1);
134 | 				}
135 | 				const provider = findProvider(modelId);
136 | 				if (!provider) {
137 | 					console.error(
138 | 						chalk.red(
139 | 							`Error: Model ID "${modelId}" not found in available models.`
140 | 						)
141 | 					);
142 | 					process.exit(1);
143 | 				}
144 | 				if (await configManager.setResearchModel(provider, modelId)) {
145 | 					console.log(
146 | 						chalk.green(
147 | 							`Research model set to: ${modelId} (Provider: ${provider})`
148 | 						)
149 | 					);
150 | 					modelSetAction = true;
151 | 				} else {
152 | 					console.error(chalk.red(`Failed to set research model.`));
153 | 					process.exit(1);
154 | 				}
155 | 			}
156 | 
157 | 			if (options.setFallback) {
158 | 				const modelId = options.setFallback;
159 | 				if (typeof modelId !== 'string' || modelId.trim() === '') {
160 | 					console.error(
161 | 						chalk.red('Error: --set-fallback flag requires a valid model ID.')
162 | 					);
163 | 					process.exit(1);
164 | 				}
165 | 				const provider = findProvider(modelId);
166 | 				if (!provider) {
167 | 					console.error(
168 | 						chalk.red(
169 | 							`Error: Model ID "${modelId}" not found in available models.`
170 | 						)
171 | 					);
172 | 					process.exit(1);
173 | 				}
174 | 				if (await configManager.setFallbackModel(provider, modelId)) {
175 | 					console.log(
176 | 						chalk.green(
177 | 							`Fallback model set to: ${modelId} (Provider: ${provider})`
178 | 						)
179 | 					);
180 | 					modelSetAction = true;
181 | 				} else {
182 | 					console.error(chalk.red(`Failed to set fallback model.`));
183 | 					process.exit(1);
184 | 				}
185 | 			}
186 | 
187 | 			if (!modelSetAction) {
188 | 				const currentMain = configManager.getMainModelId();
189 | 				const currentResearch = configManager.getResearchModelId();
190 | 				const currentFallback = configManager.getFallbackModelId();
191 | 
192 | 				if (!availableModels || availableModels.length === 0) {
193 | 					console.log(chalk.yellow('No models defined in configuration.'));
194 | 					return;
195 | 				}
196 | 
197 | 				// Create a mock table for testing - avoid using Table constructor
198 | 				const mockTableData = [];
199 | 				availableModels.forEach((model) => {
200 | 					if (model.id.startsWith('[') && model.id.endsWith(']')) return;
201 | 					mockTableData.push([
202 | 						model.id,
203 | 						model.name || 'N/A',
204 | 						model.provider || 'N/A',
205 | 						model.id === currentMain ? chalk.green('   ✓') : '',
206 | 						model.id === currentResearch ? chalk.green('     ✓') : '',
207 | 						model.id === currentFallback ? chalk.green('     ✓') : ''
208 | 					]);
209 | 				});
210 | 
211 | 				// In a real implementation, we would use cli-table3, but for testing
212 | 				// we'll just log 'Mock Table Output'
213 | 				console.log('Mock Table Output');
214 | 			}
215 | 		} catch (error) {
216 | 			// Use ui.log mock if available, otherwise console.error
217 | 			(ui.log || console.error)(
218 | 				`Error processing models command: ${error.message}`,
219 | 				'error'
220 | 			);
221 | 			if (error.stack) {
222 | 				(ui.log || console.error)(error.stack, 'debug');
223 | 			}
224 | 			throw error; // Re-throw for test failure
225 | 		}
226 | 	}
227 | 	// --- End of Action Handler Logic ---
228 | 
229 | 	let originalConsoleLog;
230 | 	let originalConsoleError;
231 | 	let originalProcessExit;
232 | 
233 | 	beforeEach(() => {
234 | 		// Reset all mocks
235 | 		jest.clearAllMocks();
236 | 
237 | 		// Save original console methods
238 | 		originalConsoleLog = console.log;
239 | 		originalConsoleError = console.error;
240 | 		originalProcessExit = process.exit;
241 | 
242 | 		// Mock console and process.exit
243 | 		console.log = jest.fn();
244 | 		console.error = jest.fn();
245 | 		process.exit = jest.fn((code) => {
246 | 			throw new Error(`process.exit(${code}) called`);
247 | 		});
248 | 	});
249 | 
250 | 	afterEach(() => {
251 | 		// Restore original console methods
252 | 		console.log = originalConsoleLog;
253 | 		console.error = originalConsoleError;
254 | 		process.exit = originalProcessExit;
255 | 	});
256 | 
257 | 	// --- Test Cases (Calling modelsAction directly) ---
258 | 
259 | 	it('should call setMainModel with correct provider and ID', async () => {
260 | 		const modelId = 'claude-3-opus';
261 | 		const expectedProvider = 'anthropic';
262 | 		await modelsAction({ setMain: modelId });
263 | 		expect(mockSetMainModel).toHaveBeenCalledWith(expectedProvider, modelId);
264 | 		expect(console.log).toHaveBeenCalledWith(
265 | 			expect.stringContaining(`Main model set to: ${modelId}`)
266 | 		);
267 | 		expect(console.log).toHaveBeenCalledWith(
268 | 			expect.stringContaining(`(Provider: ${expectedProvider})`)
269 | 		);
270 | 	});
271 | 
272 | 	it('should show an error if --set-main model ID is not found', async () => {
273 | 		await expect(
274 | 			modelsAction({ setMain: 'non-existent-model' })
275 | 		).rejects.toThrow(/process.exit/); // Expect exit call
276 | 		expect(mockSetMainModel).not.toHaveBeenCalled();
277 | 		expect(console.error).toHaveBeenCalledWith(
278 | 			expect.stringContaining('Model ID "non-existent-model" not found')
279 | 		);
280 | 	});
281 | 
282 | 	it('should call setResearchModel with correct provider and ID', async () => {
283 | 		const modelId = 'gpt-4-turbo';
284 | 		const expectedProvider = 'openai';
285 | 		await modelsAction({ setResearch: modelId });
286 | 		expect(mockSetResearchModel).toHaveBeenCalledWith(
287 | 			expectedProvider,
288 | 			modelId
289 | 		);
290 | 		expect(console.log).toHaveBeenCalledWith(
291 | 			expect.stringContaining(`Research model set to: ${modelId}`)
292 | 		);
293 | 		expect(console.log).toHaveBeenCalledWith(
294 | 			expect.stringContaining(`(Provider: ${expectedProvider})`)
295 | 		);
296 | 	});
297 | 
298 | 	it('should call setFallbackModel with correct provider and ID', async () => {
299 | 		const modelId = 'claude-3-haiku';
300 | 		const expectedProvider = 'anthropic';
301 | 		await modelsAction({ setFallback: modelId });
302 | 		expect(mockSetFallbackModel).toHaveBeenCalledWith(
303 | 			expectedProvider,
304 | 			modelId
305 | 		);
306 | 		expect(console.log).toHaveBeenCalledWith(
307 | 			expect.stringContaining(`Fallback model set to: ${modelId}`)
308 | 		);
309 | 		expect(console.log).toHaveBeenCalledWith(
310 | 			expect.stringContaining(`(Provider: ${expectedProvider})`)
311 | 		);
312 | 	});
313 | 
314 | 	it('should call all set*Model functions when all flags are used', async () => {
315 | 		const mainModelId = 'claude-3-opus';
316 | 		const researchModelId = 'gpt-4-turbo';
317 | 		const fallbackModelId = 'claude-3-haiku';
318 | 		const mainProvider = 'anthropic';
319 | 		const researchProvider = 'openai';
320 | 		const fallbackProvider = 'anthropic';
321 | 
322 | 		await modelsAction({
323 | 			setMain: mainModelId,
324 | 			setResearch: researchModelId,
325 | 			setFallback: fallbackModelId
326 | 		});
327 | 		expect(mockSetMainModel).toHaveBeenCalledWith(mainProvider, mainModelId);
328 | 		expect(mockSetResearchModel).toHaveBeenCalledWith(
329 | 			researchProvider,
330 | 			researchModelId
331 | 		);
332 | 		expect(mockSetFallbackModel).toHaveBeenCalledWith(
333 | 			fallbackProvider,
334 | 			fallbackModelId
335 | 		);
336 | 	});
337 | 
338 | 	it('should call specific get*ModelId and getAvailableModels and log table when run without flags', async () => {
339 | 		await modelsAction({}); // Call with empty options
340 | 
341 | 		expect(mockGetMainModelId).toHaveBeenCalled();
342 | 		expect(mockGetResearchModelId).toHaveBeenCalled();
343 | 		expect(mockGetFallbackModelId).toHaveBeenCalled();
344 | 		expect(mockGetAvailableModels).toHaveBeenCalled();
345 | 
346 | 		expect(console.log).toHaveBeenCalled();
347 | 		// Check the mocked Table.toString() was used via console.log
348 | 		expect(console.log).toHaveBeenCalledWith('Mock Table Output');
349 | 	});
350 | });
351 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/tests/integration/storage/activity-logger.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import os from 'os';
  2 | import path from 'path';
  3 | import fs from 'fs-extra';
  4 | import { afterEach, beforeEach, describe, expect, it } from 'vitest';
  5 | import {
  6 | 	filterActivityLog,
  7 | 	logActivity,
  8 | 	readActivityLog
  9 | } from '../../../src/storage/activity-logger.js';
 10 | 
 11 | describe('Activity Logger', () => {
 12 | 	let testDir: string;
 13 | 	let activityPath: string;
 14 | 
 15 | 	beforeEach(async () => {
 16 | 		// Create a unique temporary test directory
 17 | 		const prefix = path.join(os.tmpdir(), 'activity-test-');
 18 | 		testDir = await fs.mkdtemp(prefix);
 19 | 		activityPath = path.join(testDir, 'activity.jsonl');
 20 | 	});
 21 | 
 22 | 	afterEach(async () => {
 23 | 		// Clean up test directory
 24 | 		await fs.remove(testDir);
 25 | 	});
 26 | 
 27 | 	describe('logActivity', () => {
 28 | 		it('should create activity log file on first write', async () => {
 29 | 			await logActivity(activityPath, {
 30 | 				type: 'phase-start',
 31 | 				phase: 'red',
 32 | 				data: {}
 33 | 			});
 34 | 
 35 | 			const exists = await fs.pathExists(activityPath);
 36 | 			expect(exists).toBe(true);
 37 | 		});
 38 | 
 39 | 		it('should append event to log file', async () => {
 40 | 			await logActivity(activityPath, {
 41 | 				type: 'phase-start',
 42 | 				phase: 'red'
 43 | 			});
 44 | 
 45 | 			const content = await fs.readFile(activityPath, 'utf-8');
 46 | 			const lines = content.trim().split(/\r?\n/);
 47 | 
 48 | 			expect(lines.length).toBe(1);
 49 | 		});
 50 | 
 51 | 		it('should write valid JSONL format', async () => {
 52 | 			await logActivity(activityPath, {
 53 | 				type: 'test-run',
 54 | 				result: 'pass'
 55 | 			});
 56 | 
 57 | 			const content = await fs.readFile(activityPath, 'utf-8');
 58 | 			const line = content.trim();
 59 | 			const parsed = JSON.parse(line);
 60 | 
 61 | 			expect(parsed).toBeDefined();
 62 | 			expect(parsed.type).toBe('test-run');
 63 | 		});
 64 | 
 65 | 		it('should include timestamp in log entry', async () => {
 66 | 			const before = new Date().toISOString();
 67 | 			await logActivity(activityPath, {
 68 | 				type: 'phase-start',
 69 | 				phase: 'red'
 70 | 			});
 71 | 			const after = new Date().toISOString();
 72 | 
 73 | 			const logs = await readActivityLog(activityPath);
 74 | 			expect(logs[0].timestamp).toBeDefined();
 75 | 			expect(logs[0].timestamp >= before).toBe(true);
 76 | 			expect(logs[0].timestamp <= after).toBe(true);
 77 | 		});
 78 | 
 79 | 		it('should append multiple events', async () => {
 80 | 			await logActivity(activityPath, { type: 'event1' });
 81 | 			await logActivity(activityPath, { type: 'event2' });
 82 | 			await logActivity(activityPath, { type: 'event3' });
 83 | 
 84 | 			const logs = await readActivityLog(activityPath);
 85 | 			expect(logs.length).toBe(3);
 86 | 			expect(logs[0].type).toBe('event1');
 87 | 			expect(logs[1].type).toBe('event2');
 88 | 			expect(logs[2].type).toBe('event3');
 89 | 		});
 90 | 
 91 | 		it('should preserve event data', async () => {
 92 | 			const eventData = {
 93 | 				type: 'git-commit',
 94 | 				hash: 'abc123',
 95 | 				message: 'test commit',
 96 | 				files: ['file1.ts', 'file2.ts']
 97 | 			};
 98 | 
 99 | 			await logActivity(activityPath, eventData);
100 | 
101 | 			const logs = await readActivityLog(activityPath);
102 | 			expect(logs[0].type).toBe('git-commit');
103 | 			expect(logs[0].hash).toBe('abc123');
104 | 			expect(logs[0].message).toBe('test commit');
105 | 			expect(logs[0].files).toEqual(['file1.ts', 'file2.ts']);
106 | 		});
107 | 
108 | 		it('should handle nested objects in event data', async () => {
109 | 			await logActivity(activityPath, {
110 | 				type: 'test-results',
111 | 				results: {
112 | 					passed: 10,
113 | 					failed: 2,
114 | 					details: { coverage: 85 }
115 | 				}
116 | 			});
117 | 
118 | 			const logs = await readActivityLog(activityPath);
119 | 			expect(logs[0].results.details.coverage).toBe(85);
120 | 		});
121 | 
122 | 		it('should handle special characters in event data', async () => {
123 | 			await logActivity(activityPath, {
124 | 				type: 'error',
125 | 				message: 'Error: "Something went wrong"\nLine 2'
126 | 			});
127 | 
128 | 			const logs = await readActivityLog(activityPath);
129 | 			expect(logs[0].message).toBe('Error: "Something went wrong"\nLine 2');
130 | 		});
131 | 
132 | 		it('should create parent directory if it does not exist', async () => {
133 | 			const nestedPath = path.join(testDir, 'nested', 'dir', 'activity.jsonl');
134 | 
135 | 			await logActivity(nestedPath, { type: 'test' });
136 | 
137 | 			const exists = await fs.pathExists(nestedPath);
138 | 			expect(exists).toBe(true);
139 | 		});
140 | 	});
141 | 
142 | 	describe('readActivityLog', () => {
143 | 		it('should read all events from log', async () => {
144 | 			await logActivity(activityPath, { type: 'event1' });
145 | 			await logActivity(activityPath, { type: 'event2' });
146 | 
147 | 			const logs = await readActivityLog(activityPath);
148 | 
149 | 			expect(logs.length).toBe(2);
150 | 			expect(logs[0].type).toBe('event1');
151 | 			expect(logs[1].type).toBe('event2');
152 | 		});
153 | 
154 | 		it('should return empty array for non-existent file', async () => {
155 | 			const logs = await readActivityLog(activityPath);
156 | 			expect(logs).toEqual([]);
157 | 		});
158 | 
159 | 		it('should parse JSONL correctly', async () => {
160 | 			await logActivity(activityPath, { type: 'event1', data: 'test1' });
161 | 			await logActivity(activityPath, { type: 'event2', data: 'test2' });
162 | 
163 | 			const logs = await readActivityLog(activityPath);
164 | 
165 | 			expect(logs[0].data).toBe('test1');
166 | 			expect(logs[1].data).toBe('test2');
167 | 		});
168 | 
169 | 		it('should handle empty lines', async () => {
170 | 			await fs.writeFile(
171 | 				activityPath,
172 | 				'{"type":"event1"}\n\n{"type":"event2"}\n'
173 | 			);
174 | 
175 | 			const logs = await readActivityLog(activityPath);
176 | 
177 | 			expect(logs.length).toBe(2);
178 | 			expect(logs[0].type).toBe('event1');
179 | 			expect(logs[1].type).toBe('event2');
180 | 		});
181 | 
182 | 		it('should throw error for invalid JSON line', async () => {
183 | 			await fs.writeFile(activityPath, '{"type":"event1"}\ninvalid json\n');
184 | 
185 | 			await expect(readActivityLog(activityPath)).rejects.toThrow(
186 | 				/Invalid JSON/i
187 | 			);
188 | 		});
189 | 
190 | 		it('should preserve chronological order', async () => {
191 | 			for (let i = 0; i < 10; i++) {
192 | 				await logActivity(activityPath, { type: 'event', index: i });
193 | 			}
194 | 
195 | 			const logs = await readActivityLog(activityPath);
196 | 
197 | 			for (let i = 0; i < 10; i++) {
198 | 				expect(logs[i].index).toBe(i);
199 | 			}
200 | 		});
201 | 	});
202 | 
203 | 	describe('filterActivityLog', () => {
204 | 		beforeEach(async () => {
205 | 			// Create sample log entries
206 | 			await logActivity(activityPath, { type: 'phase-start', phase: 'red' });
207 | 			await logActivity(activityPath, { type: 'test-run', result: 'fail' });
208 | 			await logActivity(activityPath, { type: 'phase-start', phase: 'green' });
209 | 			await logActivity(activityPath, { type: 'test-run', result: 'pass' });
210 | 			await logActivity(activityPath, { type: 'git-commit', hash: 'abc123' });
211 | 		});
212 | 
213 | 		it('should filter by event type', async () => {
214 | 			const filtered = await filterActivityLog(activityPath, {
215 | 				type: 'phase-start'
216 | 			});
217 | 
218 | 			expect(filtered.length).toBe(2);
219 | 			expect(filtered[0].type).toBe('phase-start');
220 | 			expect(filtered[1].type).toBe('phase-start');
221 | 		});
222 | 
223 | 		it('should filter by multiple criteria', async () => {
224 | 			const filtered = await filterActivityLog(activityPath, {
225 | 				type: 'test-run',
226 | 				result: 'pass'
227 | 			});
228 | 
229 | 			expect(filtered.length).toBe(1);
230 | 			expect(filtered[0].result).toBe('pass');
231 | 		});
232 | 
233 | 		it('should return all events when no filter provided', async () => {
234 | 			const filtered = await filterActivityLog(activityPath, {});
235 | 
236 | 			expect(filtered.length).toBe(5);
237 | 		});
238 | 
239 | 		it('should filter by timestamp range', async () => {
240 | 			const logs = await readActivityLog(activityPath);
241 | 			const midpoint = logs[2].timestamp;
242 | 
243 | 			const filtered = await filterActivityLog(activityPath, {
244 | 				timestampFrom: midpoint
245 | 			});
246 | 
247 | 			// Should get events from midpoint onwards (inclusive)
248 | 			// Expect at least 3 events, may be more due to timestamp collisions
249 | 			expect(filtered.length).toBeGreaterThanOrEqual(3);
250 | 			expect(filtered.length).toBeLessThanOrEqual(5);
251 | 		});
252 | 
253 | 		it('should filter by custom predicate', async () => {
254 | 			const filtered = await filterActivityLog(activityPath, {
255 | 				predicate: (event: any) => event.phase === 'red'
256 | 			});
257 | 
258 | 			expect(filtered.length).toBe(1);
259 | 			expect(filtered[0].phase).toBe('red');
260 | 		});
261 | 
262 | 		it('should return empty array for non-matching filter', async () => {
263 | 			const filtered = await filterActivityLog(activityPath, {
264 | 				type: 'non-existent'
265 | 			});
266 | 
267 | 			expect(filtered).toEqual([]);
268 | 		});
269 | 
270 | 		it('should handle nested property filters', async () => {
271 | 			await logActivity(activityPath, {
272 | 				type: 'test-results',
273 | 				results: { coverage: 85 }
274 | 			});
275 | 
276 | 			const filtered = await filterActivityLog(activityPath, {
277 | 				predicate: (event: any) => event.results?.coverage > 80
278 | 			});
279 | 
280 | 			expect(filtered.length).toBe(1);
281 | 			expect(filtered[0].results.coverage).toBe(85);
282 | 		});
283 | 	});
284 | 
285 | 	describe('Event types', () => {
286 | 		it('should support phase-transition events', async () => {
287 | 			await logActivity(activityPath, {
288 | 				type: 'phase-transition',
289 | 				from: 'red',
290 | 				to: 'green'
291 | 			});
292 | 
293 | 			const logs = await readActivityLog(activityPath);
294 | 			expect(logs[0].type).toBe('phase-transition');
295 | 			expect(logs[0].from).toBe('red');
296 | 			expect(logs[0].to).toBe('green');
297 | 		});
298 | 
299 | 		it('should support test-run events', async () => {
300 | 			await logActivity(activityPath, {
301 | 				type: 'test-run',
302 | 				result: 'pass',
303 | 				testsRun: 50,
304 | 				testsPassed: 50,
305 | 				testsFailed: 0,
306 | 				coverage: 85.5
307 | 			});
308 | 
309 | 			const logs = await readActivityLog(activityPath);
310 | 			expect(logs[0].testsRun).toBe(50);
311 | 			expect(logs[0].coverage).toBe(85.5);
312 | 		});
313 | 
314 | 		it('should support git-operation events', async () => {
315 | 			await logActivity(activityPath, {
316 | 				type: 'git-commit',
317 | 				hash: 'abc123def456',
318 | 				message: 'feat: add new feature',
319 | 				files: ['file1.ts', 'file2.ts']
320 | 			});
321 | 
322 | 			const logs = await readActivityLog(activityPath);
323 | 			expect(logs[0].hash).toBe('abc123def456');
324 | 			expect(logs[0].files.length).toBe(2);
325 | 		});
326 | 
327 | 		it('should support error events', async () => {
328 | 			await logActivity(activityPath, {
329 | 				type: 'error',
330 | 				phase: 'red',
331 | 				error: 'Test failed',
332 | 				stack: 'Error stack trace...'
333 | 			});
334 | 
335 | 			const logs = await readActivityLog(activityPath);
336 | 			expect(logs[0].type).toBe('error');
337 | 			expect(logs[0].error).toBe('Test failed');
338 | 		});
339 | 	});
340 | 
341 | 	describe('Concurrency handling', () => {
342 | 		it('should handle rapid concurrent writes', async () => {
343 | 			const writes: Promise<void>[] = [];
344 | 			for (let i = 0; i < 50; i++) {
345 | 				writes.push(logActivity(activityPath, { type: 'event', index: i }));
346 | 			}
347 | 
348 | 			await Promise.all(writes);
349 | 
350 | 			const logs = await readActivityLog(activityPath);
351 | 			expect(logs.length).toBe(50);
352 | 		});
353 | 
354 | 		it('should maintain data integrity with concurrent writes', async () => {
355 | 			const writes: Promise<void>[] = [];
356 | 			for (let i = 0; i < 20; i++) {
357 | 				writes.push(
358 | 					logActivity(activityPath, {
359 | 						type: 'concurrent-test',
360 | 						id: i,
361 | 						data: `data-${i}`
362 | 					})
363 | 				);
364 | 			}
365 | 
366 | 			await Promise.all(writes);
367 | 
368 | 			const logs = await readActivityLog(activityPath);
369 | 
370 | 			// All events should be present
371 | 			expect(logs.length).toBe(20);
372 | 			// Validate ids set
373 | 			const ids = new Set(logs.map((l) => l.id));
374 | 			expect([...ids].sort((a, b) => a - b)).toEqual([...Array(20).keys()]);
375 | 			// Validate shape
376 | 			for (const log of logs) {
377 | 				expect(log.type).toBe('concurrent-test');
378 | 				expect(typeof log.id).toBe('number');
379 | 				expect(log.data).toMatch(/^data-\d+$/);
380 | 			}
381 | 		});
382 | 	});
383 | 
384 | 	describe('File integrity', () => {
385 | 		it('should maintain valid JSONL after many operations', async () => {
386 | 			for (let i = 0; i < 100; i++) {
387 | 				await logActivity(activityPath, { type: 'test', iteration: i });
388 | 			}
389 | 
390 | 			const content = await fs.readFile(activityPath, 'utf-8');
391 | 			const lines = content.trim().split(/\r?\n/);
392 | 
393 | 			expect(lines.length).toBe(100);
394 | 
395 | 			// All lines should be valid JSON
396 | 			for (const line of lines) {
397 | 				expect(() => JSON.parse(line)).not.toThrow();
398 | 			}
399 | 		});
400 | 	});
401 | });
402 | 
```

--------------------------------------------------------------------------------
/tests/unit/mcp/tools/analyze-complexity.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Tests for the analyze_project_complexity MCP tool
  3 |  *
  4 |  * Note: This test does NOT test the actual implementation. It tests that:
  5 |  * 1. The tool is registered correctly with the correct parameters
  6 |  * 2. Arguments are passed correctly to analyzeTaskComplexityDirect
  7 |  * 3. The threshold parameter is properly validated
  8 |  * 4. Error handling works as expected
  9 |  *
 10 |  * We do NOT import the real implementation - everything is mocked
 11 |  */
 12 | 
 13 | import { jest } from '@jest/globals';
 14 | 
 15 | // Mock EVERYTHING
 16 | const mockAnalyzeTaskComplexityDirect = jest.fn();
 17 | jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
 18 | 	analyzeTaskComplexityDirect: mockAnalyzeTaskComplexityDirect
 19 | }));
 20 | 
 21 | const mockHandleApiResult = jest.fn((result) => result);
 22 | const mockGetProjectRootFromSession = jest.fn(() => '/mock/project/root');
 23 | const mockCreateErrorResponse = jest.fn((msg) => ({
 24 | 	success: false,
 25 | 	error: { code: 'ERROR', message: msg }
 26 | }));
 27 | 
 28 | jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
 29 | 	getProjectRootFromSession: mockGetProjectRootFromSession,
 30 | 	handleApiResult: mockHandleApiResult,
 31 | 	createErrorResponse: mockCreateErrorResponse,
 32 | 	createContentResponse: jest.fn((content) => ({
 33 | 		success: true,
 34 | 		data: content
 35 | 	})),
 36 | 	executeTaskMasterCommand: jest.fn()
 37 | }));
 38 | 
 39 | // This is a more complex mock of Zod to test actual validation
 40 | const createZodMock = () => {
 41 | 	// Storage for validation rules
 42 | 	const validationRules = {
 43 | 		threshold: {
 44 | 			type: 'coerce.number',
 45 | 			min: 1,
 46 | 			max: 10,
 47 | 			optional: true
 48 | 		}
 49 | 	};
 50 | 
 51 | 	// Create validator functions
 52 | 	const validateThreshold = (value) => {
 53 | 		if (value === undefined && validationRules.threshold.optional) {
 54 | 			return true;
 55 | 		}
 56 | 
 57 | 		// Attempt to coerce to number (if string)
 58 | 		const numValue = typeof value === 'string' ? Number(value) : value;
 59 | 
 60 | 		// Check if it's a valid number
 61 | 		if (isNaN(numValue)) {
 62 | 			throw new Error(`Invalid type for parameter 'threshold'`);
 63 | 		}
 64 | 
 65 | 		// Check min/max constraints
 66 | 		if (numValue < validationRules.threshold.min) {
 67 | 			throw new Error(
 68 | 				`Threshold must be at least ${validationRules.threshold.min}`
 69 | 			);
 70 | 		}
 71 | 
 72 | 		if (numValue > validationRules.threshold.max) {
 73 | 			throw new Error(
 74 | 				`Threshold must be at most ${validationRules.threshold.max}`
 75 | 			);
 76 | 		}
 77 | 
 78 | 		return true;
 79 | 	};
 80 | 
 81 | 	// Create actual validators for parameters
 82 | 	const validators = {
 83 | 		threshold: validateThreshold
 84 | 	};
 85 | 
 86 | 	// Main validation function for the entire object
 87 | 	const validateObject = (obj) => {
 88 | 		// Validate each field
 89 | 		if (obj.threshold !== undefined) {
 90 | 			validators.threshold(obj.threshold);
 91 | 		}
 92 | 
 93 | 		// If we get here, all validations passed
 94 | 		return obj;
 95 | 	};
 96 | 
 97 | 	// Base object with chainable methods
 98 | 	const zodBase = {
 99 | 		optional: () => {
100 | 			return zodBase;
101 | 		},
102 | 		describe: (desc) => {
103 | 			return zodBase;
104 | 		}
105 | 	};
106 | 
107 | 	// Number-specific methods
108 | 	const zodNumber = {
109 | 		...zodBase,
110 | 		min: (value) => {
111 | 			return zodNumber;
112 | 		},
113 | 		max: (value) => {
114 | 			return zodNumber;
115 | 		}
116 | 	};
117 | 
118 | 	// Main mock implementation
119 | 	const mockZod = {
120 | 		object: () => ({
121 | 			...zodBase,
122 | 			// This parse method will be called by the tool execution
123 | 			parse: validateObject
124 | 		}),
125 | 		string: () => zodBase,
126 | 		boolean: () => zodBase,
127 | 		number: () => zodNumber,
128 | 		coerce: {
129 | 			number: () => zodNumber
130 | 		},
131 | 		union: (schemas) => zodBase,
132 | 		_def: {
133 | 			shape: () => ({
134 | 				output: {},
135 | 				model: {},
136 | 				threshold: {},
137 | 				file: {},
138 | 				research: {},
139 | 				projectRoot: {}
140 | 			})
141 | 		}
142 | 	};
143 | 
144 | 	return mockZod;
145 | };
146 | 
147 | // Create our Zod mock
148 | const mockZod = createZodMock();
149 | 
150 | jest.mock('zod', () => ({
151 | 	z: mockZod
152 | }));
153 | 
154 | // DO NOT import the real module - create a fake implementation
155 | // This is the fake implementation of registerAnalyzeTool
156 | const registerAnalyzeTool = (server) => {
157 | 	// Create simplified version of the tool config
158 | 	const toolConfig = {
159 | 		name: 'analyze_project_complexity',
160 | 		description:
161 | 			'Analyze task complexity and generate expansion recommendations',
162 | 		parameters: mockZod.object(),
163 | 
164 | 		// Create a simplified mock of the execute function
165 | 		execute: (args, context) => {
166 | 			const { log, session } = context;
167 | 
168 | 			try {
169 | 				log.info &&
170 | 					log.info(
171 | 						`Analyzing task complexity with args: ${JSON.stringify(args)}`
172 | 					);
173 | 
174 | 				// Get project root
175 | 				const rootFolder = mockGetProjectRootFromSession(session, log);
176 | 
177 | 				// Call analyzeTaskComplexityDirect
178 | 				const result = mockAnalyzeTaskComplexityDirect(
179 | 					{
180 | 						...args,
181 | 						projectRoot: rootFolder
182 | 					},
183 | 					log,
184 | 					{ session }
185 | 				);
186 | 
187 | 				// Handle result
188 | 				return mockHandleApiResult(result, log);
189 | 			} catch (error) {
190 | 				log.error && log.error(`Error in analyze tool: ${error.message}`);
191 | 				return mockCreateErrorResponse(error.message);
192 | 			}
193 | 		}
194 | 	};
195 | 
196 | 	// Register the tool with the server
197 | 	server.addTool(toolConfig);
198 | };
199 | 
200 | describe('MCP Tool: analyze_project_complexity', () => {
201 | 	// Create mock server
202 | 	let mockServer;
203 | 	let executeFunction;
204 | 
205 | 	// Create mock logger
206 | 	const mockLogger = {
207 | 		debug: jest.fn(),
208 | 		info: jest.fn(),
209 | 		warn: jest.fn(),
210 | 		error: jest.fn()
211 | 	};
212 | 
213 | 	// Test data
214 | 	const validArgs = {
215 | 		output: 'output/path/report.json',
216 | 		model: 'claude-3-opus-20240229',
217 | 		threshold: 5,
218 | 		research: true
219 | 	};
220 | 
221 | 	// Standard responses
222 | 	const successResponse = {
223 | 		success: true,
224 | 		data: {
225 | 			message: 'Task complexity analysis complete',
226 | 			reportPath: '/mock/project/root/output/path/report.json',
227 | 			reportSummary: {
228 | 				taskCount: 10,
229 | 				highComplexityTasks: 3,
230 | 				mediumComplexityTasks: 5,
231 | 				lowComplexityTasks: 2
232 | 			}
233 | 		}
234 | 	};
235 | 
236 | 	const errorResponse = {
237 | 		success: false,
238 | 		error: {
239 | 			code: 'ANALYZE_ERROR',
240 | 			message: 'Failed to analyze task complexity'
241 | 		}
242 | 	};
243 | 
244 | 	beforeEach(() => {
245 | 		// Reset all mocks
246 | 		jest.clearAllMocks();
247 | 
248 | 		// Create mock server
249 | 		mockServer = {
250 | 			addTool: jest.fn((config) => {
251 | 				executeFunction = config.execute;
252 | 			})
253 | 		};
254 | 
255 | 		// Setup default successful response
256 | 		mockAnalyzeTaskComplexityDirect.mockReturnValue(successResponse);
257 | 
258 | 		// Register the tool
259 | 		registerAnalyzeTool(mockServer);
260 | 	});
261 | 
262 | 	test('should register the tool correctly', () => {
263 | 		// Verify tool was registered
264 | 		expect(mockServer.addTool).toHaveBeenCalledWith(
265 | 			expect.objectContaining({
266 | 				name: 'analyze_project_complexity',
267 | 				description:
268 | 					'Analyze task complexity and generate expansion recommendations',
269 | 				parameters: expect.any(Object),
270 | 				execute: expect.any(Function)
271 | 			})
272 | 		);
273 | 
274 | 		// Verify the tool config was passed
275 | 		const toolConfig = mockServer.addTool.mock.calls[0][0];
276 | 		expect(toolConfig).toHaveProperty('parameters');
277 | 		expect(toolConfig).toHaveProperty('execute');
278 | 	});
279 | 
280 | 	test('should execute the tool with valid threshold as number', () => {
281 | 		// Setup context
282 | 		const mockContext = {
283 | 			log: mockLogger,
284 | 			session: { workingDirectory: '/mock/dir' }
285 | 		};
286 | 
287 | 		// Test with valid numeric threshold
288 | 		const args = { ...validArgs, threshold: 7 };
289 | 		executeFunction(args, mockContext);
290 | 
291 | 		// Verify analyzeTaskComplexityDirect was called with correct arguments
292 | 		expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
293 | 			expect.objectContaining({
294 | 				threshold: 7,
295 | 				projectRoot: '/mock/project/root'
296 | 			}),
297 | 			mockLogger,
298 | 			{ session: mockContext.session }
299 | 		);
300 | 
301 | 		// Verify handleApiResult was called
302 | 		expect(mockHandleApiResult).toHaveBeenCalledWith(
303 | 			successResponse,
304 | 			mockLogger
305 | 		);
306 | 	});
307 | 
308 | 	test('should execute the tool with valid threshold as string', () => {
309 | 		// Setup context
310 | 		const mockContext = {
311 | 			log: mockLogger,
312 | 			session: { workingDirectory: '/mock/dir' }
313 | 		};
314 | 
315 | 		// Test with valid string threshold
316 | 		const args = { ...validArgs, threshold: '7' };
317 | 		executeFunction(args, mockContext);
318 | 
319 | 		// The mock doesn't actually coerce the string, just verify that the string is passed correctly
320 | 		expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
321 | 			expect.objectContaining({
322 | 				threshold: '7', // Expect string value, not coerced to number in our mock
323 | 				projectRoot: '/mock/project/root'
324 | 			}),
325 | 			mockLogger,
326 | 			{ session: mockContext.session }
327 | 		);
328 | 	});
329 | 
330 | 	test('should execute the tool with decimal threshold', () => {
331 | 		// Setup context
332 | 		const mockContext = {
333 | 			log: mockLogger,
334 | 			session: { workingDirectory: '/mock/dir' }
335 | 		};
336 | 
337 | 		// Test with decimal threshold
338 | 		const args = { ...validArgs, threshold: 6.5 };
339 | 		executeFunction(args, mockContext);
340 | 
341 | 		// Verify it was passed correctly
342 | 		expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
343 | 			expect.objectContaining({
344 | 				threshold: 6.5,
345 | 				projectRoot: '/mock/project/root'
346 | 			}),
347 | 			mockLogger,
348 | 			{ session: mockContext.session }
349 | 		);
350 | 	});
351 | 
352 | 	test('should execute the tool without threshold parameter', () => {
353 | 		// Setup context
354 | 		const mockContext = {
355 | 			log: mockLogger,
356 | 			session: { workingDirectory: '/mock/dir' }
357 | 		};
358 | 
359 | 		// Test without threshold (should use default)
360 | 		const { threshold, ...argsWithoutThreshold } = validArgs;
361 | 		executeFunction(argsWithoutThreshold, mockContext);
362 | 
363 | 		// Verify threshold is undefined
364 | 		expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
365 | 			expect.objectContaining({
366 | 				projectRoot: '/mock/project/root'
367 | 			}),
368 | 			mockLogger,
369 | 			{ session: mockContext.session }
370 | 		);
371 | 
372 | 		// Check threshold is not included
373 | 		const callArgs = mockAnalyzeTaskComplexityDirect.mock.calls[0][0];
374 | 		expect(callArgs).not.toHaveProperty('threshold');
375 | 	});
376 | 
377 | 	test('should handle errors from analyzeTaskComplexityDirect', () => {
378 | 		// Setup error response
379 | 		mockAnalyzeTaskComplexityDirect.mockReturnValueOnce(errorResponse);
380 | 
381 | 		// Setup context
382 | 		const mockContext = {
383 | 			log: mockLogger,
384 | 			session: { workingDirectory: '/mock/dir' }
385 | 		};
386 | 
387 | 		// Execute the function
388 | 		executeFunction(validArgs, mockContext);
389 | 
390 | 		// Verify analyzeTaskComplexityDirect was called
391 | 		expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalled();
392 | 
393 | 		// Verify handleApiResult was called with error response
394 | 		expect(mockHandleApiResult).toHaveBeenCalledWith(errorResponse, mockLogger);
395 | 	});
396 | 
397 | 	test('should handle unexpected errors', () => {
398 | 		// Setup error
399 | 		const testError = new Error('Unexpected error');
400 | 		mockAnalyzeTaskComplexityDirect.mockImplementationOnce(() => {
401 | 			throw testError;
402 | 		});
403 | 
404 | 		// Setup context
405 | 		const mockContext = {
406 | 			log: mockLogger,
407 | 			session: { workingDirectory: '/mock/dir' }
408 | 		};
409 | 
410 | 		// Execute the function
411 | 		executeFunction(validArgs, mockContext);
412 | 
413 | 		// Verify error was logged
414 | 		expect(mockLogger.error).toHaveBeenCalledWith(
415 | 			'Error in analyze tool: Unexpected error'
416 | 		);
417 | 
418 | 		// Verify error response was created
419 | 		expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
420 | 	});
421 | 
422 | 	test('should verify research parameter is correctly passed', () => {
423 | 		// Setup context
424 | 		const mockContext = {
425 | 			log: mockLogger,
426 | 			session: { workingDirectory: '/mock/dir' }
427 | 		};
428 | 
429 | 		// Test with research=true
430 | 		executeFunction(
431 | 			{
432 | 				...validArgs,
433 | 				research: true
434 | 			},
435 | 			mockContext
436 | 		);
437 | 
438 | 		// Verify analyzeTaskComplexityDirect was called with research=true
439 | 		expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
440 | 			expect.objectContaining({
441 | 				research: true
442 | 			}),
443 | 			expect.any(Object),
444 | 			expect.any(Object)
445 | 		);
446 | 
447 | 		// Reset mocks
448 | 		jest.clearAllMocks();
449 | 
450 | 		// Test with research=false
451 | 		executeFunction(
452 | 			{
453 | 				...validArgs,
454 | 				research: false
455 | 			},
456 | 			mockContext
457 | 		);
458 | 
459 | 		// Verify analyzeTaskComplexityDirect was called with research=false
460 | 		expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
461 | 			expect.objectContaining({
462 | 				research: false
463 | 			}),
464 | 			expect.any(Object),
465 | 			expect.any(Object)
466 | 		);
467 | 	});
468 | });
469 | 
```

--------------------------------------------------------------------------------
/tests/unit/mcp/tools/tool-registration.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * tool-registration.test.js
  3 |  * Comprehensive unit tests for the Task Master MCP tool registration system
  4 |  * Tests environment variable control system covering all configuration modes and edge cases
  5 |  */
  6 | 
  7 | import {
  8 | 	describe,
  9 | 	it,
 10 | 	expect,
 11 | 	beforeEach,
 12 | 	afterEach,
 13 | 	jest
 14 | } from '@jest/globals';
 15 | 
 16 | import {
 17 | 	EXPECTED_TOOL_COUNTS,
 18 | 	EXPECTED_CORE_TOOLS,
 19 | 	validateToolCounts,
 20 | 	validateToolStructure
 21 | } from '../../../helpers/tool-counts.js';
 22 | 
 23 | import { registerTaskMasterTools } from '../../../../mcp-server/src/tools/index.js';
 24 | import {
 25 | 	toolRegistry,
 26 | 	coreTools,
 27 | 	standardTools
 28 | } from '../../../../mcp-server/src/tools/tool-registry.js';
 29 | 
 30 | // Derive constants from imported registry to avoid brittle magic numbers
 31 | const ALL_COUNT = Object.keys(toolRegistry).length;
 32 | const CORE_COUNT = coreTools.length;
 33 | const STANDARD_COUNT = standardTools.length;
 34 | 
 35 | describe('Task Master Tool Registration System', () => {
 36 | 	let mockServer;
 37 | 	let originalEnv;
 38 | 
 39 | 	beforeEach(() => {
 40 | 		originalEnv = process.env.TASK_MASTER_TOOLS;
 41 | 
 42 | 		mockServer = {
 43 | 			tools: [],
 44 | 			addTool: jest.fn((tool) => {
 45 | 				mockServer.tools.push(tool);
 46 | 				return tool;
 47 | 			})
 48 | 		};
 49 | 
 50 | 		delete process.env.TASK_MASTER_TOOLS;
 51 | 	});
 52 | 
 53 | 	afterEach(() => {
 54 | 		if (originalEnv !== undefined) {
 55 | 			process.env.TASK_MASTER_TOOLS = originalEnv;
 56 | 		} else {
 57 | 			delete process.env.TASK_MASTER_TOOLS;
 58 | 		}
 59 | 
 60 | 		jest.clearAllMocks();
 61 | 	});
 62 | 
 63 | 	describe('Test Environment Setup', () => {
 64 | 		it('should have properly configured mock server', () => {
 65 | 			expect(mockServer).toBeDefined();
 66 | 			expect(typeof mockServer.addTool).toBe('function');
 67 | 			expect(Array.isArray(mockServer.tools)).toBe(true);
 68 | 			expect(mockServer.tools.length).toBe(0);
 69 | 		});
 70 | 
 71 | 		it('should have correct tool registry structure', () => {
 72 | 			const validation = validateToolCounts();
 73 | 			expect(validation.isValid).toBe(true);
 74 | 
 75 | 			if (!validation.isValid) {
 76 | 				console.error('Tool count validation failed:', validation);
 77 | 			}
 78 | 
 79 | 			expect(validation.actual.total).toBe(EXPECTED_TOOL_COUNTS.total);
 80 | 			expect(validation.actual.core).toBe(EXPECTED_TOOL_COUNTS.core);
 81 | 			expect(validation.actual.standard).toBe(EXPECTED_TOOL_COUNTS.standard);
 82 | 		});
 83 | 
 84 | 		it('should have correct core tools', () => {
 85 | 			const structure = validateToolStructure();
 86 | 			expect(structure.isValid).toBe(true);
 87 | 
 88 | 			if (!structure.isValid) {
 89 | 				console.error('Tool structure validation failed:', structure);
 90 | 			}
 91 | 
 92 | 			expect(coreTools).toEqual(expect.arrayContaining(EXPECTED_CORE_TOOLS));
 93 | 			expect(coreTools.length).toBe(EXPECTED_TOOL_COUNTS.core);
 94 | 		});
 95 | 
 96 | 		it('should have correct standard tools that include all core tools', () => {
 97 | 			const structure = validateToolStructure();
 98 | 			expect(structure.details.coreInStandard).toBe(true);
 99 | 			expect(standardTools.length).toBe(EXPECTED_TOOL_COUNTS.standard);
100 | 
101 | 			coreTools.forEach((tool) => {
102 | 				expect(standardTools).toContain(tool);
103 | 			});
104 | 		});
105 | 
106 | 		it('should have all expected tools in registry', () => {
107 | 			const expectedTools = [
108 | 				'initialize_project',
109 | 				'models',
110 | 				'research',
111 | 				'add_tag',
112 | 				'delete_tag',
113 | 				'get_tasks',
114 | 				'next_task',
115 | 				'get_task'
116 | 			];
117 | 			expectedTools.forEach((tool) => {
118 | 				expect(toolRegistry).toHaveProperty(tool);
119 | 			});
120 | 		});
121 | 	});
122 | 
123 | 	describe('Configuration Modes', () => {
124 | 		it(`should register all tools (${ALL_COUNT}) when TASK_MASTER_TOOLS is not set (default behavior)`, () => {
125 | 			delete process.env.TASK_MASTER_TOOLS;
126 | 
127 | 			registerTaskMasterTools(mockServer);
128 | 
129 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(
130 | 				EXPECTED_TOOL_COUNTS.total
131 | 			);
132 | 		});
133 | 
134 | 		it(`should register all tools (${ALL_COUNT}) when TASK_MASTER_TOOLS=all`, () => {
135 | 			process.env.TASK_MASTER_TOOLS = 'all';
136 | 
137 | 			registerTaskMasterTools(mockServer);
138 | 
139 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
140 | 		});
141 | 
142 | 		it(`should register exactly ${CORE_COUNT} core tools when TASK_MASTER_TOOLS=core`, () => {
143 | 			process.env.TASK_MASTER_TOOLS = 'core';
144 | 
145 | 			registerTaskMasterTools(mockServer, 'core');
146 | 
147 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(
148 | 				EXPECTED_TOOL_COUNTS.core
149 | 			);
150 | 		});
151 | 
152 | 		it(`should register exactly ${STANDARD_COUNT} standard tools when TASK_MASTER_TOOLS=standard`, () => {
153 | 			process.env.TASK_MASTER_TOOLS = 'standard';
154 | 
155 | 			registerTaskMasterTools(mockServer, 'standard');
156 | 
157 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(
158 | 				EXPECTED_TOOL_COUNTS.standard
159 | 			);
160 | 		});
161 | 
162 | 		it(`should treat lean as alias for core mode (${CORE_COUNT} tools)`, () => {
163 | 			process.env.TASK_MASTER_TOOLS = 'lean';
164 | 
165 | 			registerTaskMasterTools(mockServer, 'lean');
166 | 
167 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT);
168 | 		});
169 | 
170 | 		it('should handle case insensitive configuration values', () => {
171 | 			process.env.TASK_MASTER_TOOLS = 'CORE';
172 | 
173 | 			registerTaskMasterTools(mockServer, 'CORE');
174 | 
175 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT);
176 | 		});
177 | 	});
178 | 
179 | 	describe('Custom Tool Selection and Edge Cases', () => {
180 | 		it('should register specific tools from comma-separated list', () => {
181 | 			process.env.TASK_MASTER_TOOLS = 'get_tasks,next_task,get_task';
182 | 
183 | 			registerTaskMasterTools(mockServer, 'get_tasks,next_task,get_task');
184 | 
185 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(3);
186 | 		});
187 | 
188 | 		it('should handle mixed valid and invalid tool names gracefully', () => {
189 | 			process.env.TASK_MASTER_TOOLS =
190 | 				'invalid_tool,get_tasks,fake_tool,next_task';
191 | 
192 | 			registerTaskMasterTools(
193 | 				mockServer,
194 | 				'invalid_tool,get_tasks,fake_tool,next_task'
195 | 			);
196 | 
197 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(2);
198 | 		});
199 | 
200 | 		it('should default to all tools with completely invalid input', () => {
201 | 			process.env.TASK_MASTER_TOOLS = 'completely_invalid';
202 | 
203 | 			registerTaskMasterTools(mockServer);
204 | 
205 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
206 | 		});
207 | 
208 | 		it('should handle empty string environment variable', () => {
209 | 			process.env.TASK_MASTER_TOOLS = '';
210 | 
211 | 			registerTaskMasterTools(mockServer);
212 | 
213 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
214 | 		});
215 | 
216 | 		it('should handle whitespace in comma-separated lists', () => {
217 | 			process.env.TASK_MASTER_TOOLS = ' get_tasks , next_task , get_task ';
218 | 
219 | 			registerTaskMasterTools(mockServer, ' get_tasks , next_task , get_task ');
220 | 
221 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(3);
222 | 		});
223 | 
224 | 		it('should ignore duplicate tools in list', () => {
225 | 			process.env.TASK_MASTER_TOOLS = 'get_tasks,get_tasks,next_task,get_tasks';
226 | 
227 | 			registerTaskMasterTools(
228 | 				mockServer,
229 | 				'get_tasks,get_tasks,next_task,get_tasks'
230 | 			);
231 | 
232 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(2);
233 | 		});
234 | 
235 | 		it('should handle only commas and empty entries', () => {
236 | 			process.env.TASK_MASTER_TOOLS = ',,,';
237 | 
238 | 			registerTaskMasterTools(mockServer);
239 | 
240 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
241 | 		});
242 | 
243 | 		it('should handle single tool selection', () => {
244 | 			process.env.TASK_MASTER_TOOLS = 'get_tasks';
245 | 
246 | 			registerTaskMasterTools(mockServer, 'get_tasks');
247 | 
248 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(1);
249 | 		});
250 | 	});
251 | 
252 | 	describe('Coverage Analysis and Integration Tests', () => {
253 | 		it('should provide 100% code coverage for environment control logic', () => {
254 | 			const testCases = [
255 | 				{
256 | 					env: undefined,
257 | 					expectedCount: ALL_COUNT,
258 | 					description: 'undefined env (all)'
259 | 				},
260 | 				{
261 | 					env: '',
262 | 					expectedCount: ALL_COUNT,
263 | 					description: 'empty string (all)'
264 | 				},
265 | 				{ env: 'all', expectedCount: ALL_COUNT, description: 'all mode' },
266 | 				{ env: 'core', expectedCount: CORE_COUNT, description: 'core mode' },
267 | 				{
268 | 					env: 'lean',
269 | 					expectedCount: CORE_COUNT,
270 | 					description: 'lean mode (alias)'
271 | 				},
272 | 				{
273 | 					env: 'standard',
274 | 					expectedCount: STANDARD_COUNT,
275 | 					description: 'standard mode'
276 | 				},
277 | 				{
278 | 					env: 'get_tasks,next_task',
279 | 					expectedCount: 2,
280 | 					description: 'custom list'
281 | 				},
282 | 				{
283 | 					env: 'invalid_tool',
284 | 					expectedCount: ALL_COUNT,
285 | 					description: 'invalid fallback'
286 | 				}
287 | 			];
288 | 
289 | 			testCases.forEach((testCase) => {
290 | 				delete process.env.TASK_MASTER_TOOLS;
291 | 				if (testCase.env !== undefined) {
292 | 					process.env.TASK_MASTER_TOOLS = testCase.env;
293 | 				}
294 | 
295 | 				mockServer.tools = [];
296 | 				mockServer.addTool.mockClear();
297 | 
298 | 				registerTaskMasterTools(mockServer, testCase.env || 'all');
299 | 
300 | 				expect(mockServer.addTool).toHaveBeenCalledTimes(
301 | 					testCase.expectedCount
302 | 				);
303 | 			});
304 | 		});
305 | 
306 | 		it('should have optimal performance characteristics', () => {
307 | 			const startTime = Date.now();
308 | 
309 | 			process.env.TASK_MASTER_TOOLS = 'all';
310 | 
311 | 			registerTaskMasterTools(mockServer);
312 | 
313 | 			const endTime = Date.now();
314 | 			const executionTime = endTime - startTime;
315 | 
316 | 			expect(executionTime).toBeLessThan(100);
317 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
318 | 		});
319 | 
320 | 		it('should validate token reduction claims', () => {
321 | 			expect(coreTools.length).toBeLessThan(standardTools.length);
322 | 			expect(standardTools.length).toBeLessThan(
323 | 				Object.keys(toolRegistry).length
324 | 			);
325 | 
326 | 			expect(coreTools.length).toBe(CORE_COUNT);
327 | 			expect(standardTools.length).toBe(STANDARD_COUNT);
328 | 			expect(Object.keys(toolRegistry).length).toBe(ALL_COUNT);
329 | 
330 | 			const allToolsCount = Object.keys(toolRegistry).length;
331 | 			const coreReduction =
332 | 				((allToolsCount - coreTools.length) / allToolsCount) * 100;
333 | 			const standardReduction =
334 | 				((allToolsCount - standardTools.length) / allToolsCount) * 100;
335 | 
336 | 			expect(coreReduction).toBeGreaterThan(80);
337 | 			expect(standardReduction).toBeGreaterThan(50);
338 | 		});
339 | 
340 | 		it('should maintain referential integrity of tool registry', () => {
341 | 			coreTools.forEach((tool) => {
342 | 				expect(standardTools).toContain(tool);
343 | 			});
344 | 
345 | 			standardTools.forEach((tool) => {
346 | 				expect(toolRegistry).toHaveProperty(tool);
347 | 			});
348 | 
349 | 			Object.keys(toolRegistry).forEach((tool) => {
350 | 				expect(typeof toolRegistry[tool]).toBe('function');
351 | 			});
352 | 		});
353 | 
354 | 		it('should handle concurrent registration attempts', () => {
355 | 			process.env.TASK_MASTER_TOOLS = 'core';
356 | 
357 | 			registerTaskMasterTools(mockServer, 'core');
358 | 			registerTaskMasterTools(mockServer, 'core');
359 | 			registerTaskMasterTools(mockServer, 'core');
360 | 
361 | 			expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT * 3);
362 | 		});
363 | 
364 | 		it('should validate all documented tool categories exist', () => {
365 | 			const allTools = Object.keys(toolRegistry);
366 | 
367 | 			const projectSetupTools = allTools.filter((tool) =>
368 | 				['initialize_project', 'models', 'rules', 'parse_prd'].includes(tool)
369 | 			);
370 | 			expect(projectSetupTools.length).toBeGreaterThan(0);
371 | 
372 | 			const taskManagementTools = allTools.filter((tool) =>
373 | 				['get_tasks', 'get_task', 'next_task', 'set_task_status'].includes(tool)
374 | 			);
375 | 			expect(taskManagementTools.length).toBeGreaterThan(0);
376 | 
377 | 			const analysisTools = allTools.filter((tool) =>
378 | 				['analyze_project_complexity', 'complexity_report'].includes(tool)
379 | 			);
380 | 			expect(analysisTools.length).toBeGreaterThan(0);
381 | 
382 | 			const tagManagementTools = allTools.filter((tool) =>
383 | 				['add_tag', 'delete_tag', 'list_tags', 'use_tag'].includes(tool)
384 | 			);
385 | 			expect(tagManagementTools.length).toBeGreaterThan(0);
386 | 		});
387 | 
388 | 		it('should handle error conditions gracefully', () => {
389 | 			const problematicInputs = [
390 | 				'null',
391 | 				'undefined',
392 | 				'   ',
393 | 				'\n\t',
394 | 				'special!@#$%^&*()characters',
395 | 				'very,very,very,very,very,very,very,long,comma,separated,list,with,invalid,tools,that,should,fallback,to,all'
396 | 			];
397 | 
398 | 			problematicInputs.forEach((input) => {
399 | 				mockServer.tools = [];
400 | 				mockServer.addTool.mockClear();
401 | 
402 | 				process.env.TASK_MASTER_TOOLS = input;
403 | 
404 | 				expect(() => registerTaskMasterTools(mockServer)).not.toThrow();
405 | 
406 | 				expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
407 | 			});
408 | 		});
409 | 	});
410 | });
411 | 
```

--------------------------------------------------------------------------------
/apps/extension/src/utils/task-master-api/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * TaskMaster API
  3 |  * Main API class that coordinates all modules
  4 |  */
  5 | 
  6 | import * as vscode from 'vscode';
  7 | import { ExtensionLogger } from '../logger';
  8 | import type { MCPClientManager } from '../mcpClient';
  9 | import { CacheManager } from './cache/cache-manager';
 10 | import { MCPClient } from './mcp-client';
 11 | import { TaskTransformer } from './transformers/task-transformer';
 12 | import type {
 13 | 	AddSubtaskOptions,
 14 | 	CacheConfig,
 15 | 	GetTasksOptions,
 16 | 	SubtaskData,
 17 | 	TaskMasterApiConfig,
 18 | 	TaskMasterApiResponse,
 19 | 	TaskMasterTask,
 20 | 	TaskUpdate,
 21 | 	UpdateSubtaskOptions,
 22 | 	UpdateTaskOptions,
 23 | 	UpdateTaskStatusOptions
 24 | } from './types';
 25 | 
 26 | // Re-export types for backward compatibility
 27 | export * from './types';
 28 | 
 29 | export class TaskMasterApi {
 30 | 	private mcpWrapper: MCPClient;
 31 | 	private cache: CacheManager;
 32 | 	private transformer: TaskTransformer;
 33 | 	private config: TaskMasterApiConfig;
 34 | 	private logger: ExtensionLogger;
 35 | 
 36 | 	private readonly defaultCacheConfig: CacheConfig = {
 37 | 		maxSize: 100,
 38 | 		enableBackgroundRefresh: true,
 39 | 		refreshInterval: 5 * 60 * 1000, // 5 minutes
 40 | 		enableAnalytics: true,
 41 | 		enablePrefetch: true,
 42 | 		compressionEnabled: false,
 43 | 		persistToDisk: false
 44 | 	};
 45 | 
 46 | 	private readonly defaultConfig: TaskMasterApiConfig = {
 47 | 		timeout: 30000,
 48 | 		retryAttempts: 3,
 49 | 		cacheDuration: 5 * 60 * 1000, // 5 minutes
 50 | 		cache: this.defaultCacheConfig
 51 | 	};
 52 | 
 53 | 	constructor(
 54 | 		mcpClient: MCPClientManager,
 55 | 		config?: Partial<TaskMasterApiConfig>
 56 | 	) {
 57 | 		this.logger = ExtensionLogger.getInstance();
 58 | 
 59 | 		// Merge config - ensure cache is always fully defined
 60 | 		const mergedCache: CacheConfig = {
 61 | 			maxSize: config?.cache?.maxSize ?? this.defaultCacheConfig.maxSize,
 62 | 			enableBackgroundRefresh:
 63 | 				config?.cache?.enableBackgroundRefresh ??
 64 | 				this.defaultCacheConfig.enableBackgroundRefresh,
 65 | 			refreshInterval:
 66 | 				config?.cache?.refreshInterval ??
 67 | 				this.defaultCacheConfig.refreshInterval,
 68 | 			enableAnalytics:
 69 | 				config?.cache?.enableAnalytics ??
 70 | 				this.defaultCacheConfig.enableAnalytics,
 71 | 			enablePrefetch:
 72 | 				config?.cache?.enablePrefetch ?? this.defaultCacheConfig.enablePrefetch,
 73 | 			compressionEnabled:
 74 | 				config?.cache?.compressionEnabled ??
 75 | 				this.defaultCacheConfig.compressionEnabled,
 76 | 			persistToDisk:
 77 | 				config?.cache?.persistToDisk ?? this.defaultCacheConfig.persistToDisk
 78 | 		};
 79 | 
 80 | 		this.config = {
 81 | 			...this.defaultConfig,
 82 | 			...config,
 83 | 			cache: mergedCache
 84 | 		};
 85 | 
 86 | 		// Initialize modules
 87 | 		this.mcpWrapper = new MCPClient(mcpClient, this.logger, {
 88 | 			timeout: this.config.timeout,
 89 | 			retryAttempts: this.config.retryAttempts
 90 | 		});
 91 | 
 92 | 		this.cache = new CacheManager(
 93 | 			{ ...mergedCache, cacheDuration: this.config.cacheDuration },
 94 | 			this.logger
 95 | 		);
 96 | 
 97 | 		this.transformer = new TaskTransformer(this.logger);
 98 | 
 99 | 		// Start background refresh if enabled
100 | 		if (this.config.cache?.enableBackgroundRefresh) {
101 | 			this.startBackgroundRefresh();
102 | 		}
103 | 
104 | 		this.logger.log('TaskMasterApi: Initialized with modular architecture');
105 | 	}
106 | 
107 | 	/**
108 | 	 * Get tasks from TaskMaster
109 | 	 */
110 | 	async getTasks(
111 | 		options?: GetTasksOptions
112 | 	): Promise<TaskMasterApiResponse<TaskMasterTask[]>> {
113 | 		const startTime = Date.now();
114 | 		const cacheKey = `get_tasks_${JSON.stringify(options || {})}`;
115 | 
116 | 		try {
117 | 			// Check cache first
118 | 			const cached = this.cache.get(cacheKey);
119 | 			if (cached) {
120 | 				return {
121 | 					success: true,
122 | 					data: cached,
123 | 					requestDuration: Date.now() - startTime
124 | 				};
125 | 			}
126 | 
127 | 			// Prepare MCP tool arguments
128 | 			const mcpArgs: Record<string, unknown> = {
129 | 				projectRoot: options?.projectRoot || this.getWorkspaceRoot(),
130 | 				withSubtasks: options?.withSubtasks ?? true
131 | 			};
132 | 
133 | 			if (options?.status) {
134 | 				mcpArgs.status = options.status;
135 | 			}
136 | 			if (options?.tag) {
137 | 				mcpArgs.tag = options.tag;
138 | 			}
139 | 
140 | 			this.logger.log('Calling get_tasks with args:', mcpArgs);
141 | 
142 | 			// Call MCP tool
143 | 			const mcpResponse = await this.mcpWrapper.callTool('get_tasks', mcpArgs);
144 | 
145 | 			// Transform response
146 | 			const transformedTasks =
147 | 				this.transformer.transformMCPTasksResponse(mcpResponse);
148 | 
149 | 			// Cache the result
150 | 			this.cache.set(cacheKey, transformedTasks);
151 | 
152 | 			return {
153 | 				success: true,
154 | 				data: transformedTasks,
155 | 				requestDuration: Date.now() - startTime
156 | 			};
157 | 		} catch (error) {
158 | 			this.logger.error('Error getting tasks:', error);
159 | 			return {
160 | 				success: false,
161 | 				error: error instanceof Error ? error.message : 'Unknown error',
162 | 				requestDuration: Date.now() - startTime
163 | 			};
164 | 		}
165 | 	}
166 | 
167 | 	/**
168 | 	 * Update task status
169 | 	 */
170 | 	async updateTaskStatus(
171 | 		taskId: string,
172 | 		status: string,
173 | 		options?: UpdateTaskStatusOptions
174 | 	): Promise<TaskMasterApiResponse<boolean>> {
175 | 		const startTime = Date.now();
176 | 
177 | 		try {
178 | 			const mcpArgs: Record<string, unknown> = {
179 | 				id: String(taskId),
180 | 				status: status,
181 | 				projectRoot: options?.projectRoot || this.getWorkspaceRoot()
182 | 			};
183 | 
184 | 			this.logger.log('Calling set_task_status with args:', mcpArgs);
185 | 
186 | 			await this.mcpWrapper.callTool('set_task_status', mcpArgs);
187 | 
188 | 			// Clear relevant caches
189 | 			this.cache.clearPattern('get_tasks');
190 | 
191 | 			return {
192 | 				success: true,
193 | 				data: true,
194 | 				requestDuration: Date.now() - startTime
195 | 			};
196 | 		} catch (error) {
197 | 			this.logger.error('Error updating task status:', error);
198 | 			return {
199 | 				success: false,
200 | 				error: error instanceof Error ? error.message : 'Unknown error',
201 | 				requestDuration: Date.now() - startTime
202 | 			};
203 | 		}
204 | 	}
205 | 
206 | 	/**
207 | 	 * Update task content
208 | 	 */
209 | 	async updateTask(
210 | 		taskId: string,
211 | 		updates: TaskUpdate,
212 | 		options?: UpdateTaskOptions
213 | 	): Promise<TaskMasterApiResponse<boolean>> {
214 | 		const startTime = Date.now();
215 | 
216 | 		try {
217 | 			// Build update prompt
218 | 			const updateFields: string[] = [];
219 | 			if (updates.title !== undefined) {
220 | 				updateFields.push(`Title: ${updates.title}`);
221 | 			}
222 | 			if (updates.description !== undefined) {
223 | 				updateFields.push(`Description: ${updates.description}`);
224 | 			}
225 | 			if (updates.details !== undefined) {
226 | 				updateFields.push(`Details: ${updates.details}`);
227 | 			}
228 | 			if (updates.priority !== undefined) {
229 | 				updateFields.push(`Priority: ${updates.priority}`);
230 | 			}
231 | 			if (updates.testStrategy !== undefined) {
232 | 				updateFields.push(`Test Strategy: ${updates.testStrategy}`);
233 | 			}
234 | 			if (updates.dependencies !== undefined) {
235 | 				updateFields.push(`Dependencies: ${updates.dependencies.join(', ')}`);
236 | 			}
237 | 
238 | 			const prompt = `Update task with the following changes:\n${updateFields.join('\n')}`;
239 | 
240 | 			const mcpArgs: Record<string, unknown> = {
241 | 				id: String(taskId),
242 | 				prompt: prompt,
243 | 				projectRoot: options?.projectRoot || this.getWorkspaceRoot()
244 | 			};
245 | 
246 | 			if (options?.append !== undefined) {
247 | 				mcpArgs.append = options.append;
248 | 			}
249 | 			if (options?.research !== undefined) {
250 | 				mcpArgs.research = options.research;
251 | 			}
252 | 
253 | 			this.logger.log('Calling update_task with args:', mcpArgs);
254 | 
255 | 			await this.mcpWrapper.callTool('update_task', mcpArgs);
256 | 
257 | 			// Clear relevant caches
258 | 			this.cache.clearPattern('get_tasks');
259 | 
260 | 			return {
261 | 				success: true,
262 | 				data: true,
263 | 				requestDuration: Date.now() - startTime
264 | 			};
265 | 		} catch (error) {
266 | 			this.logger.error('Error updating task:', error);
267 | 			return {
268 | 				success: false,
269 | 				error: error instanceof Error ? error.message : 'Unknown error',
270 | 				requestDuration: Date.now() - startTime
271 | 			};
272 | 		}
273 | 	}
274 | 
275 | 	/**
276 | 	 * Update subtask content
277 | 	 */
278 | 	async updateSubtask(
279 | 		taskId: string,
280 | 		prompt: string,
281 | 		options?: UpdateSubtaskOptions
282 | 	): Promise<TaskMasterApiResponse<boolean>> {
283 | 		const startTime = Date.now();
284 | 
285 | 		try {
286 | 			const mcpArgs: Record<string, unknown> = {
287 | 				id: String(taskId),
288 | 				prompt: prompt,
289 | 				projectRoot: options?.projectRoot || this.getWorkspaceRoot()
290 | 			};
291 | 
292 | 			if (options?.research !== undefined) {
293 | 				mcpArgs.research = options.research;
294 | 			}
295 | 
296 | 			this.logger.log('Calling update_subtask with args:', mcpArgs);
297 | 
298 | 			await this.mcpWrapper.callTool('update_subtask', mcpArgs);
299 | 
300 | 			// Clear relevant caches
301 | 			this.cache.clearPattern('get_tasks');
302 | 
303 | 			return {
304 | 				success: true,
305 | 				data: true,
306 | 				requestDuration: Date.now() - startTime
307 | 			};
308 | 		} catch (error) {
309 | 			this.logger.error('Error updating subtask:', error);
310 | 			return {
311 | 				success: false,
312 | 				error: error instanceof Error ? error.message : 'Unknown error',
313 | 				requestDuration: Date.now() - startTime
314 | 			};
315 | 		}
316 | 	}
317 | 
318 | 	/**
319 | 	 * Add a new subtask
320 | 	 */
321 | 	async addSubtask(
322 | 		parentTaskId: string,
323 | 		subtaskData: SubtaskData,
324 | 		options?: AddSubtaskOptions
325 | 	): Promise<TaskMasterApiResponse<boolean>> {
326 | 		const startTime = Date.now();
327 | 
328 | 		try {
329 | 			const mcpArgs: Record<string, unknown> = {
330 | 				id: String(parentTaskId),
331 | 				title: subtaskData.title,
332 | 				projectRoot: options?.projectRoot || this.getWorkspaceRoot()
333 | 			};
334 | 
335 | 			if (subtaskData.description) {
336 | 				mcpArgs.description = subtaskData.description;
337 | 			}
338 | 			if (subtaskData.dependencies && subtaskData.dependencies.length > 0) {
339 | 				mcpArgs.dependencies = subtaskData.dependencies.join(',');
340 | 			}
341 | 			if (subtaskData.status) {
342 | 				mcpArgs.status = subtaskData.status;
343 | 			}
344 | 
345 | 			this.logger.log('Calling add_subtask with args:', mcpArgs);
346 | 
347 | 			await this.mcpWrapper.callTool('add_subtask', mcpArgs);
348 | 
349 | 			// Clear relevant caches
350 | 			this.cache.clearPattern('get_tasks');
351 | 
352 | 			return {
353 | 				success: true,
354 | 				data: true,
355 | 				requestDuration: Date.now() - startTime
356 | 			};
357 | 		} catch (error) {
358 | 			this.logger.error('Error adding subtask:', error);
359 | 			return {
360 | 				success: false,
361 | 				error: error instanceof Error ? error.message : 'Unknown error',
362 | 				requestDuration: Date.now() - startTime
363 | 			};
364 | 		}
365 | 	}
366 | 
367 | 	/**
368 | 	 * Get connection status
369 | 	 */
370 | 	getConnectionStatus(): { isConnected: boolean; error?: string } {
371 | 		const status = this.mcpWrapper.getStatus();
372 | 		return {
373 | 			isConnected: status.isRunning,
374 | 			error: status.error
375 | 		};
376 | 	}
377 | 
378 | 	/**
379 | 	 * Test connection
380 | 	 */
381 | 	async testConnection(): Promise<TaskMasterApiResponse<boolean>> {
382 | 		const startTime = Date.now();
383 | 
384 | 		try {
385 | 			const isConnected = await this.mcpWrapper.testConnection();
386 | 			return {
387 | 				success: true,
388 | 				data: isConnected,
389 | 				requestDuration: Date.now() - startTime
390 | 			};
391 | 		} catch (error) {
392 | 			this.logger.error('Connection test failed:', error);
393 | 			return {
394 | 				success: false,
395 | 				error:
396 | 					error instanceof Error ? error.message : 'Connection test failed',
397 | 				requestDuration: Date.now() - startTime
398 | 			};
399 | 		}
400 | 	}
401 | 
402 | 	/**
403 | 	 * Clear all cached data
404 | 	 */
405 | 	clearCache(): void {
406 | 		this.cache.clear();
407 | 	}
408 | 
409 | 	/**
410 | 	 * Get cache analytics
411 | 	 */
412 | 	getCacheAnalytics() {
413 | 		return this.cache.getAnalytics();
414 | 	}
415 | 
416 | 	/**
417 | 	 * Cleanup resources
418 | 	 */
419 | 	destroy(): void {
420 | 		this.cache.destroy();
421 | 		this.logger.log('TaskMasterApi: Destroyed and cleaned up resources');
422 | 	}
423 | 
424 | 	/**
425 | 	 * Start background refresh
426 | 	 */
427 | 	private startBackgroundRefresh(): void {
428 | 		const interval = this.config.cache?.refreshInterval || 5 * 60 * 1000;
429 | 		setInterval(() => {
430 | 			this.performBackgroundRefresh();
431 | 		}, interval);
432 | 	}
433 | 
434 | 	/**
435 | 	 * Perform background refresh of frequently accessed cache entries
436 | 	 */
437 | 	private async performBackgroundRefresh(): Promise<void> {
438 | 		if (!this.config.cache?.enableBackgroundRefresh) {
439 | 			return;
440 | 		}
441 | 
442 | 		this.logger.log('Starting background cache refresh');
443 | 		const candidates = this.cache.getRefreshCandidates();
444 | 
445 | 		let refreshedCount = 0;
446 | 		for (const [key, entry] of candidates) {
447 | 			try {
448 | 				const optionsMatch = key.match(/get_tasks_(.+)/);
449 | 				if (optionsMatch) {
450 | 					const options = JSON.parse(optionsMatch[1]);
451 | 					await this.getTasks(options);
452 | 					refreshedCount++;
453 | 					this.cache.incrementRefreshes();
454 | 				}
455 | 			} catch (error) {
456 | 				this.logger.warn(`Background refresh failed for key ${key}:`, error);
457 | 			}
458 | 		}
459 | 
460 | 		this.logger.log(
461 | 			`Background refresh completed, refreshed ${refreshedCount} entries`
462 | 		);
463 | 	}
464 | 
465 | 	/**
466 | 	 * Get workspace root path
467 | 	 */
468 | 	private getWorkspaceRoot(): string {
469 | 		return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
470 | 	}
471 | }
472 | 
```
Page 32/69FirstPrevNextLast