Ralph Khreish 72502416c6
chore: v0.17 features and improvements (#771)
* chore: task management and small bug fix.

* chore: task management

* feat: implement research command with enhanced context gathering - Add comprehensive research command with AI-powered queries - Implement ContextGatherer utility for reusable context extraction - Support multiple context types: tasks, files, custom text, project tree - Add fuzzy search integration for automatic task discovery - Implement detailed token breakdown display with syntax highlighting - Add enhanced UI with boxed output and code block formatting - Support different detail levels (low, medium, high) for responses - Include project-specific context for more relevant AI responses - Add token counting with gpt-tokens library integration - Create reusable patterns for future context-aware commands - Task 94.4 completed

* docs: add context gathering rule and update existing rules

- Create comprehensive context_gathering.mdc rule documenting ContextGatherer utility patterns, FuzzyTaskSearch integration, token breakdown display, code block syntax highlighting, and enhanced result display patterns
- Update new_features.mdc to include context gathering step
- Update commands.mdc with context-aware command pattern
- Update ui.mdc with enhanced display patterns and syntax highlighting
- Update utilities.mdc to document new context gathering utilities
- Update glossary.mdc to include new context_gathering rule
- Establishes standardized patterns for building intelligent, context-aware commands that can leverage project knowledge for better AI assistance

* feat(fuzzy): improves fuzzy search to introspect into subtasks as well. might still need improvement.

* fix(move): adjusts logic to prevent an issue when moving from parent to subtask if the target parent has no subtasks.

* fix(move-task): Fix critical bugs in task move functionality

- Fixed parent-to-parent task moves where original task would remain as duplicate
- Fixed moving tasks to become subtasks of empty parents (validation errors)
- Fixed moving subtasks between different parent tasks
- Improved comma-separated batch moves with proper error handling
- Updated MCP tool to use core logic instead of custom implementation
- Resolves task duplication issues and enables proper task hierarchy reorganization

* feat(research): Add subtasks to fuzzy search and follow-up questions

- Enhanced fuzzy search to include subtasks in discovery - Added interactive follow-up question functionality using inquirer
- Improved context discovery by including both tasks and subtasks
- Follow-up option for research with default to 'n' for quick workflow

* chore: removes task004 chat that had like 11k lines lol.

* chore: formatting

* feat(show): add comma-separated ID support for multi-task viewing

- Enhanced get-task/show command to support comma-separated task IDs for efficient batch operations.
- New features include multiple task retrieval, smart display logic, interactive action menu with batch operations, MCP array response for AI agent efficiency, and support for mixed parent tasks and subtasks.
- Implementation includes updated CLI show command, enhanced MCP get_task tool, modified showTaskDirect function, and maintained full backward compatibility.
- Documentation updated across all relevant files.

Benefits include faster context gathering for AI agents, improved workflow with interactive batch operations, better UX with responsive layout, and enhanced API efficiency.

* feat(research): Adds MCP tool for  command

- New MCP Tool: research tool enables AI-powered research with project context
- Context Integration: Supports task IDs, file paths, custom context, and project tree
- Fuzzy Task Discovery: Automatically finds relevant tasks using semantic search
- Token Management: Detailed token counting and breakdown by context type
- Multiple Detail Levels: Support for low, medium, and high detail research responses
- Telemetry Integration: Full cost tracking and usage analytics
- Direct Function: researchDirect with comprehensive parameter validation
- Silent Mode: Prevents console output interference with MCP JSON responses
- Error Handling: Robust error handling with proper MCP response formatting

This completes subtasks 94.5 (Direct Function) and 94.6 (MCP Tool) for the research command implementation, providing a powerful research interface for integrated development environments like Cursor.

Updated documentation across taskmaster.mdc, README.md, command-reference.md, examples.md, tutorial.md, and docs/README.md to highlight research capabilities and usage patterns.

* chore: task management

* chore: task management and removes mistakenly staged changes

* fix(move): Fix move command bug that left duplicate tasks

- Fixed logic in moveTaskToNewId function that was incorrectly treating task-to-task moves as subtask creation instead of task replacement
- Updated moveTaskToNewId to properly handle replacing existing destination tasks instead of just placeholders
- The move command now correctly replaces destination tasks and cleans up properly without leaving duplicates

- Task Management: Moved task 93 (Google Vertex AI Provider) to position 88, Moved task 94 (Azure OpenAI Provider) to position 89, Updated task dependencies and regenerated task files, Cleaned up orphaned task files automatically
- All important validations remain in place: Prevents moving tasks to themselves, Prevents moving parent tasks to their own subtasks, Prevents circular dependencies
- Resolves the issue where moving tasks would leave both source and destination tasks in tasks.json and file system

* chore: formatting

* feat: Add .taskmaster directory (#619)

* chore: apply requested changes from next branch (#629)

* chore: rc version bump

* chore: cleanup migration-guide

* fix: bedrock set model and other fixes (#641)

* Fix: MCP log errors (#648)

* fix: projectRoot duplicate .taskmaster directory (#655)

* Version Packages

* chore: add package-lock.json

* Version Packages

* Version Packages

* fix: markdown format (#622)

* Version Packages

* Version Packages

* Fixed the Typo in cursor rules Issue:#675 (#677)

Fixed the typo in the Api keys

* Add one-click MCP server installation for Cursor (#671)

* Update README.md - Remove trailing commas (#673)

JSON doesn't allow for trailing commas, so these need to be removed in order for this to work

* chore: rc version bump

* fix: findTasksPath function

* fix: update MCP tool

* feat(ui): replace emoji complexity indicators with clean filled circle characters

Replace 🟢, 🟡, 🔴 emojis with ● character in getComplexityWithColor function

Update corresponding unit tests to expect ● instead of emojis

Improves UI continuity

* fix(ai-providers): change generateObject mode from 'tool' to 'auto' for better provider compatibility

Fixes Perplexity research role failing with 'tool-mode object generation' error

The hardcoded 'tool' mode was incompatible with providers like Perplexity that support structured JSON output but not function calling/tool use

Using 'auto' mode allows the AI SDK to choose the best approach for each provider

* Adds qwen3-235n-a22b:free to supported models. Closes #687)

* chore: adds a warning when custom openrouter model is a free model which suffers from lower rate limits, restricted context, and, worst of all, no access to tool_use.

* refactor: enhance add-task fuzzy search and fix duplicate banner display

- **Remove hardcoded category system** in add-task that always matched 'Task management'
- **Eliminate arbitrary limits** in fuzzy search results (5→25 high relevance, 3→10 medium relevance, 8→20 detailed tasks)
- **Improve semantic weighting** in Fuse.js search (details=3, description=2, title=1.5) for better relevance
- **Fix duplicate banner issue** by removing console.clear() and redundant displayBanner() calls from UI functions
- **Enhance context generation** to rely on semantic similarity rather than rigid pattern matching
- **Preserve terminal history** to address GitHub issue #553 about eating terminal lines
- **Remove displayBanner() calls** from: displayHelp, displayNextTask, displayTaskById, displayComplexityReport, set-task-status, clear-subtasks, dependency-manager functions

The add-task system now provides truly relevant task context based on semantic similarity rather than arbitrary categories and limits, while maintaining a cleaner terminal experience.

Changes span: add-task.js, ui.js, set-task-status.js, clear-subtasks.js, list-tasks.js, dependency-manager.js

Closes #553

* chore: changeset

* chore: passes tests and linting

* chore: more linting

* ninja(sync): add sync-readme command for GitHub README export with UTM tracking and professional markdown formatting. Experimental

* chore: changeset adjustment

* docs: Auto-update and format models.md

* chore: updates readme with npm download badges and mentions AI Jason who is joining the taskmaster core team.

* chore: fixes urls in readme npm packages

* chore: fixes urls in readme npm packages again

* fix: readme typo

* readme: fix twitter urls.

* readme: removes the taskmaster list output which is too overwhelming given its size with subtasks. may re-add later. fixes likely issues in the json for manual config in cursor and windsurf in the readme.

* chore: small readme nitpicks

* chore: adjusts changeset from minor to patch to avoid version bump to 0.17

* readme: moves up the documentation links higher up in the readme. same with the cursor one-click install.

* Fix Cursor deeplink installation with copy-paste instructions (#723)

* solve merge conflics with next. not gonna deal with these much longer.

* chore: update task files during rebase

* chore: task management

* feat: implement research command with enhanced context gathering - Add comprehensive research command with AI-powered queries - Implement ContextGatherer utility for reusable context extraction - Support multiple context types: tasks, files, custom text, project tree - Add fuzzy search integration for automatic task discovery - Implement detailed token breakdown display with syntax highlighting - Add enhanced UI with boxed output and code block formatting - Support different detail levels (low, medium, high) for responses - Include project-specific context for more relevant AI responses - Add token counting with gpt-tokens library integration - Create reusable patterns for future context-aware commands - Task 94.4 completed

* fix(move): adjusts logic to prevent an issue when moving from parent to subtask if the target parent has no subtasks.

* fix(move-task): Fix critical bugs in task move functionality

- Fixed parent-to-parent task moves where original task would remain as duplicate
- Fixed moving tasks to become subtasks of empty parents (validation errors)
- Fixed moving subtasks between different parent tasks
- Improved comma-separated batch moves with proper error handling
- Updated MCP tool to use core logic instead of custom implementation
- Resolves task duplication issues and enables proper task hierarchy reorganization

* chore: removes task004 chat that had like 11k lines lol.

* feat(show): add comma-separated ID support for multi-task viewing

- Enhanced get-task/show command to support comma-separated task IDs for efficient batch operations.
- New features include multiple task retrieval, smart display logic, interactive action menu with batch operations, MCP array response for AI agent efficiency, and support for mixed parent tasks and subtasks.
- Implementation includes updated CLI show command, enhanced MCP get_task tool, modified showTaskDirect function, and maintained full backward compatibility.
- Documentation updated across all relevant files.

Benefits include faster context gathering for AI agents, improved workflow with interactive batch operations, better UX with responsive layout, and enhanced API efficiency.

* feat(research): Adds MCP tool for  command

- New MCP Tool: research tool enables AI-powered research with project context
- Context Integration: Supports task IDs, file paths, custom context, and project tree
- Fuzzy Task Discovery: Automatically finds relevant tasks using semantic search
- Token Management: Detailed token counting and breakdown by context type
- Multiple Detail Levels: Support for low, medium, and high detail research responses
- Telemetry Integration: Full cost tracking and usage analytics
- Direct Function: researchDirect with comprehensive parameter validation
- Silent Mode: Prevents console output interference with MCP JSON responses
- Error Handling: Robust error handling with proper MCP response formatting

This completes subtasks 94.5 (Direct Function) and 94.6 (MCP Tool) for the research command implementation, providing a powerful research interface for integrated development environments like Cursor.

Updated documentation across taskmaster.mdc, README.md, command-reference.md, examples.md, tutorial.md, and docs/README.md to highlight research capabilities and usage patterns.

* chore: task management

* fix(move): Fix move command bug that left duplicate tasks

- Fixed logic in moveTaskToNewId function that was incorrectly treating task-to-task moves as subtask creation instead of task replacement
- Updated moveTaskToNewId to properly handle replacing existing destination tasks instead of just placeholders
- The move command now correctly replaces destination tasks and cleans up properly without leaving duplicates

- Task Management: Moved task 93 (Google Vertex AI Provider) to position 88, Moved task 94 (Azure OpenAI Provider) to position 89, Updated task dependencies and regenerated task files, Cleaned up orphaned task files automatically
- All important validations remain in place: Prevents moving tasks to themselves, Prevents moving parent tasks to their own subtasks, Prevents circular dependencies
- Resolves the issue where moving tasks would leave both source and destination tasks in tasks.json and file system

* chore: moves to new task master config setup

* feat: add comma-separated status filtering to list-tasks

- supports multiple statuses like 'blocked,deferred' with comprehensive test coverage and backward compatibility

- also adjusts biome.json to stop bitching about templating.

* chore: linting ffs

* fix(generate): Fix generate command creating tasks in legacy location

- Update generate command default output directory from 'tasks' to '.taskmaster/tasks'
- Fix path.dirname() usage to properly derive output directory from tasks file location
- Update MCP tool description and documentation to reflect new structure
- Disable Biome linting rules for noUnusedTemplateLiteral and useArrowFunction
- Fixes issue where generate command was creating task files in the old 'tasks/' directory instead of the new '.taskmaster/tasks/' structure after the refactor

* chore: task management

* chore: task management some more

* fix(get-task): makes the projectRoot argument required to prevent errors when getting tasks.

* feat(tags): Implement tagged task lists migration system (Part 1/2)

This commit introduces the foundational infrastructure for tagged task lists,
enabling multi-context task management without remote storage to prevent merge conflicts.

CORE ARCHITECTURE:
• Silent migration system transforms tasks.json from old format { "tasks": [...] }
  to new tagged format { "master": { "tasks": [...] } }
• Tag resolution layer provides complete backward compatibility - existing code continues to work
• Automatic configuration and state management for seamless user experience

SILENT MIGRATION SYSTEM:
• Automatic detection and migration of legacy tasks.json format
• Complete project migration: tasks.json + config.json + state.json
• Transparent tag resolution returns old format to maintain compatibility
• Zero breaking changes - all existing functionality preserved

CONFIGURATION MANAGEMENT:
• Added global.defaultTag setting (defaults to 'master')
• New tags section with gitIntegration placeholders for future features
• Automatic config.json migration during first run
• Proper state.json creation with migration tracking

USER EXPERIENCE:
• Clean, one-time FYI notice after migration (no emojis, professional styling)
• Notice appears after 'Suggested Next Steps' and is tracked in state.json
• Silent operation - users unaware migration occurred unless explicitly shown

TECHNICAL IMPLEMENTATION:
• Enhanced readJSON() with automatic migration detection and processing
• New utility functions: getCurrentTag(), resolveTag(), getTasksForTag(), setTasksForTag()
• Complete migration orchestration via performCompleteTagMigration()
• Robust error handling and fallback mechanisms

BACKWARD COMPATIBILITY:
• 100% backward compatibility maintained
• Existing CLI commands and MCP tools continue to work unchanged
• Legacy tasks.json format automatically upgraded on first read
• All existing workflows preserved

TESTING VERIFIED:
• Complete migration from legacy state works correctly
• Config.json properly updated with tagged system settings
• State.json created with correct initial values
• Migration notice system functions as designed
• All existing functionality continues to work normally

Part 2 will implement tag management commands (add-tag, use-tag, list-tags)
and MCP tool updates for full tagged task system functionality.

Related: Task 103 - Implement Tagged Task Lists System for Multi-Context Task Management

* docs: Update documentation and rules for tagged task lists system

- Updated task-structure.md with comprehensive tagged format explanation
- Updated all .cursor/rules/*.mdc files to reflect tagged system
- Completed subtask 103.16: Update Documentation for Tagged Task Lists System

* feat(mcp): Add tagInfo to responses and integrate ContextGatherer

Enhances the MCP server to include 'tagInfo' (currentTag, availableTags) in all tool responses, providing better client-side context.

- Introduces a new 'ContextGatherer' utility to standardize the collection of file, task, and project context for AI-powered commands. This refactors several task-manager modules ('expand-task', 'research', 'update-task', etc.) to use the new utility.

- Fixes an issue in 'get-task' and 'get-tasks' MCP tools where the 'projectRoot' was not being passed correctly, preventing tag information from being included in their responses.

- Adds subtask '103.17' to track the implementation of the task template importing feature.

- Updates documentation ('.cursor/rules', 'docs/') to align with the new tagged task system and context gatherer logic.

* fix: include tagInfo in AI service responses for MCP tools

- Update all core functions that call AI services to extract and return tagInfo
- Update all direct functions to include tagInfo in MCP response data
- Fixes issue where add_task, expand_task, and other AI commands were not including current tag and available tags information
- tagInfo includes currentTag from state.json and availableTags list
- Ensures tagged task lists system information is properly propagated through the full chain: AI service -> core function -> direct function -> MCP client

* fix(move-task): Update move functionality for tagged task system compatibility

- incorporate GitHub commit fixes and resolve readJSON data handling

* feat(tagged-tasks): Complete core tag management system implementation

- Implements comprehensive tagged task lists system for multi-context task management including core tag management functions (Task 103.11), MCP integration updates, and foundational infrastructure for tagged task operations. Features tag CRUD operations, validation, metadata tracking, deep task copying, and full backward compatibility.

* fix(core): Fixed move-task.js writing _rawTaggedData directly, updated writeJSON to filter tag fields, fixed CLI move command missing projectRoot, added ensureTagMetadata utility

* fix(tasks): ensure list tasks triggers silent migration if necessary.

* feat(tags): Complete show and add-task command tag support
- show command: Added --tag flag, fixed projectRoot passing to UI functions
- add-task command: Already had proper tag support and projectRoot handling
- Both commands now work correctly with tagged task lists system
- Migration logic works properly when viewing and adding tasks
- Updated subtask 103.5 with progress on high-priority command fixes

* fix(tags): Clean up rogue created properties and fix taskCount calculation
- Enhanced writeJSON to automatically filter rogue created/description properties from tag objects
- Fixed tags command error by making taskCount calculation dynamic instead of hardcoded
- Cleaned up existing rogue created property in master tag through forced write operation
- All created properties now properly located in metadata objects only
- Tags command working perfectly with proper task count display
- Data integrity maintained with automatic cleanup during write operations

* fix(tags): Resolve critical tag deletion and migration notice bugs

Major Issues Fixed:

1. Tag Deletion Bug: Fixed critical issue where creating subtasks would delete other tags

   - Root cause: writeJSON function wasn't accepting projectRoot/tag parameters

   - Fixed writeJSON signature and logic to handle tagged data structure

   - Added proper merging of resolved tag data back into full tagged structure

2. Persistent Migration Notice: Fixed FYI notice showing after every command

   - Root cause: markMigrationForNotice was resetting migrationNoticeShown to false

   - Fixed migration logic to only trigger on actual legacy->tagged migrations

   - Added proper _rawTaggedData checks to prevent false migration detection

3. Data Corruption Prevention: Enhanced data integrity safeguards

   - Fixed writeJSON to filter out internal properties

   - Added automatic cleanup of rogue properties

   - Improved hasTaggedStructure detection logic

Commands Fixed: add-subtask, remove-subtask, and all commands now preserve tags correctly

* fix(tags): Resolve tag deletion bug in remove-task command

Refactored the core 'removeTask' function to be fully tag-aware, preventing data corruption.

- The function now correctly reads the full tagged data structure by prioritizing '_rawTaggedData' instead of operating on a resolved single-tag view.

- All subsequent operations (task removal, dependency cleanup, file writing) now correctly reference the full multi-tag data object, preserving the integrity of 'tasks.json'.

- This resolves the critical bug where removing a task would delete all other tags.

* fix(tasks): Ensure new task IDs are sequential within the target tag

Modified the ID generation logic in 'add-task.js' to calculate the next task ID based on the highest ID within the specified tag, rather than globally across all tags.

This fixes a critical bug where creating a task in a new tag would result in a high, non-sequential ID, such as ID 105 for the first task in a tag.

* fix(commands): Add missing context parameters to dependency and remove-subtask commands

- Add projectRoot and tag context to all dependency commands
- Add projectRoot and tag context to remove-subtask command
- Add --tag option to remove-subtask command
- Fixes critical bug where remove-subtask was deleting other tags due to missing context
- All dependency and subtask commands now properly handle tagged task lists

* feat(tags): Add --tag flag support to core commands for multi-context task management
- parse-prd now supports creating tasks in specific contexts
- Fixed tag preservation logic to prevent data loss
- analyze-complexity generates tag-specific reports
- Non-existent tags created automatically
- Enables rapid prototyping and parallel development workflows

* feat(tags): Complete tagged task lists system with enhanced use-tag command

- Multi-context task management with full CLI support
- Enhanced use-tag command shows next available task after switching
- Universal --tag flag support across all commands
- Seamless migration with zero disruption
- Complete tag management suite (add, delete, rename, copy, list)
- Smart confirmation logic and data integrity protection
- State management and configuration integration
- Real-world use cases for teams, features, and releases

* feat(tags): Complete tag support for remaining CLI commands

- Add --tag flag to update, move, and set-status commands
- Ensure all task operation commands now support tag context
- Fix missing tag context passing to core functions
- Complete comprehensive tag-aware command coverage

* feat(ui): add tag indicator to all CLI commands
- shows 🏷️ tag: tagname for complete context visibility across 15+ commands

* fix(ui): resolve dependency 'Not found' issue when filtering

- now correctly displays dependencies that exist but are filtered out of view

* feat(research): Add comprehensive AI-powered research command with interactive follow-ups, save functionality, intelligent context gathering, fuzzy task discovery, multi-source context support, enhanced display with syntax highlighting, clean inquirer menus, comprehensive help, and MCP integration with saveTo parameter

* feat(tags): Implement full MCP support for Tagged Task Lists and update-task append mode

* chore: task management

* feat(research): Enhance research command with follow-up menu, save functionality, and fix ContextGatherer token counting

* feat(git-workflow): Add automatic git branch-tag integration

- Implement automatic tag creation when switching to new git branches

- Add branch-tag mapping system for seamless context switching

- Enable auto-switch of task contexts based on current git branch

- Provide isolated task contexts per branch to prevent merge conflicts

- Add configuration support for enabling/disabling git workflow features

- Fix ES module compatibility issues in git-utils module

- Maintain zero migration impact with automatic 'master' tag creation

- Support parallel development with branch-specific task contexts

The git workflow system automatically detects branch changes and creates corresponding empty task tags, enabling developers to maintain separate task contexts for different features/branches while preventing task-related merge conflicts during collaborative development.

Resolves git workflow integration requirements for multi-context development.

* feat(git-workflow): Simplify git integration with --from-branch option

- Remove automatic git workflow and branch-tag switching - we are not ready for it yet

- Add --from-branch option to add-tag command for manual tag creation from git branch

- Remove git workflow configuration from config.json and assets

- Disable automatic tag switching functions in git-utils.js

- Add createTagFromBranch function for branch-based tag creation

- Support both CLI and MCP interfaces for --from-branch functionality

- Fix ES module imports in git-utils.js and utils.js

- Maintain user control over tag contexts without forced automation

The simplified approach allows users to create tags from their current git branch when desired, without the complexity and rigidity of automatic branch-tag synchronization. Users maintain full control over their tag contexts while having convenient tools for git-based workflows when needed.

* docs: Update rule files to reflect simplified git integration approach

- Remove automatic git workflow features, update to manual --from-branch option, change Part 2 references to completed status

* fix(commands): Fix add-tag --from-branch requiring tagName argument
- Made tagName optional when using --from-branch - Added validation for either tagName or --from-branch
- Fixes 'missing required argument' error with --from-branch option

* fix(mcp): Prevent tag deletion on subtask update

Adds a safety net to the writeJSON utility to prevent data loss when updating subtasks via the MCP server.

The MCP process was inadvertently causing the _rawTaggedData property, which holds the complete multi-tag structure, to be lost. When writeJSON received the data for only a single tag, it would overwrite the entire tasks.json file, deleting all other tags.

This fix makes writeJSON more robust. If it receives data that looks like a single, resolved tag without the complete structure, it re-reads the full tasks.json file from disk. It then carefully merges the updated data back into the correct tag within the full structure, preserving all other tags.

* fix: resolve all remaining test failures and improve test reliability

- Fix clear-subtasks test by implementing deep copy of mock data to prevent mutation issues between tests
- Fix add-task test by uncommenting and properly configuring generateTaskFiles call with correct parameters
- Fix analyze-task-complexity tests by properly mocking fs.writeFileSync with shared mock function
- Update test expectations to match actual function signatures and data structures
- Improve mock setup consistency across all test suites
- Ensure all tests now pass (329 total: 318 passed, 11 skipped, 0 failed)

* chore: task management

---------

Co-authored-by: Eyal Toledano <eyal@microangel.so>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ibrahim H. <bitsnaps@yahoo.fr>
Co-authored-by: Saksham Goel <sakshamgoel1107@gmail.com>
Co-authored-by: Joe Danziger <joe@ticc.net>
Co-authored-by: Aaron Gabriel Neyer <ag@unforced.org>
2025-06-14 11:04:26 -04:00

1353 lines
38 KiB
JavaScript

/**
* utils.js
* Utility functions for the Task Master CLI
*/
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import dotenv from 'dotenv';
// Import specific config getters needed here
import { getLogLevel, getDebugFlag } from './config-manager.js';
import * as gitUtils from './utils/git-utils.js';
import {
COMPLEXITY_REPORT_FILE,
LEGACY_COMPLEXITY_REPORT_FILE,
LEGACY_CONFIG_FILE
} from '../../src/constants/paths.js';
// Global silent mode flag
let silentMode = false;
// --- Environment Variable Resolution Utility ---
/**
* Resolves an environment variable's value.
* Precedence:
* 1. session.env (if session provided)
* 2. process.env
* 3. .env file at projectRoot (if projectRoot provided)
* @param {string} key - The environment variable key.
* @param {object|null} [session=null] - The MCP session object.
* @param {string|null} [projectRoot=null] - The project root directory (for .env fallback).
* @returns {string|undefined} The value of the environment variable or undefined if not found.
*/
function resolveEnvVariable(key, session = null, projectRoot = null) {
// 1. Check session.env
if (session?.env?.[key]) {
return session.env[key];
}
// 2. Read .env file at projectRoot
if (projectRoot) {
const envPath = path.join(projectRoot, '.env');
if (fs.existsSync(envPath)) {
try {
const envFileContent = fs.readFileSync(envPath, 'utf-8');
const parsedEnv = dotenv.parse(envFileContent); // Use dotenv to parse
if (parsedEnv && parsedEnv[key]) {
// console.log(`DEBUG: Found key ${key} in ${envPath}`); // Optional debug log
return parsedEnv[key];
}
} catch (error) {
// Log error but don't crash, just proceed as if key wasn't found in file
log('warn', `Could not read or parse ${envPath}: ${error.message}`);
}
}
}
// 3. Fallback: Check process.env
if (process.env[key]) {
return process.env[key];
}
// Not found anywhere
return undefined;
}
// --- Project Root Finding Utility ---
/**
* Recursively searches upwards for project root starting from a given directory.
* @param {string} [startDir=process.cwd()] - The directory to start searching from.
* @param {string[]} [markers=['package.json', '.git', LEGACY_CONFIG_FILE]] - Marker files/dirs to look for.
* @returns {string|null} The path to the project root, or null if not found.
*/
function findProjectRoot(
startDir = process.cwd(),
markers = ['package.json', '.git', LEGACY_CONFIG_FILE]
) {
let currentPath = path.resolve(startDir);
const rootPath = path.parse(currentPath).root;
while (currentPath !== rootPath) {
// Check if any marker exists in the current directory
const hasMarker = markers.some((marker) => {
const markerPath = path.join(currentPath, marker);
return fs.existsSync(markerPath);
});
if (hasMarker) {
return currentPath;
}
// Move up one directory
currentPath = path.dirname(currentPath);
}
// Check the root directory as well
const hasMarkerInRoot = markers.some((marker) => {
const markerPath = path.join(rootPath, marker);
return fs.existsSync(markerPath);
});
return hasMarkerInRoot ? rootPath : null;
}
// --- Dynamic Configuration Function --- (REMOVED)
// --- Logging and Utility Functions ---
// Set up logging based on log level
const LOG_LEVELS = {
debug: 0,
info: 1,
warn: 2,
error: 3,
success: 1 // Treat success like info level
};
/**
* Returns the task manager module
* @returns {Promise<Object>} The task manager module object
*/
async function getTaskManager() {
return import('./task-manager.js');
}
/**
* Enable silent logging mode
*/
function enableSilentMode() {
silentMode = true;
}
/**
* Disable silent logging mode
*/
function disableSilentMode() {
silentMode = false;
}
/**
* Check if silent mode is enabled
* @returns {boolean} True if silent mode is enabled
*/
function isSilentMode() {
return silentMode;
}
/**
* Logs a message at the specified level
* @param {string} level - The log level (debug, info, warn, error)
* @param {...any} args - Arguments to log
*/
function log(level, ...args) {
// Immediately return if silentMode is enabled
if (isSilentMode()) {
return;
}
// GUARD: Prevent circular dependency during config loading
// Use a simple fallback log level instead of calling getLogLevel()
let configLevel = 'info'; // Default fallback
try {
// Only try to get config level if we're not in the middle of config loading
configLevel = getLogLevel() || 'info';
} catch (error) {
// If getLogLevel() fails (likely due to circular dependency),
// use default 'info' level and continue
configLevel = 'info';
}
// Use text prefixes instead of emojis
const prefixes = {
debug: chalk.gray('[DEBUG]'),
info: chalk.blue('[INFO]'),
warn: chalk.yellow('[WARN]'),
error: chalk.red('[ERROR]'),
success: chalk.green('[SUCCESS]')
};
// Ensure level exists, default to info if not
const currentLevel = LOG_LEVELS.hasOwnProperty(level) ? level : 'info';
// Check log level configuration
if (
LOG_LEVELS[currentLevel] >= (LOG_LEVELS[configLevel] ?? LOG_LEVELS.info)
) {
const prefix = prefixes[currentLevel] || '';
// Use console.log for all levels, let chalk handle coloring
// Construct the message properly
const message = args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg))
.join(' ');
console.log(`${prefix} ${message}`);
}
}
/**
* Checks if the data object has a tagged structure (contains tag objects with tasks arrays)
* @param {Object} data - The data object to check
* @returns {boolean} True if the data has a tagged structure
*/
function hasTaggedStructure(data) {
if (!data || typeof data !== 'object') {
return false;
}
// Check if any top-level properties are objects with tasks arrays
for (const key in data) {
if (
data.hasOwnProperty(key) &&
typeof data[key] === 'object' &&
Array.isArray(data[key].tasks)
) {
return true;
}
}
return false;
}
/**
* Reads and parses a JSON file
* @param {string} filepath - Path to the JSON file
* @param {string} [projectRoot] - Optional project root for tag resolution (used by MCP)
* @param {string} [tag] - Optional tag to use instead of current tag resolution
* @returns {Object|null} The parsed JSON data or null if error
*/
function readJSON(filepath, projectRoot = null, tag = null) {
// GUARD: Prevent circular dependency during config loading
let isDebug = false; // Default fallback
try {
// Only try to get debug flag if we're not in the middle of config loading
isDebug = getDebugFlag();
} catch (error) {
// If getDebugFlag() fails (likely due to circular dependency),
// use default false and continue
}
if (isDebug) {
console.log(
`readJSON called with: ${filepath}, projectRoot: ${projectRoot}, tag: ${tag}`
);
}
if (!filepath) {
return null;
}
let data;
try {
data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
if (isDebug) {
console.log(`Successfully read JSON from ${filepath}`);
}
} catch (err) {
if (isDebug) {
console.log(`Failed to read JSON from ${filepath}: ${err.message}`);
}
return null;
}
// If it's not a tasks.json file, return as-is
if (!filepath.includes('tasks.json') || !data) {
if (isDebug) {
console.log(`File is not tasks.json or data is null, returning as-is`);
}
return data;
}
// Check if this is legacy format that needs migration
// Only migrate if we have tasks at the ROOT level AND no tag-like structure
if (
Array.isArray(data.tasks) &&
!data._rawTaggedData &&
!hasTaggedStructure(data)
) {
if (isDebug) {
console.log(`File is in legacy format, performing migration...`);
}
// This is legacy format - migrate it to tagged format
const migratedData = {
master: {
tasks: data.tasks,
metadata: data.metadata || {
created: new Date().toISOString(),
updated: new Date().toISOString(),
description: 'Tasks for master context'
}
}
};
// Write the migrated data back to the file
try {
writeJSON(filepath, migratedData);
if (isDebug) {
console.log(`Successfully migrated legacy format to tagged format`);
}
// Perform complete migration (config.json, state.json)
performCompleteTagMigration(filepath);
// Check and auto-switch git tags if enabled (after migration)
// This needs to run synchronously BEFORE tag resolution
if (projectRoot) {
try {
// Run git integration synchronously
gitUtils.checkAndAutoSwitchGitTagSync(projectRoot, filepath);
} catch (error) {
// Silent fail - don't break normal operations
}
}
// Mark for migration notice
markMigrationForNotice(filepath);
} catch (writeError) {
if (isDebug) {
console.log(`Error writing migrated data: ${writeError.message}`);
}
// If write fails, continue with the original data
}
// Continue processing with the migrated data structure
data = migratedData;
}
// If we have tagged data, we need to resolve which tag to use
if (typeof data === 'object' && !data.tasks) {
// This is tagged format
if (isDebug) {
console.log(`File is in tagged format, resolving tag...`);
}
// Ensure all tags have proper metadata before proceeding
for (const tagName in data) {
if (
data.hasOwnProperty(tagName) &&
typeof data[tagName] === 'object' &&
data[tagName].tasks
) {
try {
ensureTagMetadata(data[tagName], {
description: `Tasks for ${tagName} context`,
skipUpdate: true // Don't update timestamp during read operations
});
} catch (error) {
// If ensureTagMetadata fails, continue without metadata
if (isDebug) {
console.log(
`Failed to ensure metadata for tag ${tagName}: ${error.message}`
);
}
}
}
}
// Store reference to the raw tagged data for functions that need it
const originalTaggedData = JSON.parse(JSON.stringify(data));
// Check and auto-switch git tags if enabled (for existing tagged format)
// This needs to run synchronously BEFORE tag resolution
if (projectRoot) {
try {
// Run git integration synchronously
gitUtils.checkAndAutoSwitchGitTagSync(projectRoot, filepath);
} catch (error) {
// Silent fail - don't break normal operations
}
}
try {
// Default to master tag if anything goes wrong
let resolvedTag = 'master';
// Try to resolve the correct tag, but don't fail if it doesn't work
try {
// If tag is provided, use it directly
if (tag) {
resolvedTag = tag;
} else if (projectRoot) {
// Use provided projectRoot
resolvedTag = resolveTag({ projectRoot });
} else {
// Try to derive projectRoot from filepath
const derivedProjectRoot = findProjectRoot(path.dirname(filepath));
if (derivedProjectRoot) {
resolvedTag = resolveTag({ projectRoot: derivedProjectRoot });
}
// If derivedProjectRoot is null, stick with 'master'
}
} catch (tagResolveError) {
if (isDebug) {
console.log(
`Tag resolution failed, using master: ${tagResolveError.message}`
);
}
// resolvedTag stays as 'master'
}
if (isDebug) {
console.log(`Resolved tag: ${resolvedTag}`);
}
// Get the data for the resolved tag
const tagData = data[resolvedTag];
if (tagData && tagData.tasks) {
// Add the _rawTaggedData property and the resolved tag to the returned data
const result = {
...tagData,
tag: resolvedTag,
_rawTaggedData: originalTaggedData
};
if (isDebug) {
console.log(
`Returning data for tag '${resolvedTag}' with ${tagData.tasks.length} tasks`
);
}
return result;
} else {
// If the resolved tag doesn't exist, fall back to master
const masterData = data.master;
if (masterData && masterData.tasks) {
if (isDebug) {
console.log(
`Tag '${resolvedTag}' not found, falling back to master with ${masterData.tasks.length} tasks`
);
}
return {
...masterData,
tag: 'master',
_rawTaggedData: originalTaggedData
};
} else {
if (isDebug) {
console.log(`No valid tag data found, returning empty structure`);
}
// Return empty structure if no valid data
return {
tasks: [],
tag: 'master',
_rawTaggedData: originalTaggedData
};
}
}
} catch (error) {
if (isDebug) {
console.log(`Error during tag resolution: ${error.message}`);
}
// If anything goes wrong, try to return master or empty
const masterData = data.master;
if (masterData && masterData.tasks) {
return {
...masterData,
_rawTaggedData: originalTaggedData
};
}
return {
tasks: [],
_rawTaggedData: originalTaggedData
};
}
}
// If we reach here, it's some other format
if (isDebug) {
console.log(`File format not recognized, returning as-is`);
}
return data;
}
/**
* Performs complete tag migration including config.json and state.json updates
* @param {string} tasksJsonPath - Path to the tasks.json file that was migrated
*/
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',
`Complete tag migration performed for project: ${projectRoot}`
);
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error during complete tag migration: ${error.message}`);
}
}
}
/**
* Migrates config.json to add tagged task system configuration
* @param {string} configPath - Path to the config.json file
*/
function migrateConfigJson(configPath) {
try {
const rawConfig = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(rawConfig);
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;
}
if (modified) {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
if (process.env.TASKMASTER_DEBUG === 'true') {
console.log(
'[DEBUG] Updated config.json with tagged task system settings'
);
}
}
} catch (error) {
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(`[WARN] Error migrating config.json: ${error.message}`);
}
}
}
/**
* Creates initial state.json file for tagged task system
* @param {string} statePath - Path where state.json should be created
*/
function createStateJson(statePath) {
try {
const initialState = {
currentTag: 'master',
lastSwitched: new Date().toISOString(),
branchTagMapping: {},
migrationNoticeShown: false
};
fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2), 'utf8');
if (process.env.TASKMASTER_DEBUG === 'true') {
console.log('[DEBUG] Created initial state.json for tagged task system');
}
} catch (error) {
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(`[WARN] Error creating state.json: ${error.message}`);
}
}
}
/**
* Marks in state.json that migration occurred and notice should be shown
* @param {string} tasksJsonPath - Path to the tasks.json file
*/
function markMigrationForNotice(tasksJsonPath) {
try {
const projectRoot = path.dirname(path.dirname(tasksJsonPath));
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
// Ensure state.json exists
if (!fs.existsSync(statePath)) {
createStateJson(statePath);
}
// Read and update state to mark migration occurred using fs directly
try {
const rawState = fs.readFileSync(statePath, 'utf8');
const stateData = JSON.parse(rawState) || {};
// Only set to false if it's not already set (i.e., first time migration)
if (stateData.migrationNoticeShown === undefined) {
stateData.migrationNoticeShown = false;
fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2), 'utf8');
}
} catch (stateError) {
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(
`[WARN] Error updating state for migration notice: ${stateError.message}`
);
}
}
} catch (error) {
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(
`[WARN] Error marking migration for notice: ${error.message}`
);
}
}
}
/**
* Writes and saves a JSON file. Handles tagged task lists properly.
* @param {string} filepath - Path to the JSON file
* @param {Object} data - Data to write (can be resolved tag data or raw tagged data)
* @param {string} projectRoot - Optional project root for tag context
* @param {string} tag - Optional tag for tag context
*/
function writeJSON(filepath, data, projectRoot = null, tag = null) {
const isDebug = process.env.TASKMASTER_DEBUG === 'true';
try {
let finalData = data;
// If data represents resolved tag data but lost _rawTaggedData (edge-case observed in MCP path)
if (
!data._rawTaggedData &&
projectRoot &&
Array.isArray(data.tasks) &&
!hasTaggedStructure(data)
) {
const resolvedTag = tag || getCurrentTag(projectRoot);
if (isDebug) {
console.log(
`writeJSON: Detected resolved tag data missing _rawTaggedData. Re-reading raw data to prevent data loss for tag '${resolvedTag}'.`
);
}
// Re-read the full file to get the complete tagged structure
const rawFullData = JSON.parse(fs.readFileSync(filepath, 'utf8'));
// Merge the updated data into the full structure
finalData = {
...rawFullData,
[resolvedTag]: {
// Preserve existing tag metadata if it exists, otherwise use what's passed
...(rawFullData[resolvedTag]?.metadata || {}),
...(data.metadata ? { metadata: data.metadata } : {}),
tasks: data.tasks // The updated tasks array is the source of truth here
}
};
}
// If we have _rawTaggedData, this means we're working with resolved tag data
// and need to merge it back into the full tagged structure
else if (data && data._rawTaggedData && projectRoot) {
const resolvedTag = tag || getCurrentTag(projectRoot);
// Get the original tagged data
const originalTaggedData = data._rawTaggedData;
// Create a clean copy of the current resolved data (without internal properties)
const { _rawTaggedData, tag: _, ...cleanResolvedData } = data;
// Update the specific tag with the resolved data
finalData = {
...originalTaggedData,
[resolvedTag]: cleanResolvedData
};
if (isDebug) {
console.log(
`writeJSON: Merging resolved data back into tag '${resolvedTag}'`
);
}
}
// Clean up any internal properties that shouldn't be persisted
let cleanData = finalData;
if (cleanData && typeof cleanData === 'object') {
// Remove any _rawTaggedData or tag properties from root level
const { _rawTaggedData, tag: tagProp, ...rootCleanData } = cleanData;
cleanData = rootCleanData;
// Additional cleanup for tag objects
if (typeof cleanData === 'object' && !Array.isArray(cleanData)) {
const finalCleanData = {};
for (const [key, value] of Object.entries(cleanData)) {
if (
value &&
typeof value === 'object' &&
Array.isArray(value.tasks)
) {
// This is a tag object - clean up any rogue root-level properties
const { created, description, ...cleanTagData } = value;
// Only keep the description if there's no metadata.description
if (
description &&
(!cleanTagData.metadata || !cleanTagData.metadata.description)
) {
cleanTagData.description = description;
}
finalCleanData[key] = cleanTagData;
} else {
finalCleanData[key] = value;
}
}
cleanData = finalCleanData;
}
}
fs.writeFileSync(filepath, JSON.stringify(cleanData, null, 2), 'utf8');
if (isDebug) {
console.log(`writeJSON: Successfully wrote to ${filepath}`);
}
} catch (error) {
log('error', `Error writing JSON file ${filepath}:`, error.message);
if (isDebug) {
log('error', 'Full error details:', error);
}
}
}
/**
* Sanitizes a prompt string for use in a shell command
* @param {string} prompt The prompt to sanitize
* @returns {string} Sanitized prompt
*/
function sanitizePrompt(prompt) {
// Replace double quotes with escaped double quotes
return prompt.replace(/"/g, '\\"');
}
/**
* Reads the complexity report from file
* @param {string} customPath - Optional custom path to the report
* @returns {Object|null} The parsed complexity report or null if not found
*/
function readComplexityReport(customPath = null) {
// GUARD: Prevent circular dependency during config loading
let isDebug = false; // Default fallback
try {
// Only try to get debug flag if we're not in the middle of config loading
isDebug = getDebugFlag();
} catch (error) {
// If getDebugFlag() fails (likely due to circular dependency),
// use default false and continue
isDebug = false;
}
try {
let reportPath;
if (customPath) {
reportPath = customPath;
} else {
// Try new location first, then fall back to legacy
const newPath = path.join(process.cwd(), COMPLEXITY_REPORT_FILE);
const legacyPath = path.join(
process.cwd(),
LEGACY_COMPLEXITY_REPORT_FILE
);
reportPath = fs.existsSync(newPath) ? newPath : legacyPath;
}
if (!fs.existsSync(reportPath)) {
if (isDebug) {
log('debug', `Complexity report not found at ${reportPath}`);
}
return null;
}
const reportData = readJSON(reportPath);
if (isDebug) {
log('debug', `Successfully read complexity report from ${reportPath}`);
}
return reportData;
} catch (error) {
if (isDebug) {
log('error', `Error reading complexity report: ${error.message}`);
}
return null;
}
}
/**
* Finds a task analysis in the complexity report
* @param {Object} report - The complexity report
* @param {number} taskId - The task ID to find
* @returns {Object|null} The task analysis or null if not found
*/
function findTaskInComplexityReport(report, taskId) {
if (
!report ||
!report.complexityAnalysis ||
!Array.isArray(report.complexityAnalysis)
) {
return null;
}
return report.complexityAnalysis.find((task) => task.taskId === taskId);
}
function addComplexityToTask(task, complexityReport) {
let taskId;
if (task.isSubtask) {
taskId = task.parentTask.id;
} else if (task.parentId) {
taskId = task.parentId;
} else {
taskId = task.id;
}
const taskAnalysis = findTaskInComplexityReport(complexityReport, taskId);
if (taskAnalysis) {
task.complexityScore = taskAnalysis.complexityScore;
}
}
/**
* Checks if a task exists in the tasks array
* @param {Array} tasks - The tasks array
* @param {string|number} taskId - The task ID to check
* @returns {boolean} True if the task exists, false otherwise
*/
function taskExists(tasks, taskId) {
if (!taskId || !tasks || !Array.isArray(tasks)) {
return false;
}
// Handle both regular task IDs and subtask IDs (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 false;
}
return parentTask.subtasks.some((st) => st.id === subtaskId);
}
const id = parseInt(taskId, 10);
return tasks.some((t) => t.id === id);
}
/**
* 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;
}
/**
* Finds a task by ID in the tasks array. Optionally filters subtasks by status.
* @param {Array} tasks - The tasks array
* @param {string|number} taskId - The task ID to find
* @param {Object|null} complexityReport - Optional pre-loaded complexity report
* @param {string} [statusFilter] - Optional status to filter subtasks by
* @returns {{task: Object|null, originalSubtaskCount: number|null, originalSubtasks: Array|null}} The task object (potentially with filtered subtasks), the original subtask count, and original subtasks array if filtered, or nulls if not found.
*/
function findTaskById(
tasks,
taskId,
complexityReport = null,
statusFilter = null
) {
if (!taskId || !tasks || !Array.isArray(tasks)) {
return { task: null, originalSubtaskCount: null };
}
// Check if it's a subtask ID (e.g., "1.2")
if (typeof taskId === 'string' && taskId.includes('.')) {
// If looking for a subtask, statusFilter doesn't apply directly here.
const [parentId, subtaskId] = taskId
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = tasks.find((t) => t.id === parentId);
if (!parentTask || !parentTask.subtasks) {
return { task: null, originalSubtaskCount: null, originalSubtasks: 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;
}
// If we found a task, check for complexity data
if (subtask && complexityReport) {
addComplexityToTask(subtask, complexityReport);
}
return {
task: subtask || null,
originalSubtaskCount: null,
originalSubtasks: null
};
}
let taskResult = null;
let originalSubtaskCount = null;
let originalSubtasks = null;
// Find the main task
const id = parseInt(taskId, 10);
const task = tasks.find((t) => t.id === id) || null;
// If task not found, return nulls
if (!task) {
return { task: null, originalSubtaskCount: null, originalSubtasks: null };
}
taskResult = task;
// If task found and statusFilter provided, filter its subtasks
if (statusFilter && task.subtasks && Array.isArray(task.subtasks)) {
// Store original subtasks and count before filtering
originalSubtasks = [...task.subtasks]; // Clone the original subtasks array
originalSubtaskCount = task.subtasks.length;
// Clone the task to avoid modifying the original array
const filteredTask = { ...task };
filteredTask.subtasks = task.subtasks.filter(
(subtask) =>
subtask.status &&
subtask.status.toLowerCase() === statusFilter.toLowerCase()
);
taskResult = filteredTask;
}
// If task found and complexityReport provided, add complexity data
if (taskResult && complexityReport) {
addComplexityToTask(taskResult, complexityReport);
}
// Return the found task, original subtask count, and original subtasks
return { task: taskResult, originalSubtaskCount, originalSubtasks };
}
/**
* 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)}...`;
}
/**
* 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)) {
// Find the position of the dependency in the path
const cycleStartIndex = path.indexOf(depId);
// The last edge in the cycle is what we want to remove
const cycleEdges = path.slice(cycleStartIndex);
// We'll remove the last edge in the cycle (the one that points back)
cyclesToBreak.push(depId);
}
}
// Remove the node from recursion stack before returning
recursionStack.delete(subtaskId);
return cyclesToBreak;
}
/**
* Convert a string from camelCase to kebab-case
* @param {string} str - The string to convert
* @returns {string} The kebab-case version of the string
*/
const toKebabCase = (str) => {
// Special handling for common acronyms
const withReplacedAcronyms = str
.replace(/ID/g, 'Id')
.replace(/API/g, 'Api')
.replace(/UI/g, 'Ui')
.replace(/URL/g, 'Url')
.replace(/URI/g, 'Uri')
.replace(/JSON/g, 'Json')
.replace(/XML/g, 'Xml')
.replace(/HTML/g, 'Html')
.replace(/CSS/g, 'Css');
// Insert hyphens before capital letters and convert to lowercase
return withReplacedAcronyms
.replace(/([A-Z])/g, '-$1')
.toLowerCase()
.replace(/^-/, ''); // Remove leading hyphen if present
};
/**
* Detect camelCase flags in command arguments
* @param {string[]} args - Command line arguments to check
* @returns {Array<{original: string, kebabCase: string}>} - List of flags that should be converted
*/
function detectCamelCaseFlags(args) {
const camelCaseFlags = [];
for (const arg of args) {
if (arg.startsWith('--')) {
const flagName = arg.split('=')[0].slice(2); // Remove -- and anything after =
// Skip single-word flags - they can't be camelCase
if (!flagName.includes('-') && !/[A-Z]/.test(flagName)) {
continue;
}
// Check for camelCase pattern (lowercase followed by uppercase)
if (/[a-z][A-Z]/.test(flagName)) {
const kebabVersion = toKebabCase(flagName);
if (kebabVersion !== flagName) {
camelCaseFlags.push({
original: flagName,
kebabCase: kebabVersion
});
}
}
}
}
return camelCaseFlags;
}
/**
* Aggregates an array of telemetry objects into a single summary object.
* @param {Array<Object>} telemetryArray - Array of telemetryData objects.
* @param {string} overallCommandName - The name for the aggregated command.
* @returns {Object|null} Aggregated telemetry object or null if input is empty.
*/
function aggregateTelemetry(telemetryArray, overallCommandName) {
if (!telemetryArray || telemetryArray.length === 0) {
return null;
}
const aggregated = {
timestamp: new Date().toISOString(), // Use current time for aggregation time
userId: telemetryArray[0].userId, // Assume userId is consistent
commandName: overallCommandName,
modelUsed: 'Multiple', // Default if models vary
providerName: 'Multiple', // Default if providers vary
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
totalCost: 0,
currency: telemetryArray[0].currency || 'USD' // Assume consistent currency or default
};
const uniqueModels = new Set();
const uniqueProviders = new Set();
const uniqueCurrencies = new Set();
telemetryArray.forEach((item) => {
aggregated.inputTokens += item.inputTokens || 0;
aggregated.outputTokens += item.outputTokens || 0;
aggregated.totalCost += item.totalCost || 0;
uniqueModels.add(item.modelUsed);
uniqueProviders.add(item.providerName);
uniqueCurrencies.add(item.currency || 'USD');
});
aggregated.totalTokens = aggregated.inputTokens + aggregated.outputTokens;
aggregated.totalCost = parseFloat(aggregated.totalCost.toFixed(6)); // Fix precision
if (uniqueModels.size === 1) {
aggregated.modelUsed = [...uniqueModels][0];
}
if (uniqueProviders.size === 1) {
aggregated.providerName = [...uniqueProviders][0];
}
if (uniqueCurrencies.size > 1) {
aggregated.currency = 'Multiple'; // Mark if currencies actually differ
} else if (uniqueCurrencies.size === 1) {
aggregated.currency = [...uniqueCurrencies][0];
}
return aggregated;
}
/**
* Gets the current tag from state.json or falls back to defaultTag from config
* @param {string} projectRoot - The project root directory (required)
* @returns {string} The current tag name
*/
function getCurrentTag(projectRoot) {
if (!projectRoot) {
throw new Error('projectRoot is required for getCurrentTag');
}
try {
// Try to read current tag from state.json using fs directly
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
if (fs.existsSync(statePath)) {
const rawState = fs.readFileSync(statePath, 'utf8');
const stateData = JSON.parse(rawState);
if (stateData && stateData.currentTag) {
return stateData.currentTag;
}
}
} catch (error) {
// Ignore errors, fall back to default
}
// Fall back to defaultTag from config using fs directly
try {
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
if (fs.existsSync(configPath)) {
const rawConfig = fs.readFileSync(configPath, 'utf8');
const configData = JSON.parse(rawConfig);
if (configData && configData.global && configData.global.defaultTag) {
return configData.global.defaultTag;
}
}
} catch (error) {
// Ignore errors, use hardcoded default
}
// Final fallback
return 'master';
}
/**
* Resolves the tag to use based on options
* @param {Object} options - Options object
* @param {string} options.projectRoot - The project root directory (required)
* @param {string} [options.tag] - Explicit tag to use
* @returns {string} The resolved tag name
*/
function resolveTag(options = {}) {
const { projectRoot, tag } = options;
if (!projectRoot) {
throw new Error('projectRoot is required for resolveTag');
}
// If explicit tag provided, use it
if (tag) {
return tag;
}
// Otherwise get current tag from state/config
return getCurrentTag(projectRoot);
}
/**
* Gets the tasks array for a specific tag from tagged tasks.json data
* @param {Object} data - The parsed tasks.json data (after migration)
* @param {string} tagName - The tag name to get tasks for
* @returns {Array} The tasks array for the specified tag, or empty array if not found
*/
function getTasksForTag(data, tagName) {
if (!data || !tagName) {
return [];
}
// Handle migrated format: { "master": { "tasks": [...] }, "otherTag": { "tasks": [...] } }
if (
data[tagName] &&
data[tagName].tasks &&
Array.isArray(data[tagName].tasks)
) {
return data[tagName].tasks;
}
return [];
}
/**
* Sets the tasks array for a specific tag in the data structure
* @param {Object} data - The tasks.json data object
* @param {string} tagName - The tag name to set tasks for
* @param {Array} tasks - The tasks array to set
* @returns {Object} The updated data object
*/
function setTasksForTag(data, tagName, tasks) {
if (!data) {
data = {};
}
if (!data[tagName]) {
data[tagName] = {};
}
data[tagName].tasks = tasks || [];
return data;
}
/**
* Flatten tasks array to include subtasks as individual searchable items
* @param {Array} tasks - Array of task objects
* @returns {Array} Flattened array including both tasks and subtasks
*/
function flattenTasksWithSubtasks(tasks) {
const flattened = [];
for (const task of tasks) {
// Add the main task
flattened.push({
...task,
searchableId: task.id.toString(), // For consistent ID handling
isSubtask: false
});
// Add subtasks if they exist
if (task.subtasks && task.subtasks.length > 0) {
for (const subtask of task.subtasks) {
flattened.push({
...subtask,
searchableId: `${task.id}.${subtask.id}`, // Format: "15.2"
isSubtask: true,
parentId: task.id,
parentTitle: task.title,
// Enhance subtask context with parent information
title: `${subtask.title} (subtask of: ${task.title})`,
description: `${subtask.description} [Parent: ${task.description}]`
});
}
}
}
return flattened;
}
/**
* Ensures the tag object has a metadata object with created/updated timestamps.
* @param {Object} tagObj - The tag object (e.g., data['master'])
* @param {Object} [opts] - Optional fields (e.g., description, skipUpdate)
* @param {string} [opts.description] - Description for the tag
* @param {boolean} [opts.skipUpdate] - If true, don't update the 'updated' timestamp
* @returns {Object} The updated tag object (for chaining)
*/
function ensureTagMetadata(tagObj, opts = {}) {
if (!tagObj || typeof tagObj !== 'object') {
throw new Error('tagObj must be a valid object');
}
const now = new Date().toISOString();
if (!tagObj.metadata) {
// Create new metadata object
tagObj.metadata = {
created: now,
updated: now,
...(opts.description ? { description: opts.description } : {})
};
} else {
// Ensure existing metadata has required fields
if (!tagObj.metadata.created) {
tagObj.metadata.created = now;
}
// Update timestamp unless explicitly skipped
if (!opts.skipUpdate) {
tagObj.metadata.updated = now;
}
// Add description if provided and not already present
if (opts.description && !tagObj.metadata.description) {
tagObj.metadata.description = opts.description;
}
}
return tagObj;
}
// Export all utility functions and configuration
export {
LOG_LEVELS,
log,
readJSON,
writeJSON,
sanitizePrompt,
readComplexityReport,
findTaskInComplexityReport,
taskExists,
formatTaskId,
findTaskById,
truncate,
findCycles,
toKebabCase,
detectCamelCaseFlags,
disableSilentMode,
enableSilentMode,
getTaskManager,
isSilentMode,
addComplexityToTask,
resolveEnvVariable,
findProjectRoot,
aggregateTelemetry,
getCurrentTag,
resolveTag,
getTasksForTag,
setTasksForTag,
performCompleteTagMigration,
migrateConfigJson,
createStateJson,
markMigrationForNotice,
flattenTasksWithSubtasks,
ensureTagMetadata
};