- Remove automatic git workflow features, update to manual --from-branch option, change Part 2 references to completed status
1176 lines
41 KiB
Plaintext
1176 lines
41 KiB
Plaintext
---
|
||
description:
|
||
globs:
|
||
alwaysApply: false
|
||
---
|
||
# Utility Function Guidelines
|
||
|
||
## General Principles
|
||
|
||
- **Function Scope**:
|
||
- ✅ DO: Create utility functions that serve multiple modules
|
||
- ✅ DO: Keep functions single-purpose and focused
|
||
- ❌ DON'T: Include business logic in utility functions
|
||
- ❌ DON'T: Create utilities with side effects
|
||
|
||
```javascript
|
||
// ✅ DO: Create focused, reusable utilities
|
||
/**
|
||
* Truncates text to a specified length
|
||
* @param {string} text - The text to truncate
|
||
* @param {number} maxLength - The maximum length
|
||
* @returns {string} The truncated text
|
||
*/
|
||
function truncate(text, maxLength) {
|
||
if (!text || text.length <= maxLength) {
|
||
return text;
|
||
}
|
||
return text.slice(0, maxLength - 3) + '...';
|
||
}
|
||
```
|
||
|
||
```javascript
|
||
// ❌ DON'T: Add side effects to utilities
|
||
function truncate(text, maxLength) {
|
||
if (!text || text.length <= maxLength) {
|
||
return text;
|
||
}
|
||
|
||
// Side effect - modifying global state or logging
|
||
console.log(`Truncating text from ${text.length} to ${maxLength} chars`);
|
||
|
||
return text.slice(0, maxLength - 3) + '...';
|
||
}
|
||
```
|
||
|
||
- **Location**:
|
||
- **Core CLI Utilities**: Place utilities used primarily by the core `task-master` CLI logic and command modules (`scripts/modules/*`) into [`scripts/modules/utils.js`](mdc:scripts/modules/utils.js).
|
||
- **MCP Server Utilities**: Place utilities specifically designed to support the MCP server implementation into the appropriate subdirectories within `mcp-server/src/`.
|
||
- Path/Core Logic Helpers: [`mcp-server/src/core/utils/`](mdc:mcp-server/src/core/utils) (e.g., `path-utils.js`).
|
||
- Tool Execution/Response Helpers: [`mcp-server/src/tools/utils.js`](mdc:mcp-server/src/tools/utils.js).
|
||
|
||
## Documentation Standards
|
||
|
||
- **JSDoc Format**:
|
||
- ✅ DO: Document all parameters and return values
|
||
- ✅ DO: Include descriptions for complex logic
|
||
- ✅ DO: Add examples for non-obvious usage
|
||
- ❌ DON'T: Skip documentation for "simple" functions
|
||
|
||
```javascript
|
||
// ✅ DO: Provide complete JSDoc documentation
|
||
/**
|
||
* Reads and parses a JSON file
|
||
* @param {string} filepath - Path to the JSON file
|
||
* @returns {Object|null} Parsed JSON data or null if error occurs
|
||
*/
|
||
function readJSON(filepath) {
|
||
try {
|
||
const rawData = fs.readFileSync(filepath, 'utf8');
|
||
return JSON.parse(rawData);
|
||
} catch (error) {
|
||
log('error', `Error reading JSON file ${filepath}:`, error.message);
|
||
if (CONFIG.debug) {
|
||
console.error(error);
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
```
|
||
|
||
## Configuration Management (via `config-manager.js`)
|
||
|
||
Taskmaster configuration (excluding API keys) is primarily managed through the `.taskmasterconfig` file located in the project root and accessed via getters in [`scripts/modules/config-manager.js`](mdc:scripts/modules/config-manager.js).
|
||
|
||
- **`.taskmasterconfig` File**:
|
||
- ✅ DO: Use this JSON file to store settings like AI model selections (main, research, fallback), parameters (temperature, maxTokens), logging level, default priority/subtasks, etc.
|
||
- ✅ DO: Manage this file using the `task-master models --setup` CLI command or the `models` MCP tool.
|
||
- ✅ DO: Rely on [`config-manager.js`](mdc:scripts/modules/config-manager.js) to load this file (using the correct project root passed from MCP or found via CLI utils), merge with defaults, and provide validated settings.
|
||
- ❌ DON'T: Store API keys in this file.
|
||
- ❌ DON'T: Manually edit this file unless necessary.
|
||
|
||
- **Configuration Getters (`config-manager.js`)**:
|
||
- ✅ DO: Import and use specific getters from `config-manager.js` (e.g., `getMainProvider()`, `getLogLevel()`, `getMainMaxTokens()`) to access configuration values *needed for application logic* (like `getDefaultSubtasks`).
|
||
- ✅ DO: Pass the `explicitRoot` parameter to getters if calling from MCP direct functions to ensure the correct project's config is loaded.
|
||
- ❌ DON'T: Call AI-specific getters (like `getMainModelId`, `getMainMaxTokens`) from core logic functions (`scripts/modules/task-manager/*`). Instead, pass the `role` to the unified AI service.
|
||
- ❌ DON'T: Access configuration values directly from environment variables (except API keys).
|
||
|
||
- **API Key Handling (`utils.js` & `ai-services-unified.js`)**:
|
||
- ✅ DO: Store API keys **only** in `.env` (for CLI, loaded by `dotenv` in `scripts/dev.js`) or `.cursor/mcp.json` (for MCP, accessed via `session.env`).
|
||
- ✅ DO: Use `isApiKeySet(providerName, session)` from `config-manager.js` to check if a provider's key is available *before* potentially attempting an AI call if needed, but note the unified service performs its own internal check.
|
||
- ✅ DO: Understand that the unified service layer (`ai-services-unified.js`) internally resolves API keys using `resolveEnvVariable(key, session)` from `utils.js`.
|
||
|
||
- **Error Handling**:
|
||
- ✅ DO: Handle potential `ConfigurationError` if the `.taskmasterconfig` file is missing or invalid when accessed via `getConfig` (e.g., in `commands.js` or direct functions).
|
||
|
||
## Logging Utilities (in `scripts/modules/utils.js`)
|
||
|
||
- **Log Levels**:
|
||
- ✅ DO: Support multiple log levels (debug, info, warn, error)
|
||
- ✅ DO: Use appropriate icons for different log levels
|
||
- ✅ DO: Respect the configured log level
|
||
- ❌ DON'T: Add direct console.log calls outside the logging utility
|
||
- **Note on Passed Loggers**: When a logger object (like the FastMCP `log` object) is passed *as a parameter* (e.g., as `mcpLog`) into core Task Master functions, the receiving function often expects specific methods (`.info`, `.warn`, `.error`, etc.) to be directly callable on that object (e.g., `mcpLog[level](mdc:...)`). If the passed logger doesn't have this exact structure, a wrapper object may be needed. See the **Handling Logging Context (`mcpLog`)** section in [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) for the standard pattern used in direct functions.
|
||
|
||
- **Logger Wrapper Pattern**:
|
||
- ✅ DO: Use the logger wrapper pattern when passing loggers to prevent `mcpLog[level] is not a function` errors:
|
||
```javascript
|
||
// Standard logWrapper pattern to wrap FastMCP's log object
|
||
const logWrapper = {
|
||
info: (message, ...args) => log.info(message, ...args),
|
||
warn: (message, ...args) => log.warn(message, ...args),
|
||
error: (message, ...args) => log.error(message, ...args),
|
||
debug: (message, ...args) => log.debug && log.debug(message, ...args),
|
||
success: (message, ...args) => log.info(message, ...args) // Map success to info
|
||
};
|
||
|
||
// Pass this wrapper as mcpLog to ensure consistent method availability
|
||
// This also ensures output format is set to 'json' in many core functions
|
||
const options = { mcpLog: logWrapper, session };
|
||
```
|
||
- ✅ DO: Implement this pattern in any direct function that calls core functions expecting `mcpLog`
|
||
- ✅ DO: Use this solution in conjunction with silent mode for complete output control
|
||
- ❌ DON'T: Pass the FastMCP `log` object directly as `mcpLog` to core functions
|
||
- **Important**: This pattern has successfully fixed multiple issues in MCP tools (e.g., `update-task`, `update-subtask`) where using or omitting `mcpLog` incorrectly led to runtime errors or JSON parsing failures.
|
||
- For complete implementation details, see the **Handling Logging Context (`mcpLog`)** section in [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc).
|
||
|
||
```javascript
|
||
// ✅ DO: Implement a proper logging utility
|
||
const LOG_LEVELS = {
|
||
debug: 0,
|
||
info: 1,
|
||
warn: 2,
|
||
error: 3
|
||
};
|
||
|
||
function log(level, ...args) {
|
||
const icons = {
|
||
debug: chalk.gray('🔍'),
|
||
info: chalk.blue('ℹ️'),
|
||
warn: chalk.yellow('⚠️'),
|
||
error: chalk.red('❌'),
|
||
success: chalk.green('✅')
|
||
};
|
||
|
||
if (LOG_LEVELS[level] >= LOG_LEVELS[CONFIG.logLevel]) {
|
||
const icon = icons[level] || '';
|
||
console.log(`${icon} ${args.join(' ')}`);
|
||
}
|
||
}
|
||
```
|
||
|
||
## Silent Mode Utilities (in `scripts/modules/utils.js`)
|
||
|
||
- **Silent Mode Control**:
|
||
- ✅ DO: Use the exported silent mode functions rather than accessing global variables
|
||
- ✅ DO: Always use `isSilentMode()` to check the current silent mode state
|
||
- ✅ DO: Ensure silent mode is disabled in a `finally` block to prevent it from staying enabled
|
||
- ❌ DON'T: Access the global `silentMode` variable directly
|
||
- ❌ DON'T: Forget to disable silent mode after enabling it
|
||
|
||
```javascript
|
||
// ✅ DO: Use the silent mode control functions properly
|
||
|
||
// Example of proper implementation in utils.js:
|
||
|
||
// Global silent mode flag (private to the module)
|
||
let silentMode = false;
|
||
|
||
// Enable silent mode
|
||
function enableSilentMode() {
|
||
silentMode = true;
|
||
}
|
||
|
||
// Disable silent mode
|
||
function disableSilentMode() {
|
||
silentMode = false;
|
||
}
|
||
|
||
// Check if silent mode is enabled
|
||
function isSilentMode() {
|
||
return silentMode;
|
||
}
|
||
|
||
// Example of proper usage in another module:
|
||
import { enableSilentMode, disableSilentMode, isSilentMode } from './utils.js';
|
||
|
||
// Check current status
|
||
if (!isSilentMode()) {
|
||
console.log('Silent mode is not enabled');
|
||
}
|
||
|
||
// Use try/finally pattern to ensure silent mode is disabled
|
||
try {
|
||
enableSilentMode();
|
||
// Do something that should suppress console output
|
||
performOperation();
|
||
} finally {
|
||
disableSilentMode();
|
||
}
|
||
```
|
||
|
||
- **Integration with Logging**:
|
||
- ✅ DO: Make the `log` function respect silent mode
|
||
```javascript
|
||
function log(level, ...args) {
|
||
// Skip logging if silent mode is enabled
|
||
if (isSilentMode()) {
|
||
return;
|
||
}
|
||
|
||
// Rest of logging logic...
|
||
}
|
||
```
|
||
|
||
- **Common Patterns for Silent Mode**:
|
||
- ✅ DO: In **direct functions** (`mcp-server/src/core/direct-functions/*`) that call **core functions** (`scripts/modules/*`), ensure console output from the core function is suppressed to avoid breaking MCP JSON responses.
|
||
- **Preferred Method**: Update the core function to accept an `outputFormat` parameter (e.g., `outputFormat = 'text'`) and make it check `outputFormat === 'text'` before displaying any UI elements (banners, spinners, boxes, direct `console.log`s). Pass `'json'` from the direct function.
|
||
- **Necessary Fallback/Guarantee**: If the core function *cannot* be modified or its output suppression via `outputFormat` is unreliable, **wrap the core function call within the direct function** using `enableSilentMode()` and `disableSilentMode()` in a `try/finally` block. This acts as a safety net.
|
||
```javascript
|
||
// Example in a direct function
|
||
export async function someOperationDirect(args, log) {
|
||
let result;
|
||
const tasksPath = findTasksJsonPath(args, log); // Get path first
|
||
|
||
// Option 1: Core function handles 'json' format (Preferred)
|
||
try {
|
||
result = await coreFunction(tasksPath, ...otherArgs, 'json'); // Pass 'json'
|
||
return { success: true, data: result, fromCache: false };
|
||
} catch (error) {
|
||
// Handle error...
|
||
}
|
||
|
||
// Option 2: Core function output unreliable (Fallback/Guarantee)
|
||
try {
|
||
enableSilentMode(); // Enable before call
|
||
result = await coreFunction(tasksPath, ...otherArgs); // Call without format param
|
||
} catch (error) {
|
||
// Handle error...
|
||
log.error(`Failed: ${error.message}`);
|
||
return { success: false, error: { /* ... */ } };
|
||
} finally {
|
||
disableSilentMode(); // ALWAYS disable in finally
|
||
}
|
||
return { success: true, data: result, fromCache: false }; // Assuming success if no error caught
|
||
}
|
||
```
|
||
- ✅ DO: For functions that accept a silent mode parameter but also need to check global state (less common):
|
||
```javascript
|
||
// Check both the passed parameter and global silent mode
|
||
const isSilent = options.silentMode || (typeof options.silentMode === 'undefined' && isSilentMode());
|
||
```
|
||
|
||
## File Operations (in `scripts/modules/utils.js`)
|
||
|
||
- **Error Handling**:
|
||
- ✅ DO: Use try/catch blocks for all file operations
|
||
- ✅ DO: Return null or a default value on failure
|
||
- ✅ DO: Log detailed error information using the `log` utility
|
||
- ❌ DON'T: Allow exceptions to propagate unhandled from simple file reads/writes
|
||
|
||
```javascript
|
||
// ✅ DO: Handle file operation errors properly in core utils
|
||
function writeJSON(filepath, data) {
|
||
try {
|
||
// Ensure directory exists (example)
|
||
const dir = path.dirname(filepath);
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
|
||
} catch (error) {
|
||
log('error', `Error writing JSON file ${filepath}:`, error.message);
|
||
if (CONFIG.debug) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## Task-Specific Utilities (in `scripts/modules/utils.js`)
|
||
|
||
- **Task ID Formatting**:
|
||
- ✅ DO: Create utilities for consistent ID handling
|
||
- ✅ DO: Support different ID formats (numeric, string, dot notation)
|
||
- ❌ DON'T: Duplicate formatting logic across modules
|
||
|
||
```javascript
|
||
// ✅ DO: Create utilities for common operations
|
||
/**
|
||
* Formats a task ID as a string
|
||
* @param {string|number} id - The task ID to format
|
||
* @returns {string} The formatted task ID
|
||
*/
|
||
function formatTaskId(id) {
|
||
if (typeof id === 'string' && id.includes('.')) {
|
||
return id; // Already formatted as a string with a dot (e.g., "1.2")
|
||
}
|
||
|
||
if (typeof id === 'number') {
|
||
return id.toString();
|
||
}
|
||
|
||
return id;
|
||
}
|
||
```
|
||
|
||
- **Task Search**:
|
||
- ✅ DO: Implement reusable task finding utilities
|
||
- ✅ DO: Support both task and subtask lookups
|
||
- ✅ DO: Add context to subtask results
|
||
|
||
```javascript
|
||
// ✅ DO: Create comprehensive search utilities
|
||
/**
|
||
* Finds a task by ID in the tasks array
|
||
* @param {Array} tasks - The tasks array
|
||
* @param {string|number} taskId - The task ID to find
|
||
* @returns {Object|null} The task object or null if not found
|
||
*/
|
||
function findTaskById(tasks, taskId) {
|
||
if (!taskId || !tasks || !Array.isArray(tasks)) {
|
||
return null;
|
||
}
|
||
|
||
// Check if it's a subtask ID (e.g., "1.2")
|
||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||
const [parentId, subtaskId] = taskId.split('.').map(id => parseInt(id, 10));
|
||
const parentTask = tasks.find(t => t.id === parentId);
|
||
|
||
if (!parentTask || !parentTask.subtasks) {
|
||
return null;
|
||
}
|
||
|
||
const subtask = parentTask.subtasks.find(st => st.id === subtaskId);
|
||
if (subtask) {
|
||
// Add reference to parent task for context
|
||
subtask.parentTask = {
|
||
id: parentTask.id,
|
||
title: parentTask.title,
|
||
status: parentTask.status
|
||
};
|
||
subtask.isSubtask = true;
|
||
}
|
||
|
||
return subtask || null;
|
||
}
|
||
|
||
const id = parseInt(taskId, 10);
|
||
return tasks.find(t => t.id === id) || null;
|
||
}
|
||
```
|
||
|
||
## Cycle Detection (in `scripts/modules/utils.js`)
|
||
|
||
- **Graph Algorithms**:
|
||
- ✅ DO: Implement cycle detection using graph traversal
|
||
- ✅ DO: Track visited nodes and recursion stack
|
||
- ✅ DO: Return specific information about cycles
|
||
|
||
```javascript
|
||
// ✅ DO: Implement proper cycle detection
|
||
/**
|
||
* Find cycles in a dependency graph using DFS
|
||
* @param {string} subtaskId - Current subtask ID
|
||
* @param {Map} dependencyMap - Map of subtask IDs to their dependencies
|
||
* @param {Set} visited - Set of visited nodes
|
||
* @param {Set} recursionStack - Set of nodes in current recursion stack
|
||
* @returns {Array} - List of dependency edges that need to be removed to break cycles
|
||
*/
|
||
function findCycles(subtaskId, dependencyMap, visited = new Set(), recursionStack = new Set(), path = []) {
|
||
// Mark the current node as visited and part of recursion stack
|
||
visited.add(subtaskId);
|
||
recursionStack.add(subtaskId);
|
||
path.push(subtaskId);
|
||
|
||
const cyclesToBreak = [];
|
||
|
||
// Get all dependencies of the current subtask
|
||
const dependencies = dependencyMap.get(subtaskId) || [];
|
||
|
||
// For each dependency
|
||
for (const depId of dependencies) {
|
||
// If not visited, recursively check for cycles
|
||
if (!visited.has(depId)) {
|
||
const cycles = findCycles(depId, dependencyMap, visited, recursionStack, [...path]);
|
||
cyclesToBreak.push(...cycles);
|
||
}
|
||
// If the dependency is in the recursion stack, we found a cycle
|
||
else if (recursionStack.has(depId)) {
|
||
// The last edge in the cycle is what we want to remove
|
||
cyclesToBreak.push(depId);
|
||
}
|
||
}
|
||
|
||
// Remove the node from recursion stack before returning
|
||
recursionStack.delete(subtaskId);
|
||
|
||
return cyclesToBreak;
|
||
}
|
||
```
|
||
|
||
## MCP Server Core Utilities (`mcp-server/src/core/utils/`)
|
||
|
||
### Project Root and Task File Path Detection (`path-utils.js`)
|
||
|
||
- **Purpose**: This module ([`mcp-server/src/core/utils/path-utils.js`](mdc:mcp-server/src/core/utils/path-utils.js)) provides the mechanism for locating the user's `tasks.json` file, used by direct functions.
|
||
- **`findTasksJsonPath(args, log)`**:
|
||
- ✅ **DO**: Call this function from within **direct function wrappers** (e.g., `listTasksDirect` in `mcp-server/src/core/direct-functions/`) to get the absolute path to the relevant `tasks.json`.
|
||
- Pass the *entire `args` object* received by the MCP tool (which should include `projectRoot` derived from the session) and the `log` object.
|
||
- Implements a **simplified precedence system** for finding the `tasks.json` path:
|
||
1. Explicit `projectRoot` passed in `args` (Expected from MCP tools).
|
||
2. Cached `lastFoundProjectRoot` (CLI fallback).
|
||
3. Search upwards from `process.cwd()` (CLI fallback).
|
||
- Throws a specific error if the `tasks.json` file cannot be located.
|
||
- Updates the `lastFoundProjectRoot` cache on success.
|
||
- **`PROJECT_MARKERS`**: An exported array of common file/directory names used to identify a likely project root during the CLI fallback search.
|
||
- **`getPackagePath()`**: Utility to find the installation path of the `task-master-ai` package itself (potentially removable).
|
||
|
||
## MCP Server Tool Utilities (`mcp-server/src/tools/utils.js`)
|
||
|
||
These utilities specifically support the implementation and execution of MCP tools.
|
||
|
||
- **`normalizeProjectRoot(rawPath, log)`**:
|
||
- **Purpose**: Takes a raw project root path (potentially URI encoded, with `file://` prefix, Windows slashes) and returns a normalized, absolute path suitable for the server's OS.
|
||
- **Logic**: Decodes URI, strips `file://`, handles Windows drive prefix (`/C:/`), replaces `\` with `/`, uses `path.resolve()`.
|
||
- **Usage**: Used internally by `withNormalizedProjectRoot` HOF.
|
||
|
||
- **`getRawProjectRootFromSession(session, log)`**:
|
||
- **Purpose**: Extracts the *raw* project root URI string from the session object (`session.roots[0].uri` or `session.roots.roots[0].uri`) without performing normalization.
|
||
- **Usage**: Used internally by `withNormalizedProjectRoot` HOF as a fallback if `args.projectRoot` isn't provided.
|
||
|
||
- **`withNormalizedProjectRoot(executeFn)`**:
|
||
- **Purpose**: A Higher-Order Function (HOF) designed to wrap a tool's `execute` method.
|
||
- **Logic**:
|
||
1. Determines the raw project root (from `args.projectRoot` or `getRawProjectRootFromSession`).
|
||
2. Normalizes the raw path using `normalizeProjectRoot`.
|
||
3. Injects the normalized, absolute path back into the `args` object as `args.projectRoot`.
|
||
4. Calls the original `executeFn` with the updated `args`.
|
||
- **Usage**: Should wrap the `execute` function of *every* MCP tool that needs a reliable, normalized project root path.
|
||
- **Example**:
|
||
```javascript
|
||
// In mcp-server/src/tools/your-tool.js
|
||
import { withNormalizedProjectRoot } from './utils.js';
|
||
|
||
export function registerYourTool(server) {
|
||
server.addTool({
|
||
// ... name, description, parameters ...
|
||
execute: withNormalizedProjectRoot(async (args, context) => {
|
||
// args.projectRoot is now normalized here
|
||
const { projectRoot /*, other args */ } = args;
|
||
// ... rest of tool logic using normalized projectRoot ...
|
||
})
|
||
});
|
||
}
|
||
```
|
||
|
||
- **`handleApiResult(result, log, errorPrefix, processFunction)`**:
|
||
- **Purpose**: Standardizes the formatting of responses returned by direct functions (`{ success, data/error, fromCache }`) into the MCP response format.
|
||
- **Usage**: Call this at the end of the tool's `execute` method, passing the result from the direct function call.
|
||
|
||
- **`createContentResponse(content)` / `createErrorResponse(errorMessage)`**:
|
||
- **Purpose**: Helper functions to create the basic MCP response structure for success or error messages.
|
||
- **Usage**: Used internally by `handleApiResult` and potentially directly for simple responses.
|
||
|
||
- **`createLogWrapper(log)`**:
|
||
- **Purpose**: Creates a logger object wrapper with standard methods (`info`, `warn`, `error`, `debug`, `success`) mapping to the passed MCP `log` object's methods. Ensures compatibility when passing loggers to core functions.
|
||
- **Usage**: Used within direct functions before passing the `log` object down to core logic that expects the standard method names.
|
||
|
||
- **`getCachedOrExecute({ cacheKey, actionFn, log })`**:
|
||
- **Purpose**: Utility for implementing caching within direct functions. Checks cache for `cacheKey`; if miss, executes `actionFn`, caches successful result, and returns.
|
||
- **Usage**: Wrap the core logic execution within a direct function call.
|
||
|
||
- **`processMCPResponseData(taskOrData, fieldsToRemove)`**:
|
||
- **Purpose**: Utility to filter potentially sensitive or large fields (like `details`, `testStrategy`) from task objects before sending the response back via MCP.
|
||
- **Usage**: Passed as the default `processFunction` to `handleApiResult`.
|
||
|
||
- **`getProjectRootFromSession(session, log)`**:
|
||
- **Purpose**: Legacy function to extract *and normalize* the project root from the session. Replaced by the HOF pattern but potentially still used.
|
||
- **Recommendation**: Prefer using the `withNormalizedProjectRoot` HOF in tools instead of calling this directly.
|
||
|
||
- **`executeTaskMasterCommand(...)`**:
|
||
- **Purpose**: Executes `task-master` CLI command as a fallback.
|
||
- **Recommendation**: Deprecated for most uses; prefer direct function calls.
|
||
|
||
## Export Organization
|
||
|
||
- **Grouping Related Functions**:
|
||
- ✅ DO: Keep utilities relevant to their location (e.g., core CLI utils in `scripts/modules/utils.js`, MCP path utils in `mcp-server/src/core/utils/path-utils.js`, MCP tool utils in `mcp-server/src/tools/utils.js`).
|
||
- ✅ DO: Export all utility functions in a single statement per file.
|
||
- ✅ DO: Group related exports together.
|
||
- ✅ DO: Export configuration constants (from `scripts/modules/utils.js`).
|
||
- ❌ DON'T: Use default exports.
|
||
- ❌ DON'T: Create circular dependencies (See [`architecture.mdc`](mdc:.cursor/rules/architecture.mdc)).
|
||
|
||
```javascript
|
||
// Example export from scripts/modules/utils.js
|
||
export {
|
||
// Configuration
|
||
CONFIG,
|
||
LOG_LEVELS,
|
||
|
||
// Logging
|
||
log,
|
||
|
||
// File operations
|
||
readJSON,
|
||
writeJSON,
|
||
|
||
// String manipulation
|
||
sanitizePrompt,
|
||
truncate,
|
||
|
||
// Task utilities
|
||
// ... (taskExists, formatTaskId, findTaskById, etc.)
|
||
|
||
// Graph algorithms
|
||
findCycles,
|
||
};
|
||
|
||
// Example export from mcp-server/src/core/utils/path-utils.js
|
||
export {
|
||
findTasksJsonPath,
|
||
getPackagePath,
|
||
PROJECT_MARKERS,
|
||
lastFoundProjectRoot // Exporting for potential direct use/reset if needed
|
||
};
|
||
|
||
// Example export from mcp-server/src/tools/utils.js
|
||
export {
|
||
getProjectRoot,
|
||
getProjectRootFromSession,
|
||
handleApiResult,
|
||
executeTaskMasterCommand,
|
||
processMCPResponseData,
|
||
createContentResponse,
|
||
createErrorResponse,
|
||
getCachedOrExecute
|
||
};
|
||
```
|
||
|
||
## Context Gathering Utilities
|
||
|
||
### **ContextGatherer** (`scripts/modules/utils/contextGatherer.js`)
|
||
|
||
- **Multi-Source Context Extraction**:
|
||
- ✅ DO: Use for AI-powered commands that need project context
|
||
- ✅ DO: Support tasks, files, custom text, and project tree context
|
||
- ✅ DO: Implement detailed token counting with `gpt-tokens` library
|
||
- ✅ DO: Provide multiple output formats (research, chat, system-prompt)
|
||
|
||
```javascript
|
||
// ✅ DO: Use ContextGatherer for consistent context extraction
|
||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||
|
||
const gatherer = new ContextGatherer(projectRoot, tasksPath);
|
||
const result = await gatherer.gather({
|
||
tasks: ['15', '16.2'],
|
||
files: ['src/api.js'],
|
||
customContext: 'Additional context',
|
||
includeProjectTree: true,
|
||
format: 'research',
|
||
includeTokenCounts: true
|
||
});
|
||
```
|
||
|
||
### **FuzzyTaskSearch** (`scripts/modules/utils/fuzzyTaskSearch.js`)
|
||
|
||
- **Intelligent Task Discovery**:
|
||
- ✅ DO: Use for automatic task relevance detection
|
||
- ✅ DO: Configure search parameters based on use case context
|
||
- ✅ DO: Implement purpose-based categorization for better matching
|
||
- ✅ DO: Sort results by relevance score and task ID
|
||
|
||
```javascript
|
||
// ✅ DO: Use FuzzyTaskSearch for intelligent task discovery
|
||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||
|
||
const fuzzySearch = new FuzzyTaskSearch(tasksData.tasks, 'research');
|
||
const searchResults = fuzzySearch.findRelevantTasks(query, {
|
||
maxResults: 8,
|
||
includeRecent: true,
|
||
includeCategoryMatches: true
|
||
});
|
||
const taskIds = fuzzySearch.getTaskIds(searchResults);
|
||
```
|
||
|
||
- **Integration Guidelines**:
|
||
- ✅ DO: Use fuzzy search to supplement user-provided task IDs
|
||
- ✅ DO: Display discovered task IDs to users for transparency
|
||
- ✅ DO: Sort discovered task IDs numerically for better readability
|
||
- ❌ DON'T: Replace explicit user task selections with fuzzy results
|
||
|
||
Refer to [`context_gathering.mdc`](mdc:.cursor/rules/context_gathering.mdc) for detailed implementation patterns, [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) and [`architecture.mdc`](mdc:.cursor/rules/architecture.mdc) for more context on MCP server architecture and integration.
|
||
|
||
## File System Operations
|
||
|
||
- **JSON File Handling**:
|
||
- ✅ DO: Use `readJSON` and `writeJSON` for all JSON operations
|
||
- ✅ DO: Include error handling for file operations
|
||
- ✅ DO: Validate JSON structure after reading
|
||
- ❌ DON'T: Use raw `fs.readFileSync` or `fs.writeFileSync` for JSON
|
||
|
||
```javascript
|
||
// ✅ DO: Use utility functions with error handling
|
||
function readJSON(filepath) {
|
||
try {
|
||
if (!fs.existsSync(filepath)) {
|
||
return null; // or appropriate default
|
||
}
|
||
|
||
let data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
|
||
|
||
// Silent migration for tasks.json files: Transform old format to tagged format
|
||
const isTasksFile = filepath.includes('tasks.json') || path.basename(filepath) === 'tasks.json';
|
||
|
||
if (data && data.tasks && Array.isArray(data.tasks) && !data.master && isTasksFile) {
|
||
// Migrate from old format { "tasks": [...] } to new format { "master": { "tasks": [...] } }
|
||
const migratedData = {
|
||
master: {
|
||
tasks: data.tasks
|
||
}
|
||
};
|
||
|
||
writeJSON(filepath, migratedData);
|
||
|
||
// Set global flag for CLI notice and perform complete migration
|
||
global.taskMasterMigrationOccurred = true;
|
||
performCompleteTagMigration(filepath);
|
||
|
||
data = migratedData;
|
||
}
|
||
|
||
return data;
|
||
} catch (error) {
|
||
log('error', `Failed to read JSON from ${filepath}: ${error.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function writeJSON(filepath, data) {
|
||
try {
|
||
const dirPath = path.dirname(filepath);
|
||
if (!fs.existsSync(dirPath)) {
|
||
fs.mkdirSync(dirPath, { recursive: true });
|
||
}
|
||
fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
|
||
} catch (error) {
|
||
log('error', `Failed to write JSON to ${filepath}: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
```
|
||
|
||
- **Path Resolution**:
|
||
- ✅ DO: Use `path.join()` for cross-platform path construction
|
||
- ✅ DO: Use `path.resolve()` for absolute paths
|
||
- ✅ DO: Validate paths before file operations
|
||
|
||
```javascript
|
||
// ✅ DO: Handle paths correctly
|
||
function findProjectRoot(startPath = process.cwd()) {
|
||
let currentPath = path.resolve(startPath);
|
||
const rootPath = path.parse(currentPath).root;
|
||
|
||
while (currentPath !== rootPath) {
|
||
const taskMasterPath = path.join(currentPath, '.taskmaster');
|
||
if (fs.existsSync(taskMasterPath)) {
|
||
return currentPath;
|
||
}
|
||
currentPath = path.dirname(currentPath);
|
||
}
|
||
|
||
return null; // Not found
|
||
}
|
||
```
|
||
|
||
## Tagged Task Lists System Utilities
|
||
|
||
- **Tag Resolution Functions**:
|
||
- ✅ DO: Use tag resolution layer for all task data access
|
||
- ✅ DO: Provide backward compatibility with legacy format
|
||
- ✅ DO: Default to "master" tag when no tag is specified
|
||
|
||
```javascript
|
||
// ✅ DO: Implement tag resolution functions
|
||
function getTasksForTag(data, tagName = 'master') {
|
||
if (!data) {
|
||
return [];
|
||
}
|
||
|
||
// Handle legacy format - direct tasks array
|
||
if (data.tasks && Array.isArray(data.tasks)) {
|
||
return data.tasks;
|
||
}
|
||
|
||
// Handle tagged format - tasks under specific tag
|
||
if (data[tagName] && data[tagName].tasks && Array.isArray(data[tagName].tasks)) {
|
||
return data[tagName].tasks;
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
function setTasksForTag(data, tagName = 'master', tasks) {
|
||
// Ensure data object exists
|
||
if (!data) {
|
||
data = {};
|
||
}
|
||
|
||
// Create tag structure if it doesn't exist
|
||
if (!data[tagName]) {
|
||
data[tagName] = {};
|
||
}
|
||
|
||
// Set tasks for the tag
|
||
data[tagName].tasks = tasks;
|
||
|
||
return data;
|
||
}
|
||
|
||
function getCurrentTag() {
|
||
// Get current tag from state.json or default to 'master'
|
||
try {
|
||
const projectRoot = findProjectRoot();
|
||
if (!projectRoot) return 'master';
|
||
|
||
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
|
||
if (fs.existsSync(statePath)) {
|
||
const state = readJSON(statePath);
|
||
return state.currentTag || 'master';
|
||
}
|
||
} catch (error) {
|
||
log('debug', `Error reading current tag: ${error.message}`);
|
||
}
|
||
|
||
return 'master';
|
||
}
|
||
```
|
||
|
||
- **Migration Functions**:
|
||
- ✅ DO: Implement complete migration for all related files
|
||
- ✅ DO: Handle configuration and state file creation
|
||
- ✅ DO: Provide migration status tracking
|
||
|
||
```javascript
|
||
// ✅ DO: Implement complete migration system
|
||
function performCompleteTagMigration(tasksJsonPath) {
|
||
try {
|
||
// Derive project root from tasks.json path
|
||
const projectRoot = findProjectRoot(path.dirname(tasksJsonPath)) || path.dirname(tasksJsonPath);
|
||
|
||
// 1. Migrate config.json - add defaultTag and tags section
|
||
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
|
||
if (fs.existsSync(configPath)) {
|
||
migrateConfigJson(configPath);
|
||
}
|
||
|
||
// 2. Create state.json if it doesn't exist
|
||
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
|
||
if (!fs.existsSync(statePath)) {
|
||
createStateJson(statePath);
|
||
}
|
||
|
||
if (getDebugFlag()) {
|
||
log('debug', 'Completed tagged task lists migration for project');
|
||
}
|
||
} catch (error) {
|
||
if (getDebugFlag()) {
|
||
log('warn', `Error during complete tag migration: ${error.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
function migrateConfigJson(configPath) {
|
||
try {
|
||
const config = readJSON(configPath);
|
||
if (!config) return;
|
||
|
||
let modified = false;
|
||
|
||
// Add global.defaultTag if missing
|
||
if (!config.global) {
|
||
config.global = {};
|
||
}
|
||
if (!config.global.defaultTag) {
|
||
config.global.defaultTag = 'master';
|
||
modified = true;
|
||
}
|
||
|
||
// Add tags section if missing
|
||
if (!config.tags) {
|
||
config.tags = {
|
||
// Git integration settings removed - now manual only
|
||
};
|
||
modified = true;
|
||
}
|
||
|
||
if (modified) {
|
||
writeJSON(configPath, config);
|
||
if (getDebugFlag()) {
|
||
log('debug', 'Updated config.json with tagged task system settings');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (getDebugFlag()) {
|
||
log('warn', `Error migrating config.json: ${error.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
function createStateJson(statePath) {
|
||
try {
|
||
const initialState = {
|
||
currentTag: 'master',
|
||
lastSwitched: new Date().toISOString(),
|
||
migrationNoticeShown: false
|
||
};
|
||
|
||
writeJSON(statePath, initialState);
|
||
if (getDebugFlag()) {
|
||
log('debug', 'Created initial state.json for tagged task system');
|
||
}
|
||
} catch (error) {
|
||
if (getDebugFlag()) {
|
||
log('warn', `Error creating state.json: ${error.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
function markMigrationForNotice() {
|
||
try {
|
||
const projectRoot = findProjectRoot();
|
||
if (!projectRoot) return;
|
||
|
||
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
|
||
const state = readJSON(statePath) || {};
|
||
|
||
state.migrationNoticeShown = false; // Reset to show notice
|
||
writeJSON(statePath, state);
|
||
} catch (error) {
|
||
if (getDebugFlag()) {
|
||
log('warn', `Error marking migration for notice: ${error.message}`);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## Logging Functions
|
||
|
||
- **Consistent Logging**:
|
||
- ✅ DO: Use the central `log` function for all output
|
||
- ✅ DO: Use appropriate log levels (info, warn, error, debug)
|
||
- ✅ DO: Support silent mode for programmatic usage
|
||
|
||
```javascript
|
||
// ✅ DO: Implement consistent logging with silent mode
|
||
let silentMode = false;
|
||
|
||
function log(level, ...messages) {
|
||
if (silentMode && level !== 'error') {
|
||
return; // Suppress non-error logs in silent mode
|
||
}
|
||
|
||
const timestamp = new Date().toISOString();
|
||
const formattedMessage = messages.join(' ');
|
||
|
||
switch (level) {
|
||
case 'error':
|
||
console.error(`[ERROR] ${formattedMessage}`);
|
||
break;
|
||
case 'warn':
|
||
console.warn(`[WARN] ${formattedMessage}`);
|
||
break;
|
||
case 'info':
|
||
console.log(`[INFO] ${formattedMessage}`);
|
||
break;
|
||
case 'debug':
|
||
if (getDebugFlag()) {
|
||
console.log(`[DEBUG] ${formattedMessage}`);
|
||
}
|
||
break;
|
||
default:
|
||
console.log(formattedMessage);
|
||
}
|
||
}
|
||
|
||
function enableSilentMode() {
|
||
silentMode = true;
|
||
}
|
||
|
||
function disableSilentMode() {
|
||
silentMode = false;
|
||
}
|
||
|
||
function isSilentMode() {
|
||
return silentMode;
|
||
}
|
||
```
|
||
|
||
## Task Utilities
|
||
|
||
- **Task Finding and Manipulation**:
|
||
- ✅ DO: Use tagged task system aware functions
|
||
- ✅ DO: Handle both task and subtask operations
|
||
- ✅ DO: Validate task IDs before operations
|
||
|
||
```javascript
|
||
// ✅ DO: Implement tag-aware task utilities
|
||
function findTaskById(tasks, taskId) {
|
||
if (!Array.isArray(tasks)) {
|
||
return null;
|
||
}
|
||
return tasks.find(task => task.id === taskId) || null;
|
||
}
|
||
|
||
function findSubtaskById(tasks, parentId, subtaskId) {
|
||
const parentTask = findTaskById(tasks, parentId);
|
||
if (!parentTask || !parentTask.subtasks) {
|
||
return null;
|
||
}
|
||
|
||
return parentTask.subtasks.find(subtask => subtask.id === subtaskId) || null;
|
||
}
|
||
|
||
function getNextTaskId(tasks) {
|
||
if (!Array.isArray(tasks) || tasks.length === 0) {
|
||
return 1;
|
||
}
|
||
|
||
const maxId = Math.max(...tasks.map(task => task.id));
|
||
return maxId + 1;
|
||
}
|
||
|
||
function getNextSubtaskId(parentTask) {
|
||
if (!parentTask.subtasks || parentTask.subtasks.length === 0) {
|
||
return 1;
|
||
}
|
||
|
||
const maxId = Math.max(...parentTask.subtasks.map(subtask => subtask.id));
|
||
return maxId + 1;
|
||
}
|
||
```
|
||
|
||
## String Utilities
|
||
|
||
- **Text Processing**:
|
||
- ✅ DO: Handle text truncation appropriately
|
||
- ✅ DO: Provide consistent formatting functions
|
||
- ✅ DO: Support different output formats
|
||
|
||
```javascript
|
||
// ✅ DO: Implement useful string utilities
|
||
function truncate(str, maxLength = 50) {
|
||
if (!str || typeof str !== 'string') {
|
||
return '';
|
||
}
|
||
|
||
if (str.length <= maxLength) {
|
||
return str;
|
||
}
|
||
|
||
return str.substring(0, maxLength - 3) + '...';
|
||
}
|
||
|
||
function formatDuration(ms) {
|
||
const seconds = Math.floor(ms / 1000);
|
||
const minutes = Math.floor(seconds / 60);
|
||
const hours = Math.floor(minutes / 60);
|
||
|
||
if (hours > 0) {
|
||
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
||
} else if (minutes > 0) {
|
||
return `${minutes}m ${seconds % 60}s`;
|
||
} else {
|
||
return `${seconds}s`;
|
||
}
|
||
}
|
||
|
||
function capitalizeFirst(str) {
|
||
if (!str || typeof str !== 'string') {
|
||
return '';
|
||
}
|
||
|
||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||
}
|
||
```
|
||
|
||
## Dependency Management Utilities
|
||
|
||
- **Dependency Analysis**:
|
||
- ✅ DO: Detect circular dependencies
|
||
- ✅ DO: Validate dependency references
|
||
- ✅ DO: Support cross-tag dependency checking (future enhancement)
|
||
|
||
```javascript
|
||
// ✅ DO: Implement dependency utilities
|
||
function findCycles(tasks) {
|
||
const cycles = [];
|
||
const visited = new Set();
|
||
const recStack = new Set();
|
||
|
||
function dfs(taskId, path = []) {
|
||
if (recStack.has(taskId)) {
|
||
// Found a cycle
|
||
const cycleStart = path.indexOf(taskId);
|
||
const cycle = path.slice(cycleStart).concat([taskId]);
|
||
cycles.push(cycle);
|
||
return;
|
||
}
|
||
|
||
if (visited.has(taskId)) {
|
||
return;
|
||
}
|
||
|
||
visited.add(taskId);
|
||
recStack.add(taskId);
|
||
|
||
const task = findTaskById(tasks, taskId);
|
||
if (task && task.dependencies) {
|
||
task.dependencies.forEach(depId => {
|
||
dfs(depId, path.concat([taskId]));
|
||
});
|
||
}
|
||
|
||
recStack.delete(taskId);
|
||
}
|
||
|
||
tasks.forEach(task => {
|
||
if (!visited.has(task.id)) {
|
||
dfs(task.id);
|
||
}
|
||
});
|
||
|
||
return cycles;
|
||
}
|
||
|
||
function validateDependencies(tasks) {
|
||
const validationErrors = [];
|
||
const taskIds = new Set(tasks.map(task => task.id));
|
||
|
||
tasks.forEach(task => {
|
||
if (task.dependencies) {
|
||
task.dependencies.forEach(depId => {
|
||
if (!taskIds.has(depId)) {
|
||
validationErrors.push({
|
||
taskId: task.id,
|
||
invalidDependency: depId,
|
||
message: `Task ${task.id} depends on non-existent task ${depId}`
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
return validationErrors;
|
||
}
|
||
```
|
||
|
||
## Environment and Configuration Utilities
|
||
|
||
- **Environment Variable Resolution**:
|
||
- ✅ DO: Support both `.env` files and MCP session environment
|
||
- ✅ DO: Provide fallbacks for missing values
|
||
- ✅ DO: Handle API key resolution correctly
|
||
|
||
```javascript
|
||
// ✅ DO: Implement flexible environment resolution
|
||
function resolveEnvVariable(key, sessionEnv = null) {
|
||
// First check session environment (for MCP)
|
||
if (sessionEnv && sessionEnv[key]) {
|
||
return sessionEnv[key];
|
||
}
|
||
|
||
// Then check process environment
|
||
if (process.env[key]) {
|
||
return process.env[key];
|
||
}
|
||
|
||
// Finally try .env file if in project root
|
||
try {
|
||
const projectRoot = findProjectRoot();
|
||
if (projectRoot) {
|
||
const envPath = path.join(projectRoot, '.env');
|
||
if (fs.existsSync(envPath)) {
|
||
const envContent = fs.readFileSync(envPath, 'utf8');
|
||
const lines = envContent.split('\n');
|
||
|
||
for (const line of lines) {
|
||
const [envKey, envValue] = line.split('=');
|
||
if (envKey && envKey.trim() === key) {
|
||
return envValue ? envValue.trim().replace(/^["']|["']$/g, '') : undefined;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
log('debug', `Error reading .env file: ${error.message}`);
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
function getDebugFlag() {
|
||
const debugFlag = resolveEnvVariable('TASKMASTER_DEBUG') ||
|
||
resolveEnvVariable('DEBUG') ||
|
||
'false';
|
||
return debugFlag.toLowerCase() === 'true';
|
||
}
|
||
```
|
||
|
||
## Export Pattern
|
||
|
||
- **Module Exports**:
|
||
- ✅ DO: Export all utility functions explicitly
|
||
- ✅ DO: Group related functions logically
|
||
- ✅ DO: Include new tagged system utilities
|
||
|
||
```javascript
|
||
// ✅ DO: Export utilities in logical groups
|
||
module.exports = {
|
||
// File system utilities
|
||
readJSON,
|
||
writeJSON,
|
||
findProjectRoot,
|
||
|
||
// Tagged task system utilities
|
||
getTasksForTag,
|
||
setTasksForTag,
|
||
getCurrentTag,
|
||
performCompleteTagMigration,
|
||
migrateConfigJson,
|
||
createStateJson,
|
||
markMigrationForNotice,
|
||
|
||
// Logging utilities
|
||
log,
|
||
enableSilentMode,
|
||
disableSilentMode,
|
||
isSilentMode,
|
||
|
||
// Task utilities
|
||
findTaskById,
|
||
findSubtaskById,
|
||
getNextTaskId,
|
||
getNextSubtaskId,
|
||
|
||
// String utilities
|
||
truncate,
|
||
formatDuration,
|
||
capitalizeFirst,
|
||
|
||
// Dependency utilities
|
||
findCycles,
|
||
validateDependencies,
|
||
|
||
// Environment utilities
|
||
resolveEnvVariable,
|
||
getDebugFlag,
|
||
|
||
// Legacy utilities (maintained for compatibility)
|
||
aggregateTelemetry
|
||
};
|
||
```
|
||
|
||
Refer to [`utils.js`](mdc:scripts/modules/utils.js) for implementation examples and [`architecture.mdc`](mdc:.cursor/rules/architecture.mdc) for integration patterns.
|