Compare commits

...

23 Commits

Author SHA1 Message Date
github-actions[bot]
501a2bf5f1 docs: auto-update documentation based on changes in next branch
This PR was automatically generated to update documentation based on recent changes.

  Original commit: feat: Add Cursor IDE custom slash commands support (#1215)\n\n\n

  Co-authored-by: Claude <claude-assistant@anthropic.com>
2025-09-26 17:29:01 +00:00
Joe Danziger
0079b7defd feat: Add Cursor IDE custom slash commands support (#1215) 2025-09-26 19:21:16 +02:00
Ralph Khreish
0b2c6967c4 fix: improve subtask & parent task management (#1251) 2025-09-26 11:04:38 +02:00
Ralph Khreish
c0682ac795 Merge pull request #1250 from eyaltoledano/chore/merge.main.september 2025-09-26 01:10:57 +02:00
Ralph Khreish
01a7faea8f Merge remote-tracking branch 'origin/main' into chore/merge.main.september 2025-09-26 01:10:12 +02:00
github-actions[bot]
b7f32eac5a Version Packages (#1249)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-09-26 01:06:52 +02:00
Ralph Khreish
044a7bfc98 fix: implement subtask status update functionality (#1248)
Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-09-26 01:01:55 +02:00
Ralph Khreish
814265cd33 chore: adjust CI to run on all PRs (#1244) 2025-09-24 20:19:09 +02:00
Ralph Khreish
9b7b2ca7b2 Merge pull request #1245 from eyaltoledano/ralph/chore/update.from.main 2025-09-24 20:14:00 +02:00
Ralph Khreish
949f091179 Merge remote-tracking branch 'origin/main' into ralph/chore/update.from.main 2025-09-24 20:10:19 +02:00
Ralph Khreish
32c2b03c23 Merge pull request #1242 from eyaltoledano/ralph/fix.main.merges
fix CI failing to release (#1232)
2025-09-24 14:28:33 +02:00
Ralph Khreish
3bfd999d81 Merge remote-tracking branch 'origin/main' into ralph/fix.main.merges 2025-09-24 14:28:09 +02:00
Ralph Khreish
9fa79eb026 chore: fix CI failing to release (#1232) 2025-09-24 14:26:41 +02:00
Jungwoo Song
875134247a Add Q Developer CLI at README.md (#1159) 2025-09-24 11:06:16 +02:00
Ralph Khreish
c2fc61ddb3 chore: mintlify fix broken links (#1237) 2025-09-23 18:32:51 +02:00
Ralph Khreish
aaacc3dae3 fix: improve docs and command help for analzye-complexity (#1235) 2025-09-23 18:19:32 +02:00
Ralph Khreish
46cd5dc186 fix: add installation instructions for claude-code mcp (#1236) 2025-09-23 18:16:40 +02:00
github-actions[bot]
49a31be416 docs: Auto-update and format models.md 2025-09-23 15:45:03 +00:00
JeonSeongHyeon
2b69936ee7 fix: update model ID for sonar deep research (#1192)
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-09-23 17:44:40 +02:00
Julien Pelletier
b5fe723f8e Fix/claude code path executable setting (#1172)
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-09-22 22:39:37 +02:00
olssonsten
d67b81d25d feat: add MCP timeout configuration for long-running operations (#1112) 2025-09-22 19:55:10 +02:00
Ralph Khreish
66c05053c0 Merge pull request #1231 from eyaltoledano/ralph/merge.from.main 2025-09-22 19:54:12 +02:00
Ralph Khreish
d7ab4609aa chore: fix CI 2025-09-22 19:25:44 +02:00
40 changed files with 963 additions and 89 deletions

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Improve `analyze-complexity` cli docs and `--research` flag documentation

View File

@@ -0,0 +1,7 @@
---
"task-master-ai": minor
---
Add Cursor IDE custom slash command support
Expose Task Master commands as Cursor slash commands by copying assets/claude/commands to .cursor/commands on profile add and cleaning up on remove.

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Change parent task back to "pending" when all subtasks are in "pending" state

View File

@@ -0,0 +1,13 @@
---
"task-master-ai": minor
---
Enhanced Roo Code profile with MCP timeout configuration for improved reliability during long-running AI operations. The Roo profile now automatically configures a 300-second timeout for MCP server operations, preventing timeouts during complex tasks like `parse-prd`, `expand-all`, `analyze-complexity`, and `research` operations. This change also replaces static MCP configuration files with programmatic generation for better maintainability.
**What's New:**
- 300-second timeout for MCP operations (up from default 60 seconds)
- Programmatic MCP configuration generation (replaces static asset files)
- Enhanced reliability for AI-powered operations
- Consistent with other AI coding assistant profiles
**Migration:** No user action required - existing Roo Code installations will automatically receive the enhanced MCP configuration on next initialization.

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Fix Claude Code settings validation for pathToClaudeCodeExecutable

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Fix sonar deep research model failing, should be called `sonar-deep-research`

View File

@@ -6,9 +6,6 @@ on:
- main
- next
pull_request:
branches:
- main
- next
workflow_dispatch:
concurrency:

View File

@@ -1,5 +1,13 @@
# task-master-ai
## 0.27.2
### Patch Changes
- [#1248](https://github.com/eyaltoledano/claude-task-master/pull/1248) [`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix set-status for subtasks:
- Parent tasks are now set as `done` when subtasks are all `done`
- Parent tasks are now set as `in-progress` when at least one subtask is `in-progress` or `done`
## 0.27.1
### Patch Changes

View File

@@ -60,6 +60,19 @@ The following documentation is also available in the `docs` directory:
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
#### Claude Code Quick Install
For Claude Code users:
```bash
claude mcp add taskmaster-ai -- npx -y task-master-ai
```
Don't forget to add your API keys to the configuration:
- in the root .env of your Project
- in the "env" section of your mcp config for taskmaster-ai
## Requirements
Taskmaster utilizes AI across several commands, and those require a separate API key. You can use a variety of models from different AI providers provided you add your API keys. For example, if you want to use Claude 3.7, you'll need an Anthropic API key.
@@ -92,10 +105,11 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
| | Project | `<project_folder>/.cursor/mcp.json` | `<project_folder>\.cursor\mcp.json` | `mcpServers` |
| **Windsurf** | Global | `~/.codeium/windsurf/mcp_config.json` | `%USERPROFILE%\.codeium\windsurf\mcp_config.json` | `mcpServers` |
| **VS Code** | Project | `<project_folder>/.vscode/mcp.json` | `<project_folder>\.vscode\mcp.json` | `servers` |
| **Q CLI** | Global | `~/.aws/amazonq/mcp.json` | | `mcpServers` |
##### Manual Configuration
###### Cursor & Windsurf (`mcpServers`)
###### Cursor & Windsurf & Q Developer CLI (`mcpServers`)
```json
{

View File

@@ -1,22 +1,24 @@
# Task Master Documentation
Welcome to the Task Master documentation. Use the links below to navigate to the information you need:
Welcome to the Task Master documentation. This documentation site provides comprehensive guides for getting started with Task Master.
## Getting Started
- [Configuration Guide](archive/configuration.md) - Set up environment variables and customize Task Master
- [Tutorial](archive/ctutorial.md) - Step-by-step guide to getting started with Task Master
- [Quick Start Guide](/getting-started/quick-start) - Complete setup and first-time usage guide
- [Requirements](/getting-started/quick-start/requirements) - What you need to get started
- [Installation](/getting-started/quick-start/installation) - How to install Task Master
## Reference
## Core Capabilities
- [Command Reference](archive/ccommand-reference.md) - Complete list of all available commands
- [Task Structure](archive/ctask-structure.md) - Understanding the task format and features
- [MCP Tools](/capabilities/mcp) - Model Control Protocol integration
- [CLI Commands](/capabilities/cli-root-commands) - Command line interface reference
- [Task Structure](/capabilities/task-structure) - Understanding tasks and subtasks
## Examples & Licensing
## Best Practices
- [Example Interactions](archive/cexamples.md) - Common Cursor AI interaction examples
- [Licensing Information](archive/clicensing.md) - Detailed information about the license
- [Advanced Configuration](/best-practices/configuration-advanced) - Detailed configuration options
- [Advanced Tasks](/best-practices/advanced-tasks) - Working with complex task structures
## Need More Help?
If you can't find what you're looking for in these docs, please check the [main README](../README.md) or visit our [GitHub repository](https://github.com/eyaltoledano/claude-task-master).
If you can't find what you're looking for in these docs, please check the root README.md or visit our [GitHub repository](https://github.com/eyaltoledano/claude-task-master).

View File

@@ -93,3 +93,57 @@ task-master generate
```
This creates individual task files in the `tasks/` directory (e.g., `task_001.txt`, `task_002.txt`), making it easier to reference specific tasks.
## Custom Slash Commands
<Check>
When you initialize Task Master with Cursor (`task-master init --cursor`), it automatically sets up custom slash commands for enhanced productivity.
</Check>
Task Master automatically copies its complete set of slash commands to `.cursor/commands/` during project initialization. This provides you with convenient shortcuts for all Task Master operations directly within Cursor's interface.
### Available Slash Commands
The following slash commands become available in Cursor after initialization:
<Accordion title="Core Workflow Commands">
- `/tm-main` - Main Task Master help and overview
- `/tm-next-task` - Get the next available task to work on
- `/tm-show-task` - View detailed information about a specific task
- `/tm-list-tasks` - Display all tasks with various filtering options
- `/tm-project-status` - View overall project status
</Accordion>
<Accordion title="Task Management Commands">
- `/tm-add-task` - Add a new task to your project
- `/tm-update-task` - Update an existing task
- `/tm-set-status-*` - Change task status (to-pending, to-in-progress, to-done, etc.)
- `/tm-remove-task` - Remove a task from your project
</Accordion>
<Accordion title="Analysis and Planning Commands">
- `/tm-parse-prd` - Generate tasks from a PRD document
- `/tm-analyze-complexity` - Analyze task complexity
- `/tm-complexity-report` - View complexity analysis report
- `/tm-expand-task` - Break down tasks into subtasks
</Accordion>
<Accordion title="Advanced Workflow Commands">
- `/tm-smart-workflow` - Intelligent task workflow management
- `/tm-auto-implement-tasks` - Automated task implementation pipeline
- `/tm-command-pipeline` - Execute multiple Task Master commands in sequence
</Accordion>
### Using Slash Commands
Simply type the command name in Cursor's chat interface, and the AI will execute the corresponding Task Master operation:
```
/tm-next-task
```
The AI will understand the context and execute the appropriate `task-master` CLI command, providing you with the results directly in the chat.
### Command Cleanup
If you remove the Cursor profile from your project using `task-master remove --cursor`, the slash commands directory is automatically cleaned up to keep your project tidy.

View File

@@ -156,7 +156,7 @@ sidebarTitle: "CLI Commands"
# Use an alternative tasks file
task-master analyze-complexity --file=custom-tasks.json
# Use Perplexity AI for research-backed complexity analysis
# Use your configured research model for research-backed complexity analysis
task-master analyze-complexity --research
```
</Accordion>

View File

@@ -108,5 +108,5 @@ You dont need to configure everything up front. Most settings can be left as
</Accordion>
<Note>
For advanced configuration options and detailed customization, see our [Advanced Configuration Guide](/docs/best-practices/configuration-advanced) page.
For advanced configuration options and detailed customization, see our [Advanced Configuration Guide](/best-practices/configuration-advanced) page.
</Note>

View File

@@ -56,4 +56,4 @@ If you ran into problems and had to debug errors you can create new rules as you
By now you have all you need to get started executing code faster and smarter with Task Master.
If you have any questions please check out [Frequently Asked Questions](/docs/getting-started/faq)
If you have any questions please check out [Frequently Asked Questions](/getting-started/faq)

View File

@@ -30,6 +30,19 @@ cursor://anysphere.cursor-deeplink/mcp/install?name=taskmaster-ai&config=eyJjb21
```
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
### Claude Code Quick Install
For Claude Code users:
```bash
claude mcp add taskmaster-ai -- npx -y task-master-ai
```
Don't forget to add your API keys to the configuration:
- in the root .env of your Project
- in the "env" section of your mcp config for taskmaster-ai
</Accordion>
## Installation Options
@@ -153,7 +166,14 @@ npx task-master init
# Initialize project with specific rules
task-master init --rules cursor,windsurf,vscode
# Initialize project for Cursor (sets up rules and custom slash commands)
task-master init --cursor
```
This will prompt you for project details and set up a new project with the necessary files and structure.
<Note>
When using the `--cursor` flag, Task Master automatically copies custom slash commands to `.cursor/commands/` for enhanced Cursor IDE integration. See the [Cursor Setup Guide](/archive/cursor-setup) for more details about these commands.
</Note>
</Accordion>

View File

@@ -6,13 +6,13 @@ sidebarTitle: "Quick Start"
This guide is for new users who want to start using Task Master with minimal setup time.
It covers:
- [Requirements](/docs/getting-started/quick-start/requirements): You will need Node.js and an AI model API Key.
- [Installation](/docs/getting-started/quick-start/installation): How to Install Task Master.
- [Configuration](/docs/getting-started/quick-start/configuration-quick): Setting up your API Key, MCP, and more.
- [PRD](/docs/getting-started/quick-start/prd-quick): Writing and parsing your first PRD.
- [Task Setup](/docs/getting-started/quick-start/tasks-quick): Preparing your tasks for execution.
- [Executing Tasks](/docs/getting-started/quick-start/execute-quick): Using Task Master to execute tasks.
- [Rules & Context](/docs/getting-started/quick-start/rules-quick): Learn how and why to build context in your project over time.
- [Requirements](/getting-started/quick-start/requirements): You will need Node.js and an AI model API Key.
- [Installation](/getting-started/quick-start/installation): How to Install Task Master.
- [Configuration](/getting-started/quick-start/configuration-quick): Setting up your API Key, MCP, and more.
- [PRD](/getting-started/quick-start/prd-quick): Writing and parsing your first PRD.
- [Task Setup](/getting-started/quick-start/tasks-quick): Preparing your tasks for execution.
- [Executing Tasks](/getting-started/quick-start/execute-quick): Using Task Master to execute tasks.
- [Rules & Context](/getting-started/quick-start/rules-quick): Learn how and why to build context in your project over time.
<Tip>
By the end of this guide, you'll have everything you need to begin working productively with Task Master.

View File

@@ -61,9 +61,25 @@ Task Master can provide a complexity report which can be helpful to read before
Can you analyze the complexity of our tasks to help me understand which ones need to be broken down further?
```
The agent will use the `analyze_project_complexity` MCP tool, or you can run it directly with the CLI command:
```bash
task-master analyze-complexity
```
For more comprehensive analysis using your configured research model, you can use:
```bash
task-master analyze-complexity --research
```
<Tip>
The `--research` flag uses whatever research model you have configured in `.taskmaster/config.json` (configurable via `task-master models --setup`) for research-backed complexity analysis, providing more informed recommendations.
</Tip>
You can view the report in a friendly table using:
```
Can you show me the complexity report in a more readable format?
```
<Check>Now you are ready to begin [executing tasks](/docs/getting-started/quick-start/execute-quick)</Check>
For more detailed CLI options, see the [Analyze Task Complexity](/capabilities/cli-root-commands#analyze-task-complexity) section.
<Check>Now you are ready to begin [executing tasks](/getting-started/quick-start/execute-quick)</Check>

View File

@@ -4,7 +4,7 @@ Welcome to v1 of the Task Master Docs. Expect weekly updates as we expand and re
We've organized the docs into three sections depending on your experience level and goals:
### Getting Started - Jump in to [Quick Start](/docs/getting-started/quick-start)
### Getting Started - Jump in to [Quick Start](/getting-started/quick-start)
Designed for first-time users. Get set up, create your first PRD, and run your first task.
### Best Practices

View File

@@ -1,5 +1,12 @@
# Change Log
## 0.25.3
### Patch Changes
- Updated dependencies [[`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011)]:
- task-master-ai@0.27.2
## 0.25.2
### Patch Changes

View File

@@ -3,7 +3,7 @@
"private": true,
"displayName": "TaskMaster",
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"version": "0.25.2",
"version": "0.25.3",
"publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {
@@ -240,7 +240,7 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
"task-master-ai": "0.27.1"
"task-master-ai": "0.27.2"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",

View File

@@ -235,6 +235,60 @@ node scripts/init.js
- "MCP provider requires session context" → Ensure running in MCP environment
- See the [MCP Provider Guide](./mcp-provider-guide.md) for detailed troubleshooting
### MCP Timeout Configuration
Long-running AI operations in taskmaster-ai can exceed the default 60-second MCP timeout. Operations like `parse_prd`, `expand_task`, `research`, and `analyze_project_complexity` may take 2-5 minutes to complete.
#### Adding Timeout Configuration
Add a `timeout` parameter to your MCP configuration to extend the timeout limit. The timeout configuration works identically across MCP clients including Cursor, Windsurf, and RooCode:
```json
{
"mcpServers": {
"task-master-ai": {
"command": "npx",
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
"timeout": 300,
"env": {
"ANTHROPIC_API_KEY": "your-anthropic-api-key"
}
}
}
}
```
**Configuration Details:**
- **`timeout: 300`** - Sets timeout to 300 seconds (5 minutes)
- **Value range**: 1-3600 seconds (1 second to 1 hour)
- **Recommended**: 300 seconds provides sufficient time for most AI operations
- **Format**: Integer value in seconds (not milliseconds)
#### Automatic Setup
When adding taskmaster rules for supported editors, the timeout configuration is automatically included:
```bash
# Automatically includes timeout configuration
task-master rules add cursor
task-master rules add roo
task-master rules add windsurf
task-master rules add vscode
```
#### Troubleshooting Timeouts
If you're still experiencing timeout errors:
1. **Verify configuration**: Check that `timeout: 300` is present in your MCP config
2. **Restart editor**: Restart your editor after making configuration changes
3. **Increase timeout**: For very complex operations, try `timeout: 600` (10 minutes)
4. **Check API keys**: Ensure required API keys are properly configured
**Expected behavior:**
- **Before fix**: Operations fail after 60 seconds with `MCP request timed out after 60000ms`
- **After fix**: Operations complete successfully within the configured timeout limit
### Google Vertex AI Configuration
Google Vertex AI is Google Cloud's enterprise AI platform and requires specific configuration:

View File

@@ -1,4 +1,4 @@
# Available Models as of September 19, 2025
# Available Models as of September 23, 2025
## Main Models
@@ -119,7 +119,7 @@
| groq | deepseek-r1-distill-llama-70b | 0.52 | 0.75 | 0.99 |
| perplexity | sonar-pro | — | 3 | 15 |
| perplexity | sonar | — | 1 | 1 |
| perplexity | deep-research | 0.211 | 2 | 8 |
| perplexity | sonar-deep-research | 0.211 | 2 | 8 |
| perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 |
| perplexity | sonar-reasoning | 0.211 | 1 | 5 |
| bedrock | us.anthropic.claude-3-opus-20240229-v1:0 | 0.725 | 15 | 75 |

76
output.txt Normal file

File diff suppressed because one or more lines are too long

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "task-master-ai",
"version": "0.27.1",
"version": "0.27.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "task-master-ai",
"version": "0.27.1",
"version": "0.27.2",
"license": "MIT WITH Commons-Clause",
"workspaces": [
"apps/*",
@@ -357,9 +357,9 @@
}
},
"apps/extension": {
"version": "0.25.2",
"version": "0.25.3",
"dependencies": {
"task-master-ai": "0.27.1"
"task-master-ai": "0.27.2"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",

View File

@@ -1,6 +1,6 @@
{
"name": "task-master-ai",
"version": "0.27.1",
"version": "0.27.2",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js",
"type": "module",

View File

@@ -3,7 +3,17 @@
* This file defines the contract for all storage implementations
*/
import type { Task, TaskMetadata } from '../types/index.js';
import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
/**
* Result type for updateTaskStatus operations
*/
export interface UpdateStatusResult {
success: boolean;
oldStatus: TaskStatus;
newStatus: TaskStatus;
taskId: string;
}
/**
* Interface for storage operations on tasks
@@ -54,6 +64,19 @@ export interface IStorage {
tag?: string
): Promise<void>;
/**
* Update task or subtask status by ID
* @param taskId - ID of the task or subtask (e.g., "1" or "1.2")
* @param newStatus - New status to set
* @param tag - Optional tag context for the task
* @returns Promise that resolves to update result with old and new status
*/
updateTaskStatus(
taskId: string,
newStatus: TaskStatus,
tag?: string
): Promise<UpdateStatusResult>;
/**
* Delete a task by ID
* @param taskId - ID of the task to delete
@@ -191,6 +214,11 @@ export abstract class BaseStorage implements IStorage {
updates: Partial<Task>,
tag?: string
): Promise<void>;
abstract updateTaskStatus(
taskId: string,
newStatus: TaskStatus,
tag?: string
): Promise<UpdateStatusResult>;
abstract deleteTask(taskId: string, tag?: string): Promise<void>;
abstract exists(tag?: string): Promise<boolean>;
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;

View File

@@ -446,7 +446,7 @@ export class TaskService {
}
/**
* Update task status
* Update task status - delegates to storage layer which handles storage-specific logic
*/
async updateTaskStatus(
taskId: string | number,
@@ -468,49 +468,28 @@ export class TaskService {
// Use provided tag or get active tag
const activeTag = tag || this.getActiveTag();
const taskIdStr = String(taskId);
// TODO: For now, assume it's a regular task and just try to update directly
// In the future, we can add subtask support if needed
if (taskIdStr.includes('.')) {
throw new TaskMasterError(
'Subtask status updates not yet supported in API storage',
ERROR_CODES.NOT_IMPLEMENTED
);
}
// Get the current task to get old status (simple, direct approach)
let currentTask: Task | null;
try {
// Try to get the task directly
currentTask = await this.storage.loadTask(taskIdStr, activeTag);
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
return await this.storage.updateTaskStatus(
taskIdStr,
newStatus,
activeTag
);
} catch (error) {
throw new TaskMasterError(
`Failed to load task ${taskIdStr}`,
ERROR_CODES.TASK_NOT_FOUND,
{ taskId: taskIdStr },
`Failed to update task status for ${taskIdStr}`,
ERROR_CODES.STORAGE_ERROR,
{
operation: 'updateTaskStatus',
resource: 'task',
taskId: taskIdStr,
newStatus,
tag: activeTag
},
error as Error
);
}
if (!currentTask) {
throw new TaskMasterError(
`Task ${taskIdStr} not found`,
ERROR_CODES.TASK_NOT_FOUND
);
}
const oldStatus = currentTask.status;
// Simple, direct update - just change the status
await this.storage.updateTask(taskIdStr, { status: newStatus }, activeTag);
return {
success: true,
oldStatus,
newStatus,
taskId: taskIdStr
};
}
}

View File

@@ -5,9 +5,15 @@
import type {
IStorage,
StorageStats
StorageStats,
UpdateStatusResult
} from '../interfaces/storage.interface.js';
import type { Task, TaskMetadata, TaskTag } from '../types/index.js';
import type {
Task,
TaskMetadata,
TaskTag,
TaskStatus
} from '../types/index.js';
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
import { TaskRepository } from '../repositories/task-repository.interface.js';
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
@@ -485,6 +491,62 @@ export class ApiStorage implements IStorage {
}
}
/**
* Update task or subtask status by ID - for API storage
*/
async updateTaskStatus(
taskId: string,
newStatus: TaskStatus,
tag?: string
): Promise<UpdateStatusResult> {
await this.ensureInitialized();
try {
const existingTask = await this.retryOperation(() =>
this.repository.getTask(this.projectId, taskId)
);
if (!existingTask) {
throw new Error(`Task ${taskId} not found`);
}
const oldStatus = existingTask.status;
if (oldStatus === newStatus) {
return {
success: true,
oldStatus,
newStatus,
taskId
};
}
// Update the task/subtask status
await this.retryOperation(() =>
this.repository.updateTask(this.projectId, taskId, {
status: newStatus,
updatedAt: new Date().toISOString()
})
);
// Note: Parent status auto-adjustment is handled by the backend API service
// which has its own business logic for managing task relationships
return {
success: true,
oldStatus,
newStatus,
taskId
};
} catch (error) {
throw new TaskMasterError(
'Failed to update task status via API',
ERROR_CODES.STORAGE_ERROR,
{ operation: 'updateTaskStatus', taskId, newStatus, tag },
error as Error
);
}
}
/**
* Get all available tags
*/

View File

@@ -2,10 +2,11 @@
* @fileoverview Refactored file-based storage implementation for Task Master
*/
import type { Task, TaskMetadata } from '../../types/index.js';
import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js';
import type {
IStorage,
StorageStats
StorageStats,
UpdateStatusResult
} from '../../interfaces/storage.interface.js';
import { FormatHandler } from './format-handler.js';
import { FileOperations } from './file-operations.js';
@@ -281,6 +282,159 @@ export class FileStorage implements IStorage {
await this.saveTasks(tasks, tag);
}
/**
* Update task or subtask status by ID - handles file storage logic with parent/subtask relationships
*/
async updateTaskStatus(
taskId: string,
newStatus: TaskStatus,
tag?: string
): Promise<UpdateStatusResult> {
const tasks = await this.loadTasks(tag);
// Check if this is a subtask (contains a dot)
if (taskId.includes('.')) {
return this.updateSubtaskStatusInFile(tasks, taskId, newStatus, tag);
}
// Handle regular task update
const taskIndex = tasks.findIndex((t) => String(t.id) === String(taskId));
if (taskIndex === -1) {
throw new Error(`Task ${taskId} not found`);
}
const oldStatus = tasks[taskIndex].status;
if (oldStatus === newStatus) {
return {
success: true,
oldStatus,
newStatus,
taskId: String(taskId)
};
}
tasks[taskIndex] = {
...tasks[taskIndex],
status: newStatus,
updatedAt: new Date().toISOString()
};
await this.saveTasks(tasks, tag);
return {
success: true,
oldStatus,
newStatus,
taskId: String(taskId)
};
}
/**
* Update subtask status within file storage - handles parent status auto-adjustment
*/
private async updateSubtaskStatusInFile(
tasks: Task[],
subtaskId: string,
newStatus: TaskStatus,
tag?: string
): Promise<UpdateStatusResult> {
// Parse the subtask ID to get parent ID and subtask ID
const parts = subtaskId.split('.');
if (parts.length !== 2) {
throw new Error(
`Invalid subtask ID format: ${subtaskId}. Expected format: parentId.subtaskId`
);
}
const [parentId, subIdRaw] = parts;
const subId = subIdRaw.trim();
if (!/^\d+$/.test(subId)) {
throw new Error(
`Invalid subtask ID: ${subId}. Subtask ID must be a positive integer.`
);
}
const subtaskNumericId = Number(subId);
// Find the parent task
const parentTaskIndex = tasks.findIndex(
(t) => String(t.id) === String(parentId)
);
if (parentTaskIndex === -1) {
throw new Error(`Parent task ${parentId} not found`);
}
const parentTask = tasks[parentTaskIndex];
// Find the subtask within the parent task
const subtaskIndex = parentTask.subtasks.findIndex(
(st) => st.id === subtaskNumericId || String(st.id) === subId
);
if (subtaskIndex === -1) {
throw new Error(
`Subtask ${subtaskId} not found in parent task ${parentId}`
);
}
const oldStatus = parentTask.subtasks[subtaskIndex].status || 'pending';
if (oldStatus === newStatus) {
return {
success: true,
oldStatus,
newStatus,
taskId: subtaskId
};
}
const now = new Date().toISOString();
// Update the subtask status
parentTask.subtasks[subtaskIndex] = {
...parentTask.subtasks[subtaskIndex],
status: newStatus,
updatedAt: now
};
// Auto-adjust parent status based on subtask statuses
const subs = parentTask.subtasks;
let parentNewStatus = parentTask.status;
if (subs.length > 0) {
const norm = (s: any) => s.status || 'pending';
const isDoneLike = (s: any) => {
const st = norm(s);
return st === 'done' || st === 'completed';
};
const allDone = subs.every(isDoneLike);
const anyInProgress = subs.some((s) => norm(s) === 'in-progress');
const anyDone = subs.some(isDoneLike);
const allPending = subs.every((s) => norm(s) === 'pending');
if (allDone) parentNewStatus = 'done';
else if (anyInProgress || anyDone) parentNewStatus = 'in-progress';
else if (allPending) parentNewStatus = 'pending';
}
// Always bump updatedAt; update status only if changed
tasks[parentTaskIndex] = {
...parentTask,
...(parentNewStatus !== parentTask.status
? { status: parentNewStatus }
: {}),
updatedAt: now
};
await this.saveTasks(tasks, tag);
return {
success: true,
oldStatus,
newStatus,
taskId: subtaskId
};
}
/**
* Delete a task
*/

View File

@@ -1847,7 +1847,7 @@ function registerCommands(programInstance) {
)
.option(
'-r, --research',
'Use Perplexity AI for research-backed complexity analysis'
'Use configured research model for research-backed complexity analysis'
)
.option(
'-i, --id <ids>',

View File

@@ -310,6 +310,7 @@ function validateProviderModelCombination(providerName, modelId) {
function validateClaudeCodeSettings(settings) {
// Define the base settings schema without commandSpecific first
const BaseSettingsSchema = z.object({
pathToClaudeCodeExecutable: z.string().optional(),
maxTurns: z.number().int().positive().optional(),
customSystemPrompt: z.string().optional(),
appendSystemPrompt: z.string().optional(),

View File

@@ -522,7 +522,7 @@
"supported": true
},
{
"id": "deep-research",
"id": "sonar-deep-research",
"swe_score": 0.211,
"cost_per_1m_tokens": {
"input": 2,

View File

@@ -1,5 +1,134 @@
// Cursor conversion profile for rule-transformer
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';
import path from 'path';
import fs from 'fs';
import { log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js';
// Helper copy; use cpSync when available, fallback to manual recursion
function copyRecursiveSync(src, dest) {
if (fs.cpSync) {
try {
fs.cpSync(src, dest, { recursive: true, force: true });
return;
} catch (err) {
throw new Error(`Failed to copy ${src} to ${dest}: ${err.message}`);
}
}
const exists = fs.existsSync(src);
let stats = null;
let isDirectory = false;
if (exists) {
try {
stats = fs.statSync(src);
isDirectory = stats.isDirectory();
} catch (err) {
// Handle TOCTOU race condition - treat as non-existent/not-a-directory
isDirectory = false;
}
}
if (isDirectory) {
try {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
for (const child of fs.readdirSync(src)) {
copyRecursiveSync(path.join(src, child), path.join(dest, child));
}
} catch (err) {
throw new Error(
`Failed to copy directory ${src} to ${dest}: ${err.message}`
);
}
} else {
try {
// ensure parent exists for file copies
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
} catch (err) {
throw new Error(`Failed to copy file ${src} to ${dest}: ${err.message}`);
}
}
}
// Helper function to recursively remove directory
function removeDirectoryRecursive(dirPath) {
if (fs.existsSync(dirPath)) {
try {
fs.rmSync(dirPath, { recursive: true, force: true });
return true;
} catch (err) {
log('error', `Failed to remove directory ${dirPath}: ${err.message}`);
return false;
}
}
return true;
}
// Resolve the Cursor profile directory from either project root, profile root, or rules dir
function resolveCursorProfileDir(baseDir) {
const base = path.basename(baseDir);
// If called with .../.cursor/rules -> return .../.cursor
if (base === 'rules' && path.basename(path.dirname(baseDir)) === '.cursor') {
return path.dirname(baseDir);
}
// If called with .../.cursor -> return as-is
if (base === '.cursor') return baseDir;
// Otherwise assume project root and append .cursor
return path.join(baseDir, '.cursor');
}
// Lifecycle functions for Cursor profile
function onAddRulesProfile(targetDir, assetsDir) {
// Copy commands directory recursively
const commandsSourceDir = path.join(assetsDir, 'claude', 'commands');
const profileDir = resolveCursorProfileDir(targetDir);
const commandsDestDir = path.join(profileDir, 'commands');
if (!fs.existsSync(commandsSourceDir)) {
log(
'warn',
`[Cursor] Source commands directory does not exist: ${commandsSourceDir}`
);
return;
}
try {
// Ensure fresh state to avoid stale command files
try {
fs.rmSync(commandsDestDir, { recursive: true, force: true });
log(
'debug',
`[Cursor] Removed existing commands directory: ${commandsDestDir}`
);
} catch (deleteErr) {
// Directory might not exist, which is fine
log(
'debug',
`[Cursor] Commands directory did not exist or could not be removed: ${deleteErr.message}`
);
}
copyRecursiveSync(commandsSourceDir, commandsDestDir);
log('debug', `[Cursor] Copied commands directory to ${commandsDestDir}`);
} catch (err) {
log(
'error',
`[Cursor] An error occurred during commands copy: ${err.message}`
);
}
}
function onRemoveRulesProfile(targetDir) {
// Remove .cursor/commands directory recursively
const profileDir = resolveCursorProfileDir(targetDir);
const commandsDir = path.join(profileDir, 'commands');
if (removeDirectoryRecursive(commandsDir)) {
log(
'debug',
`[Cursor] Ensured commands directory removed at ${commandsDir}`
);
}
}
// Create and export cursor profile using the base factory
export const cursorProfile = createProfile({
@@ -8,5 +137,10 @@ export const cursorProfile = createProfile({
url: 'cursor.so',
docsUrl: 'docs.cursor.com',
targetExtension: '.mdc', // Cursor keeps .mdc extension
supportsRulesSubdirectories: true
supportsRulesSubdirectories: true,
onAdd: onAddRulesProfile,
onRemove: onRemoveRulesProfile
});
// Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile };

View File

@@ -5,6 +5,40 @@ import { isSilentMode, log } from '../../scripts/modules/utils.js';
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';
import { ROO_MODES } from '../constants/profiles.js';
// Import the shared MCP configuration helper
import { formatJSONWithTabs } from '../utils/create-mcp-config.js';
// Roo-specific MCP configuration enhancements
function enhanceRooMCPConfiguration(mcpPath) {
if (!fs.existsSync(mcpPath)) {
log('warn', `[Roo] MCP configuration file not found at ${mcpPath}`);
return;
}
try {
// Read the existing configuration
const mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
if (mcpConfig.mcpServers && mcpConfig.mcpServers['task-master-ai']) {
const server = mcpConfig.mcpServers['task-master-ai'];
// Add Roo-specific timeout enhancement for long-running AI operations
server.timeout = 300;
// Write the enhanced configuration back
fs.writeFileSync(mcpPath, formatJSONWithTabs(mcpConfig) + '\n');
log(
'debug',
`[Roo] Enhanced MCP configuration with timeout at ${mcpPath}`
);
} else {
log('warn', `[Roo] task-master-ai server not found in MCP configuration`);
}
} catch (error) {
log('error', `[Roo] Failed to enhance MCP configuration: ${error.message}`);
}
}
// Lifecycle functions for Roo profile
function onAddRulesProfile(targetDir, assetsDir) {
// Use the provided assets directory to find the roocode directory
@@ -32,6 +66,9 @@ function onAddRulesProfile(targetDir, assetsDir) {
}
}
// Note: MCP configuration is now handled by the base profile system
// The base profile will call setupMCPConfiguration, and we enhance it in onPostConvert
for (const mode of ROO_MODES) {
const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`);
const dest = path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`);
@@ -78,6 +115,15 @@ function onRemoveRulesProfile(targetDir) {
const rooDir = path.join(targetDir, '.roo');
if (fs.existsSync(rooDir)) {
// Remove MCP configuration
const mcpPath = path.join(rooDir, 'mcp.json');
try {
fs.rmSync(mcpPath, { force: true });
log('debug', `[Roo] Removed MCP configuration from ${mcpPath}`);
} catch (err) {
log('error', `[Roo] Failed to remove MCP configuration: ${err.message}`);
}
fs.readdirSync(rooDir).forEach((entry) => {
if (entry.startsWith('rules-')) {
const modeDir = path.join(rooDir, entry);
@@ -101,7 +147,13 @@ function onRemoveRulesProfile(targetDir) {
}
function onPostConvertRulesProfile(targetDir, assetsDir) {
onAddRulesProfile(targetDir, assetsDir);
// Enhance the MCP configuration with Roo-specific features after base setup
const mcpPath = path.join(targetDir, '.roo', 'mcp.json');
try {
enhanceRooMCPConfiguration(mcpPath);
} catch (err) {
log('error', `[Roo] Failed to enhance MCP configuration: ${err.message}`);
}
}
// Create and export roo profile using the base factory
@@ -111,6 +163,7 @@ export const rooProfile = createProfile({
url: 'roocode.com',
docsUrl: 'docs.roocode.com',
toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE,
mcpConfig: true, // Enable MCP config - we enhance it with Roo-specific features
onAdd: onAddRulesProfile,
onRemove: onRemoveRulesProfile,
onPostConvert: onPostConvertRulesProfile

View File

@@ -262,3 +262,6 @@ export function removeTaskMasterMCPConfiguration(projectRoot, mcpConfigPath) {
return result;
}
// Export the formatting function for use by other modules
export { formatJSONWithTabs };

View File

@@ -210,7 +210,7 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
if (typeof profile.onAddRulesProfile === 'function') {
try {
const assetsDir = getAssetsDir();
profile.onAddRulesProfile(projectRoot, assetsDir);
profile.onAddRulesProfile(targetDir, assetsDir);
log(
'debug',
`[Rule Transformer] Called onAddRulesProfile for ${profile.profileName}`
@@ -305,7 +305,7 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
if (typeof profile.onPostConvertRulesProfile === 'function') {
try {
const assetsDir = getAssetsDir();
profile.onPostConvertRulesProfile(projectRoot, assetsDir);
profile.onPostConvertRulesProfile(targetDir, assetsDir);
log(
'debug',
`[Rule Transformer] Called onPostConvertRulesProfile for ${profile.profileName}`
@@ -347,7 +347,7 @@ export function removeProfileRules(projectRoot, profile) {
// 1. Call onRemoveRulesProfile first (for custom cleanup like removing assets)
if (typeof profile.onRemoveRulesProfile === 'function') {
try {
profile.onRemoveRulesProfile(projectRoot);
profile.onRemoveRulesProfile(targetDir);
log(
'debug',
`[Rule Transformer] Called onRemoveRulesProfile for ${profile.profileName}`

View File

@@ -54,4 +54,33 @@ describe('Cursor Profile Initialization Functionality', () => {
);
expect(cursorProfile.conversionConfig.toolNames.search).toBe('search');
});
test('cursor.js has lifecycle functions for command copying', () => {
// Check that the source file contains our new lifecycle functions
expect(cursorProfileContent).toContain('function onAddRulesProfile');
expect(cursorProfileContent).toContain('function onRemoveRulesProfile');
expect(cursorProfileContent).toContain('copyRecursiveSync');
expect(cursorProfileContent).toContain('removeDirectoryRecursive');
});
test('cursor.js copies commands from claude/commands to .cursor/commands', () => {
// Check that the onAddRulesProfile function copies from the correct source
expect(cursorProfileContent).toContain(
"path.join(assetsDir, 'claude', 'commands')"
);
// Destination path is built via a resolver to handle both project root and rules dir
expect(cursorProfileContent).toContain('resolveCursorProfileDir(');
expect(cursorProfileContent).toMatch(
/path\.join\(\s*profileDir\s*,\s*['"]commands['"]\s*\)/
);
expect(cursorProfileContent).toContain(
'copyRecursiveSync(commandsSourceDir, commandsDestDir)'
);
// Check that lifecycle functions are properly registered with the profile
expect(cursorProfile.onAddRulesProfile).toBeDefined();
expect(cursorProfile.onRemoveRulesProfile).toBeDefined();
expect(typeof cursorProfile.onAddRulesProfile).toBe('function');
expect(typeof cursorProfile.onRemoveRulesProfile).toBe('function');
});
});

View File

@@ -26,7 +26,7 @@ describe('Roo Profile Initialization Functionality', () => {
expect(rooProfile.displayName).toBe('Roo Code');
expect(rooProfile.profileDir).toBe('.roo'); // default
expect(rooProfile.rulesDir).toBe('.roo/rules'); // default
expect(rooProfile.mcpConfig).toBe(true); // default
expect(rooProfile.mcpConfig).toBe(true); // now uses standard MCP configuration with Roo enhancements
});
test('roo.js uses custom ROO_STYLE tool mappings', () => {

View File

@@ -8,18 +8,37 @@ jest.mock('child_process', () => ({
execSync: jest.fn()
}));
// Mock console methods
jest.mock('console', () => ({
// Mock console methods to avoid chalk issues
const mockLog = jest.fn();
const originalConsole = global.console;
const mockConsole = {
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
clear: jest.fn()
};
global.console = mockConsole;
// Mock utils logger to avoid chalk dependency issues
await jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
default: undefined,
log: mockLog,
isSilentMode: () => false
}));
// Import the cursor profile after mocking
const { cursorProfile, onAddRulesProfile, onRemoveRulesProfile } = await import(
'../../../src/profiles/cursor.js'
);
describe('Cursor Integration', () => {
let tempDir;
afterAll(() => {
global.console = originalConsole;
});
beforeEach(() => {
jest.clearAllMocks();
@@ -75,4 +94,127 @@ describe('Cursor Integration', () => {
{ recursive: true }
);
});
test('cursor profile has lifecycle functions for command copying', () => {
// Assert that the profile exports the lifecycle functions
expect(typeof onAddRulesProfile).toBe('function');
expect(typeof onRemoveRulesProfile).toBe('function');
expect(cursorProfile.onAddRulesProfile).toBe(onAddRulesProfile);
expect(cursorProfile.onRemoveRulesProfile).toBe(onRemoveRulesProfile);
});
describe('command copying lifecycle', () => {
let mockAssetsDir;
let mockTargetDir;
beforeEach(() => {
mockAssetsDir = path.join(tempDir, 'assets');
mockTargetDir = path.join(tempDir, 'target');
// Reset all mocks
jest.clearAllMocks();
// Mock fs methods for the lifecycle functions
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
const pathStr = filePath.toString();
if (pathStr.includes('claude/commands')) {
return true; // Mock that source commands exist
}
return false;
});
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['tm']);
jest
.spyOn(fs, 'statSync')
.mockImplementation(() => ({ isDirectory: () => true }));
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
jest.spyOn(fs, 'rmSync').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
test('onAddRulesProfile copies commands from assets to .cursor/commands', () => {
// Detect if cpSync exists and set up appropriate spy
if (fs.cpSync) {
const cpSpy = jest.spyOn(fs, 'cpSync').mockImplementation(() => {});
// Act
onAddRulesProfile(mockTargetDir, mockAssetsDir);
// Assert
expect(fs.existsSync).toHaveBeenCalledWith(
path.join(mockAssetsDir, 'claude', 'commands')
);
expect(cpSpy).toHaveBeenCalledWith(
path.join(mockAssetsDir, 'claude', 'commands'),
path.join(mockTargetDir, '.cursor', 'commands'),
expect.objectContaining({ recursive: true, force: true })
);
} else {
// Act
onAddRulesProfile(mockTargetDir, mockAssetsDir);
// Assert
expect(fs.existsSync).toHaveBeenCalledWith(
path.join(mockAssetsDir, 'claude', 'commands')
);
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(mockTargetDir, '.cursor', 'commands'),
{ recursive: true }
);
expect(fs.copyFileSync).toHaveBeenCalled();
}
});
test('onAddRulesProfile handles missing source directory gracefully', () => {
// Arrange - mock source directory not existing
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
// Act
onAddRulesProfile(mockTargetDir, mockAssetsDir);
// Assert - should not attempt to copy anything
expect(fs.mkdirSync).not.toHaveBeenCalled();
expect(fs.copyFileSync).not.toHaveBeenCalled();
});
test('onRemoveRulesProfile removes .cursor/commands directory', () => {
// Arrange - mock directory exists
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
// Act
onRemoveRulesProfile(mockTargetDir);
// Assert
expect(fs.rmSync).toHaveBeenCalledWith(
path.join(mockTargetDir, '.cursor', 'commands'),
{ recursive: true, force: true }
);
});
test('onRemoveRulesProfile handles missing directory gracefully', () => {
// Arrange - mock directory doesn't exist
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
// Act
onRemoveRulesProfile(mockTargetDir);
// Assert - should still return true but not attempt removal
expect(fs.rmSync).not.toHaveBeenCalled();
});
test('onRemoveRulesProfile handles removal errors gracefully', () => {
// Arrange - mock directory exists but removal fails
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
jest.spyOn(fs, 'rmSync').mockImplementation(() => {
throw new Error('Permission denied');
});
// Act & Assert - should not throw
expect(() => onRemoveRulesProfile(mockTargetDir)).not.toThrow();
});
});
});

View File

@@ -266,10 +266,10 @@ describe('MCP Configuration Validation', () => {
expect(mcpEnabledProfiles).toContain('cursor');
expect(mcpEnabledProfiles).toContain('gemini');
expect(mcpEnabledProfiles).toContain('opencode');
expect(mcpEnabledProfiles).toContain('roo');
expect(mcpEnabledProfiles).toContain('vscode');
expect(mcpEnabledProfiles).toContain('windsurf');
expect(mcpEnabledProfiles).toContain('zed');
expect(mcpEnabledProfiles).toContain('roo');
expect(mcpEnabledProfiles).not.toContain('cline');
expect(mcpEnabledProfiles).not.toContain('codex');
expect(mcpEnabledProfiles).not.toContain('trae');
@@ -384,6 +384,7 @@ describe('MCP Configuration Validation', () => {
'claude',
'cursor',
'gemini',
'kiro',
'opencode',
'roo',
'windsurf',