Compare commits
6 Commits
main
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74997468de | ||
|
|
d7fca1844f | ||
|
|
a98d96ef04 | ||
|
|
a69d8c91dc | ||
|
|
474a86cebb | ||
|
|
3283506444 |
7
.changeset/fix-parent-directory-traversal.md
Normal file
7
.changeset/fix-parent-directory-traversal.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Enable Task Master commands to traverse parent directories to find project root from nested paths
|
||||
|
||||
Fixes #1301
|
||||
5
.changeset/fix-warning-box-alignment.md
Normal file
5
.changeset/fix-warning-box-alignment.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tm/cli": patch
|
||||
---
|
||||
|
||||
Fix warning message box width to match dashboard box width for consistent UI alignment
|
||||
35
.changeset/light-owls-stay.md
Normal file
35
.changeset/light-owls-stay.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add configurable MCP tool loading to optimize LLM context usage
|
||||
|
||||
You can now control which Task Master MCP tools are loaded by setting the `TASK_MASTER_TOOLS` environment variable in your MCP configuration. This helps reduce context usage for LLMs by only loading the tools you need.
|
||||
|
||||
**Configuration Options:**
|
||||
|
||||
- `all` (default): Load all 36 tools
|
||||
- `core` or `lean`: Load only 7 essential tools for daily development
|
||||
- Includes: `get_tasks`, `next_task`, `get_task`, `set_task_status`, `update_subtask`, `parse_prd`, `expand_task`
|
||||
- `standard`: Load 15 commonly used tools (all core tools plus 8 more)
|
||||
- Additional tools: `initialize_project`, `analyze_project_complexity`, `expand_all`, `add_subtask`, `remove_task`, `generate`, `add_task`, `complexity_report`
|
||||
- Custom list: Comma-separated tool names (e.g., `get_tasks,next_task,set_task_status`)
|
||||
|
||||
**Example .mcp.json configuration:**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"env": {
|
||||
"TASK_MASTER_TOOLS": "standard",
|
||||
"ANTHROPIC_API_KEY": "your_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For complete details on all available tools, configuration examples, and usage guidelines, see the [MCP Tools documentation](https://docs.task-master.dev/capabilities/mcp#configurable-tool-loading).
|
||||
5
.changeset/metal-rocks-help.md
Normal file
5
.changeset/metal-rocks-help.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Improve next command to work with remote
|
||||
@@ -14,4 +14,4 @@ OLLAMA_API_KEY=YOUR_OLLAMA_API_KEY_HERE
|
||||
VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
VERTEX_LOCATION=us-central1
|
||||
# Optional: Path to service account credentials JSON file (alternative to API key)
|
||||
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-credentials.json
|
||||
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-credentials.json
|
||||
74
README.md
74
README.md
@@ -119,6 +119,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"env": {
|
||||
// "TASK_MASTER_TOOLS": "all", // Options: "all", "standard", "core", or comma-separated list of tools
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
"OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE",
|
||||
@@ -148,6 +149,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"env": {
|
||||
// "TASK_MASTER_TOOLS": "all", // Options: "all", "standard", "core", or comma-separated list of tools
|
||||
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
|
||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||
"OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE",
|
||||
@@ -196,7 +198,7 @@ Initialize taskmaster-ai in my project
|
||||
|
||||
#### 5. Make sure you have a PRD (Recommended)
|
||||
|
||||
For **new projects**: Create your PRD at `.taskmaster/docs/prd.txt`
|
||||
For **new projects**: Create your PRD at `.taskmaster/docs/prd.txt`.
|
||||
For **existing projects**: You can use `scripts/prd.txt` or migrate with `task-master migrate`
|
||||
|
||||
An example PRD template is available after initialization in `.taskmaster/templates/example_prd.txt`.
|
||||
@@ -282,6 +284,76 @@ task-master generate
|
||||
task-master rules add windsurf,roo,vscode
|
||||
```
|
||||
|
||||
## Tool Loading Configuration
|
||||
|
||||
### Optimizing MCP Tool Loading
|
||||
|
||||
Task Master's MCP server supports selective tool loading to reduce context window usage. By default, all 36 tools are loaded (~21,000 tokens) to maintain backward compatibility with existing installations.
|
||||
|
||||
You can optimize performance by configuring the `TASK_MASTER_TOOLS` environment variable:
|
||||
|
||||
### Available Modes
|
||||
|
||||
| Mode | Tools | Context Usage | Use Case |
|
||||
|------|-------|--------------|----------|
|
||||
| `all` (default) | 36 | ~21,000 tokens | Complete feature set - all tools available |
|
||||
| `standard` | 15 | ~10,000 tokens | Common task management operations |
|
||||
| `core` (or `lean`) | 7 | ~5,000 tokens | Essential daily development workflow |
|
||||
| `custom` | Variable | Variable | Comma-separated list of specific tools |
|
||||
|
||||
### Configuration Methods
|
||||
|
||||
#### Method 1: Environment Variable in MCP Configuration
|
||||
|
||||
Add `TASK_MASTER_TOOLS` to your MCP configuration file's `env` section:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"mcpServers": { // or "servers" for VS Code
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"TASK_MASTER_TOOLS": "standard", // Options: "all", "standard", "core", "lean", or comma-separated list
|
||||
"ANTHROPIC_API_KEY": "your-key-here",
|
||||
// ... other API keys
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Method 2: Claude Code CLI (One-Time Setup)
|
||||
|
||||
For Claude Code users, you can set the mode during installation:
|
||||
|
||||
```bash
|
||||
# Core mode example (~70% token reduction)
|
||||
claude mcp add task-master-ai --scope user \
|
||||
--env TASK_MASTER_TOOLS="core" \
|
||||
-- npx -y task-master-ai@latest
|
||||
|
||||
# Custom tools example
|
||||
claude mcp add task-master-ai --scope user \
|
||||
--env TASK_MASTER_TOOLS="get_tasks,next_task,set_task_status" \
|
||||
-- npx -y task-master-ai@latest
|
||||
```
|
||||
|
||||
### Tool Sets Details
|
||||
|
||||
**Core Tools (7):** `get_tasks`, `next_task`, `get_task`, `set_task_status`, `update_subtask`, `parse_prd`, `expand_task`
|
||||
|
||||
**Standard Tools (15):** All core tools plus `initialize_project`, `analyze_project_complexity`, `expand_all`, `add_subtask`, `remove_task`, `generate`, `add_task`, `complexity_report`
|
||||
|
||||
**All Tools (36):** Complete set including project setup, task management, analysis, dependencies, tags, research, and more
|
||||
|
||||
### Recommendations
|
||||
|
||||
- **New users**: Start with `"standard"` mode for a good balance
|
||||
- **Large projects**: Use `"core"` mode to minimize token usage
|
||||
- **Complex workflows**: Use `"all"` mode or custom selection
|
||||
- **Backward compatibility**: If not specified, defaults to `"all"` mode
|
||||
|
||||
## Claude Code Support
|
||||
|
||||
Task Master now supports Claude models through the Claude Code CLI, which requires no API key:
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Command } from 'commander';
|
||||
// Import all commands
|
||||
import { ListTasksCommand } from './commands/list.command.js';
|
||||
import { ShowCommand } from './commands/show.command.js';
|
||||
import { NextCommand } from './commands/next.command.js';
|
||||
import { AuthCommand } from './commands/auth.command.js';
|
||||
import { ContextCommand } from './commands/context.command.js';
|
||||
import { StartCommand } from './commands/start.command.js';
|
||||
@@ -45,6 +46,12 @@ export class CommandRegistry {
|
||||
commandClass: ShowCommand as any,
|
||||
category: 'task'
|
||||
},
|
||||
{
|
||||
name: 'next',
|
||||
description: 'Find the next available task to work on',
|
||||
commandClass: NextCommand as any,
|
||||
category: 'task'
|
||||
},
|
||||
{
|
||||
name: 'start',
|
||||
description: 'Start working on a task with claude-code',
|
||||
|
||||
247
apps/cli/src/commands/next.command.ts
Normal file
247
apps/cli/src/commands/next.command.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* @fileoverview NextCommand using Commander's native class pattern
|
||||
* Extends Commander.Command for better integration with the framework
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
import { displayHeader } from '../ui/index.js';
|
||||
|
||||
/**
|
||||
* Options interface for the next command
|
||||
*/
|
||||
export interface NextCommandOptions {
|
||||
tag?: string;
|
||||
format?: 'text' | 'json';
|
||||
silent?: boolean;
|
||||
project?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result type from next command
|
||||
*/
|
||||
export interface NextTaskResult {
|
||||
task: Task | null;
|
||||
found: boolean;
|
||||
tag: string;
|
||||
storageType: Exclude<StorageType, 'auto'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* NextCommand extending Commander's Command class
|
||||
* This is a thin presentation layer over @tm/core
|
||||
*/
|
||||
export class NextCommand extends Command {
|
||||
private tmCore?: TaskMasterCore;
|
||||
private lastResult?: NextTaskResult;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name || 'next');
|
||||
|
||||
// Configure the command
|
||||
this.description('Find the next available task to work on')
|
||||
.option('-t, --tag <tag>', 'Filter by tag')
|
||||
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
||||
.option('--silent', 'Suppress output (useful for programmatic usage)')
|
||||
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
||||
.action(async (options: NextCommandOptions) => {
|
||||
await this.executeCommand(options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the next command
|
||||
*/
|
||||
private async executeCommand(options: NextCommandOptions): Promise<void> {
|
||||
try {
|
||||
// Validate options (throws on invalid options)
|
||||
this.validateOptions(options);
|
||||
|
||||
// Initialize tm-core
|
||||
await this.initializeCore(options.project || process.cwd());
|
||||
|
||||
// Get next task from core
|
||||
const result = await this.getNextTask(options);
|
||||
|
||||
// Store result for programmatic access
|
||||
this.setLastResult(result);
|
||||
|
||||
// Display results
|
||||
if (!options.silent) {
|
||||
this.displayResults(result, options);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.getSanitizedDetails?.() ?? {
|
||||
message: error?.message ?? String(error)
|
||||
};
|
||||
|
||||
// Allow error to propagate for library compatibility
|
||||
throw new Error(msg.message || 'Unexpected error in next command');
|
||||
} finally {
|
||||
// Always clean up resources, even on error
|
||||
await this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate command options
|
||||
*/
|
||||
private validateOptions(options: NextCommandOptions): void {
|
||||
// Validate format
|
||||
if (options.format && !['text', 'json'].includes(options.format)) {
|
||||
throw new Error(
|
||||
`Invalid format: ${options.format}. Valid formats are: text, json`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TaskMasterCore
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
const resolved = path.resolve(projectRoot);
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: resolved });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next task from tm-core
|
||||
*/
|
||||
private async getNextTask(
|
||||
options: NextCommandOptions
|
||||
): Promise<NextTaskResult> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TaskMasterCore not initialized');
|
||||
}
|
||||
|
||||
// Call tm-core to get next task
|
||||
const task = await this.tmCore.getNextTask(options.tag);
|
||||
|
||||
// Get storage type and active tag
|
||||
const storageType = this.tmCore.getStorageType();
|
||||
if (storageType === 'auto') {
|
||||
throw new Error('Storage type must be resolved before use');
|
||||
}
|
||||
const activeTag = options.tag || this.tmCore.getActiveTag();
|
||||
|
||||
return {
|
||||
task,
|
||||
found: task !== null,
|
||||
tag: activeTag,
|
||||
storageType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display results based on format
|
||||
*/
|
||||
private displayResults(
|
||||
result: NextTaskResult,
|
||||
options: NextCommandOptions
|
||||
): void {
|
||||
const format = options.format || 'text';
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
this.displayJson(result);
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
this.displayText(result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display in JSON format
|
||||
*/
|
||||
private displayJson(result: NextTaskResult): void {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display in text format
|
||||
*/
|
||||
private displayText(result: NextTaskResult): void {
|
||||
// Display header with tag (no file path for next command)
|
||||
displayHeader({
|
||||
tag: result.tag || 'master'
|
||||
});
|
||||
|
||||
if (!result.found || !result.task) {
|
||||
// No next task available
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.yellow(
|
||||
'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.'
|
||||
),
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'yellow',
|
||||
title: '⚠ NO TASKS AVAILABLE ⚠',
|
||||
titleAlignment: 'center'
|
||||
}
|
||||
)
|
||||
);
|
||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
||||
console.log(
|
||||
`\n${chalk.dim('Tip: Try')} ${chalk.cyan('task-master list --status pending')} ${chalk.dim('to see all pending tasks')}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const task = result.task;
|
||||
|
||||
// Display the task details using the same component as 'show' command
|
||||
// with a custom header indicating this is the next task
|
||||
const customHeader = `Next Task: #${task.id} - ${task.title}`;
|
||||
displayTaskDetails(task, {
|
||||
customHeader,
|
||||
headerColor: 'green',
|
||||
showSuggestedActions: true
|
||||
});
|
||||
|
||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last result for programmatic access
|
||||
*/
|
||||
private setLastResult(result: NextTaskResult): void {
|
||||
this.lastResult = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last result (for programmatic usage)
|
||||
*/
|
||||
getLastResult(): NextTaskResult | undefined {
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.tmCore) {
|
||||
await this.tmCore.close();
|
||||
this.tmCore = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this command on an existing program
|
||||
*/
|
||||
static register(program: Command, name?: string): NextCommand {
|
||||
const nextCommand = new NextCommand(name);
|
||||
program.addCommand(nextCommand);
|
||||
return nextCommand;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
// Commands
|
||||
export { ListTasksCommand } from './commands/list.command.js';
|
||||
export { ShowCommand } from './commands/show.command.js';
|
||||
export { NextCommand } from './commands/next.command.js';
|
||||
export { AuthCommand } from './commands/auth.command.js';
|
||||
export { ContextCommand } from './commands/context.command.js';
|
||||
export { StartCommand } from './commands/start.command.js';
|
||||
|
||||
@@ -25,9 +25,9 @@ export function displayHeader(options: HeaderOptions = {}): void {
|
||||
let tagInfo = '';
|
||||
|
||||
if (tag && tag !== 'master') {
|
||||
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
|
||||
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
|
||||
} else {
|
||||
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
|
||||
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
|
||||
}
|
||||
|
||||
console.log(tagInfo);
|
||||
@@ -39,7 +39,5 @@ export function displayHeader(options: HeaderOptions = {}): void {
|
||||
: `${process.cwd()}/${filePath}`;
|
||||
console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`);
|
||||
}
|
||||
|
||||
console.log(); // Empty line for spacing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import type { Task } from '@tm/core/types';
|
||||
import { getComplexityWithColor } from '../../utils/ui.js';
|
||||
import { getComplexityWithColor, getBoxWidth } from '../../utils/ui.js';
|
||||
|
||||
/**
|
||||
* Next task display options
|
||||
@@ -113,7 +113,7 @@ export function displayRecommendedNextTask(
|
||||
borderColor: '#FFA500', // Orange color
|
||||
title: chalk.hex('#FFA500')('⚡ RECOMMENDED NEXT TASK ⚡'),
|
||||
titleAlignment: 'center',
|
||||
width: process.stdout.columns * 0.97,
|
||||
width: getBoxWidth(0.97),
|
||||
fullscreen: false
|
||||
})
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { getBoxWidth } from '../../utils/ui.js';
|
||||
|
||||
/**
|
||||
* Display suggested next steps section
|
||||
@@ -24,7 +25,7 @@ export function displaySuggestedNextSteps(): void {
|
||||
margin: { top: 0, bottom: 1 },
|
||||
borderStyle: 'round',
|
||||
borderColor: 'gray',
|
||||
width: process.stdout.columns * 0.97
|
||||
width: getBoxWidth(0.97)
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
158
apps/cli/src/utils/ui.spec.ts
Normal file
158
apps/cli/src/utils/ui.spec.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* CLI UI utilities tests
|
||||
* Tests for apps/cli/src/utils/ui.ts
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import type { MockInstance } from 'vitest';
|
||||
import { getBoxWidth } from './ui.js';
|
||||
|
||||
describe('CLI UI Utilities', () => {
|
||||
describe('getBoxWidth', () => {
|
||||
let columnsSpy: MockInstance;
|
||||
let originalDescriptor: PropertyDescriptor | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
// Store original descriptor if it exists
|
||||
originalDescriptor = Object.getOwnPropertyDescriptor(
|
||||
process.stdout,
|
||||
'columns'
|
||||
);
|
||||
|
||||
// If columns doesn't exist or isn't a getter, define it as one
|
||||
if (!originalDescriptor || !originalDescriptor.get) {
|
||||
const currentValue = process.stdout.columns || 80;
|
||||
Object.defineProperty(process.stdout, 'columns', {
|
||||
get() {
|
||||
return currentValue;
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
// Now spy on the getter
|
||||
columnsSpy = vi.spyOn(process.stdout, 'columns', 'get');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore the spy
|
||||
columnsSpy.mockRestore();
|
||||
|
||||
// Restore original descriptor or delete the property
|
||||
if (originalDescriptor) {
|
||||
Object.defineProperty(process.stdout, 'columns', originalDescriptor);
|
||||
} else {
|
||||
delete (process.stdout as any).columns;
|
||||
}
|
||||
});
|
||||
|
||||
it('should calculate width as percentage of terminal width', () => {
|
||||
columnsSpy.mockReturnValue(100);
|
||||
const width = getBoxWidth(0.9, 40);
|
||||
expect(width).toBe(90);
|
||||
});
|
||||
|
||||
it('should use default percentage of 0.9 when not specified', () => {
|
||||
columnsSpy.mockReturnValue(100);
|
||||
const width = getBoxWidth();
|
||||
expect(width).toBe(90);
|
||||
});
|
||||
|
||||
it('should use default minimum width of 40 when not specified', () => {
|
||||
columnsSpy.mockReturnValue(30);
|
||||
const width = getBoxWidth();
|
||||
expect(width).toBe(40); // Should enforce minimum
|
||||
});
|
||||
|
||||
it('should enforce minimum width when terminal is too narrow', () => {
|
||||
columnsSpy.mockReturnValue(50);
|
||||
const width = getBoxWidth(0.9, 60);
|
||||
expect(width).toBe(60); // Should use minWidth instead of 45
|
||||
});
|
||||
|
||||
it('should handle undefined process.stdout.columns', () => {
|
||||
columnsSpy.mockReturnValue(undefined);
|
||||
const width = getBoxWidth(0.9, 40);
|
||||
// Should fall back to 80 columns: Math.floor(80 * 0.9) = 72
|
||||
expect(width).toBe(72);
|
||||
});
|
||||
|
||||
it('should handle custom percentage values', () => {
|
||||
columnsSpy.mockReturnValue(100);
|
||||
expect(getBoxWidth(0.95, 40)).toBe(95);
|
||||
expect(getBoxWidth(0.8, 40)).toBe(80);
|
||||
expect(getBoxWidth(0.5, 40)).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle custom minimum width values', () => {
|
||||
columnsSpy.mockReturnValue(60);
|
||||
expect(getBoxWidth(0.9, 70)).toBe(70); // 60 * 0.9 = 54, but min is 70
|
||||
expect(getBoxWidth(0.9, 50)).toBe(54); // 60 * 0.9 = 54, min is 50
|
||||
});
|
||||
|
||||
it('should floor the calculated width', () => {
|
||||
columnsSpy.mockReturnValue(99);
|
||||
const width = getBoxWidth(0.9, 40);
|
||||
// 99 * 0.9 = 89.1, should floor to 89
|
||||
expect(width).toBe(89);
|
||||
});
|
||||
|
||||
it('should match warning box width calculation', () => {
|
||||
// Test the specific case from displayWarning()
|
||||
columnsSpy.mockReturnValue(80);
|
||||
const width = getBoxWidth(0.9, 40);
|
||||
expect(width).toBe(72);
|
||||
});
|
||||
|
||||
it('should match table width calculation', () => {
|
||||
// Test the specific case from createTaskTable()
|
||||
columnsSpy.mockReturnValue(111);
|
||||
const width = getBoxWidth(0.9, 100);
|
||||
// 111 * 0.9 = 99.9, floor to 99, but max(99, 100) = 100
|
||||
expect(width).toBe(100);
|
||||
});
|
||||
|
||||
it('should match recommended task box width calculation', () => {
|
||||
// Test the specific case from displayRecommendedNextTask()
|
||||
columnsSpy.mockReturnValue(120);
|
||||
const width = getBoxWidth(0.97, 40);
|
||||
// 120 * 0.97 = 116.4, floor to 116
|
||||
expect(width).toBe(116);
|
||||
});
|
||||
|
||||
it('should handle edge case of zero terminal width', () => {
|
||||
columnsSpy.mockReturnValue(0);
|
||||
const width = getBoxWidth(0.9, 40);
|
||||
// When columns is 0, it uses fallback of 80: Math.floor(80 * 0.9) = 72
|
||||
expect(width).toBe(72);
|
||||
});
|
||||
|
||||
it('should handle very large terminal widths', () => {
|
||||
columnsSpy.mockReturnValue(1000);
|
||||
const width = getBoxWidth(0.9, 40);
|
||||
expect(width).toBe(900);
|
||||
});
|
||||
|
||||
it('should handle very small percentages', () => {
|
||||
columnsSpy.mockReturnValue(100);
|
||||
const width = getBoxWidth(0.1, 5);
|
||||
// 100 * 0.1 = 10, which is greater than min 5
|
||||
expect(width).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle percentage of 1.0 (100%)', () => {
|
||||
columnsSpy.mockReturnValue(80);
|
||||
const width = getBoxWidth(1.0, 40);
|
||||
expect(width).toBe(80);
|
||||
});
|
||||
|
||||
it('should consistently return same value for same inputs', () => {
|
||||
columnsSpy.mockReturnValue(100);
|
||||
const width1 = getBoxWidth(0.9, 40);
|
||||
const width2 = getBoxWidth(0.9, 40);
|
||||
const width3 = getBoxWidth(0.9, 40);
|
||||
expect(width1).toBe(width2);
|
||||
expect(width2).toBe(width3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -126,6 +126,20 @@ export function getComplexityWithScore(complexity: number | undefined): string {
|
||||
return color(`${complexity}/10 (${label})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate box width as percentage of terminal width
|
||||
* @param percentage - Percentage of terminal width to use (default: 0.9)
|
||||
* @param minWidth - Minimum width to enforce (default: 40)
|
||||
* @returns Calculated box width
|
||||
*/
|
||||
export function getBoxWidth(
|
||||
percentage: number = 0.9,
|
||||
minWidth: number = 40
|
||||
): number {
|
||||
const terminalWidth = process.stdout.columns || 80;
|
||||
return Math.max(Math.floor(terminalWidth * percentage), minWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text to specified length
|
||||
*/
|
||||
@@ -176,6 +190,8 @@ export function displayBanner(title: string = 'Task Master'): void {
|
||||
* Display an error message (matches scripts/modules/ui.js style)
|
||||
*/
|
||||
export function displayError(message: string, details?: string): void {
|
||||
const boxWidth = getBoxWidth();
|
||||
|
||||
console.error(
|
||||
boxen(
|
||||
chalk.red.bold('X Error: ') +
|
||||
@@ -184,7 +200,8 @@ export function displayError(message: string, details?: string): void {
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'red'
|
||||
borderColor: 'red',
|
||||
width: boxWidth
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -194,13 +211,16 @@ export function displayError(message: string, details?: string): void {
|
||||
* Display a success message
|
||||
*/
|
||||
export function displaySuccess(message: string): void {
|
||||
const boxWidth = getBoxWidth();
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green.bold(String.fromCharCode(8730) + ' ') + chalk.white(message),
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'green'
|
||||
borderColor: 'green',
|
||||
width: boxWidth
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -210,11 +230,14 @@ export function displaySuccess(message: string): void {
|
||||
* Display a warning message
|
||||
*/
|
||||
export function displayWarning(message: string): void {
|
||||
const boxWidth = getBoxWidth();
|
||||
|
||||
console.log(
|
||||
boxen(chalk.yellow.bold('⚠ ') + chalk.white(message), {
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'yellow'
|
||||
borderColor: 'yellow',
|
||||
width: boxWidth
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -223,11 +246,14 @@ export function displayWarning(message: string): void {
|
||||
* Display info message
|
||||
*/
|
||||
export function displayInfo(message: string): void {
|
||||
const boxWidth = getBoxWidth();
|
||||
|
||||
console.log(
|
||||
boxen(chalk.blue.bold('i ') + chalk.white(message), {
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'blue'
|
||||
borderColor: 'blue',
|
||||
width: boxWidth
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -282,23 +308,23 @@ export function createTaskTable(
|
||||
} = options || {};
|
||||
|
||||
// Calculate dynamic column widths based on terminal width
|
||||
const terminalWidth = process.stdout.columns * 0.9 || 100;
|
||||
const tableWidth = getBoxWidth(0.9, 100);
|
||||
// Adjust column widths to better match the original layout
|
||||
const baseColWidths = showComplexity
|
||||
? [
|
||||
Math.floor(terminalWidth * 0.1),
|
||||
Math.floor(terminalWidth * 0.4),
|
||||
Math.floor(terminalWidth * 0.15),
|
||||
Math.floor(terminalWidth * 0.1),
|
||||
Math.floor(terminalWidth * 0.2),
|
||||
Math.floor(terminalWidth * 0.1)
|
||||
Math.floor(tableWidth * 0.1),
|
||||
Math.floor(tableWidth * 0.4),
|
||||
Math.floor(tableWidth * 0.15),
|
||||
Math.floor(tableWidth * 0.1),
|
||||
Math.floor(tableWidth * 0.2),
|
||||
Math.floor(tableWidth * 0.1)
|
||||
] // ID, Title, Status, Priority, Dependencies, Complexity
|
||||
: [
|
||||
Math.floor(terminalWidth * 0.08),
|
||||
Math.floor(terminalWidth * 0.4),
|
||||
Math.floor(terminalWidth * 0.18),
|
||||
Math.floor(terminalWidth * 0.12),
|
||||
Math.floor(terminalWidth * 0.2)
|
||||
Math.floor(tableWidth * 0.08),
|
||||
Math.floor(tableWidth * 0.4),
|
||||
Math.floor(tableWidth * 0.18),
|
||||
Math.floor(tableWidth * 0.12),
|
||||
Math.floor(tableWidth * 0.2)
|
||||
]; // ID, Title, Status, Priority, Dependencies
|
||||
|
||||
const headers = [
|
||||
|
||||
@@ -35,6 +35,18 @@ sidebarTitle: "CLI Commands"
|
||||
```bash
|
||||
# Show the next task to work on based on dependencies and status
|
||||
task-master next
|
||||
|
||||
# Filter by tag
|
||||
task-master next --tag <tag>
|
||||
|
||||
# Output in JSON format (useful for programmatic usage)
|
||||
task-master next --format json
|
||||
|
||||
# Suppress output (useful for scripts)
|
||||
task-master next --silent
|
||||
|
||||
# Specify project directory
|
||||
task-master next --project /path/to/project
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -13,6 +13,126 @@ The MCP interface is built on top of the `fastmcp` library and registers a set o
|
||||
|
||||
Each tool is defined with a name, a description, and a set of parameters that are validated using the `zod` library. The `execute` function of each tool calls the corresponding core logic function from `scripts/modules/task-manager.js`.
|
||||
|
||||
## Configurable Tool Loading
|
||||
|
||||
To optimize LLM context usage, you can control which Task Master MCP tools are loaded using the `TASK_MASTER_TOOLS` environment variable. This is particularly useful when working with LLMs that have context limits or when you only need a subset of tools.
|
||||
|
||||
### Configuration Modes
|
||||
|
||||
#### All Tools (Default)
|
||||
Loads all 36 available tools. Use when you need full Task Master functionality.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"env": {
|
||||
"TASK_MASTER_TOOLS": "all",
|
||||
"ANTHROPIC_API_KEY": "your_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `TASK_MASTER_TOOLS` is not set, all tools are loaded by default.
|
||||
|
||||
#### Core Tools (Lean Mode)
|
||||
Loads only 7 essential tools for daily development. Ideal for minimal context usage.
|
||||
|
||||
**Core tools included:**
|
||||
- `get_tasks` - List all tasks
|
||||
- `next_task` - Find the next task to work on
|
||||
- `get_task` - Get detailed task information
|
||||
- `set_task_status` - Update task status
|
||||
- `update_subtask` - Add implementation notes
|
||||
- `parse_prd` - Generate tasks from PRD
|
||||
- `expand_task` - Break down tasks into subtasks
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"env": {
|
||||
"TASK_MASTER_TOOLS": "core",
|
||||
"ANTHROPIC_API_KEY": "your_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also use `"lean"` as an alias for `"core"`.
|
||||
|
||||
#### Standard Tools
|
||||
Loads 15 commonly used tools. Balances functionality with context efficiency.
|
||||
|
||||
**Standard tools include all core tools plus:**
|
||||
- `initialize_project` - Set up new projects
|
||||
- `analyze_project_complexity` - Analyze task complexity
|
||||
- `expand_all` - Expand all eligible tasks
|
||||
- `add_subtask` - Add subtasks manually
|
||||
- `remove_task` - Remove tasks
|
||||
- `generate` - Generate task markdown files
|
||||
- `add_task` - Create new tasks
|
||||
- `complexity_report` - View complexity analysis
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"env": {
|
||||
"TASK_MASTER_TOOLS": "standard",
|
||||
"ANTHROPIC_API_KEY": "your_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom Tool Selection
|
||||
Specify exactly which tools to load using a comma-separated list. Tool names are case-insensitive and support both underscores and hyphens.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "task-master-ai"],
|
||||
"env": {
|
||||
"TASK_MASTER_TOOLS": "get_tasks,next_task,set_task_status,update_subtask",
|
||||
"ANTHROPIC_API_KEY": "your_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Choosing the Right Configuration
|
||||
|
||||
- **Use `core`/`lean`**: When working with basic task management workflows or when context limits are strict
|
||||
- **Use `standard`**: For most development workflows that include task creation and analysis
|
||||
- **Use `all`**: When you need full functionality including tag management, dependencies, and advanced features
|
||||
- **Use custom list**: When you have specific tool requirements or want to experiment with minimal sets
|
||||
|
||||
### Verification
|
||||
|
||||
When the MCP server starts, it logs which tools were loaded:
|
||||
|
||||
```
|
||||
Task Master MCP Server starting...
|
||||
Tool mode configuration: standard
|
||||
Loading standard tools
|
||||
Registering 15 MCP tools (mode: standard)
|
||||
Successfully registered 15/15 tools
|
||||
```
|
||||
|
||||
## Tool Categories
|
||||
|
||||
The MCP tools can be categorized in the same way as the core functionalities:
|
||||
|
||||
@@ -37,6 +37,25 @@ For MCP/Cursor usage: Configure keys in the env section of your .cursor/mcp.json
|
||||
}
|
||||
```
|
||||
|
||||
<Tip>
|
||||
**Optimize Context Usage**: You can control which Task Master MCP tools are loaded using the `TASK_MASTER_TOOLS` environment variable. This helps reduce LLM context usage by only loading the tools you need.
|
||||
|
||||
Options:
|
||||
- `all` (default) - All 36 tools
|
||||
- `standard` - 15 commonly used tools
|
||||
- `core` or `lean` - 7 essential tools
|
||||
|
||||
Example:
|
||||
```json
|
||||
"env": {
|
||||
"TASK_MASTER_TOOLS": "standard",
|
||||
"ANTHROPIC_API_KEY": "your_key_here"
|
||||
}
|
||||
```
|
||||
|
||||
See the [MCP Tools documentation](/capabilities/mcp#configurable-tool-loading) for details.
|
||||
</Tip>
|
||||
|
||||
### CLI Usage: `.env` File
|
||||
|
||||
Create a `.env` file in your project root and include the keys for the providers you plan to use:
|
||||
|
||||
@@ -59,6 +59,76 @@ Taskmaster uses two primary methods for configuration:
|
||||
- **Migration:** Use `task-master migrate` to move this to `.taskmaster/config.json`.
|
||||
- **Deprecation:** While still supported, you'll see warnings encouraging migration to the new structure.
|
||||
|
||||
## MCP Tool Loading Configuration
|
||||
|
||||
### TASK_MASTER_TOOLS Environment Variable
|
||||
|
||||
The `TASK_MASTER_TOOLS` environment variable controls which tools are loaded by the Task Master MCP server. This allows you to optimize token usage based on your workflow needs.
|
||||
|
||||
> Note
|
||||
> Prefer setting `TASK_MASTER_TOOLS` in your MCP client's `env` block (e.g., `.cursor/mcp.json`) or in CI/deployment env. The `.env` file is reserved for API keys/endpoints; avoid persisting non-secret settings there.
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
- **`all`** (default): Loads all 36 available tools (~21,000 tokens)
|
||||
- Best for: Users who need the complete feature set
|
||||
- Use when: Working with complex projects requiring all Task Master features
|
||||
- Backward compatibility: This is the default to maintain compatibility with existing installations
|
||||
|
||||
- **`standard`**: Loads 15 commonly used tools (~10,000 tokens, 50% reduction)
|
||||
- Best for: Regular task management workflows
|
||||
- Tools included: All core tools plus project initialization, complexity analysis, task generation, and more
|
||||
- Use when: You need a balanced set of features with reduced token usage
|
||||
|
||||
- **`core`** (or `lean`): Loads 7 essential tools (~5,000 tokens, 70% reduction)
|
||||
- Best for: Daily development with minimal token overhead
|
||||
- Tools included: `get_tasks`, `next_task`, `get_task`, `set_task_status`, `update_subtask`, `parse_prd`, `expand_task`
|
||||
- Use when: Working in large contexts where token usage is critical
|
||||
- Note: "lean" is an alias for "core" (same tools, token estimate and recommended use). You can refer to it as either "core" or "lean" when configuring.
|
||||
|
||||
- **Custom list**: Comma-separated list of specific tool names
|
||||
- Best for: Specialized workflows requiring specific tools
|
||||
- Example: `"get_tasks,next_task,set_task_status"`
|
||||
- Use when: You know exactly which tools you need
|
||||
|
||||
#### How to Configure
|
||||
|
||||
1. **In MCP configuration files** (`.cursor/mcp.json`, `.vscode/mcp.json`, etc.) - **Recommended**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"env": {
|
||||
"TASK_MASTER_TOOLS": "standard", // Set tool loading mode
|
||||
// API keys can still use .env for security
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Via Claude Code CLI**:
|
||||
|
||||
```bash
|
||||
claude mcp add task-master-ai --scope user \
|
||||
--env TASK_MASTER_TOOLS="core" \
|
||||
-- npx -y task-master-ai@latest
|
||||
```
|
||||
|
||||
3. **In CI/deployment environment variables**:
|
||||
```bash
|
||||
export TASK_MASTER_TOOLS="standard"
|
||||
node mcp-server/server.js
|
||||
```
|
||||
|
||||
#### Tool Loading Behavior
|
||||
|
||||
- When `TASK_MASTER_TOOLS` is unset or empty, the system defaults to `"all"`
|
||||
- Invalid tool names in a user-specified list are ignored (a warning is emitted for each)
|
||||
- If every tool name in a custom list is invalid, the system falls back to `"all"`
|
||||
- Tool names are case-insensitive (e.g., `"CORE"`, `"core"`, and `"Core"` are treated identically)
|
||||
|
||||
## Environment Variables (`.env` file or MCP `env` block - For API Keys Only)
|
||||
|
||||
- Used **exclusively** for sensitive API keys and specific endpoint URLs.
|
||||
@@ -223,10 +293,10 @@ node scripts/init.js
|
||||
```bash
|
||||
# Set MCP provider for main role
|
||||
task-master models set-main --provider mcp --model claude-3-5-sonnet-20241022
|
||||
|
||||
# Set MCP provider for research role
|
||||
|
||||
# Set MCP provider for research role
|
||||
task-master models set-research --provider mcp --model claude-3-opus-20240229
|
||||
|
||||
|
||||
# Verify configuration
|
||||
task-master models list
|
||||
```
|
||||
@@ -357,7 +427,7 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c
|
||||
"temperature": 0.7
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "azure",
|
||||
"provider": "azure",
|
||||
"modelId": "gpt-4o-mini",
|
||||
"maxTokens": 10000,
|
||||
"temperature": 0.7
|
||||
@@ -376,7 +446,7 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "azure",
|
||||
"modelId": "gpt-4o",
|
||||
"modelId": "gpt-4o",
|
||||
"maxTokens": 16000,
|
||||
"temperature": 0.7,
|
||||
"baseURL": "https://your-resource-name.azure.com/openai/deployments"
|
||||
@@ -390,7 +460,7 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c
|
||||
"fallback": {
|
||||
"provider": "azure",
|
||||
"modelId": "gpt-4o-mini",
|
||||
"maxTokens": 10000,
|
||||
"maxTokens": 10000,
|
||||
"temperature": 0.7,
|
||||
"baseURL": "https://your-resource-name.azure.com/openai/deployments"
|
||||
}
|
||||
@@ -402,7 +472,7 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c
|
||||
```bash
|
||||
# In .env file
|
||||
AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
|
||||
|
||||
|
||||
# Optional: Override endpoint for all Azure models
|
||||
AZURE_OPENAI_ENDPOINT=https://your-resource-name.azure.com/openai/deployments
|
||||
```
|
||||
|
||||
@@ -4,12 +4,14 @@ import dotenv from 'dotenv';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
import logger from './logger.js';
|
||||
import { registerTaskMasterTools } from './tools/index.js';
|
||||
import {
|
||||
registerTaskMasterTools,
|
||||
getToolsConfiguration
|
||||
} from './tools/index.js';
|
||||
import ProviderRegistry from '../../src/provider-registry/index.js';
|
||||
import { MCPProvider } from './providers/mcp-provider.js';
|
||||
import packageJson from '../../package.json' with { type: 'json' };
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
// Constants
|
||||
@@ -29,12 +31,10 @@ class TaskMasterMCPServer {
|
||||
this.server = new FastMCP(this.options);
|
||||
this.initialized = false;
|
||||
|
||||
// Bind methods
|
||||
this.init = this.init.bind(this);
|
||||
this.start = this.start.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
|
||||
// Setup logging
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@@ -44,8 +44,34 @@ class TaskMasterMCPServer {
|
||||
async init() {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Pass the manager instance to the tool registration function
|
||||
registerTaskMasterTools(this.server, this.asyncManager);
|
||||
const normalizedToolMode = getToolsConfiguration();
|
||||
|
||||
this.logger.info('Task Master MCP Server starting...');
|
||||
this.logger.info(`Tool mode configuration: ${normalizedToolMode}`);
|
||||
|
||||
const registrationResult = registerTaskMasterTools(
|
||||
this.server,
|
||||
normalizedToolMode
|
||||
);
|
||||
|
||||
this.logger.info(
|
||||
`Normalized tool mode: ${registrationResult.normalizedMode}`
|
||||
);
|
||||
this.logger.info(
|
||||
`Registered ${registrationResult.registeredTools.length} tools successfully`
|
||||
);
|
||||
|
||||
if (registrationResult.registeredTools.length > 0) {
|
||||
this.logger.debug(
|
||||
`Registered tools: ${registrationResult.registeredTools.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (registrationResult.failedTools.length > 0) {
|
||||
this.logger.warn(
|
||||
`Failed to register ${registrationResult.failedTools.length} tools: ${registrationResult.failedTools.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
|
||||
@@ -3,109 +3,238 @@
|
||||
* Export all Task Master CLI tools for MCP server
|
||||
*/
|
||||
|
||||
import { registerListTasksTool } from './get-tasks.js';
|
||||
import logger from '../logger.js';
|
||||
import { registerSetTaskStatusTool } from './set-task-status.js';
|
||||
import { registerParsePRDTool } from './parse-prd.js';
|
||||
import { registerUpdateTool } from './update.js';
|
||||
import { registerUpdateTaskTool } from './update-task.js';
|
||||
import { registerUpdateSubtaskTool } from './update-subtask.js';
|
||||
import { registerGenerateTool } from './generate.js';
|
||||
import { registerShowTaskTool } from './get-task.js';
|
||||
import { registerNextTaskTool } from './next-task.js';
|
||||
import { registerExpandTaskTool } from './expand-task.js';
|
||||
import { registerAddTaskTool } from './add-task.js';
|
||||
import { registerAddSubtaskTool } from './add-subtask.js';
|
||||
import { registerRemoveSubtaskTool } from './remove-subtask.js';
|
||||
import { registerAnalyzeProjectComplexityTool } from './analyze.js';
|
||||
import { registerClearSubtasksTool } from './clear-subtasks.js';
|
||||
import { registerExpandAllTool } from './expand-all.js';
|
||||
import { registerRemoveDependencyTool } from './remove-dependency.js';
|
||||
import { registerValidateDependenciesTool } from './validate-dependencies.js';
|
||||
import { registerFixDependenciesTool } from './fix-dependencies.js';
|
||||
import { registerComplexityReportTool } from './complexity-report.js';
|
||||
import { registerAddDependencyTool } from './add-dependency.js';
|
||||
import { registerRemoveTaskTool } from './remove-task.js';
|
||||
import { registerInitializeProjectTool } from './initialize-project.js';
|
||||
import { registerModelsTool } from './models.js';
|
||||
import { registerMoveTaskTool } from './move-task.js';
|
||||
import { registerResponseLanguageTool } from './response-language.js';
|
||||
import { registerAddTagTool } from './add-tag.js';
|
||||
import { registerDeleteTagTool } from './delete-tag.js';
|
||||
import { registerListTagsTool } from './list-tags.js';
|
||||
import { registerUseTagTool } from './use-tag.js';
|
||||
import { registerRenameTagTool } from './rename-tag.js';
|
||||
import { registerCopyTagTool } from './copy-tag.js';
|
||||
import { registerResearchTool } from './research.js';
|
||||
import { registerRulesTool } from './rules.js';
|
||||
import { registerScopeUpTool } from './scope-up.js';
|
||||
import { registerScopeDownTool } from './scope-down.js';
|
||||
import {
|
||||
toolRegistry,
|
||||
coreTools,
|
||||
standardTools,
|
||||
getAvailableTools,
|
||||
getToolRegistration,
|
||||
isValidTool
|
||||
} from './tool-registry.js';
|
||||
|
||||
/**
|
||||
* Register all Task Master tools with the MCP server
|
||||
* @param {Object} server - FastMCP server instance
|
||||
* Helper function to safely read and normalize the TASK_MASTER_TOOLS environment variable
|
||||
* @returns {string} The tools configuration string, defaults to 'all'
|
||||
*/
|
||||
export function registerTaskMasterTools(server) {
|
||||
export function getToolsConfiguration() {
|
||||
const rawValue = process.env.TASK_MASTER_TOOLS;
|
||||
|
||||
if (!rawValue || rawValue.trim() === '') {
|
||||
logger.debug('No TASK_MASTER_TOOLS env var found, defaulting to "all"');
|
||||
return 'all';
|
||||
}
|
||||
|
||||
const normalizedValue = rawValue.trim();
|
||||
logger.debug(`TASK_MASTER_TOOLS env var: "${normalizedValue}"`);
|
||||
return normalizedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Task Master tools with the MCP server
|
||||
* Supports selective tool loading via TASK_MASTER_TOOLS environment variable
|
||||
* @param {Object} server - FastMCP server instance
|
||||
* @param {string} toolMode - The tool mode configuration (defaults to 'all')
|
||||
* @returns {Object} Object containing registered tools, failed tools, and normalized mode
|
||||
*/
|
||||
export function registerTaskMasterTools(server, toolMode = 'all') {
|
||||
const registeredTools = [];
|
||||
const failedTools = [];
|
||||
|
||||
try {
|
||||
// Register each tool in a logical workflow order
|
||||
const enabledTools = toolMode.trim();
|
||||
let toolsToRegister = [];
|
||||
|
||||
// Group 1: Initialization & Setup
|
||||
registerInitializeProjectTool(server);
|
||||
registerModelsTool(server);
|
||||
registerRulesTool(server);
|
||||
registerParsePRDTool(server);
|
||||
const lowerCaseConfig = enabledTools.toLowerCase();
|
||||
|
||||
// Group 2: Task Analysis & Expansion
|
||||
registerAnalyzeProjectComplexityTool(server);
|
||||
registerExpandTaskTool(server);
|
||||
registerExpandAllTool(server);
|
||||
registerScopeUpTool(server);
|
||||
registerScopeDownTool(server);
|
||||
switch (lowerCaseConfig) {
|
||||
case 'all':
|
||||
toolsToRegister = Object.keys(toolRegistry);
|
||||
logger.info('Loading all available tools');
|
||||
break;
|
||||
case 'core':
|
||||
case 'lean':
|
||||
toolsToRegister = coreTools;
|
||||
logger.info('Loading core tools only');
|
||||
break;
|
||||
case 'standard':
|
||||
toolsToRegister = standardTools;
|
||||
logger.info('Loading standard tools');
|
||||
break;
|
||||
default:
|
||||
const requestedTools = enabledTools
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0);
|
||||
|
||||
// Group 3: Task Listing & Viewing
|
||||
registerListTasksTool(server);
|
||||
registerShowTaskTool(server);
|
||||
registerNextTaskTool(server);
|
||||
registerComplexityReportTool(server);
|
||||
const uniqueTools = new Set();
|
||||
const unknownTools = [];
|
||||
|
||||
// Group 4: Task Status & Management
|
||||
registerSetTaskStatusTool(server);
|
||||
registerGenerateTool(server);
|
||||
const aliasMap = {
|
||||
response_language: 'response-language'
|
||||
};
|
||||
|
||||
// Group 5: Task Creation & Modification
|
||||
registerAddTaskTool(server);
|
||||
registerAddSubtaskTool(server);
|
||||
registerUpdateTool(server);
|
||||
registerUpdateTaskTool(server);
|
||||
registerUpdateSubtaskTool(server);
|
||||
registerRemoveTaskTool(server);
|
||||
registerRemoveSubtaskTool(server);
|
||||
registerClearSubtasksTool(server);
|
||||
registerMoveTaskTool(server);
|
||||
for (const toolName of requestedTools) {
|
||||
let resolvedName = null;
|
||||
const lowerToolName = toolName.toLowerCase();
|
||||
|
||||
// Group 6: Dependency Management
|
||||
registerAddDependencyTool(server);
|
||||
registerRemoveDependencyTool(server);
|
||||
registerValidateDependenciesTool(server);
|
||||
registerFixDependenciesTool(server);
|
||||
registerResponseLanguageTool(server);
|
||||
if (aliasMap[lowerToolName]) {
|
||||
const aliasTarget = aliasMap[lowerToolName];
|
||||
for (const registryKey of Object.keys(toolRegistry)) {
|
||||
if (registryKey.toLowerCase() === aliasTarget.toLowerCase()) {
|
||||
resolvedName = registryKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group 7: Tag Management
|
||||
registerListTagsTool(server);
|
||||
registerAddTagTool(server);
|
||||
registerDeleteTagTool(server);
|
||||
registerUseTagTool(server);
|
||||
registerRenameTagTool(server);
|
||||
registerCopyTagTool(server);
|
||||
if (!resolvedName) {
|
||||
for (const registryKey of Object.keys(toolRegistry)) {
|
||||
if (registryKey.toLowerCase() === lowerToolName) {
|
||||
resolvedName = registryKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group 8: Research Features
|
||||
registerResearchTool(server);
|
||||
if (!resolvedName) {
|
||||
const withHyphens = lowerToolName.replace(/_/g, '-');
|
||||
for (const registryKey of Object.keys(toolRegistry)) {
|
||||
if (registryKey.toLowerCase() === withHyphens) {
|
||||
resolvedName = registryKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedName) {
|
||||
const withUnderscores = lowerToolName.replace(/-/g, '_');
|
||||
for (const registryKey of Object.keys(toolRegistry)) {
|
||||
if (registryKey.toLowerCase() === withUnderscores) {
|
||||
resolvedName = registryKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedName) {
|
||||
uniqueTools.add(resolvedName);
|
||||
logger.debug(`Resolved tool "${toolName}" to "${resolvedName}"`);
|
||||
} else {
|
||||
unknownTools.push(toolName);
|
||||
logger.warn(`Unknown tool specified: "${toolName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
toolsToRegister = Array.from(uniqueTools);
|
||||
|
||||
if (unknownTools.length > 0) {
|
||||
logger.warn(`Unknown tools: ${unknownTools.join(', ')}`);
|
||||
}
|
||||
|
||||
if (toolsToRegister.length === 0) {
|
||||
logger.warn(
|
||||
`No valid tools found in custom list. Loading all tools as fallback.`
|
||||
);
|
||||
toolsToRegister = Object.keys(toolRegistry);
|
||||
} else {
|
||||
logger.info(
|
||||
`Loading ${toolsToRegister.length} custom tools from list (${uniqueTools.size} unique after normalization)`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Registering ${toolsToRegister.length} MCP tools (mode: ${enabledTools})`
|
||||
);
|
||||
|
||||
toolsToRegister.forEach((toolName) => {
|
||||
try {
|
||||
const registerFunction = getToolRegistration(toolName);
|
||||
if (registerFunction) {
|
||||
registerFunction(server);
|
||||
logger.debug(`Registered tool: ${toolName}`);
|
||||
registeredTools.push(toolName);
|
||||
} else {
|
||||
logger.warn(`Tool ${toolName} not found in registry`);
|
||||
failedTools.push(toolName);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.message && error.message.includes('already registered')) {
|
||||
logger.debug(`Tool ${toolName} already registered, skipping`);
|
||||
registeredTools.push(toolName);
|
||||
} else {
|
||||
logger.error(`Failed to register tool ${toolName}: ${error.message}`);
|
||||
failedTools.push(toolName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Successfully registered ${registeredTools.length}/${toolsToRegister.length} tools`
|
||||
);
|
||||
if (failedTools.length > 0) {
|
||||
logger.warn(`Failed tools: ${failedTools.join(', ')}`);
|
||||
}
|
||||
|
||||
return {
|
||||
registeredTools,
|
||||
failedTools,
|
||||
normalizedMode: lowerCaseConfig
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error registering Task Master tools: ${error.message}`);
|
||||
throw error;
|
||||
logger.error(
|
||||
`Error parsing TASK_MASTER_TOOLS environment variable: ${error.message}`
|
||||
);
|
||||
logger.info('Falling back to loading all tools');
|
||||
|
||||
const fallbackTools = Object.keys(toolRegistry);
|
||||
for (const toolName of fallbackTools) {
|
||||
const registerFunction = getToolRegistration(toolName);
|
||||
if (registerFunction) {
|
||||
try {
|
||||
registerFunction(server);
|
||||
registeredTools.push(toolName);
|
||||
} catch (err) {
|
||||
if (err.message && err.message.includes('already registered')) {
|
||||
logger.debug(
|
||||
`Fallback tool ${toolName} already registered, skipping`
|
||||
);
|
||||
registeredTools.push(toolName);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Failed to register fallback tool '${toolName}': ${err.message}`
|
||||
);
|
||||
failedTools.push(toolName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Tool '${toolName}' not found in registry`);
|
||||
failedTools.push(toolName);
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
`Successfully registered ${registeredTools.length} fallback tools`
|
||||
);
|
||||
|
||||
return {
|
||||
registeredTools,
|
||||
failedTools,
|
||||
normalizedMode: 'all'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
toolRegistry,
|
||||
coreTools,
|
||||
standardTools,
|
||||
getAvailableTools,
|
||||
getToolRegistration,
|
||||
isValidTool
|
||||
};
|
||||
|
||||
export default {
|
||||
registerTaskMasterTools
|
||||
};
|
||||
|
||||
168
mcp-server/src/tools/tool-registry.js
Normal file
168
mcp-server/src/tools/tool-registry.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* tool-registry.js
|
||||
* Tool Registry Object Structure - Maps all 36 tool names to registration functions
|
||||
*/
|
||||
|
||||
import { registerListTasksTool } from './get-tasks.js';
|
||||
import { registerSetTaskStatusTool } from './set-task-status.js';
|
||||
import { registerParsePRDTool } from './parse-prd.js';
|
||||
import { registerUpdateTool } from './update.js';
|
||||
import { registerUpdateTaskTool } from './update-task.js';
|
||||
import { registerUpdateSubtaskTool } from './update-subtask.js';
|
||||
import { registerGenerateTool } from './generate.js';
|
||||
import { registerShowTaskTool } from './get-task.js';
|
||||
import { registerNextTaskTool } from './next-task.js';
|
||||
import { registerExpandTaskTool } from './expand-task.js';
|
||||
import { registerAddTaskTool } from './add-task.js';
|
||||
import { registerAddSubtaskTool } from './add-subtask.js';
|
||||
import { registerRemoveSubtaskTool } from './remove-subtask.js';
|
||||
import { registerAnalyzeProjectComplexityTool } from './analyze.js';
|
||||
import { registerClearSubtasksTool } from './clear-subtasks.js';
|
||||
import { registerExpandAllTool } from './expand-all.js';
|
||||
import { registerRemoveDependencyTool } from './remove-dependency.js';
|
||||
import { registerValidateDependenciesTool } from './validate-dependencies.js';
|
||||
import { registerFixDependenciesTool } from './fix-dependencies.js';
|
||||
import { registerComplexityReportTool } from './complexity-report.js';
|
||||
import { registerAddDependencyTool } from './add-dependency.js';
|
||||
import { registerRemoveTaskTool } from './remove-task.js';
|
||||
import { registerInitializeProjectTool } from './initialize-project.js';
|
||||
import { registerModelsTool } from './models.js';
|
||||
import { registerMoveTaskTool } from './move-task.js';
|
||||
import { registerResponseLanguageTool } from './response-language.js';
|
||||
import { registerAddTagTool } from './add-tag.js';
|
||||
import { registerDeleteTagTool } from './delete-tag.js';
|
||||
import { registerListTagsTool } from './list-tags.js';
|
||||
import { registerUseTagTool } from './use-tag.js';
|
||||
import { registerRenameTagTool } from './rename-tag.js';
|
||||
import { registerCopyTagTool } from './copy-tag.js';
|
||||
import { registerResearchTool } from './research.js';
|
||||
import { registerRulesTool } from './rules.js';
|
||||
import { registerScopeUpTool } from './scope-up.js';
|
||||
import { registerScopeDownTool } from './scope-down.js';
|
||||
|
||||
/**
|
||||
* Comprehensive tool registry mapping all 36 tool names to their registration functions
|
||||
* Used for dynamic tool registration and validation
|
||||
*/
|
||||
export const toolRegistry = {
|
||||
initialize_project: registerInitializeProjectTool,
|
||||
models: registerModelsTool,
|
||||
rules: registerRulesTool,
|
||||
parse_prd: registerParsePRDTool,
|
||||
'response-language': registerResponseLanguageTool,
|
||||
analyze_project_complexity: registerAnalyzeProjectComplexityTool,
|
||||
expand_task: registerExpandTaskTool,
|
||||
expand_all: registerExpandAllTool,
|
||||
scope_up_task: registerScopeUpTool,
|
||||
scope_down_task: registerScopeDownTool,
|
||||
get_tasks: registerListTasksTool,
|
||||
get_task: registerShowTaskTool,
|
||||
next_task: registerNextTaskTool,
|
||||
complexity_report: registerComplexityReportTool,
|
||||
set_task_status: registerSetTaskStatusTool,
|
||||
generate: registerGenerateTool,
|
||||
add_task: registerAddTaskTool,
|
||||
add_subtask: registerAddSubtaskTool,
|
||||
update: registerUpdateTool,
|
||||
update_task: registerUpdateTaskTool,
|
||||
update_subtask: registerUpdateSubtaskTool,
|
||||
remove_task: registerRemoveTaskTool,
|
||||
remove_subtask: registerRemoveSubtaskTool,
|
||||
clear_subtasks: registerClearSubtasksTool,
|
||||
move_task: registerMoveTaskTool,
|
||||
add_dependency: registerAddDependencyTool,
|
||||
remove_dependency: registerRemoveDependencyTool,
|
||||
validate_dependencies: registerValidateDependenciesTool,
|
||||
fix_dependencies: registerFixDependenciesTool,
|
||||
list_tags: registerListTagsTool,
|
||||
add_tag: registerAddTagTool,
|
||||
delete_tag: registerDeleteTagTool,
|
||||
use_tag: registerUseTagTool,
|
||||
rename_tag: registerRenameTagTool,
|
||||
copy_tag: registerCopyTagTool,
|
||||
research: registerResearchTool
|
||||
};
|
||||
|
||||
/**
|
||||
* Core tools array containing the 7 essential tools for daily development
|
||||
* These represent the minimal set needed for basic task management operations
|
||||
*/
|
||||
export const coreTools = [
|
||||
'get_tasks',
|
||||
'next_task',
|
||||
'get_task',
|
||||
'set_task_status',
|
||||
'update_subtask',
|
||||
'parse_prd',
|
||||
'expand_task'
|
||||
];
|
||||
|
||||
/**
|
||||
* Standard tools array containing the 15 most commonly used tools
|
||||
* Includes all core tools plus frequently used additional tools
|
||||
*/
|
||||
export const standardTools = [
|
||||
...coreTools,
|
||||
'initialize_project',
|
||||
'analyze_project_complexity',
|
||||
'expand_all',
|
||||
'add_subtask',
|
||||
'remove_task',
|
||||
'generate',
|
||||
'add_task',
|
||||
'complexity_report'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all available tool names
|
||||
* @returns {string[]} Array of tool names
|
||||
*/
|
||||
export function getAvailableTools() {
|
||||
return Object.keys(toolRegistry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool counts for all categories
|
||||
* @returns {Object} Object with core, standard, and total counts
|
||||
*/
|
||||
export function getToolCounts() {
|
||||
return {
|
||||
core: coreTools.length,
|
||||
standard: standardTools.length,
|
||||
total: Object.keys(toolRegistry).length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool arrays organized by category
|
||||
* @returns {Object} Object with arrays for each category
|
||||
*/
|
||||
export function getToolCategories() {
|
||||
const allTools = Object.keys(toolRegistry);
|
||||
return {
|
||||
core: [...coreTools],
|
||||
standard: [...standardTools],
|
||||
all: [...allTools],
|
||||
extended: allTools.filter((t) => !standardTools.includes(t))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registration function for a specific tool
|
||||
* @param {string} toolName - Name of the tool
|
||||
* @returns {Function|null} Registration function or null if not found
|
||||
*/
|
||||
export function getToolRegistration(toolName) {
|
||||
return toolRegistry[toolName] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a tool exists in the registry
|
||||
* @param {string} toolName - Name of the tool
|
||||
* @returns {boolean} True if tool exists
|
||||
*/
|
||||
export function isValidTool(toolName) {
|
||||
return toolName in toolRegistry;
|
||||
}
|
||||
|
||||
export default toolRegistry;
|
||||
30
output.txt
Normal file
30
output.txt
Normal file
File diff suppressed because one or more lines are too long
@@ -2441,57 +2441,6 @@ ${result.result}
|
||||
}
|
||||
});
|
||||
|
||||
// next command
|
||||
programInstance
|
||||
.command('next')
|
||||
.description(
|
||||
`Show the next task to work on based on dependencies and status${chalk.reset('')}`
|
||||
)
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.option(
|
||||
'-r, --report <report>',
|
||||
'Path to the complexity report file',
|
||||
COMPLEXITY_REPORT_FILE
|
||||
)
|
||||
.option('--tag <tag>', 'Specify tag context for task operations')
|
||||
.action(async (options) => {
|
||||
const initOptions = {
|
||||
tasksPath: options.file || true,
|
||||
tag: options.tag
|
||||
};
|
||||
|
||||
if (options.report && options.report !== COMPLEXITY_REPORT_FILE) {
|
||||
initOptions.complexityReportPath = options.report;
|
||||
}
|
||||
|
||||
// Initialize TaskMaster
|
||||
const taskMaster = initTaskMaster({
|
||||
tasksPath: options.file || true,
|
||||
tag: options.tag,
|
||||
complexityReportPath: options.report || false
|
||||
});
|
||||
|
||||
const tag = taskMaster.getCurrentTag();
|
||||
|
||||
const context = {
|
||||
projectRoot: taskMaster.getProjectRoot(),
|
||||
tag
|
||||
};
|
||||
|
||||
// Show current tag context
|
||||
displayCurrentTagIndicator(tag);
|
||||
|
||||
await displayNextTask(
|
||||
taskMaster.getTasksPath(),
|
||||
taskMaster.getComplexityReportPath(),
|
||||
context
|
||||
);
|
||||
});
|
||||
|
||||
// add-dependency command
|
||||
programInstance
|
||||
.command('add-dependency')
|
||||
|
||||
@@ -47,21 +47,33 @@ export function normalizeProjectRoot(projectRoot) {
|
||||
|
||||
/**
|
||||
* Find the project root directory by looking for project markers
|
||||
* @param {string} startDir - Directory to start searching from
|
||||
* @returns {string|null} - Project root path or null if not found
|
||||
* Traverses upwards from startDir until a project marker is found or filesystem root is reached
|
||||
* Limited to 50 parent directory levels to prevent excessive traversal
|
||||
* @param {string} startDir - Directory to start searching from (defaults to process.cwd())
|
||||
* @returns {string} - Project root path (falls back to current directory if no markers found)
|
||||
*/
|
||||
export function findProjectRoot(startDir = process.cwd()) {
|
||||
// Define project markers that indicate a project root
|
||||
// Prioritize Task Master specific markers first
|
||||
const projectMarkers = [
|
||||
'.taskmaster',
|
||||
TASKMASTER_TASKS_FILE,
|
||||
'tasks.json',
|
||||
LEGACY_TASKS_FILE,
|
||||
'.git',
|
||||
'.svn',
|
||||
'package.json',
|
||||
'yarn.lock',
|
||||
'package-lock.json',
|
||||
'pnpm-lock.yaml'
|
||||
'.taskmaster', // Task Master directory (highest priority)
|
||||
TASKMASTER_CONFIG_FILE, // .taskmaster/config.json
|
||||
TASKMASTER_TASKS_FILE, // .taskmaster/tasks/tasks.json
|
||||
LEGACY_CONFIG_FILE, // .taskmasterconfig (legacy)
|
||||
LEGACY_TASKS_FILE, // tasks/tasks.json (legacy)
|
||||
'tasks.json', // Root tasks.json (legacy)
|
||||
'.git', // Git repository
|
||||
'.svn', // SVN repository
|
||||
'package.json', // Node.js project
|
||||
'yarn.lock', // Yarn project
|
||||
'package-lock.json', // npm project
|
||||
'pnpm-lock.yaml', // pnpm project
|
||||
'Cargo.toml', // Rust project
|
||||
'go.mod', // Go project
|
||||
'pyproject.toml', // Python project
|
||||
'requirements.txt', // Python project
|
||||
'Gemfile', // Ruby project
|
||||
'composer.json' // PHP project
|
||||
];
|
||||
|
||||
let currentDir = path.resolve(startDir);
|
||||
@@ -69,19 +81,36 @@ export function findProjectRoot(startDir = process.cwd()) {
|
||||
const maxDepth = 50; // Reasonable limit to prevent infinite loops
|
||||
let depth = 0;
|
||||
|
||||
// Traverse upwards looking for project markers
|
||||
while (currentDir !== rootDir && depth < maxDepth) {
|
||||
// Check if current directory contains any project markers
|
||||
for (const marker of projectMarkers) {
|
||||
const markerPath = path.join(currentDir, marker);
|
||||
if (fs.existsSync(markerPath)) {
|
||||
return currentDir;
|
||||
try {
|
||||
if (fs.existsSync(markerPath)) {
|
||||
// Found a project marker - return this directory as project root
|
||||
return currentDir;
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore permission errors and continue searching
|
||||
continue;
|
||||
}
|
||||
}
|
||||
currentDir = path.dirname(currentDir);
|
||||
|
||||
// Move up one directory level
|
||||
const parentDir = path.dirname(currentDir);
|
||||
|
||||
// Safety check: if dirname returns the same path, we've hit the root
|
||||
if (parentDir === currentDir) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentDir = parentDir;
|
||||
depth++;
|
||||
}
|
||||
|
||||
// Fallback to current working directory if no project root found
|
||||
// This ensures the function always returns a valid path
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
|
||||
123
tests/helpers/tool-counts.js
Normal file
123
tests/helpers/tool-counts.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* tool-counts.js
|
||||
* Shared helper for validating tool counts across tests and validation scripts
|
||||
*/
|
||||
|
||||
import {
|
||||
getToolCounts,
|
||||
getToolCategories
|
||||
} from '../../mcp-server/src/tools/tool-registry.js';
|
||||
|
||||
/**
|
||||
* Expected tool counts - update these when tools are added/removed
|
||||
* These serve as the canonical source of truth for expected counts
|
||||
*/
|
||||
export const EXPECTED_TOOL_COUNTS = {
|
||||
core: 7,
|
||||
standard: 15,
|
||||
total: 36
|
||||
};
|
||||
|
||||
/**
|
||||
* Expected core tools list for validation
|
||||
*/
|
||||
export const EXPECTED_CORE_TOOLS = [
|
||||
'get_tasks',
|
||||
'next_task',
|
||||
'get_task',
|
||||
'set_task_status',
|
||||
'update_subtask',
|
||||
'parse_prd',
|
||||
'expand_task'
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate that actual tool counts match expected counts
|
||||
* @returns {Object} Validation result with isValid flag and details
|
||||
*/
|
||||
export function validateToolCounts() {
|
||||
const actual = getToolCounts();
|
||||
const expected = EXPECTED_TOOL_COUNTS;
|
||||
|
||||
const isValid =
|
||||
actual.core === expected.core &&
|
||||
actual.standard === expected.standard &&
|
||||
actual.total === expected.total;
|
||||
|
||||
return {
|
||||
isValid,
|
||||
actual,
|
||||
expected,
|
||||
differences: {
|
||||
core: actual.core - expected.core,
|
||||
standard: actual.standard - expected.standard,
|
||||
total: actual.total - expected.total
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that tool categories have correct structure and content
|
||||
* @returns {Object} Validation result
|
||||
*/
|
||||
export function validateToolStructure() {
|
||||
const categories = getToolCategories();
|
||||
const counts = getToolCounts();
|
||||
|
||||
// Check that core tools are subset of standard tools
|
||||
const coreInStandard = categories.core.every((tool) =>
|
||||
categories.standard.includes(tool)
|
||||
);
|
||||
|
||||
// Check that standard tools are subset of all tools
|
||||
const standardInAll = categories.standard.every((tool) =>
|
||||
categories.all.includes(tool)
|
||||
);
|
||||
|
||||
// Check that expected core tools match actual
|
||||
const expectedCoreMatch =
|
||||
EXPECTED_CORE_TOOLS.every((tool) => categories.core.includes(tool)) &&
|
||||
categories.core.every((tool) => EXPECTED_CORE_TOOLS.includes(tool));
|
||||
|
||||
// Check array lengths match counts
|
||||
const lengthsMatch =
|
||||
categories.core.length === counts.core &&
|
||||
categories.standard.length === counts.standard &&
|
||||
categories.all.length === counts.total;
|
||||
|
||||
return {
|
||||
isValid:
|
||||
coreInStandard && standardInAll && expectedCoreMatch && lengthsMatch,
|
||||
details: {
|
||||
coreInStandard,
|
||||
standardInAll,
|
||||
expectedCoreMatch,
|
||||
lengthsMatch
|
||||
},
|
||||
categories,
|
||||
counts
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a detailed report of all tool information
|
||||
* @returns {Object} Comprehensive tool information
|
||||
*/
|
||||
export function getToolReport() {
|
||||
const counts = getToolCounts();
|
||||
const categories = getToolCategories();
|
||||
const validation = validateToolCounts();
|
||||
const structure = validateToolStructure();
|
||||
|
||||
return {
|
||||
counts,
|
||||
categories,
|
||||
validation,
|
||||
structure,
|
||||
summary: {
|
||||
totalValid: validation.isValid && structure.isValid,
|
||||
countsValid: validation.isValid,
|
||||
structureValid: structure.isValid
|
||||
}
|
||||
};
|
||||
}
|
||||
410
tests/unit/mcp/tools/tool-registration.test.js
Normal file
410
tests/unit/mcp/tools/tool-registration.test.js
Normal file
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* tool-registration.test.js
|
||||
* Comprehensive unit tests for the Task Master MCP tool registration system
|
||||
* Tests environment variable control system covering all configuration modes and edge cases
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
jest
|
||||
} from '@jest/globals';
|
||||
|
||||
import {
|
||||
EXPECTED_TOOL_COUNTS,
|
||||
EXPECTED_CORE_TOOLS,
|
||||
validateToolCounts,
|
||||
validateToolStructure
|
||||
} from '../../../helpers/tool-counts.js';
|
||||
|
||||
import { registerTaskMasterTools } from '../../../../mcp-server/src/tools/index.js';
|
||||
import {
|
||||
toolRegistry,
|
||||
coreTools,
|
||||
standardTools
|
||||
} from '../../../../mcp-server/src/tools/tool-registry.js';
|
||||
|
||||
// Derive constants from imported registry to avoid brittle magic numbers
|
||||
const ALL_COUNT = Object.keys(toolRegistry).length;
|
||||
const CORE_COUNT = coreTools.length;
|
||||
const STANDARD_COUNT = standardTools.length;
|
||||
|
||||
describe('Task Master Tool Registration System', () => {
|
||||
let mockServer;
|
||||
let originalEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.TASK_MASTER_TOOLS;
|
||||
|
||||
mockServer = {
|
||||
tools: [],
|
||||
addTool: jest.fn((tool) => {
|
||||
mockServer.tools.push(tool);
|
||||
return tool;
|
||||
})
|
||||
};
|
||||
|
||||
delete process.env.TASK_MASTER_TOOLS;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.TASK_MASTER_TOOLS = originalEnv;
|
||||
} else {
|
||||
delete process.env.TASK_MASTER_TOOLS;
|
||||
}
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Test Environment Setup', () => {
|
||||
it('should have properly configured mock server', () => {
|
||||
expect(mockServer).toBeDefined();
|
||||
expect(typeof mockServer.addTool).toBe('function');
|
||||
expect(Array.isArray(mockServer.tools)).toBe(true);
|
||||
expect(mockServer.tools.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should have correct tool registry structure', () => {
|
||||
const validation = validateToolCounts();
|
||||
expect(validation.isValid).toBe(true);
|
||||
|
||||
if (!validation.isValid) {
|
||||
console.error('Tool count validation failed:', validation);
|
||||
}
|
||||
|
||||
expect(validation.actual.total).toBe(EXPECTED_TOOL_COUNTS.total);
|
||||
expect(validation.actual.core).toBe(EXPECTED_TOOL_COUNTS.core);
|
||||
expect(validation.actual.standard).toBe(EXPECTED_TOOL_COUNTS.standard);
|
||||
});
|
||||
|
||||
it('should have correct core tools', () => {
|
||||
const structure = validateToolStructure();
|
||||
expect(structure.isValid).toBe(true);
|
||||
|
||||
if (!structure.isValid) {
|
||||
console.error('Tool structure validation failed:', structure);
|
||||
}
|
||||
|
||||
expect(coreTools).toEqual(expect.arrayContaining(EXPECTED_CORE_TOOLS));
|
||||
expect(coreTools.length).toBe(EXPECTED_TOOL_COUNTS.core);
|
||||
});
|
||||
|
||||
it('should have correct standard tools that include all core tools', () => {
|
||||
const structure = validateToolStructure();
|
||||
expect(structure.details.coreInStandard).toBe(true);
|
||||
expect(standardTools.length).toBe(EXPECTED_TOOL_COUNTS.standard);
|
||||
|
||||
coreTools.forEach((tool) => {
|
||||
expect(standardTools).toContain(tool);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have all expected tools in registry', () => {
|
||||
const expectedTools = [
|
||||
'initialize_project',
|
||||
'models',
|
||||
'research',
|
||||
'add_tag',
|
||||
'delete_tag',
|
||||
'get_tasks',
|
||||
'next_task',
|
||||
'get_task'
|
||||
];
|
||||
expectedTools.forEach((tool) => {
|
||||
expect(toolRegistry).toHaveProperty(tool);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration Modes', () => {
|
||||
it(`should register all tools (${ALL_COUNT}) when TASK_MASTER_TOOLS is not set (default behavior)`, () => {
|
||||
delete process.env.TASK_MASTER_TOOLS;
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(
|
||||
EXPECTED_TOOL_COUNTS.total
|
||||
);
|
||||
});
|
||||
|
||||
it(`should register all tools (${ALL_COUNT}) when TASK_MASTER_TOOLS=all`, () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'all';
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
|
||||
it(`should register exactly ${CORE_COUNT} core tools when TASK_MASTER_TOOLS=core`, () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'core';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'core');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(
|
||||
EXPECTED_TOOL_COUNTS.core
|
||||
);
|
||||
});
|
||||
|
||||
it(`should register exactly ${STANDARD_COUNT} standard tools when TASK_MASTER_TOOLS=standard`, () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'standard';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'standard');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(
|
||||
EXPECTED_TOOL_COUNTS.standard
|
||||
);
|
||||
});
|
||||
|
||||
it(`should treat lean as alias for core mode (${CORE_COUNT} tools)`, () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'lean';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'lean');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT);
|
||||
});
|
||||
|
||||
it('should handle case insensitive configuration values', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'CORE';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'CORE');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Tool Selection and Edge Cases', () => {
|
||||
it('should register specific tools from comma-separated list', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'get_tasks,next_task,get_task';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'get_tasks,next_task,get_task');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should handle mixed valid and invalid tool names gracefully', () => {
|
||||
process.env.TASK_MASTER_TOOLS =
|
||||
'invalid_tool,get_tasks,fake_tool,next_task';
|
||||
|
||||
registerTaskMasterTools(
|
||||
mockServer,
|
||||
'invalid_tool,get_tasks,fake_tool,next_task'
|
||||
);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should default to all tools with completely invalid input', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'completely_invalid';
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
|
||||
it('should handle empty string environment variable', () => {
|
||||
process.env.TASK_MASTER_TOOLS = '';
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
|
||||
it('should handle whitespace in comma-separated lists', () => {
|
||||
process.env.TASK_MASTER_TOOLS = ' get_tasks , next_task , get_task ';
|
||||
|
||||
registerTaskMasterTools(mockServer, ' get_tasks , next_task , get_task ');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should ignore duplicate tools in list', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'get_tasks,get_tasks,next_task,get_tasks';
|
||||
|
||||
registerTaskMasterTools(
|
||||
mockServer,
|
||||
'get_tasks,get_tasks,next_task,get_tasks'
|
||||
);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle only commas and empty entries', () => {
|
||||
process.env.TASK_MASTER_TOOLS = ',,,';
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
|
||||
it('should handle single tool selection', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'get_tasks';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'get_tasks');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Coverage Analysis and Integration Tests', () => {
|
||||
it('should provide 100% code coverage for environment control logic', () => {
|
||||
const testCases = [
|
||||
{
|
||||
env: undefined,
|
||||
expectedCount: ALL_COUNT,
|
||||
description: 'undefined env (all)'
|
||||
},
|
||||
{
|
||||
env: '',
|
||||
expectedCount: ALL_COUNT,
|
||||
description: 'empty string (all)'
|
||||
},
|
||||
{ env: 'all', expectedCount: ALL_COUNT, description: 'all mode' },
|
||||
{ env: 'core', expectedCount: CORE_COUNT, description: 'core mode' },
|
||||
{
|
||||
env: 'lean',
|
||||
expectedCount: CORE_COUNT,
|
||||
description: 'lean mode (alias)'
|
||||
},
|
||||
{
|
||||
env: 'standard',
|
||||
expectedCount: STANDARD_COUNT,
|
||||
description: 'standard mode'
|
||||
},
|
||||
{
|
||||
env: 'get_tasks,next_task',
|
||||
expectedCount: 2,
|
||||
description: 'custom list'
|
||||
},
|
||||
{
|
||||
env: 'invalid_tool',
|
||||
expectedCount: ALL_COUNT,
|
||||
description: 'invalid fallback'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
delete process.env.TASK_MASTER_TOOLS;
|
||||
if (testCase.env !== undefined) {
|
||||
process.env.TASK_MASTER_TOOLS = testCase.env;
|
||||
}
|
||||
|
||||
mockServer.tools = [];
|
||||
mockServer.addTool.mockClear();
|
||||
|
||||
registerTaskMasterTools(mockServer, testCase.env || 'all');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(
|
||||
testCase.expectedCount
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have optimal performance characteristics', () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
process.env.TASK_MASTER_TOOLS = 'all';
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
const endTime = Date.now();
|
||||
const executionTime = endTime - startTime;
|
||||
|
||||
expect(executionTime).toBeLessThan(100);
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
|
||||
it('should validate token reduction claims', () => {
|
||||
expect(coreTools.length).toBeLessThan(standardTools.length);
|
||||
expect(standardTools.length).toBeLessThan(
|
||||
Object.keys(toolRegistry).length
|
||||
);
|
||||
|
||||
expect(coreTools.length).toBe(CORE_COUNT);
|
||||
expect(standardTools.length).toBe(STANDARD_COUNT);
|
||||
expect(Object.keys(toolRegistry).length).toBe(ALL_COUNT);
|
||||
|
||||
const allToolsCount = Object.keys(toolRegistry).length;
|
||||
const coreReduction =
|
||||
((allToolsCount - coreTools.length) / allToolsCount) * 100;
|
||||
const standardReduction =
|
||||
((allToolsCount - standardTools.length) / allToolsCount) * 100;
|
||||
|
||||
expect(coreReduction).toBeGreaterThan(80);
|
||||
expect(standardReduction).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
it('should maintain referential integrity of tool registry', () => {
|
||||
coreTools.forEach((tool) => {
|
||||
expect(standardTools).toContain(tool);
|
||||
});
|
||||
|
||||
standardTools.forEach((tool) => {
|
||||
expect(toolRegistry).toHaveProperty(tool);
|
||||
});
|
||||
|
||||
Object.keys(toolRegistry).forEach((tool) => {
|
||||
expect(typeof toolRegistry[tool]).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle concurrent registration attempts', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'core';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'core');
|
||||
registerTaskMasterTools(mockServer, 'core');
|
||||
registerTaskMasterTools(mockServer, 'core');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT * 3);
|
||||
});
|
||||
|
||||
it('should validate all documented tool categories exist', () => {
|
||||
const allTools = Object.keys(toolRegistry);
|
||||
|
||||
const projectSetupTools = allTools.filter((tool) =>
|
||||
['initialize_project', 'models', 'rules', 'parse_prd'].includes(tool)
|
||||
);
|
||||
expect(projectSetupTools.length).toBeGreaterThan(0);
|
||||
|
||||
const taskManagementTools = allTools.filter((tool) =>
|
||||
['get_tasks', 'get_task', 'next_task', 'set_task_status'].includes(tool)
|
||||
);
|
||||
expect(taskManagementTools.length).toBeGreaterThan(0);
|
||||
|
||||
const analysisTools = allTools.filter((tool) =>
|
||||
['analyze_project_complexity', 'complexity_report'].includes(tool)
|
||||
);
|
||||
expect(analysisTools.length).toBeGreaterThan(0);
|
||||
|
||||
const tagManagementTools = allTools.filter((tool) =>
|
||||
['add_tag', 'delete_tag', 'list_tags', 'use_tag'].includes(tool)
|
||||
);
|
||||
expect(tagManagementTools.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle error conditions gracefully', () => {
|
||||
const problematicInputs = [
|
||||
'null',
|
||||
'undefined',
|
||||
' ',
|
||||
'\n\t',
|
||||
'special!@#$%^&*()characters',
|
||||
'very,very,very,very,very,very,very,long,comma,separated,list,with,invalid,tools,that,should,fallback,to,all'
|
||||
];
|
||||
|
||||
problematicInputs.forEach((input) => {
|
||||
mockServer.tools = [];
|
||||
mockServer.addTool.mockClear();
|
||||
|
||||
process.env.TASK_MASTER_TOOLS = input;
|
||||
|
||||
expect(() => registerTaskMasterTools(mockServer)).not.toThrow();
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
223
tests/unit/path-utils-find-project-root.test.js
Normal file
223
tests/unit/path-utils-find-project-root.test.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Unit tests for findProjectRoot() function
|
||||
* Tests the parent directory traversal functionality
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// Import the function to test
|
||||
import { findProjectRoot } from '../../src/utils/path-utils.js';
|
||||
|
||||
describe('findProjectRoot', () => {
|
||||
describe('Parent Directory Traversal', () => {
|
||||
test('should find .taskmaster in parent directory', () => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
mockExistsSync.mockImplementation((checkPath) => {
|
||||
const normalized = path.normalize(checkPath);
|
||||
// .taskmaster exists only at /project
|
||||
return normalized === path.normalize('/project/.taskmaster');
|
||||
});
|
||||
|
||||
const result = findProjectRoot('/project/subdir');
|
||||
|
||||
expect(result).toBe('/project');
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
|
||||
test('should find .git in parent directory', () => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
mockExistsSync.mockImplementation((checkPath) => {
|
||||
const normalized = path.normalize(checkPath);
|
||||
return normalized === path.normalize('/project/.git');
|
||||
});
|
||||
|
||||
const result = findProjectRoot('/project/subdir');
|
||||
|
||||
expect(result).toBe('/project');
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
|
||||
test('should find package.json in parent directory', () => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
mockExistsSync.mockImplementation((checkPath) => {
|
||||
const normalized = path.normalize(checkPath);
|
||||
return normalized === path.normalize('/project/package.json');
|
||||
});
|
||||
|
||||
const result = findProjectRoot('/project/subdir');
|
||||
|
||||
expect(result).toBe('/project');
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
|
||||
test('should traverse multiple levels to find project root', () => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
mockExistsSync.mockImplementation((checkPath) => {
|
||||
const normalized = path.normalize(checkPath);
|
||||
// Only exists at /project, not in any subdirectories
|
||||
return normalized === path.normalize('/project/.taskmaster');
|
||||
});
|
||||
|
||||
const result = findProjectRoot('/project/subdir/deep/nested');
|
||||
|
||||
expect(result).toBe('/project');
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
|
||||
test('should return current directory as fallback when no markers found', () => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
// No project markers exist anywhere
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = findProjectRoot('/some/random/path');
|
||||
|
||||
// Should fall back to process.cwd()
|
||||
expect(result).toBe(process.cwd());
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
|
||||
test('should find markers at current directory before checking parent', () => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
mockExistsSync.mockImplementation((checkPath) => {
|
||||
const normalized = path.normalize(checkPath);
|
||||
// .git exists at /project/subdir, .taskmaster exists at /project
|
||||
if (normalized.includes('/project/subdir/.git')) return true;
|
||||
if (normalized.includes('/project/.taskmaster')) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
const result = findProjectRoot('/project/subdir');
|
||||
|
||||
// Should find /project/subdir first because .git exists there,
|
||||
// even though .taskmaster is earlier in the marker array
|
||||
expect(result).toBe('/project/subdir');
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
|
||||
test('should handle permission errors gracefully', () => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
mockExistsSync.mockImplementation((checkPath) => {
|
||||
const normalized = path.normalize(checkPath);
|
||||
// Throw permission error for checks in /project/subdir
|
||||
if (normalized.startsWith('/project/subdir/')) {
|
||||
throw new Error('EACCES: permission denied');
|
||||
}
|
||||
// Return true only for .taskmaster at /project
|
||||
return normalized.includes('/project/.taskmaster');
|
||||
});
|
||||
|
||||
const result = findProjectRoot('/project/subdir');
|
||||
|
||||
// Should handle permission errors in subdirectory and traverse to parent
|
||||
expect(result).toBe('/project');
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
|
||||
test('should detect filesystem root correctly', () => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
// No markers exist
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = findProjectRoot('/');
|
||||
|
||||
// Should stop at root and fall back to process.cwd()
|
||||
expect(result).toBe(process.cwd());
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
|
||||
test('should recognize various project markers', () => {
|
||||
const projectMarkers = [
|
||||
'.taskmaster',
|
||||
'.git',
|
||||
'package.json',
|
||||
'Cargo.toml',
|
||||
'go.mod',
|
||||
'pyproject.toml',
|
||||
'requirements.txt',
|
||||
'Gemfile',
|
||||
'composer.json'
|
||||
];
|
||||
|
||||
projectMarkers.forEach((marker) => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
mockExistsSync.mockImplementation((checkPath) => {
|
||||
const normalized = path.normalize(checkPath);
|
||||
return normalized.includes(`/project/${marker}`);
|
||||
});
|
||||
|
||||
const result = findProjectRoot('/project/subdir');
|
||||
|
||||
expect(result).toBe('/project');
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
test('should handle empty string as startDir', () => {
|
||||
const result = findProjectRoot('');
|
||||
|
||||
// Should use process.cwd() or fall back appropriately
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should handle relative paths', () => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
mockExistsSync.mockImplementation((checkPath) => {
|
||||
// Simulate .git existing in the resolved path
|
||||
return checkPath.includes('.git');
|
||||
});
|
||||
|
||||
const result = findProjectRoot('./subdir');
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
|
||||
test('should not exceed max depth limit', () => {
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
|
||||
// Track how many times existsSync is called
|
||||
let callCount = 0;
|
||||
mockExistsSync.mockImplementation(() => {
|
||||
callCount++;
|
||||
return false; // Never find a marker
|
||||
});
|
||||
|
||||
// Create a very deep path
|
||||
const deepPath = '/a/'.repeat(100) + 'deep';
|
||||
const result = findProjectRoot(deepPath);
|
||||
|
||||
// Should stop after max depth (50) and not check 100 levels
|
||||
// Each level checks multiple markers, so callCount will be high but bounded
|
||||
expect(callCount).toBeLessThan(1000); // Reasonable upper bound
|
||||
// With 18 markers and max depth of 50, expect around 900 calls maximum
|
||||
expect(callCount).toBeLessThanOrEqual(50 * 18);
|
||||
|
||||
mockExistsSync.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user