/**
 * fuzzyTaskSearch.js
 * Reusable fuzzy search utility for finding relevant tasks based on semantic similarity
 */

import Fuse from 'fuse.js';

/**
 * Configuration for different search contexts
 */
const SEARCH_CONFIGS = {
	research: {
		threshold: 0.5, // More lenient for research (broader context)
		limit: 20,
		keys: [
			{ name: 'title', weight: 2.0 },
			{ name: 'description', weight: 1.0 },
			{ name: 'details', weight: 0.5 },
			{ name: 'dependencyTitles', weight: 0.5 }
		]
	},
	addTask: {
		threshold: 0.4, // Stricter for add-task (more precise context)
		limit: 15,
		keys: [
			{ name: 'title', weight: 2.0 },
			{ name: 'description', weight: 1.5 },
			{ name: 'details', weight: 0.8 },
			{ name: 'dependencyTitles', weight: 0.5 }
		]
	},
	default: {
		threshold: 0.4,
		limit: 15,
		keys: [
			{ name: 'title', weight: 2.0 },
			{ name: 'description', weight: 1.5 },
			{ name: 'details', weight: 1.0 },
			{ name: 'dependencyTitles', weight: 0.5 }
		]
	}
};

/**
 * Purpose categories for pattern-based task matching
 */
const PURPOSE_CATEGORIES = [
	{ pattern: /(command|cli|flag)/i, label: 'CLI commands' },
	{ pattern: /(task|subtask|add)/i, label: 'Task management' },
	{ pattern: /(dependency|depend)/i, label: 'Dependency handling' },
	{ pattern: /(AI|model|prompt|research)/i, label: 'AI integration' },
	{ pattern: /(UI|display|show|interface)/i, label: 'User interface' },
	{ pattern: /(schedule|time|cron)/i, label: 'Scheduling' },
	{ pattern: /(config|setting|option)/i, label: 'Configuration' },
	{ pattern: /(test|testing|spec)/i, label: 'Testing' },
	{ pattern: /(auth|login|user)/i, label: 'Authentication' },
	{ pattern: /(database|db|data)/i, label: 'Data management' },
	{ pattern: /(api|endpoint|route)/i, label: 'API development' },
	{ pattern: /(deploy|build|release)/i, label: 'Deployment' },
	{ pattern: /(security|auth|login|user)/i, label: 'Security' },
	{ pattern: /.*/, label: 'Other' }
];

/**
 * Relevance score thresholds
 */
const RELEVANCE_THRESHOLDS = {
	high: 0.25,
	medium: 0.4,
	low: 0.6
};

/**
 * Fuzzy search utility class for finding relevant tasks
 */
export

/**
 * Factory function to create a fuzzy search instance
 * @param {Array} tasks - Array of task objects
 * @param {string} [searchType='default'] - Type of search configuration to use
 * @returns {FuzzyTaskSearch} Fuzzy search instance
 */
export function createFuzzyTaskSearch(tasks, searchType = 'default') {
	return new FuzzyTaskSearch(tasks, searchType);
}

/**
 * Quick utility function to find relevant task IDs for a prompt
 * @param {Array} tasks - Array of task objects
 * @param {string} prompt - Search prompt
 * @param {Object} options - Search options
 * @returns {Array<string>} Array of relevant task ID strings
 */
export function findRelevantTaskIds(tasks, prompt, options = {}) {
	const {
		searchType = 'default',
		maxResults = 8,
		includeSubtasks = false
	} = options;

	const fuzzySearch = new FuzzyTaskSearch(tasks, searchType);
	const results = fuzzySearch.findRelevantTasks(prompt, { maxResults });

	return includeSubtasks
		? fuzzySearch.getTaskIdsWithSubtasks(results, true)
		: fuzzySearch.getTaskIds(results);
}

export default FuzzyTaskSearch;

class FuzzyTaskSearch {
	constructor(tasks, searchType = 'default') {
		this.tasks = tasks;
		this.config = SEARCH_CONFIGS[searchType] || SEARCH_CONFIGS.default;
		this.searchableTasks = this._prepareSearchableTasks(tasks);
		this.fuse = new Fuse(this.searchableTasks, {
			includeScore: true,
			threshold: this.config.threshold,
			keys: this.config.keys,
			shouldSort: true,
			useExtendedSearch: true,
			limit: this.config.limit
		});
	}
	_prepareSearchableTasks(tasks) {
		return tasks.map((task) => {
			const dependencyTitles =
				task.dependencies?.length > 0
					? task.dependencies
							.map((depId) => {
								const depTask = tasks.find((t) => t.id === depId);
								return depTask ? depTask.title : '';
							})
							.filter((title) => title)
							.join(' ')
					: '';
			return {
				...task,
				dependencyTitles
			};
		});
	}
	_extractPromptWords(prompt) {
		return prompt
			.toLowerCase()
			.replace(/[^\w\s-]/g, ' ')
			.split(/\s+/)
			.filter((word) => word.length > 3);
	}
	findRelevantTasks(prompt, options = {}) {
		const {
			maxResults = 8,
			includeRecent = true,
			includeCategoryMatches = true
		} = options;
		const promptWords = this._extractPromptWords(prompt);
		const fuzzyResults = this.fuse.search(prompt);
		let wordResults = [];
		for (const word of promptWords) {
			if (word.length > 5) {
				const results = this.fuse.search(word);
				if (results.length > 0) {
					wordResults.push(...results);
				}
			}
		}
		const mergedResults = [...fuzzyResults];
		for (const wordResult of wordResults) {
			if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
				mergedResults.push(wordResult);
			}
		}
		const highRelevance = mergedResults
			.filter((result) => result.score < RELEVANCE_THRESHOLDS.high)
			.map((result) => ({ ...result.item, score: result.score }));
		const mediumRelevance = mergedResults
			.filter(
				(result) =>
					result.score >= RELEVANCE_THRESHOLDS.high &&
					result.score < RELEVANCE_THRESHOLDS.medium
			)
			.map((result) => ({ ...result.item, score: result.score }));
		const lowRelevance = mergedResults
			.filter(
				(result) =>
					result.score >= RELEVANCE_THRESHOLDS.medium &&
					result.score < RELEVANCE_THRESHOLDS.low
			)
			.map((result) => ({ ...result.item, score: result.score }));
		const recentTasks = includeRecent
			? [...this.tasks].sort((a, b) => b.id - a.id).slice(0, 5)
			: [];
		let categoryTasks = [];
		let promptCategory = null;
		if (includeCategoryMatches) {
			promptCategory = PURPOSE_CATEGORIES.find((cat) =>
				cat.pattern.test(prompt)
			);
			categoryTasks = promptCategory
				? this.tasks
						.filter(
							(t) =>
								promptCategory.pattern.test(t.title) ||
								promptCategory.pattern.test(t.description) ||
								(t.details && promptCategory.pattern.test(t.details))
						)
						.slice(0, 3)
				: [];
		}
		const allRelevantTasks = [...highRelevance];
		for (const task of mediumRelevance) {
			if (!allRelevantTasks.some((t) => t.id === task.id)) {
				allRelevantTasks.push(task);
			}
		}
		for (const task of lowRelevance) {
			if (!allRelevantTasks.some((t) => t.id === task.id)) {
				allRelevantTasks.push(task);
			}
		}
		for (const task of categoryTasks) {
			if (!allRelevantTasks.some((t) => t.id === task.id)) {
				allRelevantTasks.push(task);
			}
		}
		for (const task of recentTasks) {
			if (!allRelevantTasks.some((t) => t.id === task.id)) {
				allRelevantTasks.push(task);
			}
		}
		const finalResults = allRelevantTasks.slice(0, maxResults);
		return {
			results: finalResults,
			breakdown: {
				highRelevance,
				mediumRelevance,
				lowRelevance,
				categoryTasks,
				recentTasks,
				promptCategory,
				promptWords
			},
			metadata: {
				totalSearched: this.tasks.length,
				fuzzyMatches: fuzzyResults.length,
				wordMatches: wordResults.length,
				finalCount: finalResults.length
			}
		};
	}
	getTaskIds(searchResults) {
		return searchResults.results.map((task) => task.id.toString());
	}
	getTaskIdsWithSubtasks(searchResults, includeSubtasks = false) {
		const taskIds = [];
		for (const task of searchResults.results) {
			taskIds.push(task.id.toString());
			if (includeSubtasks && task.subtasks && task.subtasks.length > 0) {
				for (const subtask of task.subtasks) {
					taskIds.push(`${task.id}.${subtask.id}`);
				}
			}
		}
		return taskIds;
	}
	formatSearchSummary(searchResults, options = {}) {
		const { includeScores = false, includeBreakdown = false } = options;
		const { results, breakdown, metadata } = searchResults;
		let summary = `Found ${results.length} relevant tasks from ${metadata.totalSearched} total tasks`;
		if (includeBreakdown && breakdown) {
			const parts = [];
			if (breakdown.highRelevance.length > 0)
				parts.push(`${breakdown.highRelevance.length} high relevance`);
			if (breakdown.mediumRelevance.length > 0)
				parts.push(`${breakdown.mediumRelevance.length} medium relevance`);
			if (breakdown.lowRelevance.length > 0)
				parts.push(`${breakdown.lowRelevance.length} low relevance`);
			if (breakdown.categoryTasks.length > 0)
				parts.push(`${breakdown.categoryTasks.length} category matches`);
			if (parts.length > 0) {
				summary += ` (${parts.join(', ')})`;
			}
			if (breakdown.promptCategory) {
				summary += `\nCategory detected: ${breakdown.promptCategory.label}`;
			}
		}
		return summary;
	}
}

/**
 * contextGatherer.js
 * Comprehensive context gathering utility for Task Master AI operations
 * Supports task context, file context, project tree, and custom context
 */

import fs from 'fs';
import path from 'path';
import pkg from 'gpt-tokens';
import Fuse from 'fuse.js';
import {
	readJSON,
	findTaskById,
	truncate,
	flattenTasksWithSubtasks
} from '../utils.js';

const { encode } = pkg;

/**
 * Context Gatherer class for collecting and formatting context from various sources
 */
export class ContextGatherer {
	constructor(projectRoot, tag) {
		this.projectRoot = projectRoot;
		this.tasksPath = path.join(
			projectRoot,
			'.taskmaster',
			'tasks',
			'tasks.json'
		);
		this.tag = tag;
		this.allTasks = this._loadAllTasks();
	}

	_loadAllTasks() {
		try {
			const data = readJSON(this.tasksPath, this.projectRoot, this.tag);
			const tasks = data?.tasks || [];
			return tasks;
		} catch (error) {
			console.warn(
				`Warning: Could not load tasks for ContextGatherer: ${error.message}`
			);
			return [];
		}
	}

	/**
	 * Count tokens in a text string using gpt-tokens
	 * @param {string} text - Text to count tokens for
	 * @returns {number} Token count
	 */
	countTokens(text) {
		if (!text || typeof text !== 'string') {
			return 0;
		}
		try {
			return encode(text).length;
		} catch (error) {
			// Fallback to rough character-based estimation if tokenizer fails
			// Rough estimate: ~4 characters per token for English text
			return Math.ceil(text.length / 4);
		}
	}

	/**
	 * Main method to gather context from multiple sources
	 * @param {Object} options - Context gathering options
	 * @param {Array<string>} [options.tasks] - Task/subtask IDs to include
	 * @param {Array<string>} [options.files] - File paths to include
	 * @param {string} [options.customContext] - Additional custom context
	 * @param {boolean} [options.includeProjectTree] - Include project file tree
	 * @param {string} [options.format] - Output format: 'research', 'chat', 'system-prompt'
	 * @param {boolean} [options.includeTokenCounts] - Whether to include token breakdown
	 * @param {string} [options.semanticQuery] - A query string for semantic task searching.
	 * @param {number} [options.maxSemanticResults] - Max number of semantic results.
	 * @param {Array<number>} [options.dependencyTasks] - Array of task IDs to build dependency graphs from.
	 * @returns {Promise<Object>} Object with context string and analysis data
	 */
	async gather(options = {}) {
		const {
			tasks = [],
			files = [],
			customContext = '',
			includeProjectTree = false,
			format = 'research',
			includeTokenCounts = false,
			semanticQuery,
			maxSemanticResults = 10,
			dependencyTasks = []
		} = options;

		const contextSections = [];
		const finalTaskIds = new Set(tasks.map(String));
		let analysisData = null;
		let tokenBreakdown = null;

		// Initialize token breakdown if requested
		if (includeTokenCounts) {
			tokenBreakdown = {
				total: 0,
				customContext: null,
				tasks: [],
				files: [],
				projectTree: null
			};
		}

		// Semantic Search
		if (semanticQuery && this.allTasks.length > 0) {
			const semanticResults = this._performSemanticSearch(
				semanticQuery,
				maxSemanticResults
			);

			// Store the analysis data for UI display
			analysisData = semanticResults.analysisData;

			semanticResults.tasks.forEach((task) => {
				finalTaskIds.add(String(task.id));
			});
		}

		// Dependency Graph Analysis
		if (dependencyTasks.length > 0) {
			const dependencyResults = this._buildDependencyGraphs(dependencyTasks);
			dependencyResults.allRelatedTaskIds.forEach((id) =>
				finalTaskIds.add(String(id))
			);
			// We can format and add dependencyResults.graphVisualization later if needed
		}

		// Add custom context first
		if (customContext && customContext.trim()) {
			const formattedCustomContext = this._formatCustomContext(
				customContext,
				format
			);
			contextSections.push(formattedCustomContext);

			// Calculate tokens for custom context if requested
			if (includeTokenCounts) {
				tokenBreakdown.customContext = {
					tokens: this.countTokens(formattedCustomContext),
					characters: formattedCustomContext.length
				};
				tokenBreakdown.total += tokenBreakdown.customContext.tokens;
			}
		}

		// Gather context for the final list of tasks
		if (finalTaskIds.size > 0) {
			const taskContextResult = await this._gatherTaskContext(
				Array.from(finalTaskIds),
				format,
				includeTokenCounts
			);
			if (taskContextResult.context) {
				contextSections.push(taskContextResult.context);

				// Add task breakdown if token counting is enabled
				if (includeTokenCounts && taskContextResult.breakdown) {
					tokenBreakdown.tasks = taskContextResult.breakdown;
					const taskTokens = taskContextResult.breakdown.reduce(
						(sum, task) => sum + task.tokens,
						0
					);
					tokenBreakdown.total += taskTokens;
				}
			}
		}

		// Add file context
		if (files.length > 0) {
			const fileContextResult = await this._gatherFileContext(
				files,
				format,
				includeTokenCounts
			);
			if (fileContextResult.context) {
				contextSections.push(fileContextResult.context);

				// Add file breakdown if token counting is enabled
				if (includeTokenCounts && fileContextResult.breakdown) {
					tokenBreakdown.files = fileContextResult.breakdown;
					const fileTokens = fileContextResult.breakdown.reduce(
						(sum, file) => sum + file.tokens,
						0
					);
					tokenBreakdown.total += fileTokens;
				}
			}
		}

		// Add project tree context
		if (includeProjectTree) {
			const treeContextResult = await this._gatherProjectTreeContext(
				format,
				includeTokenCounts
			);
			if (treeContextResult.context) {
				contextSections.push(treeContextResult.context);

				// Add tree breakdown if token counting is enabled
				if (includeTokenCounts && treeContextResult.breakdown) {
					tokenBreakdown.projectTree = treeContextResult.breakdown;
					tokenBreakdown.total += treeContextResult.breakdown.tokens;
				}
			}
		}

		const finalContext = this._joinContextSections(contextSections, format);

		const result = {
			context: finalContext,
			analysisData: analysisData,
			contextSections: contextSections.length,
			finalTaskIds: Array.from(finalTaskIds)
		};

		// Only include tokenBreakdown if it was requested
		if (includeTokenCounts) {
			result.tokenBreakdown = tokenBreakdown;
		}

		return result;
	}

	_performSemanticSearch(query, maxResults) {
		const searchableTasks = this.allTasks.map((task) => {
			const dependencyTitles =
				task.dependencies?.length > 0
					? task.dependencies
							.map((depId) => this.allTasks.find((t) => t.id === depId)?.title)
							.filter(Boolean)
							.join(' ')
					: '';
			return { ...task, dependencyTitles };
		});

		// Use the exact same approach as add-task.js
		const searchOptions = {
			includeScore: true, // Return match scores
			threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
			keys: [
				{ name: 'title', weight: 1.5 }, // Title is most important
				{ name: 'description', weight: 2 }, // Description is very important
				{ name: 'details', weight: 3 }, // Details is most important
				// Search dependencies to find tasks that depend on similar things
				{ name: 'dependencyTitles', weight: 0.5 }
			],
			// Sort matches by score (lower is better)
			shouldSort: true,
			// Allow searching in nested properties
			useExtendedSearch: true,
			// Return up to 50 matches
			limit: 50
		};

		// Create search index using Fuse.js
		const fuse = new Fuse(searchableTasks, searchOptions);

		// Extract significant words and phrases from the prompt (like add-task.js does)
		const promptWords = query
			.toLowerCase()
			.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
			.split(/\s+/)
			.filter((word) => word.length > 3); // Words at least 4 chars

		// Use the user's prompt for fuzzy search
		const fuzzyResults = fuse.search(query);

		// Also search for each significant word to catch different aspects
		const wordResults = [];
		for (const word of promptWords) {
			if (word.length > 5) {
				// Only use significant words
				const results = fuse.search(word);
				if (results.length > 0) {
					wordResults.push(...results);
				}
			}
		}

		// Merge and deduplicate results
		const mergedResults = [...fuzzyResults];

		// Add word results that aren't already in fuzzyResults
		for (const wordResult of wordResults) {
			if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
				mergedResults.push(wordResult);
			}
		}

		// Group search results by relevance
		const highRelevance = mergedResults
			.filter((result) => result.score < 0.25)
			.map((result) => result.item);

		const mediumRelevance = mergedResults
			.filter((result) => result.score >= 0.25 && result.score < 0.4)
			.map((result) => result.item);

		// Get recent tasks (newest first)
		const recentTasks = [...this.allTasks]
			.sort((a, b) => b.id - a.id)
			.slice(0, 5);

		// Combine high relevance, medium relevance, and recent tasks
		// Prioritize high relevance first
		const allRelevantTasks = [...highRelevance];

		// Add medium relevance if not already included
		for (const task of mediumRelevance) {
			if (!allRelevantTasks.some((t) => t.id === task.id)) {
				allRelevantTasks.push(task);
			}
		}

		// Add recent tasks if not already included
		for (const task of recentTasks) {
			if (!allRelevantTasks.some((t) => t.id === task.id)) {
				allRelevantTasks.push(task);
			}
		}

		// Get top N results for context
		const finalResults = allRelevantTasks.slice(0, maxResults);
		return {
			tasks: finalResults,
			analysisData: {
				highRelevance: highRelevance,
				mediumRelevance: mediumRelevance,
				recentTasks: recentTasks,
				allRelevantTasks: allRelevantTasks
			}
		};
	}

	_buildDependencyContext(taskIds) {
		const { allRelatedTaskIds, graphs, depthMap } =
			this._buildDependencyGraphs(taskIds);
		if (allRelatedTaskIds.size === 0) return '';

		const dependentTasks = Array.from(allRelatedTaskIds)
			.map((id) => this.allTasks.find((t) => t.id === id))
			.filter(Boolean)
			.sort((a, b) => (depthMap.get(a.id) || 0) - (depthMap.get(b.id) || 0));

		const uniqueDetailedTasks = dependentTasks.slice(0, 8);

		let context = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.`;

		const directDeps = this.allTasks.filter((t) => taskIds.includes(t.id));
		if (directDeps.length > 0) {
			context += `\n\nDirect dependencies:\n${directDeps
				.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
				.join('\n')}`;
		}

		const indirectDeps = dependentTasks.filter((t) => !taskIds.includes(t.id));
		if (indirectDeps.length > 0) {
			context += `\n\nIndirect dependencies (dependencies of dependencies):\n${indirectDeps
				.slice(0, 5)
				.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
				.join('\n')}`;
			if (indirectDeps.length > 5)
				context += `\n- ... and ${
					indirectDeps.length - 5
				} more indirect dependencies`;
		}

		context += `\n\nDetailed information about dependencies:`;
		for (const depTask of uniqueDetailedTasks) {
			const isDirect = taskIds.includes(depTask.id)
				? ' [DIRECT DEPENDENCY]'
				: '';
			context += `\n\n------ Task ${depTask.id}${isDirect}: ${depTask.title} ------\n`;
			context += `Description: ${depTask.description}\n`;
			if (depTask.dependencies?.length) {
				context += `Dependencies: ${depTask.dependencies.join(', ')}\n`;
			}
			if (depTask.details) {
				context += `Implementation Details: ${truncate(
					depTask.details,
					400
				)}\n`;
			}
		}

		if (graphs.length > 0) {
			context += '\n\nDependency Chain Visualization:';
			context += graphs
				.map((graph) => this._formatDependencyChain(graph))
				.join('');
		}

		return context;
	}

	_buildDependencyGraphs(taskIds) {
		const visited = new Set();
		const depthMap = new Map();
		const graphs = [];

		for (const id of taskIds) {
			const graph = this._buildDependencyGraph(id, visited, depthMap);
			if (graph) graphs.push(graph);
		}

		return { allRelatedTaskIds: visited, graphs, depthMap };
	}

	_buildDependencyGraph(taskId, visited, depthMap, depth = 0) {
		if (visited.has(taskId) || depth > 5) return null; // Limit recursion depth
		const task = this.allTasks.find((t) => t.id === taskId);
		if (!task) return null;

		visited.add(taskId);
		if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) {
			depthMap.set(taskId, depth);
		}

		const dependencies =
			task.dependencies
				?.map((depId) =>
					this._buildDependencyGraph(depId, visited, depthMap, depth + 1)
				)
				.filter(Boolean) || [];

		return { ...task, dependencies };
	}

	_formatDependencyChain(node, prefix = '', isLast = true, depth = 0) {
		if (depth > 3) return '';
		const connector = isLast ? '└── ' : '├── ';
		let result = `${prefix}${connector}Task ${node.id}: ${node.title}`;
		if (node.dependencies?.length) {
			const childPrefix = prefix + (isLast ? '    ' : '│   ');
			result += node.dependencies
				.map((dep, index) =>
					this._formatDependencyChain(
						dep,
						childPrefix,
						index === node.dependencies.length - 1,
						depth + 1
					)
				)
				.join('');
		}
		return '\n' + result;
	}

	/**
	 * Parse task ID strings into structured format
	 * Supports formats: "15", "15.2", "16,17.1"
	 * @param {Array<string>} taskIds - Array of task ID strings
	 * @returns {Array<Object>} Parsed task identifiers
	 */
	_parseTaskIds(taskIds) {
		const parsed = [];

		for (const idStr of taskIds) {
			if (idStr.includes('.')) {
				// Subtask format: "15.2"
				const [parentId, subtaskId] = idStr.split('.');
				parsed.push({
					type: 'subtask',
					parentId: parseInt(parentId, 10),
					subtaskId: parseInt(subtaskId, 10),
					fullId: idStr
				});
			} else {
				// Task format: "15"
				parsed.push({
					type: 'task',
					taskId: parseInt(idStr, 10),
					fullId: idStr
				});
			}
		}

		return parsed;
	}

	/**
	 * Gather context from tasks and subtasks
	 * @param {Array<string>} taskIds - Task/subtask IDs
	 * @param {string} format - Output format
	 * @param {boolean} includeTokenCounts - Whether to include token breakdown
	 * @returns {Promise<Object>} Task context result with breakdown
	 */
	async _gatherTaskContext(taskIds, format, includeTokenCounts = false) {
		try {
			if (!this.allTasks || this.allTasks.length === 0) {
				return { context: null, breakdown: [] };
			}

			const parsedIds = this._parseTaskIds(taskIds);
			const contextItems = [];
			const breakdown = [];

			for (const parsed of parsedIds) {
				let formattedItem = null;
				let itemInfo = null;

				if (parsed.type === 'task') {
					const result = findTaskById(this.allTasks, parsed.taskId);
					if (result.task) {
						formattedItem = this._formatTaskForContext(result.task, format);
						itemInfo = {
							id: parsed.fullId,
							type: 'task',
							title: result.task.title,
							tokens: includeTokenCounts ? this.countTokens(formattedItem) : 0,
							characters: formattedItem.length
						};
					}
				} else if (parsed.type === 'subtask') {
					const parentResult = findTaskById(this.allTasks, parsed.parentId);
					if (parentResult.task && parentResult.task.subtasks) {
						const subtask = parentResult.task.subtasks.find(
							(st) => st.id === parsed.subtaskId
						);
						if (subtask) {
							formattedItem = this._formatSubtaskForContext(
								subtask,
								parentResult.task,
								format
							);
							itemInfo = {
								id: parsed.fullId,
								type: 'subtask',
								title: subtask.title,
								parentTitle: parentResult.task.title,
								tokens: includeTokenCounts
									? this.countTokens(formattedItem)
									: 0,
								characters: formattedItem.length
							};
						}
					}
				}

				if (formattedItem && itemInfo) {
					contextItems.push(formattedItem);
					if (includeTokenCounts) {
						breakdown.push(itemInfo);
					}
				}
			}

			if (contextItems.length === 0) {
				return { context: null, breakdown: [] };
			}

			const finalContext = this._formatTaskContextSection(contextItems, format);
			return {
				context: finalContext,
				breakdown: includeTokenCounts ? breakdown : []
			};
		} catch (error) {
			console.warn(`Warning: Could not gather task context: ${error.message}`);
			return { context: null, breakdown: [] };
		}
	}

	/**
	 * Format a task for context inclusion
	 * @param {Object} task - Task object
	 * @param {string} format - Output format
	 * @returns {string} Formatted task context
	 */
	_formatTaskForContext(task, format) {
		const sections = [];

		sections.push(`**Task ${task.id}: ${task.title}**`);
		sections.push(`Description: ${task.description}`);
		sections.push(`Status: ${task.status || 'pending'}`);
		sections.push(`Priority: ${task.priority || 'medium'}`);

		if (task.dependencies && task.dependencies.length > 0) {
			sections.push(`Dependencies: ${task.dependencies.join(', ')}`);
		}

		if (task.details) {
			const details = truncate(task.details, 500);
			sections.push(`Implementation Details: ${details}`);
		}

		if (task.testStrategy) {
			const testStrategy = truncate(task.testStrategy, 300);
			sections.push(`Test Strategy: ${testStrategy}`);
		}

		if (task.subtasks && task.subtasks.length > 0) {
			sections.push(`Subtasks: ${task.subtasks.length} subtasks defined`);
		}

		return sections.join('\n');
	}

	/**
	 * Format a subtask for context inclusion
	 * @param {Object} subtask - Subtask object
	 * @param {Object} parentTask - Parent task object
	 * @param {string} format - Output format
	 * @returns {string} Formatted subtask context
	 */
	_formatSubtaskForContext(subtask, parentTask, format) {
		const sections = [];

		sections.push(
			`**Subtask ${parentTask.id}.${subtask.id}: ${subtask.title}**`
		);
		sections.push(`Parent Task: ${parentTask.title}`);
		sections.push(`Description: ${subtask.description}`);
		sections.push(`Status: ${subtask.status || 'pending'}`);

		if (subtask.dependencies && subtask.dependencies.length > 0) {
			sections.push(`Dependencies: ${subtask.dependencies.join(', ')}`);
		}

		if (subtask.details) {
			const details = truncate(subtask.details, 500);
			sections.push(`Implementation Details: ${details}`);
		}

		return sections.join('\n');
	}

	/**
	 * Gather context from files
	 * @param {Array<string>} filePaths - File paths to read
	 * @param {string} format - Output format
	 * @param {boolean} includeTokenCounts - Whether to include token breakdown
	 * @returns {Promise<Object>} File context result with breakdown
	 */
	async _gatherFileContext(filePaths, format, includeTokenCounts = false) {
		const fileContents = [];
		const breakdown = [];

		for (const filePath of filePaths) {
			try {
				const fullPath = path.isAbsolute(filePath)
					? filePath
					: path.join(this.projectRoot, filePath);

				if (!fs.existsSync(fullPath)) {
					continue;
				}

				const stats = fs.statSync(fullPath);
				if (!stats.isFile()) {
					continue;
				}

				// Check file size (limit to 50KB for context)
				if (stats.size > 50 * 1024) {
					continue;
				}

				const content = fs.readFileSync(fullPath, 'utf-8');
				const relativePath = path.relative(this.projectRoot, fullPath);

				const fileData = {
					path: relativePath,
					size: stats.size,
					content: content,
					lastModified: stats.mtime
				};

				fileContents.push(fileData);

				// Calculate tokens for this individual file if requested
				if (includeTokenCounts) {
					const formattedFile = this._formatSingleFileForContext(
						fileData,
						format
					);
					breakdown.push({
						path: relativePath,
						sizeKB: Math.round(stats.size / 1024),
						tokens: this.countTokens(formattedFile),
						characters: formattedFile.length
					});
				}
			} catch (error) {
				console.warn(
					`Warning: Could not read file ${filePath}: ${error.message}`
				);
			}
		}

		if (fileContents.length === 0) {
			return { context: null, breakdown: [] };
		}

		const finalContext = this._formatFileContextSection(fileContents, format);
		return {
			context: finalContext,
			breakdown: includeTokenCounts ? breakdown : []
		};
	}

	/**
	 * Generate project file tree context
	 * @param {string} format - Output format
	 * @param {boolean} includeTokenCounts - Whether to include token breakdown
	 * @returns {Promise<Object>} Project tree context result with breakdown
	 */
	async _gatherProjectTreeContext(format, includeTokenCounts = false) {
		try {
			const tree = this._generateFileTree(this.projectRoot, 5); // Max depth 5
			const finalContext = this._formatProjectTreeSection(tree, format);

			const breakdown = includeTokenCounts
				? {
						tokens: this.countTokens(finalContext),
						characters: finalContext.length,
						fileCount: tree.fileCount || 0,
						dirCount: tree.dirCount || 0
					}
				: null;

			return {
				context: finalContext,
				breakdown: breakdown
			};
		} catch (error) {
			console.warn(
				`Warning: Could not generate project tree: ${error.message}`
			);
			return { context: null, breakdown: null };
		}
	}

	/**
	 * Format a single file for context (used for token counting)
	 * @param {Object} fileData - File data object
	 * @param {string} format - Output format
	 * @returns {string} Formatted file context
	 */
	_formatSingleFileForContext(fileData, format) {
		const header = `**File: ${fileData.path}** (${Math.round(fileData.size / 1024)}KB)`;
		const content = `\`\`\`\n${fileData.content}\n\`\`\``;
		return `${header}\n\n${content}`;
	}

	/**
	 * Generate file tree structure
	 * @param {string} dirPath - Directory path
	 * @param {number} maxDepth - Maximum depth to traverse
	 * @param {number} currentDepth - Current depth
	 * @returns {Object} File tree structure
	 */
	_generateFileTree(dirPath, maxDepth, currentDepth = 0) {
		const ignoreDirs = [
			'.git',
			'node_modules',
			'.env',
			'coverage',
			'dist',
			'build'
		];
		const ignoreFiles = ['.DS_Store', '.env', '.env.local', '.env.production'];

		if (currentDepth >= maxDepth) {
			return null;
		}

		try {
			const items = fs.readdirSync(dirPath);
			const tree = {
				name: path.basename(dirPath),
				type: 'directory',
				children: [],
				fileCount: 0,
				dirCount: 0
			};

			for (const item of items) {
				if (ignoreDirs.includes(item) || ignoreFiles.includes(item)) {
					continue;
				}

				const itemPath = path.join(dirPath, item);
				const stats = fs.statSync(itemPath);

				if (stats.isDirectory()) {
					tree.dirCount++;
					if (currentDepth < maxDepth - 1) {
						const subtree = this._generateFileTree(
							itemPath,
							maxDepth,
							currentDepth + 1
						);
						if (subtree) {
							tree.children.push(subtree);
						}
					}
				} else {
					tree.fileCount++;
					tree.children.push({
						name: item,
						type: 'file',
						size: stats.size
					});
				}
			}

			return tree;
		} catch (error) {
			return null;
		}
	}

	/**
	 * Format custom context section
	 * @param {string} customContext - Custom context string
	 * @param {string} format - Output format
	 * @returns {string} Formatted custom context
	 */
	_formatCustomContext(customContext, format) {
		switch (format) {
			case 'research':
				return `## Additional Context\n\n${customContext}`;
			case 'chat':
				return `**Additional Context:**\n${customContext}`;
			case 'system-prompt':
				return `Additional context: ${customContext}`;
			default:
				return customContext;
		}
	}

	/**
	 * Format task context section
	 * @param {Array<string>} taskItems - Formatted task items
	 * @param {string} format - Output format
	 * @returns {string} Formatted task context section
	 */
	_formatTaskContextSection(taskItems, format) {
		switch (format) {
			case 'research':
				return `## Task Context\n\n${taskItems.join('\n\n---\n\n')}`;
			case 'chat':
				return `**Task Context:**\n\n${taskItems.join('\n\n')}`;
			case 'system-prompt':
				return `Task context: ${taskItems.join(' | ')}`;
			default:
				return taskItems.join('\n\n');
		}
	}

	/**
	 * Format file context section
	 * @param {Array<Object>} fileContents - File content objects
	 * @param {string} format - Output format
	 * @returns {string} Formatted file context section
	 */
	_formatFileContextSection(fileContents, format) {
		const fileItems = fileContents.map((file) => {
			const header = `**File: ${file.path}** (${Math.round(file.size / 1024)}KB)`;
			const content = `\`\`\`\n${file.content}\n\`\`\``;
			return `${header}\n\n${content}`;
		});

		switch (format) {
			case 'research':
				return `## File Context\n\n${fileItems.join('\n\n---\n\n')}`;
			case 'chat':
				return `**File Context:**\n\n${fileItems.join('\n\n')}`;
			case 'system-prompt':
				return `File context: ${fileContents.map((f) => `${f.path} (${f.content.substring(0, 200)}...)`).join(' | ')}`;
			default:
				return fileItems.join('\n\n');
		}
	}

	/**
	 * Format project tree section
	 * @param {Object} tree - File tree structure
	 * @param {string} format - Output format
	 * @returns {string} Formatted project tree section
	 */
	_formatProjectTreeSection(tree, format) {
		const treeString = this._renderFileTree(tree);

		switch (format) {
			case 'research':
				return `## Project Structure\n\n\`\`\`\n${treeString}\n\`\`\``;
			case 'chat':
				return `**Project Structure:**\n\`\`\`\n${treeString}\n\`\`\``;
			case 'system-prompt':
				return `Project structure: ${treeString.replace(/\n/g, ' | ')}`;
			default:
				return treeString;
		}
	}

	/**
	 * Render file tree as string
	 * @param {Object} tree - File tree structure
	 * @param {string} prefix - Current prefix for indentation
	 * @returns {string} Rendered tree string
	 */
	_renderFileTree(tree, prefix = '') {
		let result = `${prefix}${tree.name}/`;

		if (tree.fileCount > 0 || tree.dirCount > 0) {
			result += ` (${tree.fileCount} files, ${tree.dirCount} dirs)`;
		}

		result += '\n';

		if (tree.children) {
			tree.children.forEach((child, index) => {
				const isLast = index === tree.children.length - 1;
				const childPrefix = prefix + (isLast ? '└── ' : '├── ');
				const nextPrefix = prefix + (isLast ? '    ' : '│   ');

				if (child.type === 'directory') {
					result += this._renderFileTree(child, childPrefix);
				} else {
					result += `${childPrefix}${child.name}\n`;
				}
			});
		}

		return result;
	}

	/**
	 * Join context sections based on format
	 * @param {Array<string>} sections - Context sections
	 * @param {string} format - Output format
	 * @returns {string} Joined context string
	 */
	_joinContextSections(sections, format) {
		if (sections.length === 0) {
			return '';
		}

		switch (format) {
			case 'research':
				return sections.join('\n\n---\n\n');
			case 'chat':
				return sections.join('\n\n');
			case 'system-prompt':
				return sections.join(' ');
			default:
				return sections.join('\n\n');
		}
	}
}

/**
 * Factory function to create a context gatherer instance
 * @param {string} projectRoot - Project root directory
 * @param {string} tag - Tag for the task
 * @returns {ContextGatherer} Context gatherer instance
 * @throws {Error} If tag is not provided
 */
export function createContextGatherer(projectRoot, tag) {
	if (!tag) {
		throw new Error('Tag is required');
	}
	return new ContextGatherer(projectRoot, tag);
}

export default ContextGatherer;

/**
 * research.js
 * Core research functionality for AI-powered queries with project context
 */

import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import inquirer from 'inquirer';
import { highlight } from 'cli-highlight';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { generateTextService } from '../ai-services-unified.js';
import { getPromptManager } from '../prompt-manager.js';
import {
	log as consoleLog,
	findProjectRoot,
	readJSON,
	flattenTasksWithSubtasks
} from '../utils.js';
import {
	displayAiUsageSummary,
	startLoadingIndicator,
	stopLoadingIndicator
} from '../ui.js';

/**
 * Perform AI-powered research with project context
 * @param {string} query - Research query/prompt
 * @param {Object} options - Research options
 * @param {Array<string>} [options.taskIds] - Task/subtask IDs for context
 * @param {Array<string>} [options.filePaths] - File paths for context
 * @param {string} [options.customContext] - Additional custom context
 * @param {boolean} [options.includeProjectTree] - Include project file tree
 * @param {string} [options.detailLevel] - Detail level: 'low', 'medium', 'high'
 * @param {string} [options.projectRoot] - Project root directory
 * @param {string} [options.tag] - Tag for the task
 * @param {boolean} [options.saveToFile] - Whether to save results to file (MCP mode)
 * @param {Object} [context] - Execution context
 * @param {Object} [context.session] - MCP session object
 * @param {Object} [context.mcpLog] - MCP logger object
 * @param {string} [context.commandName] - Command name for telemetry
 * @param {string} [context.outputType] - Output type ('cli' or 'mcp')
 * @param {string} [outputFormat] - Output format ('text' or 'json')
 * @param {boolean} [allowFollowUp] - Whether to allow follow-up questions (default: true)
 * @returns {Promise<Object>} Research results with telemetry data
 */
async function performResearch(
	query,
	options = {},
	context = {},
	outputFormat = 'text',
	allowFollowUp = true
) {
	const {
		taskIds = [],
		filePaths = [],
		customContext = '',
		includeProjectTree = false,
		detailLevel = 'medium',
		projectRoot: providedProjectRoot,
		tag,
		saveToFile = false
	} = options;

	const {
		session,
		mcpLog,
		commandName = 'research',
		outputType = 'cli'
	} = context;
	const isMCP = !!mcpLog;

	// Determine project root
	const projectRoot = providedProjectRoot || findProjectRoot();
	if (!projectRoot) {
		throw new Error('Could not determine project root directory');
	}

	// Create consistent logger
	const logFn = isMCP
		? mcpLog
		: {
				info: (...args) => consoleLog('info', ...args),
				warn: (...args) => consoleLog('warn', ...args),
				error: (...args) => consoleLog('error', ...args),
				debug: (...args) => consoleLog('debug', ...args),
				success: (...args) => consoleLog('success', ...args)
			};

	// Show UI banner for CLI mode
	if (outputFormat === 'text') {
		console.log(
			boxen(chalk.cyan.bold(`🔍 AI Research Query`), {
				padding: 1,
				borderColor: 'cyan',
				borderStyle: 'round',
				margin: { top: 1, bottom: 1 }
			})
		);
	}

	try {
		// Initialize context gatherer
		const contextGatherer = new ContextGatherer(projectRoot, tag);

		// Auto-discover relevant tasks using fuzzy search to supplement provided tasks
		let finalTaskIds = [...taskIds]; // Start with explicitly provided tasks
		let autoDiscoveredIds = [];

		try {
			const tasksPath = path.join(
				projectRoot,
				'.taskmaster',
				'tasks',
				'tasks.json'
			);
			const tasksData = await readJSON(tasksPath, projectRoot, tag);

			if (tasksData && tasksData.tasks && tasksData.tasks.length > 0) {
				// Flatten tasks to include subtasks for fuzzy search
				const flattenedTasks = flattenTasksWithSubtasks(tasksData.tasks);
				const fuzzySearch = new FuzzyTaskSearch(flattenedTasks, 'research');
				const searchResults = fuzzySearch.findRelevantTasks(query, {
					maxResults: 8,
					includeRecent: true,
					includeCategoryMatches: true
				});

				autoDiscoveredIds = fuzzySearch.getTaskIds(searchResults);

				// Remove any auto-discovered tasks that were already explicitly provided
				const uniqueAutoDiscovered = autoDiscoveredIds.filter(
					(id) => !finalTaskIds.includes(id)
				);

				// Add unique auto-discovered tasks to the final list
				finalTaskIds = [...finalTaskIds, ...uniqueAutoDiscovered];

				if (outputFormat === 'text' && finalTaskIds.length > 0) {
					// Sort task IDs numerically for better display
					const sortedTaskIds = finalTaskIds
						.map((id) => parseInt(id))
						.sort((a, b) => a - b)
						.map((id) => id.toString());

					// Show different messages based on whether tasks were explicitly provided
					if (taskIds.length > 0) {
						const sortedProvidedIds = taskIds
							.map((id) => parseInt(id))
							.sort((a, b) => a - b)
							.map((id) => id.toString());

						console.log(
							chalk.gray('Provided tasks: ') +
								chalk.cyan(sortedProvidedIds.join(', '))
						);

						if (uniqueAutoDiscovered.length > 0) {
							const sortedAutoIds = uniqueAutoDiscovered
								.map((id) => parseInt(id))
								.sort((a, b) => a - b)
								.map((id) => id.toString());

							console.log(
								chalk.gray('+ Auto-discovered related tasks: ') +
									chalk.cyan(sortedAutoIds.join(', '))
							);
						}
					} else {
						console.log(
							chalk.gray('Auto-discovered relevant tasks: ') +
								chalk.cyan(sortedTaskIds.join(', '))
						);
					}
				}
			}
		} catch (error) {
			// Silently continue without auto-discovered tasks if there's an error
			logFn.debug(`Could not auto-discover tasks: ${error.message}`);
		}

		const contextResult = await contextGatherer.gather({
			tasks: finalTaskIds,
			files: filePaths,
			customContext,
			includeProjectTree,
			format: 'research', // Use research format for AI consumption
			includeTokenCounts: true
		});

		const gatheredContext = contextResult.context;
		const tokenBreakdown = contextResult.tokenBreakdown;

		// Load prompts using PromptManager
		const promptManager = getPromptManager();

		const promptParams = {
			query: query,
			gatheredContext: gatheredContext || '',
			detailLevel: detailLevel,
			projectInfo: {
				root: projectRoot,
				taskCount: finalTaskIds.length,
				fileCount: filePaths.length
			}
		};

		// Load prompts - the research template handles detail level internally
		const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
			'research',
			promptParams
		);

		// Count tokens for system and user prompts
		const systemPromptTokens = contextGatherer.countTokens(systemPrompt);
		const userPromptTokens = contextGatherer.countTokens(userPrompt);
		const totalInputTokens = systemPromptTokens + userPromptTokens;

		if (outputFormat === 'text') {
			// Display detailed token breakdown in a clean box
			displayDetailedTokenBreakdown(
				tokenBreakdown,
				systemPromptTokens,
				userPromptTokens
			);
		}

		// Only log detailed info in debug mode or MCP
		if (outputFormat !== 'text') {
			logFn.info(
				`Calling AI service with research role, context size: ${tokenBreakdown.total} tokens (${gatheredContext.length} characters)`
			);
		}

		// Start loading indicator for CLI mode
		let loadingIndicator = null;
		if (outputFormat === 'text') {
			loadingIndicator = startLoadingIndicator('Researching with AI...\n');
		}

		let aiResult;
		try {
			// Call AI service with research role
			aiResult = await generateTextService({
				role: 'research', // Always use research role for research command
				session,
				projectRoot,
				systemPrompt,
				prompt: userPrompt,
				commandName,
				outputType
			});
		} catch (error) {
			if (loadingIndicator) {
				stopLoadingIndicator(loadingIndicator);
			}
			throw error;
		} finally {
			if (loadingIndicator) {
				stopLoadingIndicator(loadingIndicator);
			}
		}

		const researchResult = aiResult.mainResult;
		const telemetryData = aiResult.telemetryData;
		const tagInfo = aiResult.tagInfo;

		// Format and display results
		// Initialize interactive save tracking
		let interactiveSaveInfo = { interactiveSaveOccurred: false };

		if (outputFormat === 'text') {
			displayResearchResults(
				researchResult,
				query,
				detailLevel,
				tokenBreakdown
			);

			// Display AI usage telemetry for CLI users
			if (telemetryData) {
				displayAiUsageSummary(telemetryData, 'cli');
			}

			// Offer follow-up question option (only for initial CLI queries, not MCP)
			if (allowFollowUp && !isMCP) {
				interactiveSaveInfo = await handleFollowUpQuestions(
					options,
					context,
					outputFormat,
					projectRoot,
					logFn,
					query,
					researchResult
				);
			}
		}

		// Handle MCP save-to-file request
		if (saveToFile && isMCP) {
			const conversationHistory = [
				{
					question: query,
					answer: researchResult,
					type: 'initial',
					timestamp: new Date().toISOString()
				}
			];

			const savedFilePath = await handleSaveToFile(
				conversationHistory,
				projectRoot,
				context,
				logFn
			);

			// Add saved file path to return data
			return {
				query,
				result: researchResult,
				contextSize: gatheredContext.length,
				contextTokens: tokenBreakdown.total,
				tokenBreakdown,
				systemPromptTokens,
				userPromptTokens,
				totalInputTokens,
				detailLevel,
				telemetryData,
				tagInfo,
				savedFilePath,
				interactiveSaveOccurred: false // MCP save-to-file doesn't count as interactive save
			};
		}

		logFn.success('Research query completed successfully');

		return {
			query,
			result: researchResult,
			contextSize: gatheredContext.length,
			contextTokens: tokenBreakdown.total,
			tokenBreakdown,
			systemPromptTokens,
			userPromptTokens,
			totalInputTokens,
			detailLevel,
			telemetryData,
			tagInfo,
			interactiveSaveOccurred:
				interactiveSaveInfo?.interactiveSaveOccurred || false
		};
	} catch (error) {
		logFn.error(`Research query failed: ${error.message}`);

		if (outputFormat === 'text') {
			console.error(chalk.red(`\n❌ Research failed: ${error.message}`));
		}

		throw error;
	}
}

/**
 * Display detailed token breakdown for context and prompts
 * @param {Object} tokenBreakdown - Token breakdown from context gatherer
 * @param {number} systemPromptTokens - System prompt token count
 * @param {number} userPromptTokens - User prompt token count
 */
function displayDetailedTokenBreakdown(
	tokenBreakdown,
	systemPromptTokens,
	userPromptTokens
) {
	const parts = [];

	// Custom context
	if (tokenBreakdown.customContext) {
		parts.push(
			chalk.cyan('Custom: ') +
				chalk.yellow(tokenBreakdown.customContext.tokens.toLocaleString())
		);
	}

	// Tasks breakdown
	if (tokenBreakdown.tasks && tokenBreakdown.tasks.length > 0) {
		const totalTaskTokens = tokenBreakdown.tasks.reduce(
			(sum, task) => sum + task.tokens,
			0
		);
		const taskDetails = tokenBreakdown.tasks
			.map((task) => {
				const titleDisplay =
					task.title.length > 30
						? task.title.substring(0, 30) + '...'
						: task.title;
				return `  ${chalk.gray(task.id)} ${chalk.white(titleDisplay)} ${chalk.yellow(task.tokens.toLocaleString())} tokens`;
			})
			.join('\n');

		parts.push(
			chalk.cyan('Tasks: ') +
				chalk.yellow(totalTaskTokens.toLocaleString()) +
				chalk.gray(` (${tokenBreakdown.tasks.length} items)`) +
				'\n' +
				taskDetails
		);
	}

	// Files breakdown
	if (tokenBreakdown.files && tokenBreakdown.files.length > 0) {
		const totalFileTokens = tokenBreakdown.files.reduce(
			(sum, file) => sum + file.tokens,
			0
		);
		const fileDetails = tokenBreakdown.files
			.map((file) => {
				const pathDisplay =
					file.path.length > 40
						? '...' + file.path.substring(file.path.length - 37)
						: file.path;
				return `  ${chalk.gray(pathDisplay)} ${chalk.yellow(file.tokens.toLocaleString())} tokens ${chalk.gray(`(${file.sizeKB}KB)`)}`;
			})
			.join('\n');

		parts.push(
			chalk.cyan('Files: ') +
				chalk.yellow(totalFileTokens.toLocaleString()) +
				chalk.gray(` (${tokenBreakdown.files.length} files)`) +
				'\n' +
				fileDetails
		);
	}

	// Project tree
	if (tokenBreakdown.projectTree) {
		parts.push(
			chalk.cyan('Project Tree: ') +
				chalk.yellow(tokenBreakdown.projectTree.tokens.toLocaleString()) +
				chalk.gray(
					` (${tokenBreakdown.projectTree.fileCount} files, ${tokenBreakdown.projectTree.dirCount} dirs)`
				)
		);
	}

	// Prompts breakdown
	const totalPromptTokens = systemPromptTokens + userPromptTokens;
	const promptDetails = [
		`  ${chalk.gray('System:')} ${chalk.yellow(systemPromptTokens.toLocaleString())} tokens`,
		`  ${chalk.gray('User:')} ${chalk.yellow(userPromptTokens.toLocaleString())} tokens`
	].join('\n');

	parts.push(
		chalk.cyan('Prompts: ') +
			chalk.yellow(totalPromptTokens.toLocaleString()) +
			chalk.gray(' (generated)') +
			'\n' +
			promptDetails
	);

	// Display the breakdown in a clean box
	if (parts.length > 0) {
		const content = parts.join('\n\n');
		const tokenBox = boxen(content, {
			title: chalk.blue.bold('Context Analysis'),
			titleAlignment: 'left',
			padding: { top: 1, bottom: 1, left: 2, right: 2 },
			margin: { top: 0, bottom: 1 },
			borderStyle: 'single',
			borderColor: 'blue'
		});
		console.log(tokenBox);
	}
}

/**
 * Process research result text to highlight code blocks
 * @param {string} text - Raw research result text
 * @returns {string} Processed text with highlighted code blocks
 */
function processCodeBlocks(text) {
	// Regex to match code blocks with optional language specification
	const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;

	return text.replace(codeBlockRegex, (match, language, code) => {
		try {
			// Default to javascript if no language specified
			const lang = language || 'javascript';

			// Highlight the code using cli-highlight
			const highlightedCode = highlight(code.trim(), {
				language: lang,
				ignoreIllegals: true // Don't fail on unrecognized syntax
			});

			// Add a subtle border around code blocks
			const codeBox = boxen(highlightedCode, {
				padding: { top: 0, bottom: 0, left: 1, right: 1 },
				margin: { top: 0, bottom: 0 },
				borderStyle: 'single',
				borderColor: 'dim'
			});

			return '\n' + codeBox + '\n';
		} catch (error) {
			// If highlighting fails, return the original code block with basic formatting
			return (
				'\n' +
				chalk.gray('```' + (language || '')) +
				'\n' +
				chalk.white(code.trim()) +
				'\n' +
				chalk.gray('```') +
				'\n'
			);
		}
	});
}

/**
 * Display research results in formatted output
 * @param {string} result - AI research result
 * @param {string} query - Original query
 * @param {string} detailLevel - Detail level used
 * @param {Object} tokenBreakdown - Detailed token usage
 */
function displayResearchResults(result, query, detailLevel, tokenBreakdown) {
	// Header with query info
	const header = boxen(
		chalk.green.bold('Research Results') +
			'\n\n' +
			chalk.gray('Query: ') +
			chalk.white(query) +
			'\n' +
			chalk.gray('Detail Level: ') +
			chalk.cyan(detailLevel),
		{
			padding: { top: 1, bottom: 1, left: 2, right: 2 },
			margin: { top: 1, bottom: 0 },
			borderStyle: 'round',
			borderColor: 'green'
		}
	);
	console.log(header);

	// Process the result to highlight code blocks
	const processedResult = processCodeBlocks(result);

	// Main research content in a clean box
	const contentBox = boxen(processedResult, {
		padding: { top: 1, bottom: 1, left: 2, right: 2 },
		margin: { top: 0, bottom: 1 },
		borderStyle: 'single',
		borderColor: 'gray'
	});
	console.log(contentBox);

	// Success footer
	console.log(chalk.green('✅ Research completed'));
}

/**
 * Handle follow-up questions and save functionality in interactive mode
 * @param {Object} originalOptions - Original research options
 * @param {Object} context - Execution context
 * @param {string} outputFormat - Output format
 * @param {string} projectRoot - Project root directory
 * @param {Object} logFn - Logger function
 * @param {string} initialQuery - Initial query for context
 * @param {string} initialResult - Initial AI result for context
 */
async function handleFollowUpQuestions(
	originalOptions,
	context,
	outputFormat,
	projectRoot,
	logFn,
	initialQuery,
	initialResult
) {
	let interactiveSaveOccurred = false;

	try {
		// Import required modules for saving
		const { readJSON } = await import('../utils.js');
		const updateTaskById = (await import('./update-task-by-id.js')).default;
		const { updateSubtaskById } = await import('./update-subtask-by-id.js');

		// Initialize conversation history with the initial Q&A
		const conversationHistory = [
			{
				question: initialQuery,
				answer: initialResult,
				type: 'initial',
				timestamp: new Date().toISOString()
			}
		];

		while (true) {
			// Get user choice
			const { action } = await inquirer.prompt([
				{
					type: 'list',
					name: 'action',
					message: 'What would you like to do next?',
					choices: [
						{ name: 'Ask a follow-up question', value: 'followup' },
						{ name: 'Save to file', value: 'savefile' },
						{ name: 'Save to task/subtask', value: 'save' },
						{ name: 'Quit', value: 'quit' }
					],
					pageSize: 4
				}
			]);

			if (action === 'quit') {
				break;
			}

			if (action === 'savefile') {
				// Handle save to file functionality
				await handleSaveToFile(
					conversationHistory,
					projectRoot,
					context,
					logFn
				);
				continue;
			}

			if (action === 'save') {
				// Handle save functionality
				const saveResult = await handleSaveToTask(
					conversationHistory,
					projectRoot,
					context,
					logFn
				);
				if (saveResult) {
					interactiveSaveOccurred = true;
				}
				continue;
			}

			if (action === 'followup') {
				// Get the follow-up question
				const { followUpQuery } = await inquirer.prompt([
					{
						type: 'input',
						name: 'followUpQuery',
						message: 'Enter your follow-up question:',
						validate: (input) => {
							if (!input || input.trim().length === 0) {
								return 'Please enter a valid question.';
							}
							return true;
						}
					}
				]);

				if (!followUpQuery || followUpQuery.trim().length === 0) {
					continue;
				}

				console.log('\n' + chalk.gray('─'.repeat(60)) + '\n');

				// Build cumulative conversation context from all previous exchanges
				const conversationContext =
					buildConversationContext(conversationHistory);

				// Create enhanced options for follow-up with full conversation context
				const followUpOptions = {
					...originalOptions,
					taskIds: [], // Clear task IDs to allow fresh fuzzy search
					customContext:
						conversationContext +
						(originalOptions.customContext
							? `\n\n--- Original Context ---\n${originalOptions.customContext}`
							: '')
				};

				// Perform follow-up research
				const followUpResult = await performResearch(
					followUpQuery.trim(),
					followUpOptions,
					context,
					outputFormat,
					false // allowFollowUp = false for nested calls
				);

				// Add this exchange to the conversation history
				conversationHistory.push({
					question: followUpQuery.trim(),
					answer: followUpResult.result,
					type: 'followup',
					timestamp: new Date().toISOString()
				});
			}
		}
	} catch (error) {
		// If there's an error with inquirer (e.g., non-interactive terminal),
		// silently continue without follow-up functionality
		logFn.debug(`Follow-up questions not available: ${error.message}`);
	}

	return { interactiveSaveOccurred };
}

/**
 * Handle saving conversation to a task or subtask
 * @param {Array} conversationHistory - Array of conversation exchanges
 * @param {string} projectRoot - Project root directory
 * @param {Object} context - Execution context
 * @param {Object} logFn - Logger function
 */
async function handleSaveToTask(
	conversationHistory,
	projectRoot,
	context,
	logFn
) {
	try {
		// Import required modules
		const { readJSON } = await import('../utils.js');
		const updateTaskById = (await import('./update-task-by-id.js')).default;
		const { updateSubtaskById } = await import('./update-subtask-by-id.js');

		// Get task ID from user
		const { taskId } = await inquirer.prompt([
			{
				type: 'input',
				name: 'taskId',
				message: 'Enter task ID (e.g., "15" for task or "15.2" for subtask):',
				validate: (input) => {
					if (!input || input.trim().length === 0) {
						return 'Please enter a task ID.';
					}

					const trimmedInput = input.trim();
					// Validate format: number or number.number
					if (!/^\d+(\.\d+)?$/.test(trimmedInput)) {
						return 'Invalid format. Use "15" for task or "15.2" for subtask.';
					}

					return true;
				}
			}
		]);

		const trimmedTaskId = taskId.trim();

		// Format conversation thread for saving
		const conversationThread = formatConversationForSaving(conversationHistory);

		// Determine if it's a task or subtask
		const isSubtask = trimmedTaskId.includes('.');

		// Try to save - first validate the ID exists
		const tasksPath = path.join(
			projectRoot,
			'.taskmaster',
			'tasks',
			'tasks.json'
		);

		if (!fs.existsSync(tasksPath)) {
			console.log(
				chalk.red('❌ Tasks file not found. Please run task-master init first.')
			);
			return;
		}

		const data = readJSON(tasksPath, projectRoot, context.tag);
		if (!data || !data.tasks) {
			console.log(chalk.red('❌ No valid tasks found.'));
			return;
		}

		if (isSubtask) {
			// Validate subtask exists
			const [parentId, subtaskId] = trimmedTaskId
				.split('.')
				.map((id) => parseInt(id, 10));
			const parentTask = data.tasks.find((t) => t.id === parentId);

			if (!parentTask) {
				console.log(chalk.red(`❌ Parent task ${parentId} not found.`));
				return;
			}

			if (
				!parentTask.subtasks ||
				!parentTask.subtasks.find((st) => st.id === subtaskId)
			) {
				console.log(chalk.red(`❌ Subtask ${trimmedTaskId} not found.`));
				return;
			}

			// Save to subtask using updateSubtaskById
			console.log(chalk.blue('💾 Saving research conversation to subtask...'));

			await updateSubtaskById(
				tasksPath,
				trimmedTaskId,
				conversationThread,
				false, // useResearch = false for simple append
				context,
				'text'
			);

			console.log(
				chalk.green(
					`✅ Research conversation saved to subtask ${trimmedTaskId}`
				)
			);
		} else {
			// Validate task exists
			const taskIdNum = parseInt(trimmedTaskId, 10);
			const task = data.tasks.find((t) => t.id === taskIdNum);

			if (!task) {
				console.log(chalk.red(`❌ Task ${trimmedTaskId} not found.`));
				return;
			}

			// Save to task using updateTaskById with append mode
			console.log(chalk.blue('💾 Saving research conversation to task...'));

			await updateTaskById(
				tasksPath,
				taskIdNum,
				conversationThread,
				false, // useResearch = false for simple append
				context,
				'text',
				true // appendMode = true
			);

			console.log(
				chalk.green(`✅ Research conversation saved to task ${trimmedTaskId}`)
			);
		}

		return true; // Indicate successful save
	} catch (error) {
		console.log(chalk.red(`❌ Error saving conversation: ${error.message}`));
		logFn.error(`Error saving conversation: ${error.message}`);
		return false; // Indicate failed save
	}
}

/**
 * Handle saving conversation to a file in .taskmaster/docs/research/
 * @param {Array} conversationHistory - Array of conversation exchanges
 * @param {string} projectRoot - Project root directory
 * @param {Object} context - Execution context
 * @param {Object} logFn - Logger function
 * @returns {Promise<string>} Path to saved file
 */
async function handleSaveToFile(
	conversationHistory,
	projectRoot,
	context,
	logFn
) {
	try {
		// Create research directory if it doesn't exist
		const researchDir = path.join(
			projectRoot,
			'.taskmaster',
			'docs',
			'research'
		);
		if (!fs.existsSync(researchDir)) {
			fs.mkdirSync(researchDir, { recursive: true });
		}

		// Generate filename from first query and timestamp
		const firstQuery = conversationHistory[0]?.question || 'research-query';
		const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format

		// Create a slug from the query (remove special chars, limit length)
		const querySlug = firstQuery
			.toLowerCase()
			.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
			.replace(/\s+/g, '-') // Replace spaces with hyphens
			.replace(/-+/g, '-') // Replace multiple hyphens with single
			.substring(0, 50) // Limit length
			.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens

		const filename = `${timestamp}_${querySlug}.md`;
		const filePath = path.join(researchDir, filename);

		// Format conversation for file
		const fileContent = formatConversationForFile(
			conversationHistory,
			firstQuery
		);

		// Write file
		fs.writeFileSync(filePath, fileContent, 'utf8');

		const relativePath = path.relative(projectRoot, filePath);
		console.log(
			chalk.green(`✅ Research saved to: ${chalk.cyan(relativePath)}`)
		);

		logFn.success(`Research conversation saved to ${relativePath}`);

		return filePath;
	} catch (error) {
		console.log(chalk.red(`❌ Error saving research file: ${error.message}`));
		logFn.error(`Error saving research file: ${error.message}`);
		throw error;
	}
}

/**
 * Format conversation history for saving to a file
 * @param {Array} conversationHistory - Array of conversation exchanges
 * @param {string} initialQuery - The initial query for metadata
 * @returns {string} Formatted file content
 */
function formatConversationForFile(conversationHistory, initialQuery) {
	const timestamp = new Date().toISOString();
	const date = new Date().toLocaleDateString();
	const time = new Date().toLocaleTimeString();

	// Create metadata header
	let content = `---
title: Research Session
query: "${initialQuery}"
date: ${date}
time: ${time}
timestamp: ${timestamp}
exchanges: ${conversationHistory.length}
---

# Research Session

`;

	// Add each conversation exchange
	conversationHistory.forEach((exchange, index) => {
		if (exchange.type === 'initial') {
			content += `## Initial Query\n\n**Question:** ${exchange.question}\n\n**Response:**\n\n${exchange.answer}\n\n`;
		} else {
			content += `## Follow-up ${index}\n\n**Question:** ${exchange.question}\n\n**Response:**\n\n${exchange.answer}\n\n`;
		}

		if (index < conversationHistory.length - 1) {
			content += '---\n\n';
		}
	});

	// Add footer
	content += `\n---\n\n*Generated by Task Master Research Command*  \n*Timestamp: ${timestamp}*\n`;

	return content;
}

/**
 * Format conversation history for saving to a task/subtask
 * @param {Array} conversationHistory - Array of conversation exchanges
 * @returns {string} Formatted conversation thread
 */
function formatConversationForSaving(conversationHistory) {
	const timestamp = new Date().toISOString();
	let formatted = `## Research Session - ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}\n\n`;

	conversationHistory.forEach((exchange, index) => {
		if (exchange.type === 'initial') {
			formatted += `**Initial Query:** ${exchange.question}\n\n`;
			formatted += `**Response:** ${exchange.answer}\n\n`;
		} else {
			formatted += `**Follow-up ${index}:** ${exchange.question}\n\n`;
			formatted += `**Response:** ${exchange.answer}\n\n`;
		}

		if (index < conversationHistory.length - 1) {
			formatted += '---\n\n';
		}
	});

	return formatted;
}

/**
 * Build conversation context string from conversation history
 * @param {Array} conversationHistory - Array of conversation exchanges
 * @returns {string} Formatted conversation context
 */
function buildConversationContext(conversationHistory) {
	if (conversationHistory.length === 0) {
		return '';
	}

	const contextParts = ['--- Conversation History ---'];

	conversationHistory.forEach((exchange, index) => {
		const questionLabel =
			exchange.type === 'initial' ? 'Initial Question' : `Follow-up ${index}`;
		const answerLabel =
			exchange.type === 'initial' ? 'Initial Answer' : `Answer ${index}`;

		contextParts.push(`\n${questionLabel}: ${exchange.question}`);
		contextParts.push(`${answerLabel}: ${exchange.answer}`);
	});

	return contextParts.join('\n');
}

export { performResearch };

/**
 * ui.js
 * User interface functions for the Task Master CLI
 */

import chalk from 'chalk';
import figlet from 'figlet';
import boxen from 'boxen';
import ora from 'ora';
import Table from 'cli-table3';
import gradient from 'gradient-string';
import readline from 'readline';
import {
	log,
	findTaskById,
	readJSON,
	truncate,
	isSilentMode
} from './utils.js';
import fs from 'fs';
import {
	findNextTask,
	analyzeTaskComplexity,
	readComplexityReport
} from './task-manager.js';
import { getProjectName, getDefaultSubtasks } from './config-manager.js';
import { TASK_STATUS_OPTIONS } from '../../src/constants/task-status.js';
import {
	TASKMASTER_CONFIG_FILE,
	TASKMASTER_TASKS_FILE
} from '../../src/constants/paths.js';
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';

// Create a color gradient for the banner
const coolGradient = gradient(['#00b4d8', '#0077b6', '#03045e']);
const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']);

/**
 * Display FYI notice about tagged task lists (only if migration occurred)
 * @param {Object} data - Data object that may contain _migrationHappened flag
 */
function displayTaggedTasksFYI(data) {
	if (isSilentMode() || !data || !data._migrationHappened) return;

	console.log(
		boxen(
			chalk.white.bold('FYI: ') +
				chalk.gray('Taskmaster now supports separate task lists per tag. ') +
				chalk.cyan(
					'Use the --tag flag to create/read/update/filter tasks by tag.'
				),
			{
				padding: { top: 0, bottom: 0, left: 2, right: 2 },
				borderColor: 'cyan',
				borderStyle: 'round',
				margin: { top: 1, bottom: 1 }
			}
		)
	);
}

/**
 * Display a small, non-intrusive indicator showing the current tag context
 * @param {string} tagName - The tag name to display
 * @param {Object} options - Display options
 * @param {boolean} [options.skipIfMaster=false] - Don't show indicator if tag is 'master'
 * @param {boolean} [options.dim=false] - Use dimmed styling
 */
function displayCurrentTagIndicator(tag, options = {}) {
	if (isSilentMode()) return;

	const { skipIfMaster = false, dim = false } = options;

	// Skip display for master tag only if explicitly requested
	if (skipIfMaster && tag === 'master') return;

	// Create a small, tasteful tag indicator
	const tagIcon = '🏷';
	const tagText = dim
		? chalk.gray(`${tagIcon} tag: ${tag}`)
		: chalk.dim(`${tagIcon} tag: `) + chalk.cyan(tag);

	console.log(tagText);
}

/**
 * Display a fancy banner for the CLI
 */
function displayBanner() {
	if (isSilentMode()) return;

	// console.clear(); // Removing this to avoid clearing the terminal per command
	const bannerText = figlet.textSync('Task Master', {
		font: 'Standard',
		horizontalLayout: 'default',
		verticalLayout: 'default'
	});

	console.log(coolGradient(bannerText));

	// Add creator credit line below the banner
	console.log(
		chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano')
	);

	// Read version directly from package.json
	const version = getTaskMasterVersion();

	console.log(
		boxen(
			chalk.white(
				`${chalk.bold('Version:')} ${version}   ${chalk.bold('Project:')} ${getProjectName(null)}`
			),
			{
				padding: 1,
				margin: { top: 0, bottom: 1 },
				borderStyle: 'round',
				borderColor: 'cyan'
			}
		)
	);
}

/**
 * Start a loading indicator with an animated spinner
 * @param {string} message - Message to display next to the spinner
 * @returns {Object} Spinner object
 */
function startLoadingIndicator(message) {
	if (isSilentMode()) return null;

	const spinner = ora({
		text: message,
		color: 'cyan'
	}).start();

	return spinner;
}

/**
 * Stop a loading indicator (basic stop, no success/fail indicator)
 * @param {Object} spinner - Spinner object to stop
 */
function stopLoadingIndicator(spinner) {
	if (spinner && typeof spinner.stop === 'function') {
		spinner.stop();
	}
}

/**
 * Complete a loading indicator with success (shows checkmark)
 * @param {Object} spinner - Spinner object to complete
 * @param {string} message - Optional success message (defaults to current text)
 */
function succeedLoadingIndicator(spinner, message = null) {
	if (spinner && typeof spinner.succeed === 'function') {
		if (message) {
			spinner.succeed(message);
		} else {
			spinner.succeed();
		}
	}
}

/**
 * Complete a loading indicator with failure (shows X)
 * @param {Object} spinner - Spinner object to fail
 * @param {string} message - Optional failure message (defaults to current text)
 */
function failLoadingIndicator(spinner, message = null) {
	if (spinner && typeof spinner.fail === 'function') {
		if (message) {
			spinner.fail(message);
		} else {
			spinner.fail();
		}
	}
}

/**
 * Complete a loading indicator with warning (shows warning symbol)
 * @param {Object} spinner - Spinner object to warn
 * @param {string} message - Optional warning message (defaults to current text)
 */
function warnLoadingIndicator(spinner, message = null) {
	if (spinner && typeof spinner.warn === 'function') {
		if (message) {
			spinner.warn(message);
		} else {
			spinner.warn();
		}
	}
}

/**
 * Complete a loading indicator with info (shows info symbol)
 * @param {Object} spinner - Spinner object to complete with info
 * @param {string} message - Optional info message (defaults to current text)
 */
function infoLoadingIndicator(spinner, message = null) {
	if (spinner && typeof spinner.info === 'function') {
		if (message) {
			spinner.info(message);
		} else {
			spinner.info();
		}
	}
}

/**
 * Create a colored progress bar
 * @param {number} percent - The completion percentage
 * @param {number} length - The total length of the progress bar in characters
 * @param {Object} statusBreakdown - Optional breakdown of non-complete statuses (e.g., {pending: 20, 'in-progress': 10})
 * @returns {string} The formatted progress bar
 */
function createProgressBar(percent, length = 30, statusBreakdown = null) {
	// Adjust the percent to treat deferred and cancelled as complete
	const effectivePercent = statusBreakdown
		? Math.min(
				100,
				percent +
					(statusBreakdown.deferred || 0) +
					(statusBreakdown.cancelled || 0)
			)
		: percent;

	// Calculate how many characters to fill for "true completion"
	const trueCompletedFilled = Math.round((percent * length) / 100);

	// Calculate how many characters to fill for "effective completion" (including deferred/cancelled)
	const effectiveCompletedFilled = Math.round(
		(effectivePercent * length) / 100
	);

	// The "deferred/cancelled" section (difference between true and effective)
	const deferredCancelledFilled =
		effectiveCompletedFilled - trueCompletedFilled;

	// Set the empty section (remaining after effective completion)
	const empty = length - effectiveCompletedFilled;

	// Determine color based on percentage for the completed section
	let completedColor;
	if (percent < 25) {
		completedColor = chalk.red;
	} else if (percent < 50) {
		completedColor = chalk.hex('#FFA500'); // Orange
	} else if (percent < 75) {
		completedColor = chalk.yellow;
	} else if (percent < 100) {
		completedColor = chalk.green;
	} else {
		completedColor = chalk.hex('#006400'); // Dark green
	}

	// Create colored sections
	const completedSection = completedColor('█'.repeat(trueCompletedFilled));

	// Gray section for deferred/cancelled items
	const deferredCancelledSection = chalk.gray(
		'█'.repeat(deferredCancelledFilled)
	);

	// If we have a status breakdown, create a multi-colored remaining section
	let remainingSection = '';

	if (statusBreakdown && empty > 0) {
		// Status colors (matching the statusConfig colors in getStatusWithColor)
		const statusColors = {
			pending: chalk.yellow,
			'in-progress': chalk.hex('#FFA500'), // Orange
			blocked: chalk.red,
			review: chalk.magenta
			// Deferred and cancelled are treated as part of the completed section
		};

		// Calculate proportions for each status
		const totalRemaining = Object.entries(statusBreakdown)
			.filter(
				([status]) =>
					!['deferred', 'cancelled', 'done', 'completed'].includes(status)
			)
			.reduce((sum, [_, val]) => sum + val, 0);

		// If no remaining tasks with tracked statuses, just use gray
		if (totalRemaining <= 0) {
			remainingSection = chalk.gray('░'.repeat(empty));
		} else {
			// Track how many characters we've added
			let addedChars = 0;

			// Add each status section proportionally
			for (const [status, percentage] of Object.entries(statusBreakdown)) {
				// Skip statuses that are considered complete
				if (['deferred', 'cancelled', 'done', 'completed'].includes(status))
					continue;

				// Calculate how many characters this status should fill
				const statusChars = Math.round((percentage / totalRemaining) * empty);

				// Make sure we don't exceed the total length due to rounding
				const actualChars = Math.min(statusChars, empty - addedChars);

				// Add colored section for this status
				const colorFn = statusColors[status] || chalk.gray;
				remainingSection += colorFn('░'.repeat(actualChars));

				addedChars += actualChars;
			}

			// If we have any remaining space due to rounding, fill with gray
			if (addedChars < empty) {
				remainingSection += chalk.gray('░'.repeat(empty - addedChars));
			}
		}
	} else {
		// Default to gray for the empty section if no breakdown provided
		remainingSection = chalk.gray('░'.repeat(empty));
	}

	// Effective percentage text color should reflect the highest category
	const percentTextColor =
		percent === 100
			? chalk.hex('#006400') // Dark green for 100%
			: effectivePercent === 100
				? chalk.gray // Gray for 100% with deferred/cancelled
				: completedColor; // Otherwise match the completed color

	// Build the complete progress bar
	return `${completedSection}${deferredCancelledSection}${remainingSection} ${percentTextColor(`${effectivePercent.toFixed(0)}%`)}`;
}

/**
 * Get a colored status string based on the status value
 * @param {string} status - Task status (e.g., "done", "pending", "in-progress")
 * @param {boolean} forTable - Whether the status is being displayed in a table
 * @returns {string} Colored status string
 */
function getStatusWithColor(status, forTable = false) {
	if (!status) {
		return chalk.gray('❓ unknown');
	}

	const statusConfig = {
		done: { color: chalk.green, icon: '✓', tableIcon: '✓' },
		completed: { color: chalk.green, icon: '✓', tableIcon: '✓' },
		pending: { color: chalk.yellow, icon: '○', tableIcon: '⏱' },
		'in-progress': { color: chalk.hex('#FFA500'), icon: '🔄', tableIcon: '►' },
		deferred: { color: chalk.gray, icon: 'x', tableIcon: '⏱' },
		blocked: { color: chalk.red, icon: '!', tableIcon: '✗' },
		review: { color: chalk.magenta, icon: '?', tableIcon: '?' },
		cancelled: { color: chalk.gray, icon: '❌', tableIcon: 'x' }
	};

	const config = statusConfig[status.toLowerCase()] || {
		color: chalk.red,
		icon: '❌',
		tableIcon: '✗'
	};

	// Use simpler icons for table display to prevent border issues
	if (forTable) {
		// Use ASCII characters instead of Unicode for completely stable display
		const simpleIcons = {
			done: '✓',
			completed: '✓',
			pending: '○',
			'in-progress': '►',
			deferred: 'x',
			blocked: '!', // Using plain x character for better compatibility
			review: '?' // Using circled dot symbol
		};
		const simpleIcon = simpleIcons[status.toLowerCase()] || 'x';
		return config.color(`${simpleIcon} ${status}`);
	}

	return config.color(`${config.icon} ${status}`);
}

/**
 * Format dependencies list with status indicators
 * @param {Array} dependencies - Array of dependency IDs
 * @param {Array} allTasks - Array of all tasks
 * @param {boolean} forConsole - Whether the output is for console display
 * @param {Object|null} complexityReport - Optional pre-loaded complexity report
 * @returns {string} Formatted dependencies string
 */
function formatDependenciesWithStatus(
	dependencies,
	allTasks,
	forConsole = false,
	complexityReport = null // Add complexityReport parameter
) {
	if (
		!dependencies ||
		!Array.isArray(dependencies) ||
		dependencies.length === 0
	) {
		return forConsole ? chalk.gray('None') : 'None';
	}

	const formattedDeps = dependencies.map((depId) => {
		const depIdStr = depId.toString(); // Ensure string format for display

		// Check if it's already a fully qualified subtask ID (like "22.1")
		if (depIdStr.includes('.')) {
			const [parentId, subtaskId] = depIdStr
				.split('.')
				.map((id) => parseInt(id, 10));

			// Find the parent task
			const parentTask = allTasks.find((t) => t.id === parentId);
			if (!parentTask || !parentTask.subtasks) {
				return forConsole
					? chalk.red(`${depIdStr} (Not found)`)
					: `${depIdStr} (Not found)`;
			}

			// Find the subtask
			const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
			if (!subtask) {
				return forConsole
					? chalk.red(`${depIdStr} (Not found)`)
					: `${depIdStr} (Not found)`;
			}

			// Format with status
			const status = subtask.status || 'pending';
			const isDone =
				status.toLowerCase() === 'done' || status.toLowerCase() === 'completed';
			const isInProgress = status.toLowerCase() === 'in-progress';

			if (forConsole) {
				if (isDone) {
					return chalk.green.bold(depIdStr);
				} else if (isInProgress) {
					return chalk.hex('#FFA500').bold(depIdStr);
				} else {
					return chalk.red.bold(depIdStr);
				}
			}

			// For plain text output (task files), return just the ID without any formatting or emoji
			return depIdStr;
		}

		// If depId is a number less than 100, it's likely a reference to a subtask ID in the current task
		// This case is typically handled elsewhere (in task-specific code) before calling this function

		// For regular task dependencies (not subtasks)
		// Convert string depId to number if needed
		const numericDepId =
			typeof depId === 'string' ? parseInt(depId, 10) : depId;

		// Look up the task using the numeric ID
		const depTaskResult = findTaskById(
			allTasks,
			numericDepId,
			complexityReport
		);
		const depTask = depTaskResult.task; // Access the task object from the result

		if (!depTask) {
			return forConsole
				? chalk.red(`${depIdStr} (Not found)`)
				: `${depIdStr} (Not found)`;
		}

		// Format with status
		const status = depTask.status || 'pending';
		const isDone =
			status.toLowerCase() === 'done' || status.toLowerCase() === 'completed';
		const isInProgress = status.toLowerCase() === 'in-progress';

		if (forConsole) {
			if (isDone) {
				return chalk.green.bold(depIdStr);
			} else if (isInProgress) {
				return chalk.yellow.bold(depIdStr);
			} else {
				return chalk.red.bold(depIdStr);
			}
		}

		// For plain text output (task files), return just the ID without any formatting or emoji
		return depIdStr;
	});

	return formattedDeps.join(', ');
}

/**
 * Display a comprehensive help guide
 */
function displayHelp() {
	// Get terminal width - moved to top of function to make it available throughout
	const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect

	console.log(
		boxen(chalk.white.bold('Task Master CLI'), {
			padding: 1,
			borderColor: 'blue',
			borderStyle: 'round',
			margin: { top: 1, bottom: 1 }
		})
	);

	// Command categories
	const commandCategories = [
		{
			title: 'Project Setup & Configuration',
			color: 'blue',
			commands: [
				{
					name: 'init',
					args: '[--name=<name>] [--description=<desc>] [-y]',
					desc: 'Initialize a new project with Task Master structure'
				},
				{
					name: 'models',
					args: '',
					desc: 'View current AI model configuration and available models'
				},
				{
					name: 'models --setup',
					args: '',
					desc: 'Run interactive setup to configure AI models'
				},
				{
					name: 'models --set-main',
					args: '<model_id>',
					desc: 'Set the primary model for task generation'
				},
				{
					name: 'models --set-research',
					args: '<model_id>',
					desc: 'Set the model for research operations'
				},
				{
					name: 'models --set-fallback',
					args: '<model_id>',
					desc: 'Set the fallback model (optional)'
				}
			]
		},
		{
			title: 'Task Generation',
			color: 'cyan',
			commands: [
				{
					name: 'parse-prd',
					args: '--input=<file.txt> [--num-tasks=10]',
					desc: 'Generate tasks from a PRD document'
				},
				{
					name: 'generate',
					args: '',
					desc: 'Create individual task files from tasks.json'
				}
			]
		},
		{
			title: 'Task Management',
			color: 'green',
			commands: [
				{
					name: 'list',
					args: '[--status=<status>] [--with-subtasks]',
					desc: 'List all tasks with their status'
				},
				{
					name: 'set-status',
					args: '--id=<id> --status=<status>',
					desc: `Update task status (${TASK_STATUS_OPTIONS.join(', ')})`
				},
				{
					name: 'sync-readme',
					args: '[--with-subtasks] [--status=<status>]',
					desc: 'Export tasks to README.md with professional formatting'
				},
				{
					name: 'update',
					args: '--from=<id> --prompt="<context>"',
					desc: 'Update multiple tasks based on new requirements'
				},
				{
					name: 'update-task',
					args: '--id=<id> --prompt="<context>"',
					desc: 'Update a single specific task with new information'
				},
				{
					name: 'update-subtask',
					args: '--id=<parentId.subtaskId> --prompt="<context>"',
					desc: 'Append additional information to a subtask'
				},
				{
					name: 'add-task',
					args: '--prompt="<text>" [--dependencies=<ids>] [--priority=<priority>]',
					desc: 'Add a new task using AI'
				},
				{
					name: 'remove-task',
					args: '--id=<id> [-y]',
					desc: 'Permanently remove a task or subtask'
				}
			]
		},
		{
			title: 'Subtask Management',
			color: 'yellow',
			commands: [
				{
					name: 'add-subtask',
					args: '--parent=<id> --title="<title>" [--description="<desc>"]',
					desc: 'Add a new subtask to a parent task'
				},
				{
					name: 'add-subtask',
					args: '--parent=<id> --task-id=<id>',
					desc: 'Convert an existing task into a subtask'
				},
				{
					name: 'remove-subtask',
					args: '--id=<parentId.subtaskId> [--convert]',
					desc: 'Remove a subtask (optionally convert to standalone task)'
				},
				{
					name: 'clear-subtasks',
					args: '--id=<id>',
					desc: 'Remove all subtasks from specified tasks'
				},
				{
					name: 'clear-subtasks --all',
					args: '',
					desc: 'Remove subtasks from all tasks'
				}
			]
		},
		{
			title: 'Task Analysis & Breakdown',
			color: 'magenta',
			commands: [
				{
					name: 'analyze-complexity',
					args: '[--research] [--threshold=5]',
					desc: 'Analyze tasks and generate expansion recommendations'
				},
				{
					name: 'complexity-report',
					args: '[--file=<path>]',
					desc: 'Display the complexity analysis report'
				},
				{
					name: 'expand',
					args: '--id=<id> [--num=5] [--research] [--prompt="<context>"]',
					desc: 'Break down tasks into detailed subtasks'
				},
				{
					name: 'expand --all',
					args: '[--force] [--research]',
					desc: 'Expand all pending tasks with subtasks'
				},
				{
					name: 'research',
					args: '"<prompt>" [-i=<task_ids>] [-f=<file_paths>] [-c="<context>"] [--tree] [-s=<save_file>] [-d=<detail_level>]',
					desc: 'Perform AI-powered research queries with project context'
				}
			]
		},
		{
			title: 'Task Navigation & Viewing',
			color: 'cyan',
			commands: [
				{
					name: 'next',
					args: '',
					desc: 'Show the next task to work on based on dependencies'
				},
				{
					name: 'show',
					args: '<id>',
					desc: 'Display detailed information about a specific task'
				}
			]
		},
		{
			title: 'Tag Management',
			color: 'magenta',
			commands: [
				{
					name: 'tags',
					args: '[--show-metadata]',
					desc: 'List all available tags with task counts'
				},
				{
					name: 'add-tag',
					args: '<tagName> [--copy-from-current] [--copy-from=<tag>] [-d="<desc>"]',
					desc: 'Create a new tag context for organizing tasks'
				},
				{
					name: 'use-tag',
					args: '<tagName>',
					desc: 'Switch to a different tag context'
				},
				{
					name: 'delete-tag',
					args: '<tagName> [--yes]',
					desc: 'Delete an existing tag and all its tasks'
				},
				{
					name: 'rename-tag',
					args: '<oldName> <newName>',
					desc: 'Rename an existing tag'
				},
				{
					name: 'copy-tag',
					args: '<sourceName> <targetName> [-d="<desc>"]',
					desc: 'Copy an existing tag to create a new tag with the same tasks'
				}
			]
		},
		{
			title: 'Dependency Management',
			color: 'blue',
			commands: [
				{
					name: 'add-dependency',
					args: '--id=<id> --depends-on=<id>',
					desc: 'Add a dependency to a task'
				},
				{
					name: 'remove-dependency',
					args: '--id=<id> --depends-on=<id>',
					desc: 'Remove a dependency from a task'
				},
				{
					name: 'validate-dependencies',
					args: '',
					desc: 'Identify invalid dependencies without fixing them'
				},
				{
					name: 'fix-dependencies',
					args: '',
					desc: 'Fix invalid dependencies automatically'
				}
			]
		}
	];

	// Display each category
	commandCategories.forEach((category) => {
		console.log(
			boxen(chalk[category.color].bold(category.title), {
				padding: { left: 2, right: 2, top: 0, bottom: 0 },
				margin: { top: 1, bottom: 0 },
				borderColor: category.color,
				borderStyle: 'round'
			})
		);

		// Calculate dynamic column widths - adjust ratios as needed
		const nameWidth = Math.max(25, Math.floor(terminalWidth * 0.2)); // 20% of width but min 25
		const argsWidth = Math.max(40, Math.floor(terminalWidth * 0.35)); // 35% of width but min 40
		const descWidth = Math.max(45, Math.floor(terminalWidth * 0.45) - 10); // 45% of width but min 45, minus some buffer

		const commandTable = new Table({
			colWidths: [nameWidth, argsWidth, descWidth],
			chars: {
				top: '',
				'top-mid': '',
				'top-left': '',
				'top-right': '',
				bottom: '',
				'bottom-mid': '',
				'bottom-left': '',
				'bottom-right': '',
				left: '',
				'left-mid': '',
				mid: '',
				'mid-mid': '',
				right: '',
				'right-mid': '',
				middle: ' '
			},
			style: { border: [], 'padding-left': 4 },
			wordWrap: true
		});

		category.commands.forEach((cmd, index) => {
			commandTable.push([
				`${chalk.yellow.bold(cmd.name)}${chalk.reset('')}`,
				`${chalk.white(cmd.args)}${chalk.reset('')}`,
				`${chalk.dim(cmd.desc)}${chalk.reset('')}`
			]);
		});

		console.log(commandTable.toString());
		console.log('');
	});

	// Display configuration section
	console.log(
		boxen(chalk.cyan.bold('Configuration'), {
			padding: { left: 2, right: 2, top: 0, bottom: 0 },
			margin: { top: 1, bottom: 0 },
			borderColor: 'cyan',
			borderStyle: 'round'
		})
	);

	// Get terminal width if not already defined
	const configTerminalWidth = terminalWidth || process.stdout.columns || 100;

	// Calculate dynamic column widths for config table
	const configKeyWidth = Math.max(30, Math.floor(configTerminalWidth * 0.25));
	const configDescWidth = Math.max(50, Math.floor(configTerminalWidth * 0.45));
	const configValueWidth = Math.max(
		30,
		Math.floor(configTerminalWidth * 0.3) - 10
	);

	const configTable = new Table({
		colWidths: [configKeyWidth, configDescWidth, configValueWidth],
		chars: {
			top: '',
			'top-mid': '',
			'top-left': '',
			'top-right': '',
			bottom: '',
			'bottom-mid': '',
			'bottom-left': '',
			'bottom-right': '',
			left: '',
			'left-mid': '',
			mid: '',
			'mid-mid': '',
			right: '',
			'right-mid': '',
			middle: ' '
		},
		style: { border: [], 'padding-left': 4 },
		wordWrap: true
	});

	configTable.push(
		[
			`${chalk.yellow(TASKMASTER_CONFIG_FILE)}${chalk.reset('')}`,
			`${chalk.white('AI model configuration file (project root)')}${chalk.reset('')}`,
			`${chalk.dim('Managed by models cmd')}${chalk.reset('')}`
		],
		[
			`${chalk.yellow('API Keys (.env)')}${chalk.reset('')}`,
			`${chalk.white('API keys for AI providers (ANTHROPIC_API_KEY, etc.)')}${chalk.reset('')}`,
			`${chalk.dim('Required in .env file')}${chalk.reset('')}`
		],
		[
			`${chalk.yellow('MCP Keys (mcp.json)')}${chalk.reset('')}`,
			`${chalk.white('API keys for Cursor integration')}${chalk.reset('')}`,
			`${chalk.dim('Required in .cursor/')}${chalk.reset('')}`
		]
	);

	console.log(configTable.toString());
	console.log('');

	// Show helpful hints
	console.log(
		boxen(
			chalk.white.bold('Quick Start:') +
				'\n\n' +
				chalk.cyan('1. Create Project: ') +
				chalk.white('task-master init') +
				'\n' +
				chalk.cyan('2. Setup Models: ') +
				chalk.white('task-master models --setup') +
				'\n' +
				chalk.cyan('3. Parse PRD: ') +
				chalk.white('task-master parse-prd --input=<prd-file>') +
				'\n' +
				chalk.cyan('4. List Tasks: ') +
				chalk.white('task-master list') +
				'\n' +
				chalk.cyan('5. Find Next Task: ') +
				chalk.white('task-master next'),
			{
				padding: 1,
				borderColor: 'yellow',
				borderStyle: 'round',
				margin: { top: 1 },
				width: Math.min(configTerminalWidth - 10, 100) // Limit width to terminal width minus padding, max 100
			}
		)
	);
}

/**
 * Get colored complexity score
 * @param {number} score - Complexity score (1-10)
 * @returns {string} Colored complexity score
 */
function getComplexityWithColor(score) {
	if (score <= 3) return chalk.green(`● ${score}`);
	if (score <= 6) return chalk.yellow(`● ${score}`);
	return chalk.red(`● ${score}`);
}

/**
 * Truncate a string to a maximum length and add ellipsis if needed
 * @param {string} str - The string to truncate
 * @param {number} maxLength - Maximum length
 * @returns {string} Truncated string
 */
function truncateString(str, maxLength) {
	if (!str) return '';
	if (str.length <= maxLength) return str;
	return str.substring(0, maxLength - 3) + '...';
}

/**
 * Display the next task to work on
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} complexityReportPath - Path to the complexity report file
 * @param {string} tag - Optional tag to override current tag resolution
 */
async function displayNextTask(
	tasksPath,
	complexityReportPath = null,
	context = {}
) {
	// Extract parameters from context
	const { projectRoot, tag } = context;

	// Read the tasks file with proper projectRoot for tag resolution
	const data = readJSON(tasksPath, projectRoot, tag);
	if (!data || !data.tasks) {
		log('error', 'No valid tasks found.');
		process.exit(1);
	}

	// Read complexity report once
	const complexityReport = readComplexityReport(complexityReportPath);

	// Find the next task
	const nextTask = findNextTask(data.tasks, complexityReport);

	if (!nextTask) {
		console.log(
			boxen(
				chalk.yellow('No eligible tasks found!\n\n') +
					'All pending tasks have unsatisfied dependencies, or all tasks are completed.',
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					borderColor: 'yellow',
					borderStyle: 'round',
					margin: { top: 1 }
				}
			)
		);
		return;
	}

	// Display the task in a nice format
	console.log(
		boxen(chalk.white.bold(`Next Task: #${nextTask.id} - ${nextTask.title}`), {
			padding: { top: 0, bottom: 0, left: 1, right: 1 },
			borderColor: 'blue',
			borderStyle: 'round',
			margin: { top: 1, bottom: 0 }
		})
	);

	// Create a table with task details
	const taskTable = new Table({
		style: {
			head: [],
			border: [],
			'padding-top': 0,
			'padding-bottom': 0,
			compact: true
		},
		chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
		colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)],
		wordWrap: true
	});

	// Priority with color
	const priorityColors = {
		high: chalk.red.bold,
		medium: chalk.yellow,
		low: chalk.gray
	};
	const priorityColor =
		priorityColors[nextTask.priority || 'medium'] || chalk.white;

	// Add task details to table
	taskTable.push(
		[chalk.cyan.bold('ID:'), nextTask.id.toString()],
		[chalk.cyan.bold('Title:'), nextTask.title],
		[
			chalk.cyan.bold('Priority:'),
			priorityColor(nextTask.priority || 'medium')
		],
		[
			chalk.cyan.bold('Dependencies:'),
			formatDependenciesWithStatus(
				nextTask.dependencies,
				data.tasks,
				true,
				complexityReport
			)
		],
		[
			chalk.cyan.bold('Complexity:'),
			nextTask.complexityScore
				? getComplexityWithColor(nextTask.complexityScore)
				: chalk.gray('N/A')
		],
		[chalk.cyan.bold('Description:'), nextTask.description]
	);

	console.log(taskTable.toString());

	// If task has details, show them in a separate box
	if (nextTask.details && nextTask.details.trim().length > 0) {
		console.log(
			boxen(
				chalk.white.bold('Implementation Details:') + '\n\n' + nextTask.details,
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					borderColor: 'cyan',
					borderStyle: 'round',
					margin: { top: 1, bottom: 0 }
				}
			)
		);
	}

	// Determine if the nextTask is a subtask
	const isSubtask = !!nextTask.parentId;

	// Show subtasks if they exist (only for parent tasks)
	if (!isSubtask && nextTask.subtasks && nextTask.subtasks.length > 0) {
		console.log(
			boxen(chalk.white.bold('Subtasks'), {
				padding: { top: 0, bottom: 0, left: 1, right: 1 },
				margin: { top: 1, bottom: 0 },
				borderColor: 'magenta',
				borderStyle: 'round'
			})
		);

		// Calculate available width for the subtask table
		const availableWidth = process.stdout.columns - 10 || 100; // Default to 100 if can't detect

		// Define percentage-based column widths
		const idWidthPct = 8;
		const statusWidthPct = 15;
		const depsWidthPct = 25;
		const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct;

		// Calculate actual column widths
		const idWidth = Math.floor(availableWidth * (idWidthPct / 100));
		const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100));
		const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100));
		const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100));

		// Create a table for subtasks with improved handling
		const subtaskTable = new Table({
			head: [
				chalk.magenta.bold('ID'),
				chalk.magenta.bold('Status'),
				chalk.magenta.bold('Title'),
				chalk.magenta.bold('Deps')
			],
			colWidths: [idWidth, statusWidth, titleWidth, depsWidth],
			style: {
				head: [],
				border: [],
				'padding-top': 0,
				'padding-bottom': 0,
				compact: true
			},
			chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
			wordWrap: true
		});

		// Add subtasks to table
		nextTask.subtasks.forEach((st) => {
			const statusColor =
				{
					done: chalk.green,
					completed: chalk.green,
					pending: chalk.yellow,
					'in-progress': chalk.blue
				}[st.status || 'pending'] || chalk.white;

			// Format subtask dependencies
			let subtaskDeps = 'None';
			if (st.dependencies && st.dependencies.length > 0) {
				// Format dependencies with correct notation
				const formattedDeps = st.dependencies.map((depId) => {
					if (typeof depId === 'number' && depId < 100) {
						const foundSubtask = nextTask.subtasks.find(
							(st) => st.id === depId
						);
						if (foundSubtask) {
							const isDone =
								foundSubtask.status === 'done' ||
								foundSubtask.status === 'completed';
							const isInProgress = foundSubtask.status === 'in-progress';

							// Use consistent color formatting instead of emojis
							if (isDone) {
								return chalk.green.bold(`${nextTask.id}.${depId}`);
							} else if (isInProgress) {
								return chalk.hex('#FFA500').bold(`${nextTask.id}.${depId}`);
							} else {
								return chalk.red.bold(`${nextTask.id}.${depId}`);
							}
						}
						return chalk.red(`${nextTask.id}.${depId} (Not found)`);
					}
					return depId;
				});

				// Join the formatted dependencies directly instead of passing to formatDependenciesWithStatus again
				subtaskDeps =
					formattedDeps.length === 1
						? formattedDeps[0]
						: formattedDeps.join(chalk.white(', '));
			}

			subtaskTable.push([
				`${nextTask.id}.${st.id}`,
				statusColor(st.status || 'pending'),
				st.title,
				subtaskDeps
			]);
		});

		console.log(subtaskTable.toString());
	}

	// Suggest expanding if no subtasks (only for parent tasks without subtasks)
	if (!isSubtask && (!nextTask.subtasks || nextTask.subtasks.length === 0)) {
		console.log(
			boxen(
				chalk.yellow('No subtasks found. Consider breaking down this task:') +
					'\n' +
					chalk.white(
						`Run: ${chalk.cyan(`task-master expand --id=${nextTask.id}`)}`
					),
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					borderColor: 'yellow',
					borderStyle: 'round',
					margin: { top: 1, bottom: 0 }
				}
			)
		);
	}

	// Show action suggestions
	let suggestedActionsContent = chalk.white.bold('Suggested Actions:') + '\n';
	if (isSubtask) {
		// Suggested actions for a subtask
		suggestedActionsContent +=
			`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` +
			`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=done`)}\n` +
			`${chalk.cyan('3.')} View parent task: ${chalk.yellow(`task-master show --id=${nextTask.parentId}`)}`;
	} else {
		// Suggested actions for a parent task
		suggestedActionsContent +=
			`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` +
			`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=done`)}\n` +
			(nextTask.subtasks && nextTask.subtasks.length > 0
				? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${nextTask.id}.1 --status=done`)}` // Example: first subtask
				: `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${nextTask.id}`)}`);
	}

	console.log(
		boxen(suggestedActionsContent, {
			padding: { top: 0, bottom: 0, left: 1, right: 1 },
			borderColor: 'green',
			borderStyle: 'round',
			margin: { top: 1 }
		})
	);

	// Show FYI notice if migration occurred
	displayTaggedTasksFYI(data);
}

/**
 * Display a specific task by ID
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string|number} taskId - The ID of the task to display
 * @param {string} complexityReportPath - Path to the complexity report file
 * @param {string} [statusFilter] - Optional status to filter subtasks by
 * @param {object} context - Context object containing projectRoot and tag
 * @param {string} context.projectRoot - Project root path
 * @param {string} context.tag - Tag for the task
 */
async function displayTaskById(
	tasksPath,
	taskId,
	complexityReportPath = null,
	statusFilter = null,
	context = {}
) {
	const { projectRoot, tag } = context;

	// Read the tasks file with proper projectRoot for tag resolution
	const data = readJSON(tasksPath, projectRoot, tag);
	if (!data || !data.tasks) {
		log('error', 'No valid tasks found.');
		process.exit(1);
	}

	// Read complexity report once
	const complexityReport = readComplexityReport(complexityReportPath);

	// Find the task by ID, applying the status filter if provided
	// Returns { task, originalSubtaskCount, originalSubtasks }
	const { task, originalSubtaskCount, originalSubtasks } = findTaskById(
		data.tasks,
		taskId,
		complexityReport,
		statusFilter
	);

	if (!task) {
		console.log(
			boxen(chalk.yellow(`Task with ID ${taskId} not found!`), {
				padding: { top: 0, bottom: 0, left: 1, right: 1 },
				borderColor: 'yellow',
				borderStyle: 'round',
				margin: { top: 1 }
			})
		);
		return;
	}

	// Handle subtask display specially (This logic remains the same)
	if (task.isSubtask || task.parentTask) {
		console.log(
			boxen(
				chalk.white.bold(
					`Subtask: #${task.parentTask.id}.${task.id} - ${task.title}`
				),
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					borderColor: 'magenta',
					borderStyle: 'round',
					margin: { top: 1, bottom: 0 }
				}
			)
		);

		const subtaskTable = new Table({
			style: {
				head: [],
				border: [],
				'padding-top': 0,
				'padding-bottom': 0,
				compact: true
			},
			chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
			colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)],
			wordWrap: true
		});
		subtaskTable.push(
			[chalk.cyan.bold('ID:'), `${task.parentTask.id}.${task.id}`],
			[
				chalk.cyan.bold('Parent Task:'),
				`#${task.parentTask.id} - ${task.parentTask.title}`
			],
			[chalk.cyan.bold('Title:'), task.title],
			[
				chalk.cyan.bold('Status:'),
				getStatusWithColor(task.status || 'pending', true)
			],
			[
				chalk.cyan.bold('Complexity:'),
				task.complexityScore
					? getComplexityWithColor(task.complexityScore)
					: chalk.gray('N/A')
			],
			[
				chalk.cyan.bold('Description:'),
				task.description || 'No description provided.'
			]
		);
		console.log(subtaskTable.toString());

		if (task.details && task.details.trim().length > 0) {
			console.log(
				boxen(
					chalk.white.bold('Implementation Details:') + '\n\n' + task.details,
					{
						padding: { top: 0, bottom: 0, left: 1, right: 1 },
						borderColor: 'cyan',
						borderStyle: 'round',
						margin: { top: 1, bottom: 0 }
					}
				)
			);
		}

		console.log(
			boxen(
				chalk.white.bold('Suggested Actions:') +
					'\n' +
					`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=in-progress`)}\n` +
					`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=done`)}\n` +
					`${chalk.cyan('3.')} View parent task: ${chalk.yellow(`task-master show --id=${task.parentTask.id}`)}`,
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					borderColor: 'green',
					borderStyle: 'round',
					margin: { top: 1 }
				}
			)
		);
		return; // Exit after displaying subtask details
	}

	// --- Display Regular Task Details ---
	console.log(
		boxen(chalk.white.bold(`Task: #${task.id} - ${task.title}`), {
			padding: { top: 0, bottom: 0, left: 1, right: 1 },
			borderColor: 'blue',
			borderStyle: 'round',
			margin: { top: 1, bottom: 0 }
		})
	);

	const taskTable = new Table({
		style: {
			head: [],
			border: [],
			'padding-top': 0,
			'padding-bottom': 0,
			compact: true
		},
		chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
		colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)],
		wordWrap: true
	});
	const priorityColors = {
		high: chalk.red.bold,
		medium: chalk.yellow,
		low: chalk.gray
	};
	const priorityColor =
		priorityColors[task.priority || 'medium'] || chalk.white;
	taskTable.push(
		[chalk.cyan.bold('ID:'), task.id.toString()],
		[chalk.cyan.bold('Title:'), task.title],
		[
			chalk.cyan.bold('Status:'),
			getStatusWithColor(task.status || 'pending', true)
		],
		[chalk.cyan.bold('Priority:'), priorityColor(task.priority || 'medium')],
		[
			chalk.cyan.bold('Dependencies:'),
			formatDependenciesWithStatus(
				task.dependencies,
				data.tasks,
				true,
				complexityReport
			)
		],
		[
			chalk.cyan.bold('Complexity:'),
			task.complexityScore
				? getComplexityWithColor(task.complexityScore)
				: chalk.gray('N/A')
		],
		[chalk.cyan.bold('Description:'), task.description]
	);
	console.log(taskTable.toString());

	if (task.details && task.details.trim().length > 0) {
		console.log(
			boxen(
				chalk.white.bold('Implementation Details:') + '\n\n' + task.details,
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					borderColor: 'cyan',
					borderStyle: 'round',
					margin: { top: 1, bottom: 0 }
				}
			)
		);
	}
	if (task.testStrategy && task.testStrategy.trim().length > 0) {
		console.log(
			boxen(chalk.white.bold('Test Strategy:') + '\n\n' + task.testStrategy, {
				padding: { top: 0, bottom: 0, left: 1, right: 1 },
				borderColor: 'cyan',
				borderStyle: 'round',
				margin: { top: 1, bottom: 0 }
			})
		);
	}

	// --- Subtask Table Display (uses filtered list: task.subtasks) ---
	if (task.subtasks && task.subtasks.length > 0) {
		console.log(
			boxen(chalk.white.bold('Subtasks'), {
				padding: { top: 0, bottom: 0, left: 1, right: 1 },
				margin: { top: 1, bottom: 0 },
				borderColor: 'magenta',
				borderStyle: 'round'
			})
		);

		const availableWidth = process.stdout.columns - 10 || 100;
		const idWidthPct = 10;
		const statusWidthPct = 15;
		const depsWidthPct = 25;
		const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct;
		const idWidth = Math.floor(availableWidth * (idWidthPct / 100));
		const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100));
		const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100));
		const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100));

		const subtaskTable = new Table({
			head: [
				chalk.magenta.bold('ID'),
				chalk.magenta.bold('Status'),
				chalk.magenta.bold('Title'),
				chalk.magenta.bold('Deps')
			],
			colWidths: [idWidth, statusWidth, titleWidth, depsWidth],
			style: {
				head: [],
				border: [],
				'padding-top': 0,
				'padding-bottom': 0,
				compact: true
			},
			chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
			wordWrap: true
		});

		// Populate table with the potentially filtered subtasks
		task.subtasks.forEach((st) => {
			const statusColorMap = {
				done: chalk.green,
				completed: chalk.green,
				pending: chalk.yellow,
				'in-progress': chalk.blue
			};
			const statusColor = statusColorMap[st.status || 'pending'] || chalk.white;
			let subtaskDeps = 'None';
			if (st.dependencies && st.dependencies.length > 0) {
				const formattedDeps = st.dependencies.map((depId) => {
					// Use the original, unfiltered list for dependency status lookup
					const sourceListForDeps = originalSubtasks || task.subtasks;
					const foundDepSubtask =
						typeof depId === 'number' && depId < 100
							? sourceListForDeps.find((sub) => sub.id === depId)
							: null;

					if (foundDepSubtask) {
						const isDone =
							foundDepSubtask.status === 'done' ||
							foundDepSubtask.status === 'completed';
						const isInProgress = foundDepSubtask.status === 'in-progress';
						const color = isDone
							? chalk.green.bold
							: isInProgress
								? chalk.hex('#FFA500').bold
								: chalk.red.bold;
						return color(`${task.id}.${depId}`);
					} else if (typeof depId === 'number' && depId < 100) {
						return chalk.red(`${task.id}.${depId} (Not found)`);
					}
					return depId; // Assume it's a top-level task ID if not a number < 100
				});
				subtaskDeps =
					formattedDeps.length === 1
						? formattedDeps[0]
						: formattedDeps.join(chalk.white(', '));
			}
			subtaskTable.push([
				`${task.id}.${st.id}`,
				statusColor(st.status || 'pending'),
				st.title,
				subtaskDeps
			]);
		});
		console.log(subtaskTable.toString());

		// Display filter summary line *immediately after the table* if a filter was applied
		if (statusFilter && originalSubtaskCount !== null) {
			console.log(
				chalk.cyan(
					`  Filtered by status: ${chalk.bold(statusFilter)}. Showing ${chalk.bold(task.subtasks.length)} of ${chalk.bold(originalSubtaskCount)} subtasks.`
				)
			);
			// Add a newline for spacing before the progress bar if the filter line was shown
			console.log();
		}
		// --- Conditional Messages for No Subtasks Shown ---
	} else if (statusFilter && originalSubtaskCount === 0) {
		// Case where filter applied, but the parent task had 0 subtasks originally
		console.log(
			boxen(
				chalk.yellow(
					`No subtasks found matching status: ${statusFilter} (Task has no subtasks)`
				),
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					margin: { top: 1, bottom: 0 },
					borderColor: 'yellow',
					borderStyle: 'round'
				}
			)
		);
	} else if (
		statusFilter &&
		originalSubtaskCount > 0 &&
		task.subtasks.length === 0
	) {
		// Case where filter applied, original subtasks existed, but none matched
		console.log(
			boxen(
				chalk.yellow(
					`No subtasks found matching status: ${statusFilter} (out of ${originalSubtaskCount} total)`
				),
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					margin: { top: 1, bottom: 0 },
					borderColor: 'yellow',
					borderStyle: 'round'
				}
			)
		);
	} else if (
		!statusFilter &&
		(!originalSubtasks || originalSubtasks.length === 0)
	) {
		// Case where NO filter applied AND the task genuinely has no subtasks
		// Use the authoritative originalSubtasks if it exists (from filtering), else check task.subtasks
		const actualSubtasks = originalSubtasks || task.subtasks;
		if (!actualSubtasks || actualSubtasks.length === 0) {
			console.log(
				boxen(
					chalk.yellow('No subtasks found. Consider breaking down this task:') +
						'\n' +
						chalk.white(
							`Run: ${chalk.cyan(`task-master expand --id=${task.id}`)}`
						),
					{
						padding: { top: 0, bottom: 0, left: 1, right: 1 },
						borderColor: 'yellow',
						borderStyle: 'round',
						margin: { top: 1, bottom: 0 }
					}
				)
			);
		}
	}

	// --- Subtask Progress Bar Display (uses originalSubtasks or task.subtasks) ---
	// Determine the list to use for progress calculation (always the original if available and filtering happened)
	const subtasksForProgress = originalSubtasks || task.subtasks; // Use original if filtering occurred, else the potentially empty task.subtasks

	// Only show progress if there are actually subtasks
	if (subtasksForProgress && subtasksForProgress.length > 0) {
		const totalSubtasks = subtasksForProgress.length;
		const completedSubtasks = subtasksForProgress.filter(
			(st) => st.status === 'done' || st.status === 'completed'
		).length;

		// Count other statuses from the original/complete list
		const inProgressSubtasks = subtasksForProgress.filter(
			(st) => st.status === 'in-progress'
		).length;
		const pendingSubtasks = subtasksForProgress.filter(
			(st) => st.status === 'pending'
		).length;
		const blockedSubtasks = subtasksForProgress.filter(
			(st) => st.status === 'blocked'
		).length;
		const deferredSubtasks = subtasksForProgress.filter(
			(st) => st.status === 'deferred'
		).length;
		const cancelledSubtasks = subtasksForProgress.filter(
			(st) => st.status === 'cancelled'
		).length;

		const statusBreakdown = {
			// Calculate breakdown based on the complete list
			'in-progress': (inProgressSubtasks / totalSubtasks) * 100,
			pending: (pendingSubtasks / totalSubtasks) * 100,
			blocked: (blockedSubtasks / totalSubtasks) * 100,
			deferred: (deferredSubtasks / totalSubtasks) * 100,
			cancelled: (cancelledSubtasks / totalSubtasks) * 100
		};
		const completionPercentage = (completedSubtasks / totalSubtasks) * 100;

		const availableWidth = process.stdout.columns || 80;
		const boxPadding = 2;
		const boxBorders = 2;
		const percentTextLength = 5;
		const progressBarLength = Math.max(
			20,
			Math.min(
				60,
				availableWidth - boxPadding - boxBorders - percentTextLength - 35
			)
		);

		const statusCounts =
			`${chalk.green('✓ Done:')} ${completedSubtasks}  ${chalk.hex('#FFA500')('► In Progress:')} ${inProgressSubtasks}  ${chalk.yellow('○ Pending:')} ${pendingSubtasks}\n` +
			`${chalk.red('! Blocked:')} ${blockedSubtasks}  ${chalk.gray('⏱ Deferred:')} ${deferredSubtasks}  ${chalk.gray('✗ Cancelled:')} ${cancelledSubtasks}`;

		console.log(
			boxen(
				chalk.white.bold('Subtask Progress:') +
					'\n\n' +
					`${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` +
					`${statusCounts}\n` +
					`${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`,
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					borderColor: 'blue',
					borderStyle: 'round',
					margin: { top: 1, bottom: 0 },
					width: Math.min(availableWidth - 10, 100),
					textAlignment: 'left'
				}
			)
		);
	}

	// --- Suggested Actions ---
	console.log(
		boxen(
			chalk.white.bold('Suggested Actions:') +
				'\n' +
				`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` +
				`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}\n` +
				// Determine action 3 based on whether subtasks *exist* (use the source list for progress)
				(subtasksForProgress && subtasksForProgress.length > 0
					? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}` // Example uses .1
					: `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${task.id}`)}`),
			{
				padding: { top: 0, bottom: 0, left: 1, right: 1 },
				borderColor: 'green',
				borderStyle: 'round',
				margin: { top: 1 }
			}
		)
	);

	// Show FYI notice if migration occurred
	displayTaggedTasksFYI(data);
}

/**
 * Display the complexity analysis report in a nice format
 * @param {string} reportPath - Path to the complexity report file
 */
async function displayComplexityReport(reportPath) {
	// Check if the report exists
	if (!fs.existsSync(reportPath)) {
		console.log(
			boxen(
				chalk.yellow(`No complexity report found at ${reportPath}\n\n`) +
					'Would you like to generate one now?',
				{
					padding: 1,
					borderColor: 'yellow',
					borderStyle: 'round',
					margin: { top: 1 }
				}
			)
		);

		const rl = readline.createInterface({
			input: process.stdin,
			output: process.stdout
		});

		const answer = await new Promise((resolve) => {
			rl.question(chalk.cyan('Generate complexity report? (y/n): '), resolve);
		});
		rl.close();

		if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
			// Call the analyze-complexity command
			console.log(chalk.blue('Generating complexity report...'));
			const tasksPath = TASKMASTER_TASKS_FILE;
			if (!fs.existsSync(tasksPath)) {
				console.error(
					'❌ No tasks.json file found. Please run "task-master init" or create a tasks.json file.'
				);
				return null;
			}

			await analyzeTaskComplexity({
				output: reportPath,
				research: false, // Default to no research for speed
				file: tasksPath
			});
			// Read the newly generated report
			return displayComplexityReport(reportPath);
		} else {
			console.log(chalk.yellow('Report generation cancelled.'));
			return;
		}
	}

	// Read the report
	let report;
	try {
		report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
	} catch (error) {
		log('error', `Error reading complexity report: ${error.message}`);
		return;
	}

	// Display report header
	console.log(
		boxen(chalk.white.bold('Task Complexity Analysis Report'), {
			padding: 1,
			borderColor: 'blue',
			borderStyle: 'round',
			margin: { top: 1, bottom: 1 }
		})
	);

	// Display metadata
	const metaTable = new Table({
		style: {
			head: [],
			border: [],
			'padding-top': 0,
			'padding-bottom': 0,
			compact: true
		},
		chars: {
			mid: '',
			'left-mid': '',
			'mid-mid': '',
			'right-mid': ''
		},
		colWidths: [20, 50]
	});

	metaTable.push(
		[
			chalk.cyan.bold('Generated:'),
			new Date(report.meta.generatedAt).toLocaleString()
		],
		[chalk.cyan.bold('Tasks Analyzed:'), report.meta.tasksAnalyzed],
		[chalk.cyan.bold('Threshold Score:'), report.meta.thresholdScore],
		[chalk.cyan.bold('Project:'), report.meta.projectName],
		[
			chalk.cyan.bold('Research-backed:'),
			report.meta.usedResearch ? 'Yes' : 'No'
		]
	);

	console.log(metaTable.toString());

	// Sort tasks by complexity score (highest first)
	const sortedTasks = [...report.complexityAnalysis].sort(
		(a, b) => b.complexityScore - a.complexityScore
	);

	// Determine which tasks need expansion based on threshold
	const tasksNeedingExpansion = sortedTasks.filter(
		(task) => task.complexityScore >= report.meta.thresholdScore
	);
	const simpleTasks = sortedTasks.filter(
		(task) => task.complexityScore < report.meta.thresholdScore
	);

	// Create progress bar to show complexity distribution
	const complexityDistribution = [0, 0, 0]; // Low (0-4), Medium (5-7), High (8-10)
	sortedTasks.forEach((task) => {
		if (task.complexityScore < 5) complexityDistribution[0]++;
		else if (task.complexityScore < 8) complexityDistribution[1]++;
		else complexityDistribution[2]++;
	});

	const percentLow = Math.round(
		(complexityDistribution[0] / sortedTasks.length) * 100
	);
	const percentMedium = Math.round(
		(complexityDistribution[1] / sortedTasks.length) * 100
	);
	const percentHigh = Math.round(
		(complexityDistribution[2] / sortedTasks.length) * 100
	);

	console.log(
		boxen(
			chalk.white.bold('Complexity Distribution\n\n') +
				`${chalk.green.bold('Low (1-4):')} ${complexityDistribution[0]} tasks (${percentLow}%)\n` +
				`${chalk.yellow.bold('Medium (5-7):')} ${complexityDistribution[1]} tasks (${percentMedium}%)\n` +
				`${chalk.red.bold('High (8-10):')} ${complexityDistribution[2]} tasks (${percentHigh}%)`,
			{
				padding: 1,
				borderColor: 'cyan',
				borderStyle: 'round',
				margin: { top: 1, bottom: 1 }
			}
		)
	);

	// Get terminal width
	const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect

	// Calculate dynamic column widths
	const idWidth = 12;
	const titleWidth = Math.floor(terminalWidth * 0.25); // 25% of width
	const scoreWidth = 8;
	const subtasksWidth = 8;
	// Command column gets the remaining space (minus some buffer for borders)
	const commandWidth =
		terminalWidth - idWidth - titleWidth - scoreWidth - subtasksWidth - 10;

	// Create table with new column widths and word wrapping
	const complexTable = new Table({
		head: [
			chalk.yellow.bold('ID'),
			chalk.yellow.bold('Title'),
			chalk.yellow.bold('Score'),
			chalk.yellow.bold('Subtasks'),
			chalk.yellow.bold('Expansion Command')
		],
		colWidths: [idWidth, titleWidth, scoreWidth, subtasksWidth, commandWidth],
		style: { head: [], border: [] },
		wordWrap: true,
		wrapOnWordBoundary: true
	});

	// When adding rows, don't truncate the expansion command
	tasksNeedingExpansion.forEach((task) => {
		const expansionCommand = `task-master expand --id=${task.taskId} --num=${task.recommendedSubtasks}${task.expansionPrompt ? ` --prompt="${task.expansionPrompt}"` : ''}`;

		complexTable.push([
			task.taskId,
			truncate(task.taskTitle, titleWidth - 3), // Still truncate title for readability
			getComplexityWithColor(task.complexityScore),
			task.recommendedSubtasks,
			chalk.cyan(expansionCommand) // Don't truncate - allow wrapping
		]);
	});

	console.log(complexTable.toString());

	// Create table for simple tasks
	if (simpleTasks.length > 0) {
		console.log(
			boxen(chalk.green.bold(`Simple Tasks (${simpleTasks.length})`), {
				padding: { left: 2, right: 2, top: 0, bottom: 0 },
				margin: { top: 1, bottom: 0 },
				borderColor: 'green',
				borderStyle: 'round'
			})
		);

		const simpleTable = new Table({
			head: [
				chalk.green.bold('ID'),
				chalk.green.bold('Title'),
				chalk.green.bold('Score'),
				chalk.green.bold('Reasoning')
			],
			colWidths: [5, 40, 8, 50],
			style: { head: [], border: [] }
		});

		simpleTasks.forEach((task) => {
			simpleTable.push([
				task.taskId,
				truncate(task.taskTitle, 37),
				getComplexityWithColor(task.complexityScore),
				truncate(task.reasoning, 47)
			]);
		});

		console.log(simpleTable.toString());
	}

	// Show action suggestions
	console.log(
		boxen(
			chalk.white.bold('Suggested Actions:') +
				'\n\n' +
				`${chalk.cyan('1.')} Expand all complex tasks: ${chalk.yellow(`task-master expand --all`)}\n` +
				`${chalk.cyan('2.')} Expand a specific task: ${chalk.yellow(`task-master expand --id=<id>`)}\n` +
				`${chalk.cyan('3.')} Regenerate with research: ${chalk.yellow(`task-master analyze-complexity --research`)}`,
			{
				padding: 1,
				borderColor: 'cyan',
				borderStyle: 'round',
				margin: { top: 1 }
			}
		)
	);
}

/**
 * Generate a prompt for complexity analysis
 * @param {Object} tasksData - Tasks data object containing tasks array
 * @returns {string} Generated prompt
 */
function generateComplexityAnalysisPrompt(tasksData) {
	const defaultSubtasks = getDefaultSubtasks(null); // Use the getter
	return `Analyze the complexity of the following tasks and provide recommendations for subtask breakdown:

${tasksData.tasks
	.map(
		(task) => `
Task ID: ${task.id}
Title: ${task.title}
Description: ${task.description}
Details: ${task.details}
Dependencies: ${JSON.stringify(task.dependencies || [])}
Priority: ${task.priority || 'medium'}
`
	)
	.join('\n---\n')}

Analyze each task and return a JSON array with the following structure for each task:
[
  {
    "taskId": number,
    "taskTitle": string,
    "complexityScore": number (1-10),
    "recommendedSubtasks": number (${Math.max(3, defaultSubtasks - 1)}-${Math.min(8, defaultSubtasks + 2)}),
    "expansionPrompt": string (a specific prompt for generating good subtasks),
    "reasoning": string (brief explanation of your assessment)
  },
  ...
]

IMPORTANT: Make sure to include an analysis for EVERY task listed above, with the correct taskId matching each task's ID.
`;
}

/**
 * Confirm overwriting existing tasks.json file
 * @param {string} tasksPath - Path to the tasks.json file
 * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise
 */
async function confirmTaskOverwrite(tasksPath) {
	console.log(
		boxen(
			chalk.yellow(
				"It looks like you've already generated tasks for this project.\n"
			) +
				chalk.yellow(
					'Executing this command will overwrite any existing tasks.'
				),
			{
				padding: 1,
				borderColor: 'yellow',
				borderStyle: 'round',
				margin: { top: 1 }
			}
		)
	);

	const rl = readline.createInterface({
		input: process.stdin,
		output: process.stdout
	});

	const answer = await new Promise((resolve) => {
		rl.question(
			chalk.cyan('Are you sure you wish to continue? (y/N): '),
			resolve
		);
	});
	rl.close();

	return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
}

/**
 * Displays the API key status for different providers.
 * @param {Array<{provider: string, cli: boolean, mcp: boolean}>} statusReport - The report generated by getApiKeyStatusReport.
 */
function displayApiKeyStatus(statusReport) {
	if (!statusReport || statusReport.length === 0) {
		console.log(chalk.yellow('No API key status information available.'));
		return;
	}

	const table = new Table({
		head: [
			chalk.cyan('Provider'),
			chalk.cyan('CLI Key (.env)'),
			chalk.cyan('MCP Key (mcp.json)')
		],
		colWidths: [15, 20, 25],
		chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }
	});

	statusReport.forEach(({ provider, cli, mcp }) => {
		const cliStatus = cli ? chalk.green('✅ Found') : chalk.red('❌ Missing');
		const mcpStatus = mcp ? chalk.green('✅ Found') : chalk.red('❌ Missing');
		// Capitalize provider name for display
		const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
		table.push([providerName, cliStatus, mcpStatus]);
	});

	console.log(chalk.bold('\n🔑 API Key Status:'));
	console.log(table.toString());
	console.log(
		chalk.gray(
			`  Note: Some providers (e.g., Azure, Ollama) may require additional endpoint configuration in ${TASKMASTER_CONFIG_FILE}.`
		)
	);
}

// --- Formatting Helpers (Potentially move some to utils.js if reusable) ---

const formatSweScoreWithTertileStars = (score, allModels) => {
	// ... (Implementation from previous version or refine) ...
	if (score === null || score === undefined || score <= 0) return 'N/A';
	const formattedPercentage = `${(score * 100).toFixed(1)}%`;

	const validScores = allModels
		.map((m) => m.sweScore)
		.filter((s) => s !== null && s !== undefined && s > 0);
	const sortedScores = [...validScores].sort((a, b) => b - a);
	const n = sortedScores.length;
	let stars = chalk.gray('☆☆☆');

	if (n > 0) {
		const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1);
		const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1);
		if (score >= sortedScores[topThirdIndex]) stars = chalk.yellow('★★★');
		else if (score >= sortedScores[midThirdIndex])
			stars = chalk.yellow('★★') + chalk.gray('☆');
		else stars = chalk.yellow('★') + chalk.gray('☆☆');
	}
	return `${formattedPercentage} ${stars}`;
};

const formatCost = (costObj) => {
	// ... (Implementation from previous version or refine) ...
	if (!costObj) return 'N/A';
	if (costObj.input === 0 && costObj.output === 0) {
		return chalk.green('Free');
	}
	const formatSingleCost = (costValue) => {
		if (costValue === null || costValue === undefined) return 'N/A';
		const isInteger = Number.isInteger(costValue);
		return `$${costValue.toFixed(isInteger ? 0 : 2)}`;
	};
	return `${formatSingleCost(costObj.input)} in, ${formatSingleCost(costObj.output)} out`;
};

// --- Display Functions ---

/**
 * Displays the currently configured active models.
 * @param {ConfigData} configData - The active configuration data.
 * @param {AvailableModel[]} allAvailableModels - Needed for SWE score tertiles.
 */
function displayModelConfiguration(configData, allAvailableModels = []) {
	console.log(chalk.cyan.bold('\nActive Model Configuration:'));
	const active = configData.activeModels;
	const activeTable = new Table({
		head: [
			'Role',
			'Provider',
			'Model ID',
			'SWE Score',
			'Cost ($/1M tkns)'
			// 'API Key Status' // Removed, handled by separate displayApiKeyStatus
		].map((h) => chalk.cyan.bold(h)),
		colWidths: [10, 14, 30, 18, 20 /*, 28 */], // Adjusted widths
		style: { head: ['cyan', 'bold'] }
	});

	activeTable.push([
		chalk.white('Main'),
		active.main.provider,
		active.main.modelId,
		formatSweScoreWithTertileStars(active.main.sweScore, allAvailableModels),
		formatCost(active.main.cost)
		// getCombinedStatus(active.main.keyStatus) // Removed
	]);
	activeTable.push([
		chalk.white('Research'),
		active.research.provider,
		active.research.modelId,
		formatSweScoreWithTertileStars(
			active.research.sweScore,
			allAvailableModels
		),
		formatCost(active.research.cost)
		// getCombinedStatus(active.research.keyStatus) // Removed
	]);
	if (active.fallback && active.fallback.provider && active.fallback.modelId) {
		activeTable.push([
			chalk.white('Fallback'),
			active.fallback.provider,
			active.fallback.modelId,
			formatSweScoreWithTertileStars(
				active.fallback.sweScore,
				allAvailableModels
			),
			formatCost(active.fallback.cost)
			// getCombinedStatus(active.fallback.keyStatus) // Removed
		]);
	} else {
		activeTable.push([
			chalk.white('Fallback'),
			chalk.gray('-'),
			chalk.gray('(Not Set)'),
			chalk.gray('-'),
			chalk.gray('-')
			// chalk.gray('-') // Removed
		]);
	}
	console.log(activeTable.toString());
}

/**
 * Displays the list of available models not currently configured.
 * @param {AvailableModel[]} availableModels - List of available models.
 */
function displayAvailableModels(availableModels) {
	if (!availableModels || availableModels.length === 0) {
		console.log(
			chalk.gray('\n(No other models available or all are configured)')
		);
		return;
	}

	console.log(chalk.cyan.bold('\nOther Available Models:'));
	const availableTable = new Table({
		head: ['Provider', 'Model ID', 'SWE Score', 'Cost ($/1M tkns)'].map((h) =>
			chalk.cyan.bold(h)
		),
		colWidths: [15, 40, 18, 25],
		style: { head: ['cyan', 'bold'] }
	});

	availableModels.forEach((model) => {
		availableTable.push([
			model.provider,
			model.modelId,
			formatSweScoreWithTertileStars(model.sweScore, availableModels), // Pass itself for comparison
			formatCost(model.cost)
		]);
	});
	console.log(availableTable.toString());

	// --- Suggested Actions Section (moved here from models command) ---
	console.log(
		boxen(
			chalk.white.bold('Next Steps:') +
				'\n' +
				chalk.cyan(
					`1. Set main model: ${chalk.yellow('task-master models --set-main <model_id>')}`
				) +
				'\n' +
				chalk.cyan(
					`2. Set research model: ${chalk.yellow('task-master models --set-research <model_id>')}`
				) +
				'\n' +
				chalk.cyan(
					`3. Set fallback model: ${chalk.yellow('task-master models --set-fallback <model_id>')}`
				) +
				'\n' +
				chalk.cyan(
					`4. Run interactive setup: ${chalk.yellow('task-master models --setup')}`
				) +
				'\n' +
				chalk.cyan(
					`5. Use custom ollama/openrouter models: ${chalk.yellow('task-master models --openrouter|ollama --set-main|research|fallback <model_id>')}`
				),
			{
				padding: 1,
				borderColor: 'yellow',
				borderStyle: 'round',
				margin: { top: 1 }
			}
		)
	);
}

/**
 * Displays AI usage telemetry summary in the CLI.
 * @param {object} telemetryData - The telemetry data object.
 * @param {string} outputType - 'cli' or 'mcp' (though typically only called for 'cli').
 */
function displayAiUsageSummary(telemetryData, outputType = 'cli') {
	if (
		(outputType !== 'cli' && outputType !== 'text') ||
		!telemetryData ||
		isSilentMode()
	) {
		return; // Only display for CLI and if data exists and not in silent mode
	}

	const {
		modelUsed,
		providerName,
		inputTokens,
		outputTokens,
		totalTokens,
		totalCost,
		commandName
	} = telemetryData;

	let summary = chalk.bold.blue('AI Usage Summary:') + '\n';
	summary += chalk.gray(`  Command: ${commandName}\n`);
	summary += chalk.gray(`  Provider: ${providerName}\n`);
	summary += chalk.gray(`  Model: ${modelUsed}\n`);
	summary += chalk.gray(
		`  Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n`
	);
	summary += chalk.gray(`  Est. Cost: $${totalCost.toFixed(6)}`);

	console.log(
		boxen(summary, {
			padding: 1,
			margin: { top: 1 },
			borderColor: 'blue',
			borderStyle: 'round',
			title: '💡 Telemetry',
			titleAlignment: 'center'
		})
	);
}

/**
 * Display multiple tasks in a compact summary format with interactive drill-down
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {Array<string>} taskIds - Array of task IDs to display
 * @param {string} complexityReportPath - Path to complexity report
 * @param {string} statusFilter - Optional status filter for subtasks
 * @param {Object} context - Context object containing projectRoot and tag
 * @param {string} [context.projectRoot] - Project root path
 * @param {string} [context.tag] - Tag for the task
 */
async function displayMultipleTasksSummary(
	tasksPath,
	taskIds,
	complexityReportPath = null,
	statusFilter = null,
	context = {}
) {
	displayBanner();

	// Extract projectRoot and tag from context
	const projectRoot = context.projectRoot || null;
	const tag = context.tag || null;

	// Read the tasks file with proper projectRoot for tag resolution
	const data = readJSON(tasksPath, projectRoot, tag);
	if (!data || !data.tasks) {
		log('error', 'No valid tasks found.');
		process.exit(1);
	}

	// Read complexity report once
	const complexityReport = readComplexityReport(complexityReportPath);

	// Find all requested tasks
	const foundTasks = [];
	const notFoundIds = [];

	taskIds.forEach((id) => {
		const { task } = findTaskById(
			data.tasks,
			id,
			complexityReport,
			statusFilter
		);
		if (task) {
			foundTasks.push(task);
		} else {
			notFoundIds.push(id);
		}
	});

	// Show not found tasks
	if (notFoundIds.length > 0) {
		console.log(
			boxen(chalk.yellow(`Tasks not found: ${notFoundIds.join(', ')}`), {
				padding: { top: 0, bottom: 0, left: 1, right: 1 },
				borderColor: 'yellow',
				borderStyle: 'round',
				margin: { top: 1, bottom: 1 }
			})
		);
	}

	if (foundTasks.length === 0) {
		console.log(
			boxen(chalk.red('No valid tasks found to display'), {
				padding: { top: 0, bottom: 0, left: 1, right: 1 },
				borderColor: 'red',
				borderStyle: 'round',
				margin: { top: 1 }
			})
		);
		return;
	}

	// Display header
	console.log(
		boxen(
			chalk.white.bold(
				`Task Summary (${foundTasks.length} task${foundTasks.length === 1 ? '' : 's'})`
			),
			{
				padding: { top: 0, bottom: 0, left: 1, right: 1 },
				borderColor: 'blue',
				borderStyle: 'round',
				margin: { top: 1, bottom: 0 }
			}
		)
	);

	// Calculate terminal width for responsive layout
	const terminalWidth = process.stdout.columns || 100;
	const availableWidth = terminalWidth - 10;

	// Create compact summary table
	const summaryTable = new Table({
		head: [
			chalk.cyan.bold('ID'),
			chalk.cyan.bold('Title'),
			chalk.cyan.bold('Status'),
			chalk.cyan.bold('Priority'),
			chalk.cyan.bold('Subtasks'),
			chalk.cyan.bold('Progress')
		],
		colWidths: [
			Math.floor(availableWidth * 0.08), // ID: 8%
			Math.floor(availableWidth * 0.35), // Title: 35%
			Math.floor(availableWidth * 0.12), // Status: 12%
			Math.floor(availableWidth * 0.1), // Priority: 10%
			Math.floor(availableWidth * 0.15), // Subtasks: 15%
			Math.floor(availableWidth * 0.2) // Progress: 20%
		],
		style: {
			head: [],
			border: [],
			'padding-top': 0,
			'padding-bottom': 0,
			compact: true
		},
		chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
		wordWrap: true
	});

	// Add each task to the summary table
	foundTasks.forEach((task) => {
		// Handle subtask case
		if (task.isSubtask || task.parentTask) {
			const parentId = task.parentTask ? task.parentTask.id : 'Unknown';
			summaryTable.push([
				`${parentId}.${task.id}`,
				truncate(task.title, Math.floor(availableWidth * 0.35) - 3),
				getStatusWithColor(task.status || 'pending', true),
				chalk.gray('(subtask)'),
				chalk.gray('N/A'),
				chalk.gray('N/A')
			]);
			return;
		}

		// Handle regular task
		const priorityColors = {
			high: chalk.red.bold,
			medium: chalk.yellow,
			low: chalk.gray
		};
		const priorityColor =
			priorityColors[task.priority || 'medium'] || chalk.white;

		// Calculate subtask summary
		let subtaskSummary = chalk.gray('None');
		let progressBar = chalk.gray('N/A');

		if (task.subtasks && task.subtasks.length > 0) {
			const total = task.subtasks.length;
			const completed = task.subtasks.filter(
				(st) => st.status === 'done' || st.status === 'completed'
			).length;
			const inProgress = task.subtasks.filter(
				(st) => st.status === 'in-progress'
			).length;
			const pending = task.subtasks.filter(
				(st) => st.status === 'pending'
			).length;

			// Compact subtask count with status indicators
			subtaskSummary = `${chalk.green(completed)}/${total}`;
			if (inProgress > 0)
				subtaskSummary += ` ${chalk.hex('#FFA500')(`+${inProgress}`)}`;
			if (pending > 0) subtaskSummary += ` ${chalk.yellow(`(${pending})`)}`;

			// Mini progress bar (shorter than usual)
			const completionPercentage = (completed / total) * 100;
			const barLength = 8; // Compact bar
			const statusBreakdown = {
				'in-progress': (inProgress / total) * 100,
				pending: (pending / total) * 100
			};
			progressBar = createProgressBar(
				completionPercentage,
				barLength,
				statusBreakdown
			);
		}

		summaryTable.push([
			task.id.toString(),
			truncate(task.title, Math.floor(availableWidth * 0.35) - 3),
			getStatusWithColor(task.status || 'pending', true),
			priorityColor(task.priority || 'medium'),
			subtaskSummary,
			progressBar
		]);
	});

	console.log(summaryTable.toString());

	// Interactive drill-down prompt
	if (foundTasks.length > 1) {
		console.log(
			boxen(
				chalk.white.bold('Interactive Options:') +
					'\n' +
					chalk.cyan('• Press Enter to view available actions for all tasks') +
					'\n' +
					chalk.cyan(
						'• Type a task ID (e.g., "3" or "3.2") to view that specific task'
					) +
					'\n' +
					chalk.cyan('• Type "q" to quit'),
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					borderColor: 'green',
					borderStyle: 'round',
					margin: { top: 1 }
				}
			)
		);

		const rl = readline.createInterface({
			input: process.stdin,
			output: process.stdout
		});

		const choice = await new Promise((resolve) => {
			rl.question(chalk.cyan('Your choice: '), resolve);
		});
		rl.close();

		if (choice.toLowerCase() === 'q') {
			return;
		} else if (choice.trim() === '') {
			// Show action menu for selected tasks
			console.log(
				boxen(
					chalk.white.bold('Available Actions for Selected Tasks:') +
						'\n' +
						chalk.cyan('1.') +
						' Mark all as in-progress' +
						'\n' +
						chalk.cyan('2.') +
						' Mark all as done' +
						'\n' +
						chalk.cyan('3.') +
						' Show next available task' +
						'\n' +
						chalk.cyan('4.') +
						' Expand all tasks (generate subtasks)' +
						'\n' +
						chalk.cyan('5.') +
						' View dependency relationships' +
						'\n' +
						chalk.cyan('6.') +
						' Generate task files' +
						'\n' +
						chalk.gray('Or type a task ID to view details'),
					{
						padding: { top: 0, bottom: 0, left: 1, right: 1 },
						borderColor: 'blue',
						borderStyle: 'round',
						margin: { top: 1 }
					}
				)
			);

			const rl2 = readline.createInterface({
				input: process.stdin,
				output: process.stdout
			});

			const actionChoice = await new Promise((resolve) => {
				rl2.question(chalk.cyan('Choose action (1-6): '), resolve);
			});
			rl2.close();

			const taskIdList = foundTasks.map((t) => t.id).join(',');

			switch (actionChoice.trim()) {
				case '1':
					console.log(
						chalk.blue(
							`\n→ Command: task-master set-status --id=${taskIdList} --status=in-progress`
						)
					);
					console.log(
						chalk.green(
							'✓ Copy and run this command to mark all tasks as in-progress'
						)
					);
					break;
				case '2':
					console.log(
						chalk.blue(
							`\n→ Command: task-master set-status --id=${taskIdList} --status=done`
						)
					);
					console.log(
						chalk.green('✓ Copy and run this command to mark all tasks as done')
					);
					break;
				case '3':
					console.log(chalk.blue(`\n→ Command: task-master next`));
					console.log(
						chalk.green(
							'✓ Copy and run this command to see the next available task'
						)
					);
					break;
				case '4':
					console.log(
						chalk.blue(
							`\n→ Command: task-master expand --id=${taskIdList} --research`
						)
					);
					console.log(
						chalk.green(
							'✓ Copy and run this command to expand all selected tasks into subtasks'
						)
					);
					break;
				case '5': {
					// Show dependency visualization
					console.log(chalk.white.bold('\nDependency Relationships:'));
					let hasDependencies = false;
					foundTasks.forEach((task) => {
						if (task.dependencies && task.dependencies.length > 0) {
							console.log(
								chalk.cyan(
									`Task ${task.id} depends on: ${task.dependencies.join(', ')}`
								)
							);
							hasDependencies = true;
						}
					});
					if (!hasDependencies) {
						console.log(chalk.gray('No dependencies found for selected tasks'));
					}
					break;
				}
				case '6':
					console.log(chalk.blue(`\n→ Command: task-master generate`));
					console.log(
						chalk.green('✓ Copy and run this command to generate task files')
					);
					break;
				default:
					if (actionChoice.trim().length > 0) {
						console.log(chalk.yellow(`Invalid choice: ${actionChoice.trim()}`));
						console.log(chalk.gray('Please choose 1-6 or type a task ID'));
					}
			}
		} else {
			// Show specific task
			await displayTaskById(
				tasksPath,
				choice.trim(),
				complexityReportPath,
				statusFilter,
				context
			);
		}
	} else {
		// Single task - show suggested actions
		const task = foundTasks[0];
		console.log(
			boxen(
				chalk.white.bold('Suggested Actions:') +
					'\n' +
					`${chalk.cyan('1.')} View full details: ${chalk.yellow(`task-master show ${task.id}`)}\n` +
					`${chalk.cyan('2.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` +
					`${chalk.cyan('3.')} Mark as done: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}`,
				{
					padding: { top: 0, bottom: 0, left: 1, right: 1 },
					borderColor: 'green',
					borderStyle: 'round',
					margin: { top: 1 }
				}
			)
		);
	}
}

/**
 * Display context analysis results with beautiful formatting
 * @param {Object} analysisData - Analysis data from ContextGatherer
 * @param {string} semanticQuery - The original query used for semantic search
 * @param {number} contextSize - Size of gathered context in characters
 */
function displayContextAnalysis(analysisData, semanticQuery, contextSize) {
	if (isSilentMode() || !analysisData) return;

	const { highRelevance, mediumRelevance, recentTasks, allRelevantTasks } =
		analysisData;

	// Create the context analysis display
	let analysisContent = chalk.white.bold('Context Analysis') + '\n\n';

	// Query info
	analysisContent +=
		chalk.gray('Query: ') + chalk.white(`"${semanticQuery}"`) + '\n';
	analysisContent +=
		chalk.gray('Context size: ') +
		chalk.cyan(`${contextSize.toLocaleString()} characters`) +
		'\n';
	analysisContent +=
		chalk.gray('Tasks found: ') +
		chalk.yellow(`${allRelevantTasks.length} relevant tasks`) +
		'\n\n';

	// High relevance matches
	if (highRelevance.length > 0) {
		analysisContent += chalk.green.bold('🎯 High Relevance Matches:') + '\n';
		highRelevance.slice(0, 3).forEach((task) => {
			analysisContent +=
				chalk.green(`  • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n';
		});
		if (highRelevance.length > 3) {
			analysisContent +=
				chalk.green(
					`  • ... and ${highRelevance.length - 3} more high relevance tasks`
				) + '\n';
		}
		analysisContent += '\n';
	}

	// Medium relevance matches
	if (mediumRelevance.length > 0) {
		analysisContent += chalk.yellow.bold('📋 Medium Relevance Matches:') + '\n';
		mediumRelevance.slice(0, 3).forEach((task) => {
			analysisContent +=
				chalk.yellow(`  • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n';
		});
		if (mediumRelevance.length > 3) {
			analysisContent +=
				chalk.yellow(
					`  • ... and ${mediumRelevance.length - 3} more medium relevance tasks`
				) + '\n';
		}
		analysisContent += '\n';
	}

	// Recent tasks (if they contributed)
	const recentTasksNotInRelevance = recentTasks.filter(
		(task) =>
			!highRelevance.some((hr) => hr.id === task.id) &&
			!mediumRelevance.some((mr) => mr.id === task.id)
	);

	if (recentTasksNotInRelevance.length > 0) {
		analysisContent += chalk.cyan.bold('🕒 Recent Tasks (for context):') + '\n';
		recentTasksNotInRelevance.slice(0, 2).forEach((task) => {
			analysisContent +=
				chalk.cyan(`  • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n';
		});
		if (recentTasksNotInRelevance.length > 2) {
			analysisContent +=
				chalk.cyan(
					`  • ... and ${recentTasksNotInRelevance.length - 2} more recent tasks`
				) + '\n';
		}
	}

	console.log(
		boxen(analysisContent, {
			padding: { top: 1, bottom: 1, left: 2, right: 2 },
			margin: { top: 1, bottom: 0 },
			borderStyle: 'round',
			borderColor: 'blue',
			title: chalk.blue('🔍 Context Gathering'),
			titleAlignment: 'center'
		})
	);
}

// Export UI functions
export {
	displayBanner,
	displayTaggedTasksFYI,
	startLoadingIndicator,
	stopLoadingIndicator,
	createProgressBar,
	getStatusWithColor,
	formatDependenciesWithStatus,
	displayHelp,
	getComplexityWithColor,
	displayNextTask,
	displayTaskById,
	displayComplexityReport,
	generateComplexityAnalysisPrompt,
	confirmTaskOverwrite,
	displayApiKeyStatus,
	displayModelConfiguration,
	displayAvailableModels,
	displayAiUsageSummary,
	displayMultipleTasksSummary,
	succeedLoadingIndicator,
	failLoadingIndicator,
	warnLoadingIndicator,
	infoLoadingIndicator,
	displayContextAnalysis,
	displayCurrentTagIndicator
};

import chalk from 'chalk';
import boxen from 'boxen';
import readline from 'readline';
import fs from 'fs';

import { log, readJSON, writeJSON, isSilentMode } from '../utils.js';

import {
	startLoadingIndicator,
	stopLoadingIndicator,
	displayAiUsageSummary
} from '../ui.js';

import { generateTextService } from '../ai-services-unified.js';

import { getDebugFlag, getProjectName } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import {
	COMPLEXITY_REPORT_FILE,
	LEGACY_TASKS_FILE
} from '../../../src/constants/paths.js';
import { resolveComplexityReportOutputPath } from '../../../src/utils/path-utils.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks } from '../utils.js';

/**
 * Generates the prompt for complexity analysis.
 * (Moved from ai-services.js and simplified)
 * @param {Object} tasksData - The tasks data object.
 * @param {string} [gatheredContext] - The gathered context for the analysis.
 * @returns {string} The generated prompt.
 */
function generateInternalComplexityAnalysisPrompt(
	tasksData,
	gatheredContext = ''
) {
	const tasksString = JSON.stringify(tasksData.tasks, null, 2);
	let prompt = `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.

Tasks:
${tasksString}`;

	if (gatheredContext) {
		prompt += `\n\n# Project Context\n\n${gatheredContext}`;
	}

	prompt += `

Respond ONLY with a valid JSON array matching the schema:
[
  {
    "taskId": <number>,
    "taskTitle": "<string>",
    "complexityScore": <number 1-10>,
    "recommendedSubtasks": <number>,
    "expansionPrompt": "<string>",
    "reasoning": "<string>"
  },
  ...
]

Do not include any explanatory text, markdown formatting, or code block markers before or after the JSON array.`;
	return prompt;
}

/**
 * Analyzes task complexity and generates expansion recommendations
 * @param {Object} options Command options
 * @param {string} options.file - Path to tasks file
 * @param {string} options.output - Path to report output file
 * @param {string|number} [options.threshold] - Complexity threshold
 * @param {boolean} [options.research] - Use research role
 * @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
 * @param {string} [options.tag] - Tag for the task
 * @param {string} [options.id] - Comma-separated list of task IDs to analyze specifically
 * @param {number} [options.from] - Starting task ID in a range to analyze
 * @param {number} [options.to] - Ending task ID in a range to analyze
 * @param {Object} [options._filteredTasksData] - Pre-filtered task data (internal use)
 * @param {number} [options._originalTaskCount] - Original task count (internal use)
 * @param {Object} context - Context object, potentially containing session and mcpLog
 * @param {Object} [context.session] - Session object from MCP server (optional)
 * @param {Object} [context.mcpLog] - MCP logger object (optional)
 * @param {function} [context.reportProgress] - Deprecated: Function to report progress (ignored)
 */
async function analyzeTaskComplexity(options, context = {}) {
	const { session, mcpLog } = context;
	const tasksPath = options.file || LEGACY_TASKS_FILE;
	const thresholdScore = parseFloat(options.threshold || '5');
	const useResearch = options.research || false;
	const projectRoot = options.projectRoot;
	const tag = options.tag;
	// New parameters for task ID filtering
	const specificIds = options.id
		? options.id
				.split(',')
				.map((id) => parseInt(id.trim(), 10))
				.filter((id) => !Number.isNaN(id))
		: null;
	const fromId = options.from !== undefined ? parseInt(options.from, 10) : null;
	const toId = options.to !== undefined ? parseInt(options.to, 10) : null;

	const outputFormat = mcpLog ? 'json' : 'text';

	const reportLog = (message, level = 'info') => {
		if (mcpLog) {
			mcpLog[level](message);
		} else if (!isSilentMode() && outputFormat === 'text') {
			log(level, message);
		}
	};

	// Resolve output path using tag-aware resolution
	const outputPath = resolveComplexityReportOutputPath(
		options.output,
		{ projectRoot, tag },
		reportLog
	);

	if (outputFormat === 'text') {
		console.log(
			chalk.blue(
				'Analyzing task complexity and generating expansion recommendations...'
			)
		);
	}

	try {
		reportLog(`Reading tasks from ${tasksPath}...`, 'info');
		let tasksData;
		let originalTaskCount = 0;
		let originalData = null;

		if (options._filteredTasksData) {
			tasksData = options._filteredTasksData;
			originalTaskCount = options._originalTaskCount || tasksData.tasks.length;
			if (!options._originalTaskCount) {
				try {
					originalData = readJSON(tasksPath, projectRoot, tag);
					if (originalData && originalData.tasks) {
						originalTaskCount = originalData.tasks.length;
					}
				} catch (e) {
					log('warn', `Could not read original tasks file: ${e.message}`);
				}
			}
		} else {
			originalData = readJSON(tasksPath, projectRoot, tag);
			if (
				!originalData ||
				!originalData.tasks ||
				!Array.isArray(originalData.tasks) ||
				originalData.tasks.length === 0
			) {
				throw new Error('No tasks found in the tasks file');
			}
			originalTaskCount = originalData.tasks.length;

			// Filter tasks based on active status
			const activeStatuses = ['pending', 'blocked', 'in-progress'];
			let filteredTasks = originalData.tasks.filter((task) =>
				activeStatuses.includes(task.status?.toLowerCase() || 'pending')
			);

			// Apply ID filtering if specified
			if (specificIds && specificIds.length > 0) {
				reportLog(
					`Filtering tasks by specific IDs: ${specificIds.join(', ')}`,
					'info'
				);
				filteredTasks = filteredTasks.filter((task) =>
					specificIds.includes(task.id)
				);

				if (outputFormat === 'text') {
					if (filteredTasks.length === 0 && specificIds.length > 0) {
						console.log(
							chalk.yellow(
								`Warning: No active tasks found with IDs: ${specificIds.join(', ')}`
							)
						);
					} else if (filteredTasks.length < specificIds.length) {
						const foundIds = filteredTasks.map((t) => t.id);
						const missingIds = specificIds.filter(
							(id) => !foundIds.includes(id)
						);
						console.log(
							chalk.yellow(
								`Warning: Some requested task IDs were not found or are not active: ${missingIds.join(', ')}`
							)
						);
					}
				}
			}
			// Apply range filtering if specified
			else if (fromId !== null || toId !== null) {
				const effectiveFromId = fromId !== null ? fromId : 1;
				const effectiveToId =
					toId !== null
						? toId
						: Math.max(...originalData.tasks.map((t) => t.id));

				reportLog(
					`Filtering tasks by ID range: ${effectiveFromId} to ${effectiveToId}`,
					'info'
				);
				filteredTasks = filteredTasks.filter(
					(task) => task.id >= effectiveFromId && task.id <= effectiveToId
				);

				if (outputFormat === 'text' && filteredTasks.length === 0) {
					console.log(
						chalk.yellow(
							`Warning: No active tasks found in range: ${effectiveFromId}-${effectiveToId}`
						)
					);
				}
			}

			tasksData = {
				...originalData,
				tasks: filteredTasks,
				_originalTaskCount: originalTaskCount
			};
		}

		// --- Context Gathering ---
		let gatheredContext = '';
		if (originalData && originalData.tasks.length > 0) {
			try {
				const contextGatherer = new ContextGatherer(projectRoot, tag);
				const allTasksFlat = flattenTasksWithSubtasks(originalData.tasks);
				const fuzzySearch = new FuzzyTaskSearch(
					allTasksFlat,
					'analyze-complexity'
				);
				// Create a query from the tasks being analyzed
				const searchQuery = tasksData.tasks
					.map((t) => `${t.title} ${t.description}`)
					.join(' ');
				const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
					maxResults: 10
				});
				const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);

				if (relevantTaskIds.length > 0) {
					const contextResult = await contextGatherer.gather({
						tasks: relevantTaskIds,
						format: 'research'
					});
					gatheredContext = contextResult.context || '';
				}
			} catch (contextError) {
				reportLog(
					`Could not gather additional context: ${contextError.message}`,
					'warn'
				);
			}
		}
		// --- End Context Gathering ---

		const skippedCount = originalTaskCount - tasksData.tasks.length;
		reportLog(
			`Found ${originalTaskCount} total tasks in the task file.`,
			'info'
		);

		// Updated messaging to reflect filtering logic
		if (specificIds || fromId !== null || toId !== null) {
			const filterMsg = specificIds
				? `Analyzing ${tasksData.tasks.length} tasks with specific IDs: ${specificIds.join(', ')}`
				: `Analyzing ${tasksData.tasks.length} tasks in range: ${fromId || 1} to ${toId || 'end'}`;

			reportLog(filterMsg, 'info');
			if (outputFormat === 'text') {
				console.log(chalk.blue(filterMsg));
			}
		} else if (skippedCount > 0) {
			const skipMessage = `Skipping ${skippedCount} tasks marked as done/cancelled/deferred. Analyzing ${tasksData.tasks.length} active tasks.`;
			reportLog(skipMessage, 'info');
			if (outputFormat === 'text') {
				console.log(chalk.yellow(skipMessage));
			}
		}

		// Check for existing report before doing analysis
		let existingReport = null;
		const existingAnalysisMap = new Map(); // For quick lookups by task ID
		try {
			if (fs.existsSync(outputPath)) {
				existingReport = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
				reportLog(`Found existing complexity report at ${outputPath}`, 'info');

				if (
					existingReport &&
					existingReport.complexityAnalysis &&
					Array.isArray(existingReport.complexityAnalysis)
				) {
					// Create lookup map of existing analysis entries
					existingReport.complexityAnalysis.forEach((item) => {
						existingAnalysisMap.set(item.taskId, item);
					});
					reportLog(
						`Existing report contains ${existingReport.complexityAnalysis.length} task analyses`,
						'info'
					);
				}
			}
		} catch (readError) {
			reportLog(
				`Warning: Could not read existing report: ${readError.message}`,
				'warn'
			);
			existingReport = null;
			existingAnalysisMap.clear();
		}

		if (tasksData.tasks.length === 0) {
			// If using ID filtering but no matching tasks, return existing report or empty
			if (existingReport && (specificIds || fromId !== null || toId !== null)) {
				reportLog(
					'No matching tasks found for analysis. Keeping existing report.',
					'info'
				);
				if (outputFormat === 'text') {
					console.log(
						chalk.yellow(
							'No matching tasks found for analysis. Keeping existing report.'
						)
					);
				}
				return {
					report: existingReport,
					telemetryData: null
				};
			}

			// Otherwise create empty report
			const emptyReport = {
				meta: {
					generatedAt: new Date().toISOString(),
					tasksAnalyzed: 0,
					thresholdScore: thresholdScore,
					projectName: getProjectName(session),
					usedResearch: useResearch
				},
				complexityAnalysis: existingReport?.complexityAnalysis || []
			};
			reportLog(`Writing complexity report to ${outputPath}...`, 'info');
			fs.writeFileSync(
				outputPath,
				JSON.stringify(emptyReport, null, '\t'),
				'utf8'
			);
			reportLog(
				`Task complexity analysis complete. Report written to ${outputPath}`,
				'success'
			);
			if (outputFormat === 'text') {
				console.log(
					chalk.green(
						`Task complexity analysis complete. Report written to ${outputPath}`
					)
				);
				const highComplexity = 0;
				const mediumComplexity = 0;
				const lowComplexity = 0;
				const totalAnalyzed = 0;

				console.log('\nComplexity Analysis Summary:');
				console.log('----------------------------');
				console.log(`Tasks in input file: ${originalTaskCount}`);
				console.log(`Tasks successfully analyzed: ${totalAnalyzed}`);
				console.log(`High complexity tasks: ${highComplexity}`);
				console.log(`Medium complexity tasks: ${mediumComplexity}`);
				console.log(`Low complexity tasks: ${lowComplexity}`);
				console.log(
					`Sum verification: ${highComplexity + mediumComplexity + lowComplexity} (should equal ${totalAnalyzed})`
				);
				console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`);
				console.log(
					`\nSee ${outputPath} for the full report and expansion commands.`
				);

				console.log(
					boxen(
						chalk.white.bold('Suggested Next Steps:') +
							'\n\n' +
							`${chalk.cyan('1.')} Run ${chalk.yellow('task-master complexity-report')} to review detailed findings\n` +
							`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down complex tasks\n` +
							`${chalk.cyan('3.')} Run ${chalk.yellow('task-master expand --all')} to expand all pending tasks based on complexity`,
						{
							padding: 1,
							borderColor: 'cyan',
							borderStyle: 'round',
							margin: { top: 1 }
						}
					)
				);
			}
			return {
				report: emptyReport,
				telemetryData: null
			};
		}

		// Continue with regular analysis path
		// Load prompts using PromptManager
		const promptManager = getPromptManager();

		const promptParams = {
			tasks: tasksData.tasks,
			gatheredContext: gatheredContext || '',
			useResearch: useResearch
		};

		const { systemPrompt, userPrompt: prompt } = await promptManager.loadPrompt(
			'analyze-complexity',
			promptParams,
			'default'
		);

		let loadingIndicator = null;
		if (outputFormat === 'text') {
			loadingIndicator = startLoadingIndicator(
				`${useResearch ? 'Researching' : 'Analyzing'} the complexity of your tasks with AI...\n`
			);
		}

		let aiServiceResponse = null;
		let complexityAnalysis = null;

		try {
			const role = useResearch ? 'research' : 'main';

			aiServiceResponse = await generateTextService({
				prompt,
				systemPrompt,
				role,
				session,
				projectRoot,
				commandName: 'analyze-complexity',
				outputType: mcpLog ? 'mcp' : 'cli'
			});

			if (loadingIndicator) {
				stopLoadingIndicator(loadingIndicator);
				loadingIndicator = null;
			}
			if (outputFormat === 'text') {
				readline.clearLine(process.stdout, 0);
				readline.cursorTo(process.stdout, 0);
				console.log(
					chalk.green('AI service call complete. Parsing response...')
				);
			}

			reportLog('Parsing complexity analysis from text response...', 'info');
			try {
				let cleanedResponse = aiServiceResponse.mainResult;
				cleanedResponse = cleanedResponse.trim();

				const codeBlockMatch = cleanedResponse.match(
					/```(?:json)?\s*([\s\S]*?)\s*```/
				);
				if (codeBlockMatch) {
					cleanedResponse = codeBlockMatch[1].trim();
				} else {
					const firstBracket = cleanedResponse.indexOf('[');
					const lastBracket = cleanedResponse.lastIndexOf(']');
					if (firstBracket !== -1 && lastBracket > firstBracket) {
						cleanedResponse = cleanedResponse.substring(
							firstBracket,
							lastBracket + 1
						);
					} else {
						reportLog(
							'Warning: Response does not appear to be a JSON array.',
							'warn'
						);
					}
				}

				if (outputFormat === 'text' && getDebugFlag(session)) {
					console.log(chalk.gray('Attempting to parse cleaned JSON...'));
					console.log(chalk.gray('Cleaned response (first 100 chars):'));
					console.log(chalk.gray(cleanedResponse.substring(0, 100)));
					console.log(chalk.gray('Last 100 chars:'));
					console.log(
						chalk.gray(cleanedResponse.substring(cleanedResponse.length - 100))
					);
				}

				complexityAnalysis = JSON.parse(cleanedResponse);
			} catch (parseError) {
				if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
				reportLog(
					`Error parsing complexity analysis JSON: ${parseError.message}`,
					'error'
				);
				if (outputFormat === 'text') {
					console.error(
						chalk.red(
							`Error parsing complexity analysis JSON: ${parseError.message}`
						)
					);
				}
				throw parseError;
			}

			const taskIds = tasksData.tasks.map((t) => t.id);
			const analysisTaskIds = complexityAnalysis.map((a) => a.taskId);
			const missingTaskIds = taskIds.filter(
				(id) => !analysisTaskIds.includes(id)
			);

			if (missingTaskIds.length > 0) {
				reportLog(
					`Missing analysis for ${missingTaskIds.length} tasks: ${missingTaskIds.join(', ')}`,
					'warn'
				);
				if (outputFormat === 'text') {
					console.log(
						chalk.yellow(
							`Missing analysis for ${missingTaskIds.length} tasks: ${missingTaskIds.join(', ')}`
						)
					);
				}
				for (const missingId of missingTaskIds) {
					const missingTask = tasksData.tasks.find((t) => t.id === missingId);
					if (missingTask) {
						reportLog(`Adding default analysis for task ${missingId}`, 'info');
						complexityAnalysis.push({
							taskId: missingId,
							taskTitle: missingTask.title,
							complexityScore: 5,
							recommendedSubtasks: 3,
							expansionPrompt: `Break down this task with a focus on ${missingTask.title.toLowerCase()}.`,
							reasoning:
								'Automatically added due to missing analysis in AI response.'
						});
					}
				}
			}

			// Merge with existing report - only keep entries from the current tag
			let finalComplexityAnalysis = [];

			if (existingReport && Array.isArray(existingReport.complexityAnalysis)) {
				// Create a map of task IDs that we just analyzed
				const analyzedTaskIds = new Set(
					complexityAnalysis.map((item) => item.taskId)
				);

				// Keep existing entries that weren't in this analysis run AND belong to the current tag
				// We determine tag membership by checking if the task ID exists in the current tag's tasks
				const currentTagTaskIds = new Set(tasksData.tasks.map((t) => t.id));
				const existingEntriesNotAnalyzed =
					existingReport.complexityAnalysis.filter(
						(item) =>
							!analyzedTaskIds.has(item.taskId) &&
							currentTagTaskIds.has(item.taskId) // Only keep entries for tasks in current tag
					);

				// Combine with new analysis
				finalComplexityAnalysis = [
					...existingEntriesNotAnalyzed,
					...complexityAnalysis
				];

				reportLog(
					`Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries from current tag`,
					'info'
				);
			} else {
				// No existing report or invalid format, just use the new analysis
				finalComplexityAnalysis = complexityAnalysis;
			}

			const report = {
				meta: {
					generatedAt: new Date().toISOString(),
					tasksAnalyzed: tasksData.tasks.length,
					totalTasks: originalTaskCount,
					analysisCount: finalComplexityAnalysis.length,
					thresholdScore: thresholdScore,
					projectName: getProjectName(session),
					usedResearch: useResearch
				},
				complexityAnalysis: finalComplexityAnalysis
			};
			reportLog(`Writing complexity report to ${outputPath}...`, 'info');
			fs.writeFileSync(outputPath, JSON.stringify(report, null, '\t'), 'utf8');

			reportLog(
				`Task complexity analysis complete. Report written to ${outputPath}`,
				'success'
			);

			if (outputFormat === 'text') {
				console.log(
					chalk.green(
						`Task complexity analysis complete. Report written to ${outputPath}`
					)
				);
				// Calculate statistics specifically for this analysis run
				const highComplexity = complexityAnalysis.filter(
					(t) => t.complexityScore >= 8
				).length;
				const mediumComplexity = complexityAnalysis.filter(
					(t) => t.complexityScore >= 5 && t.complexityScore < 8
				).length;
				const lowComplexity = complexityAnalysis.filter(
					(t) => t.complexityScore < 5
				).length;
				const totalAnalyzed = complexityAnalysis.length;

				console.log('\nCurrent Analysis Summary:');
				console.log('----------------------------');
				console.log(`Tasks analyzed in this run: ${totalAnalyzed}`);
				console.log(`High complexity tasks: ${highComplexity}`);
				console.log(`Medium complexity tasks: ${mediumComplexity}`);
				console.log(`Low complexity tasks: ${lowComplexity}`);

				if (existingReport) {
					console.log('\nUpdated Report Summary:');
					console.log('----------------------------');
					console.log(
						`Total analyses in report: ${finalComplexityAnalysis.length}`
					);
					console.log(
						`Analyses from previous runs: ${finalComplexityAnalysis.length - totalAnalyzed}`
					);
					console.log(`New/updated analyses: ${totalAnalyzed}`);
				}

				console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`);
				console.log(
					`\nSee ${outputPath} for the full report and expansion commands.`
				);

				console.log(
					boxen(
						chalk.white.bold('Suggested Next Steps:') +
							'\n\n' +
							`${chalk.cyan('1.')} Run ${chalk.yellow('task-master complexity-report')} to review detailed findings\n` +
							`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down complex tasks\n` +
							`${chalk.cyan('3.')} Run ${chalk.yellow('task-master expand --all')} to expand all pending tasks based on complexity`,
						{
							padding: 1,
							borderColor: 'cyan',
							borderStyle: 'round',
							margin: { top: 1 }
						}
					)
				);

				if (getDebugFlag(session)) {
					console.debug(
						chalk.gray(
							`Final analysis object: ${JSON.stringify(report, null, 2)}`
						)
					);
				}

				if (aiServiceResponse.telemetryData) {
					displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
				}
			}

			return {
				report: report,
				telemetryData: aiServiceResponse?.telemetryData,
				tagInfo: aiServiceResponse?.tagInfo
			};
		} catch (aiError) {
			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
			reportLog(`Error during AI service call: ${aiError.message}`, 'error');
			if (outputFormat === 'text') {
				console.error(
					chalk.red(`Error during AI service call: ${aiError.message}`)
				);
				if (aiError.message.includes('API key')) {
					console.log(
						chalk.yellow(
							'\nPlease ensure your API keys are correctly configured in .env or ~/.taskmaster/.env'
						)
					);
					console.log(
						chalk.yellow("Run 'task-master models --setup' if needed.")
					);
				}
			}
			throw aiError;
		}
	} catch (error) {
		reportLog(`Error analyzing task complexity: ${error.message}`, 'error');
		if (outputFormat === 'text') {
			console.error(
				chalk.red(`Error analyzing task complexity: ${error.message}`)
			);
			if (getDebugFlag(session)) {
				console.error(error);
			}
			process.exit(1);
		} else {
			throw error;
		}
	}
}

export default analyzeTaskComplexity;

import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import { z } from 'zod'; // Keep Zod for post-parse validation

import {
	log as consoleLog,
	readJSON,
	writeJSON,
	truncate,
	isSilentMode,
	flattenTasksWithSubtasks,
	findProjectRoot
} from '../utils.js';

import {
	getStatusWithColor,
	startLoadingIndicator,
	stopLoadingIndicator,
	displayAiUsageSummary
} from '../ui.js';

import { generateTextService } from '../ai-services-unified.js';
import { getDebugFlag, isApiKeySet } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';

// Zod schema for post-parsing validation of the updated task object
const updatedTaskSchema = z
	.object({
		id: z.number().int(),
		title: z.string(), // Title should be preserved, but check it exists
		description: z.string(),
		status: z.string(),
		dependencies: z.array(z.union([z.number().int(), z.string()])),
		priority: z.string().nullable().default('medium'),
		details: z.string().nullable().default(''),
		testStrategy: z.string().nullable().default(''),
		subtasks: z
			.array(
				z.object({
					id: z
						.number()
						.int()
						.positive()
						.describe('Sequential subtask ID starting from 1'),
					title: z.string(),
					description: z.string(),
					status: z.string(),
					dependencies: z.array(z.number().int()).nullable().default([]),
					details: z.string().nullable().default(''),
					testStrategy: z.string().nullable().default('')
				})
			)
			.nullable()
			.default([])
	})
	.strip(); // Allows parsing even if AI adds extra fields, but validation focuses on schema

/**
 * Parses a single updated task object from AI's text response.
 * @param {string} text - Response text from AI.
 * @param {number} expectedTaskId - The ID of the task expected.
 * @param {Function | Object} logFn - Logging function or MCP logger.
 * @param {boolean} isMCP - Flag indicating MCP context.
 * @returns {Object} Parsed and validated task object.
 * @throws {Error} If parsing or validation fails.
 */
function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
	// Report helper consistent with the established pattern
	const report = (level, ...args) => {
		if (isMCP) {
			if (typeof logFn[level] === 'function') logFn[level](...args);
			else logFn.info(...args);
		} else if (!isSilentMode()) {
			logFn(level, ...args);
		}
	};

	report(
		'info',
		'Attempting to parse updated task object from text response...'
	);
	if (!text || text.trim() === '')
		throw new Error('AI response text is empty.');

	let cleanedResponse = text.trim();
	const originalResponseForDebug = cleanedResponse;
	let parseMethodUsed = 'raw'; // Keep track of which method worked

	// --- NEW Step 1: Try extracting between {} first ---
	const firstBraceIndex = cleanedResponse.indexOf('{');
	const lastBraceIndex = cleanedResponse.lastIndexOf('}');
	let potentialJsonFromBraces = null;

	if (firstBraceIndex !== -1 && lastBraceIndex > firstBraceIndex) {
		potentialJsonFromBraces = cleanedResponse.substring(
			firstBraceIndex,
			lastBraceIndex + 1
		);
		if (potentialJsonFromBraces.length <= 2) {
			potentialJsonFromBraces = null; // Ignore empty braces {}
		}
	}

	// If {} extraction yielded something, try parsing it immediately
	if (potentialJsonFromBraces) {
		try {
			const testParse = JSON.parse(potentialJsonFromBraces);
			// It worked! Use this as the primary cleaned response.
			cleanedResponse = potentialJsonFromBraces;
			parseMethodUsed = 'braces';
		} catch (e) {
			report(
				'info',
				'Content between {} looked promising but failed initial parse. Proceeding to other methods.'
			);
			// Reset cleanedResponse to original if brace parsing failed
			cleanedResponse = originalResponseForDebug;
		}
	}

	// --- Step 2: If brace parsing didn't work or wasn't applicable, try code block extraction ---
	if (parseMethodUsed === 'raw') {
		const codeBlockMatch = cleanedResponse.match(
			/```(?:json|javascript)?\s*([\s\S]*?)\s*```/i
		);
		if (codeBlockMatch) {
			cleanedResponse = codeBlockMatch[1].trim();
			parseMethodUsed = 'codeblock';
			report('info', 'Extracted JSON content from Markdown code block.');
		} else {
			// --- Step 3: If code block failed, try stripping prefixes ---
			const commonPrefixes = [
				'json\n',
				'javascript\n'
				// ... other prefixes ...
			];
			let prefixFound = false;
			for (const prefix of commonPrefixes) {
				if (cleanedResponse.toLowerCase().startsWith(prefix)) {
					cleanedResponse = cleanedResponse.substring(prefix.length).trim();
					parseMethodUsed = 'prefix';
					report('info', `Stripped prefix: "${prefix.trim()}"`);
					prefixFound = true;
					break;
				}
			}
			if (!prefixFound) {
				report(
					'warn',
					'Response does not appear to contain {}, code block, or known prefix. Attempting raw parse.'
				);
			}
		}
	}

	// --- Step 4: Attempt final parse ---
	let parsedTask;
	try {
		parsedTask = JSON.parse(cleanedResponse);
	} catch (parseError) {
		report('error', `Failed to parse JSON object: ${parseError.message}`);
		report(
			'error',
			`Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}`
		);
		report(
			'error',
			`Original Raw Response (first 500 chars): ${originalResponseForDebug.substring(0, 500)}`
		);
		throw new Error(
			`Failed to parse JSON response object: ${parseError.message}`
		);
	}

	if (!parsedTask || typeof parsedTask !== 'object') {
		report(
			'error',
			`Parsed content is not an object. Type: ${typeof parsedTask}`
		);
		report(
			'error',
			`Parsed content sample: ${JSON.stringify(parsedTask).substring(0, 200)}`
		);
		throw new Error('Parsed AI response is not a valid JSON object.');
	}

	// Preprocess the task to ensure subtasks have proper structure
	const preprocessedTask = {
		...parsedTask,
		status: parsedTask.status || 'pending',
		dependencies: Array.isArray(parsedTask.dependencies)
			? parsedTask.dependencies
			: [],
		details:
			typeof parsedTask.details === 'string'
				? parsedTask.details
				: String(parsedTask.details || ''),
		testStrategy:
			typeof parsedTask.testStrategy === 'string'
				? parsedTask.testStrategy
				: String(parsedTask.testStrategy || ''),
		// Ensure subtasks is an array and each subtask has required fields
		subtasks: Array.isArray(parsedTask.subtasks)
			? parsedTask.subtasks.map((subtask) => ({
					...subtask,
					title: subtask.title || '',
					description: subtask.description || '',
					status: subtask.status || 'pending',
					dependencies: Array.isArray(subtask.dependencies)
						? subtask.dependencies
						: [],
					details:
						typeof subtask.details === 'string'
							? subtask.details
							: String(subtask.details || ''),
					testStrategy:
						typeof subtask.testStrategy === 'string'
							? subtask.testStrategy
							: String(subtask.testStrategy || '')
				}))
			: []
	};

	// Validate the parsed task object using Zod
	const validationResult = updatedTaskSchema.safeParse(preprocessedTask);
	if (!validationResult.success) {
		report('error', 'Parsed task object failed Zod validation.');
		validationResult.error.errors.forEach((err) => {
			report('error', `  - Field '${err.path.join('.')}': ${err.message}`);
		});
		throw new Error(
			`AI response failed task structure validation: ${validationResult.error.message}`
		);
	}

	// Final check: ensure ID matches expected ID (AI might hallucinate)
	if (validationResult.data.id !== expectedTaskId) {
		report(
			'warn',
			`AI returned task with ID ${validationResult.data.id}, but expected ${expectedTaskId}. Overwriting ID.`
		);
		validationResult.data.id = expectedTaskId; // Enforce correct ID
	}

	report('info', 'Successfully validated updated task structure.');
	return validationResult.data; // Return the validated task data
}

/**
 * Update a task by ID with new information using the unified AI service.
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {number} taskId - ID of the task to update
 * @param {string} prompt - Prompt for generating updated task information
 * @param {boolean} [useResearch=false] - Whether to use the research AI role.
 * @param {Object} context - Context object containing session and mcpLog.
 * @param {Object} [context.session] - Session object from MCP server.
 * @param {Object} [context.mcpLog] - MCP logger object.
 * @param {string} [context.projectRoot] - Project root path.
 * @param {string} [context.tag] - Tag for the task
 * @param {string} [outputFormat='text'] - Output format ('text' or 'json').
 * @param {boolean} [appendMode=false] - If true, append to details instead of full update.
 * @returns {Promise<Object|null>} - The updated task or null if update failed.
 */
async function updateTaskById(
	tasksPath,
	taskId,
	prompt,
	useResearch = false,
	context = {},
	outputFormat = 'text',
	appendMode = false
) {
	const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
	const logFn = mcpLog || consoleLog;
	const isMCP = !!mcpLog;

	// Use report helper for logging
	const report = (level, ...args) => {
		if (isMCP) {
			if (typeof logFn[level] === 'function') logFn[level](...args);
			else logFn.info(...args);
		} else if (!isSilentMode()) {
			logFn(level, ...args);
		}
	};

	try {
		report('info', `Updating single task ${taskId} with prompt: "${prompt}"`);

		// --- Input Validations (Keep existing) ---
		if (!Number.isInteger(taskId) || taskId <= 0)
			throw new Error(
				`Invalid task ID: ${taskId}. Task ID must be a positive integer.`
			);
		if (!prompt || typeof prompt !== 'string' || prompt.trim() === '')
			throw new Error('Prompt cannot be empty.');
		if (useResearch && !isApiKeySet('perplexity', session)) {
			report(
				'warn',
				'Perplexity research requested but API key not set. Falling back.'
			);
			if (outputFormat === 'text')
				console.log(
					chalk.yellow('Perplexity AI not available. Falling back to main AI.')
				);
			useResearch = false;
		}
		if (!fs.existsSync(tasksPath))
			throw new Error(`Tasks file not found: ${tasksPath}`);
		// --- End Input Validations ---

		// Determine project root
		const projectRoot = providedProjectRoot || findProjectRoot();
		if (!projectRoot) {
			throw new Error('Could not determine project root directory');
		}

		// --- Task Loading and Status Check (Keep existing) ---
		const data = readJSON(tasksPath, projectRoot, tag);
		if (!data || !data.tasks)
			throw new Error(`No valid tasks found in ${tasksPath}.`);
		const taskIndex = data.tasks.findIndex((task) => task.id === taskId);
		if (taskIndex === -1) throw new Error(`Task with ID ${taskId} not found.`);
		const taskToUpdate = data.tasks[taskIndex];
		if (taskToUpdate.status === 'done' || taskToUpdate.status === 'completed') {
			report(
				'warn',
				`Task ${taskId} is already marked as done and cannot be updated`
			);

			// Only show warning box for text output (CLI)
			if (outputFormat === 'text') {
				console.log(
					boxen(
						chalk.yellow(
							`Task ${taskId} is already marked as ${taskToUpdate.status} and cannot be updated.`
						) +
							'\n\n' +
							chalk.white(
								'Completed tasks are locked to maintain consistency. To modify a completed task, you must first:'
							) +
							'\n' +
							chalk.white(
								'1. Change its status to "pending" or "in-progress"'
							) +
							'\n' +
							chalk.white('2. Then run the update-task command'),
						{ padding: 1, borderColor: 'yellow', borderStyle: 'round' }
					)
				);
			}
			return null;
		}
		// --- End Task Loading ---

		// --- Context Gathering ---
		let gatheredContext = '';
		try {
			const contextGatherer = new ContextGatherer(projectRoot, tag);
			const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
			const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-task');
			const searchQuery = `${taskToUpdate.title} ${taskToUpdate.description} ${prompt}`;
			const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
				maxResults: 5,
				includeSelf: true
			});
			const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);

			const finalTaskIds = [
				...new Set([taskId.toString(), ...relevantTaskIds])
			];

			if (finalTaskIds.length > 0) {
				const contextResult = await contextGatherer.gather({
					tasks: finalTaskIds,
					format: 'research'
				});
				gatheredContext = contextResult.context || '';
			}
		} catch (contextError) {
			report('warn', `Could not gather context: ${contextError.message}`);
		}
		// --- End Context Gathering ---

		// --- Display Task Info (CLI Only - Keep existing) ---
		if (outputFormat === 'text') {
			// Show the task that will be updated
			const table = new Table({
				head: [
					chalk.cyan.bold('ID'),
					chalk.cyan.bold('Title'),
					chalk.cyan.bold('Status')
				],
				colWidths: [5, 60, 10]
			});

			table.push([
				taskToUpdate.id,
				truncate(taskToUpdate.title, 57),
				getStatusWithColor(taskToUpdate.status)
			]);

			console.log(
				boxen(chalk.white.bold(`Updating Task #${taskId}`), {
					padding: 1,
					borderColor: 'blue',
					borderStyle: 'round',
					margin: { top: 1, bottom: 0 }
				})
			);

			console.log(table.toString());

			// Display a message about how completed subtasks are handled
			console.log(
				boxen(
					chalk.cyan.bold('How Completed Subtasks Are Handled:') +
						'\n\n' +
						chalk.white(
							'• Subtasks marked as "done" or "completed" will be preserved\n'
						) +
						chalk.white(
							'• New subtasks will build upon what has already been completed\n'
						) +
						chalk.white(
							'• If completed work needs revision, a new subtask will be created instead of modifying done items\n'
						) +
						chalk.white(
							'• This approach maintains a clear record of completed work and new requirements'
						),
					{
						padding: 1,
						borderColor: 'blue',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 }
					}
				)
			);
		}

		// --- Build Prompts using PromptManager ---
		const promptManager = getPromptManager();

		const promptParams = {
			task: taskToUpdate,
			taskJson: JSON.stringify(taskToUpdate, null, 2),
			updatePrompt: prompt,
			appendMode: appendMode,
			useResearch: useResearch,
			currentDetails: taskToUpdate.details || '(No existing details)',
			gatheredContext: gatheredContext || ''
		};

		const variantKey = appendMode
			? 'append'
			: useResearch
				? 'research'
				: 'default';

		report(
			'info',
			`Loading prompt template with variant: ${variantKey}, appendMode: ${appendMode}, useResearch: ${useResearch}`
		);

		let systemPrompt;
		let userPrompt;
		try {
			const promptResult = await promptManager.loadPrompt(
				'update-task',
				promptParams,
				variantKey
			);
			report(
				'info',
				`Prompt result type: ${typeof promptResult}, keys: ${promptResult ? Object.keys(promptResult).join(', ') : 'null'}`
			);

			// Extract prompts - loadPrompt returns { systemPrompt, userPrompt, metadata }
			systemPrompt = promptResult.systemPrompt;
			userPrompt = promptResult.userPrompt;

			report(
				'info',
				`Loaded prompts - systemPrompt length: ${systemPrompt?.length}, userPrompt length: ${userPrompt?.length}`
			);
		} catch (error) {
			report('error', `Failed to load prompt template: ${error.message}`);
			throw new Error(`Failed to load prompt template: ${error.message}`);
		}

		// If prompts are still not set, throw an error
		if (!systemPrompt || !userPrompt) {
			throw new Error(
				`Failed to load prompts: systemPrompt=${!!systemPrompt}, userPrompt=${!!userPrompt}`
			);
		}
		// --- End Build Prompts ---

		let loadingIndicator = null;
		let aiServiceResponse = null;

		if (!isMCP && outputFormat === 'text') {
			loadingIndicator = startLoadingIndicator(
				useResearch ? 'Updating task with research...\n' : 'Updating task...\n'
			);
		}

		try {
			const serviceRole = useResearch ? 'research' : 'main';
			aiServiceResponse = await generateTextService({
				role: serviceRole,
				session: session,
				projectRoot: projectRoot,
				systemPrompt: systemPrompt,
				prompt: userPrompt,
				commandName: 'update-task',
				outputType: isMCP ? 'mcp' : 'cli'
			});

			if (loadingIndicator)
				stopLoadingIndicator(loadingIndicator, 'AI update complete.');

			if (appendMode) {
				// Append mode: handle as plain text
				const generatedContentString = aiServiceResponse.mainResult;
				let newlyAddedSnippet = '';

				if (generatedContentString && generatedContentString.trim()) {
					const timestamp = new Date().toISOString();
					const formattedBlock = `<info added on ${timestamp}>\n${generatedContentString.trim()}\n</info added on ${timestamp}>`;
					newlyAddedSnippet = formattedBlock;

					// Append to task details
					taskToUpdate.details =
						(taskToUpdate.details ? taskToUpdate.details + '\n' : '') +
						formattedBlock;
				} else {
					report(
						'warn',
						'AI response was empty or whitespace after trimming. Original details remain unchanged.'
					);
					newlyAddedSnippet = 'No new details were added by the AI.';
				}

				// Update description with timestamp if prompt is short
				if (prompt.length < 100) {
					if (taskToUpdate.description) {
						taskToUpdate.description += ` [Updated: ${new Date().toLocaleDateString()}]`;
					}
				}

				// Write the updated task back to file
				data.tasks[taskIndex] = taskToUpdate;
				writeJSON(tasksPath, data, projectRoot, tag);
				report('success', `Successfully appended to task ${taskId}`);

				// Display success message for CLI
				if (outputFormat === 'text') {
					console.log(
						boxen(
							chalk.green(`Successfully appended to task #${taskId}`) +
								'\n\n' +
								chalk.white.bold('Title:') +
								' ' +
								taskToUpdate.title +
								'\n\n' +
								chalk.white.bold('Newly Added Content:') +
								'\n' +
								chalk.white(newlyAddedSnippet),
							{ padding: 1, borderColor: 'green', borderStyle: 'round' }
						)
					);
				}

				// Display AI usage telemetry for CLI users
				if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
					displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
				}

				// Return the updated task
				return {
					updatedTask: taskToUpdate,
					telemetryData: aiServiceResponse.telemetryData,
					tagInfo: aiServiceResponse.tagInfo
				};
			}

			// Full update mode: Use mainResult (text) for parsing
			const updatedTask = parseUpdatedTaskFromText(
				aiServiceResponse.mainResult,
				taskId,
				logFn,
				isMCP
			);

			// --- Task Validation/Correction (Keep existing logic) ---
			if (!updatedTask || typeof updatedTask !== 'object')
				throw new Error('Received invalid task object from AI.');
			if (!updatedTask.title || !updatedTask.description)
				throw new Error('Updated task missing required fields.');
			// Preserve ID if AI changed it
			if (updatedTask.id !== taskId) {
				report('warn', `AI changed task ID. Restoring original ID ${taskId}.`);
				updatedTask.id = taskId;
			}
			// Preserve status if AI changed it
			if (
				updatedTask.status !== taskToUpdate.status &&
				!prompt.toLowerCase().includes('status')
			) {
				report(
					'warn',
					`AI changed task status. Restoring original status '${taskToUpdate.status}'.`
				);
				updatedTask.status = taskToUpdate.status;
			}
			// Fix subtask IDs if they exist (ensure they are numeric and sequential)
			if (updatedTask.subtasks && Array.isArray(updatedTask.subtasks)) {
				let currentSubtaskId = 1;
				updatedTask.subtasks = updatedTask.subtasks.map((subtask) => {
					// Fix AI-generated subtask IDs that might be strings or use parent ID as prefix
					const correctedSubtask = {
						...subtask,
						id: currentSubtaskId, // Override AI-generated ID with correct sequential ID
						dependencies: Array.isArray(subtask.dependencies)
							? subtask.dependencies
									.map((dep) =>
										typeof dep === 'string' ? parseInt(dep, 10) : dep
									)
									.filter(
										(depId) =>
											!Number.isNaN(depId) &&
											depId >= 1 &&
											depId < currentSubtaskId
									)
							: [],
						status: subtask.status || 'pending'
					};
					currentSubtaskId++;
					return correctedSubtask;
				});
				report(
					'info',
					`Fixed ${updatedTask.subtasks.length} subtask IDs to be sequential numeric IDs.`
				);
			}

			// Preserve completed subtasks (Keep existing logic)
			if (taskToUpdate.subtasks?.length > 0) {
				if (!updatedTask.subtasks) {
					report(
						'warn',
						'Subtasks removed by AI. Restoring original subtasks.'
					);
					updatedTask.subtasks = taskToUpdate.subtasks;
				} else {
					const completedOriginal = taskToUpdate.subtasks.filter(
						(st) => st.status === 'done' || st.status === 'completed'
					);
					completedOriginal.forEach((compSub) => {
						const updatedSub = updatedTask.subtasks.find(
							(st) => st.id === compSub.id
						);
						if (
							!updatedSub ||
							JSON.stringify(updatedSub) !== JSON.stringify(compSub)
						) {
							report(
								'warn',
								`Completed subtask ${compSub.id} was modified or removed. Restoring.`
							);
							// Remove potentially modified version
							updatedTask.subtasks = updatedTask.subtasks.filter(
								(st) => st.id !== compSub.id
							);
							// Add back original
							updatedTask.subtasks.push(compSub);
						}
					});
					// Deduplicate just in case
					const subtaskIds = new Set();
					updatedTask.subtasks = updatedTask.subtasks.filter((st) => {
						if (!subtaskIds.has(st.id)) {
							subtaskIds.add(st.id);
							return true;
						}
						report('warn', `Duplicate subtask ID ${st.id} removed.`);
						return false;
					});
				}
			}
			// --- End Task Validation/Correction ---

			// --- Update Task Data (Keep existing) ---
			data.tasks[taskIndex] = updatedTask;
			// --- End Update Task Data ---

			// --- Write File and Generate (Unchanged) ---
			writeJSON(tasksPath, data, projectRoot, tag);
			report('success', `Successfully updated task ${taskId}`);
			// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
			// --- End Write File ---

			// --- Display CLI Telemetry ---
			if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
				displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); // <<< ADD display
			}

			// --- Return Success with Telemetry ---
			return {
				updatedTask: updatedTask, // Return the updated task object
				telemetryData: aiServiceResponse.telemetryData, // <<< ADD telemetryData
				tagInfo: aiServiceResponse.tagInfo
			};
		} catch (error) {
			// Catch errors from generateTextService
			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
			report('error', `Error during AI service call: ${error.message}`);
			if (error.message.includes('API key')) {
				report('error', 'Please ensure API keys are configured correctly.');
			}
			throw error; // Re-throw error
		}
	} catch (error) {
		// General error catch
		// --- General Error Handling (Keep existing) ---
		report('error', `Error updating task: ${error.message}`);
		if (outputFormat === 'text') {
			console.error(chalk.red(`Error: ${error.message}`));
			// ... helpful hints ...
			if (getDebugFlag(session)) console.error(error);
			process.exit(1);
		} else {
			throw error; // Re-throw for MCP
		}
		return null; // Indicate failure in CLI case if process doesn't exit
		// --- End General Error Handling ---
	}
}

export default updateTaskById;

import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';

import {
	log,
	readJSON,
	truncate,
	readComplexityReport,
	addComplexityToTask
} from '../utils.js';
import findNextTask from './find-next-task.js';

import {
	displayBanner,
	getStatusWithColor,
	formatDependenciesWithStatus,
	getComplexityWithColor,
	createProgressBar
} from '../ui.js';

/**
 * List all tasks
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} statusFilter - Filter by status (single status or comma-separated list, e.g., 'pending' or 'blocked,deferred')
 * @param {string} reportPath - Path to the complexity report
 * @param {boolean} withSubtasks - Whether to show subtasks
 * @param {string} outputFormat - Output format (text or json)
 * @param {Object} context - Context object (required)
 * @param {string} context.projectRoot - Project root path
 * @param {string} context.tag - Tag for the task
 * @returns {Object} - Task list result for json format
 */
function listTasks(
	tasksPath,
	statusFilter,
	reportPath = null,
	withSubtasks = false,
	outputFormat = 'text',
	context = {}
) {
	const { projectRoot, tag } = context;
	try {
		// Extract projectRoot from context if provided
		const data = readJSON(tasksPath, projectRoot, tag); // Pass projectRoot to readJSON
		if (!data || !data.tasks) {
			throw new Error(`No valid tasks found in ${tasksPath}`);
		}

		// Add complexity scores to tasks if report exists
		// `reportPath` is already tag-aware (resolved at the CLI boundary).
		const complexityReport = readComplexityReport(reportPath);
		// Apply complexity scores to tasks
		if (complexityReport && complexityReport.complexityAnalysis) {
			data.tasks.forEach((task) => addComplexityToTask(task, complexityReport));
		}

		// Filter tasks by status if specified - now supports comma-separated statuses
		let filteredTasks;
		if (statusFilter && statusFilter.toLowerCase() !== 'all') {
			// Handle comma-separated statuses
			const allowedStatuses = statusFilter
				.split(',')
				.map((s) => s.trim().toLowerCase())
				.filter((s) => s.length > 0); // Remove empty strings

			filteredTasks = data.tasks.filter(
				(task) =>
					task.status && allowedStatuses.includes(task.status.toLowerCase())
			);
		} else {
			// Default to all tasks if no filter or filter is 'all'
			filteredTasks = data.tasks;
		}

		// Calculate completion statistics
		const totalTasks = data.tasks.length;
		const completedTasks = data.tasks.filter(
			(task) => task.status === 'done' || task.status === 'completed'
		).length;
		const completionPercentage =
			totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;

		// Count statuses for tasks
		const doneCount = completedTasks;
		const inProgressCount = data.tasks.filter(
			(task) => task.status === 'in-progress'
		).length;
		const pendingCount = data.tasks.filter(
			(task) => task.status === 'pending'
		).length;
		const blockedCount = data.tasks.filter(
			(task) => task.status === 'blocked'
		).length;
		const deferredCount = data.tasks.filter(
			(task) => task.status === 'deferred'
		).length;
		const cancelledCount = data.tasks.filter(
			(task) => task.status === 'cancelled'
		).length;
		const reviewCount = data.tasks.filter(
			(task) => task.status === 'review'
		).length;

		// Count subtasks and their statuses
		let totalSubtasks = 0;
		let completedSubtasks = 0;
		let inProgressSubtasks = 0;
		let pendingSubtasks = 0;
		let blockedSubtasks = 0;
		let deferredSubtasks = 0;
		let cancelledSubtasks = 0;
		let reviewSubtasks = 0;

		data.tasks.forEach((task) => {
			if (task.subtasks && task.subtasks.length > 0) {
				totalSubtasks += task.subtasks.length;
				completedSubtasks += task.subtasks.filter(
					(st) => st.status === 'done' || st.status === 'completed'
				).length;
				inProgressSubtasks += task.subtasks.filter(
					(st) => st.status === 'in-progress'
				).length;
				pendingSubtasks += task.subtasks.filter(
					(st) => st.status === 'pending'
				).length;
				blockedSubtasks += task.subtasks.filter(
					(st) => st.status === 'blocked'
				).length;
				deferredSubtasks += task.subtasks.filter(
					(st) => st.status === 'deferred'
				).length;
				cancelledSubtasks += task.subtasks.filter(
					(st) => st.status === 'cancelled'
				).length;
				reviewSubtasks += task.subtasks.filter(
					(st) => st.status === 'review'
				).length;
			}
		});

		const subtaskCompletionPercentage =
			totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;

		// Calculate dependency statistics (moved up to be available for all output formats)
		const completedTaskIds = new Set(
			data.tasks
				.filter((t) => t.status === 'done' || t.status === 'completed')
				.map((t) => t.id)
		);

		const tasksWithNoDeps = data.tasks.filter(
			(t) =>
				t.status !== 'done' &&
				t.status !== 'completed' &&
				(!t.dependencies || t.dependencies.length === 0)
		).length;

		const tasksWithAllDepsSatisfied = data.tasks.filter(
			(t) =>
				t.status !== 'done' &&
				t.status !== 'completed' &&
				t.dependencies &&
				t.dependencies.length > 0 &&
				t.dependencies.every((depId) => completedTaskIds.has(depId))
		).length;

		const tasksWithUnsatisfiedDeps = data.tasks.filter(
			(t) =>
				t.status !== 'done' &&
				t.status !== 'completed' &&
				t.dependencies &&
				t.dependencies.length > 0 &&
				!t.dependencies.every((depId) => completedTaskIds.has(depId))
		).length;

		// Calculate total tasks ready to work on (no deps + satisfied deps)
		const tasksReadyToWork = tasksWithNoDeps + tasksWithAllDepsSatisfied;

		// Calculate most depended-on tasks
		const dependencyCount = {};
		data.tasks.forEach((task) => {
			if (task.dependencies && task.dependencies.length > 0) {
				task.dependencies.forEach((depId) => {
					dependencyCount[depId] = (dependencyCount[depId] || 0) + 1;
				});
			}
		});

		// Find the most depended-on task
		let mostDependedOnTaskId = null;
		let maxDependents = 0;

		for (const [taskId, count] of Object.entries(dependencyCount)) {
			if (count > maxDependents) {
				maxDependents = count;
				mostDependedOnTaskId = parseInt(taskId);
			}
		}

		// Get the most depended-on task
		const mostDependedOnTask =
			mostDependedOnTaskId !== null
				? data.tasks.find((t) => t.id === mostDependedOnTaskId)
				: null;

		// Calculate average dependencies per task
		const totalDependencies = data.tasks.reduce(
			(sum, task) => sum + (task.dependencies ? task.dependencies.length : 0),
			0
		);
		const avgDependenciesPerTask = totalDependencies / data.tasks.length;

		// Find next task to work on, passing the complexity report
		const nextItem = findNextTask(data.tasks, complexityReport);

		// For JSON output, return structured data
		if (outputFormat === 'json') {
			// *** Modification: Remove 'details' field for JSON output ***
			const tasksWithoutDetails = filteredTasks.map((task) => {
				// <-- USES filteredTasks!
				// Omit 'details' from the parent task
				const { details, ...taskRest } = task;

				// If subtasks exist, omit 'details' from them too
				if (taskRest.subtasks && Array.isArray(taskRest.subtasks)) {
					taskRest.subtasks = taskRest.subtasks.map((subtask) => {
						const { details: subtaskDetails, ...subtaskRest } = subtask;
						return subtaskRest;
					});
				}
				return taskRest;
			});
			// *** End of Modification ***

			return {
				tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED
				filter: statusFilter || 'all', // Return the actual filter used
				stats: {
					total: totalTasks,
					completed: doneCount,
					inProgress: inProgressCount,
					pending: pendingCount,
					blocked: blockedCount,
					deferred: deferredCount,
					cancelled: cancelledCount,
					review: reviewCount,
					completionPercentage,
					subtasks: {
						total: totalSubtasks,
						completed: completedSubtasks,
						inProgress: inProgressSubtasks,
						pending: pendingSubtasks,
						blocked: blockedSubtasks,
						deferred: deferredSubtasks,
						cancelled: cancelledSubtasks,
						completionPercentage: subtaskCompletionPercentage
					}
				}
			};
		}

		// For markdown-readme output, return formatted markdown
		if (outputFormat === 'markdown-readme') {
			return generateMarkdownOutput(data, filteredTasks, {
				totalTasks,
				completedTasks,
				completionPercentage,
				doneCount,
				inProgressCount,
				pendingCount,
				blockedCount,
				deferredCount,
				cancelledCount,
				totalSubtasks,
				completedSubtasks,
				subtaskCompletionPercentage,
				inProgressSubtasks,
				pendingSubtasks,
				blockedSubtasks,
				deferredSubtasks,
				cancelledSubtasks,
				reviewSubtasks,
				tasksWithNoDeps,
				tasksReadyToWork,
				tasksWithUnsatisfiedDeps,
				mostDependedOnTask,
				mostDependedOnTaskId,
				maxDependents,
				avgDependenciesPerTask,
				complexityReport,
				withSubtasks,
				nextItem
			});
		}

		// ... existing code for text output ...

		// Calculate status breakdowns as percentages of total
		const taskStatusBreakdown = {
			'in-progress': totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0,
			pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0,
			blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0,
			deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0,
			cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0,
			review: totalTasks > 0 ? (reviewCount / totalTasks) * 100 : 0
		};

		const subtaskStatusBreakdown = {
			'in-progress':
				totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0,
			pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0,
			blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0,
			deferred:
				totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0,
			cancelled:
				totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0,
			review: totalSubtasks > 0 ? (reviewSubtasks / totalSubtasks) * 100 : 0
		};

		// Create progress bars with status breakdowns
		const taskProgressBar = createProgressBar(
			completionPercentage,
			30,
			taskStatusBreakdown
		);
		const subtaskProgressBar = createProgressBar(
			subtaskCompletionPercentage,
			30,
			subtaskStatusBreakdown
		);

		// Get terminal width - more reliable method
		let terminalWidth;
		try {
			// Try to get the actual terminal columns
			terminalWidth = process.stdout.columns;
		} catch (e) {
			// Fallback if columns cannot be determined
			log('debug', 'Could not determine terminal width, using default');
		}
		// Ensure we have a reasonable default if detection fails
		terminalWidth = terminalWidth || 80;

		// Ensure terminal width is at least a minimum value to prevent layout issues
		terminalWidth = Math.max(terminalWidth, 80);

		// Create dashboard content
		const projectDashboardContent =
			chalk.white.bold('Project Dashboard') +
			'\n' +
			`Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` +
			`Done: ${chalk.green(doneCount)}  In Progress: ${chalk.blue(inProgressCount)}  Pending: ${chalk.yellow(pendingCount)}  Blocked: ${chalk.red(blockedCount)}  Deferred: ${chalk.gray(deferredCount)}  Cancelled: ${chalk.gray(cancelledCount)}\n\n` +
			`Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` +
			`Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks}  In Progress: ${chalk.blue(inProgressSubtasks)}  Pending: ${chalk.yellow(pendingSubtasks)}  Blocked: ${chalk.red(blockedSubtasks)}  Deferred: ${chalk.gray(deferredSubtasks)}  Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` +
			chalk.cyan.bold('Priority Breakdown:') +
			'\n' +
			`${chalk.red('•')} ${chalk.white('High priority:')} ${data.tasks.filter((t) => t.priority === 'high').length}\n` +
			`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${data.tasks.filter((t) => t.priority === 'medium').length}\n` +
			`${chalk.green('•')} ${chalk.white('Low priority:')} ${data.tasks.filter((t) => t.priority === 'low').length}`;

		const dependencyDashboardContent =
			chalk.white.bold('Dependency Status & Next Task') +
			'\n' +
			chalk.cyan.bold('Dependency Metrics:') +
			'\n' +
			`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${tasksWithNoDeps}\n` +
			`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${tasksReadyToWork}\n` +
			`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${tasksWithUnsatisfiedDeps}\n` +
			`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray('None')}\n` +
			`${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${avgDependenciesPerTask.toFixed(1)}\n\n` +
			chalk.cyan.bold('Next Task to Work On:') +
			'\n' +
			`ID: ${chalk.cyan(nextItem ? nextItem.id : 'N/A')} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow('No task available')}
` +
			`Priority: ${nextItem ? chalk.white(nextItem.priority || 'medium') : ''}  Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ''}
` +
			`Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray('N/A')}`;

		// Calculate width for side-by-side display
		// Box borders, padding take approximately 4 chars on each side
		const minDashboardWidth = 50; // Minimum width for dashboard
		const minDependencyWidth = 50; // Minimum width for dependency dashboard
		const totalMinWidth = minDashboardWidth + minDependencyWidth + 4; // Extra 4 chars for spacing

		// If terminal is wide enough, show boxes side by side with responsive widths
		if (terminalWidth >= totalMinWidth) {
			// Calculate widths proportionally for each box - use exact 50% width each
			const availableWidth = terminalWidth;
			const halfWidth = Math.floor(availableWidth / 2);

			// Account for border characters (2 chars on each side)
			const boxContentWidth = halfWidth - 4;

			// Create boxen options with precise widths
			const dashboardBox = boxen(projectDashboardContent, {
				padding: 1,
				borderColor: 'blue',
				borderStyle: 'round',
				width: boxContentWidth,
				dimBorder: false
			});

			const dependencyBox = boxen(dependencyDashboardContent, {
				padding: 1,
				borderColor: 'magenta',
				borderStyle: 'round',
				width: boxContentWidth,
				dimBorder: false
			});

			// Create a better side-by-side layout with exact spacing
			const dashboardLines = dashboardBox.split('\n');
			const dependencyLines = dependencyBox.split('\n');

			// Make sure both boxes have the same height
			const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);

			// For each line of output, pad the dashboard line to exactly halfWidth chars
			// This ensures the dependency box starts at exactly the right position
			const combinedLines = [];
			for (let i = 0; i < maxHeight; i++) {
				// Get the dashboard line (or empty string if we've run out of lines)
				const dashLine = i < dashboardLines.length ? dashboardLines[i] : '';
				// Get the dependency line (or empty string if we've run out of lines)
				const depLine = i < dependencyLines.length ? dependencyLines[i] : '';

				// Remove any trailing spaces from dashLine before padding to exact width
				const trimmedDashLine = dashLine.trimEnd();
				// Pad the dashboard line to exactly halfWidth chars with no extra spaces
				const paddedDashLine = trimmedDashLine.padEnd(halfWidth, ' ');

				// Join the lines with no space in between
				combinedLines.push(paddedDashLine + depLine);
			}

			// Join all lines and output
			console.log(combinedLines.join('\n'));
		} else {
			// Terminal too narrow, show boxes stacked vertically
			const dashboardBox = boxen(projectDashboardContent, {
				padding: 1,
				borderColor: 'blue',
				borderStyle: 'round',
				margin: { top: 0, bottom: 1 }
			});

			const dependencyBox = boxen(dependencyDashboardContent, {
				padding: 1,
				borderColor: 'magenta',
				borderStyle: 'round',
				margin: { top: 0, bottom: 1 }
			});

			// Display stacked vertically
			console.log(dashboardBox);
			console.log(dependencyBox);
		}

		if (filteredTasks.length === 0) {
			console.log(
				boxen(
					statusFilter
						? chalk.yellow(`No tasks with status '${statusFilter}' found`)
						: chalk.yellow('No tasks found'),
					{ padding: 1, borderColor: 'yellow', borderStyle: 'round' }
				)
			);
			return;
		}

		// COMPLETELY REVISED TABLE APPROACH
		// Define percentage-based column widths and calculate actual widths
		// Adjust percentages based on content type and user requirements

		// Adjust ID width if showing subtasks (subtask IDs are longer: e.g., "1.2")
		const idWidthPct = withSubtasks ? 10 : 7;

		// Calculate max status length to accommodate "in-progress"
		const statusWidthPct = 15;

		// Increase priority column width as requested
		const priorityWidthPct = 12;

		// Make dependencies column smaller as requested (-20%)
		const depsWidthPct = 20;

		const complexityWidthPct = 10;

		// Calculate title/description width as remaining space (+20% from dependencies reduction)
		const titleWidthPct =
			100 -
			idWidthPct -
			statusWidthPct -
			priorityWidthPct -
			depsWidthPct -
			complexityWidthPct;

		// Allow 10 characters for borders and padding
		const availableWidth = terminalWidth - 10;

		// Calculate actual column widths based on percentages
		const idWidth = Math.floor(availableWidth * (idWidthPct / 100));
		const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100));
		const priorityWidth = Math.floor(availableWidth * (priorityWidthPct / 100));
		const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100));
		const complexityWidth = Math.floor(
			availableWidth * (complexityWidthPct / 100)
		);
		const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100));

		// Create a table with correct borders and spacing
		const table = new Table({
			head: [
				chalk.cyan.bold('ID'),
				chalk.cyan.bold('Title'),
				chalk.cyan.bold('Status'),
				chalk.cyan.bold('Priority'),
				chalk.cyan.bold('Dependencies'),
				chalk.cyan.bold('Complexity')
			],
			colWidths: [
				idWidth,
				titleWidth,
				statusWidth,
				priorityWidth,
				depsWidth,
				complexityWidth // Added complexity column width
			],
			style: {
				head: [], // No special styling for header
				border: [], // No special styling for border
				compact: false // Use default spacing
			},
			wordWrap: true,
			wrapOnWordBoundary: true
		});

		// Process tasks for the table
		filteredTasks.forEach((task) => {
			// Format dependencies with status indicators (colored)
			let depText = 'None';
			if (task.dependencies && task.dependencies.length > 0) {
				// Use the proper formatDependenciesWithStatus function for colored status
				depText = formatDependenciesWithStatus(
					task.dependencies,
					data.tasks,
					true,
					complexityReport
				);
			} else {
				depText = chalk.gray('None');
			}

			// Clean up any ANSI codes or confusing characters
			const cleanTitle = task.title.replace(/\n/g, ' ');

			// Get priority color
			const priorityColor =
				{
					high: chalk.red,
					medium: chalk.yellow,
					low: chalk.gray
				}[task.priority || 'medium'] || chalk.white;

			// Format status
			const status = getStatusWithColor(task.status, true);

			// Add the row without truncating dependencies
			table.push([
				task.id.toString(),
				truncate(cleanTitle, titleWidth - 3),
				status,
				priorityColor(truncate(task.priority || 'medium', priorityWidth - 2)),
				depText,
				task.complexityScore
					? getComplexityWithColor(task.complexityScore)
					: chalk.gray('N/A')
			]);

			// Add subtasks if requested
			if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
				task.subtasks.forEach((subtask) => {
					// Format subtask dependencies with status indicators
					let subtaskDepText = 'None';
					if (subtask.dependencies && subtask.dependencies.length > 0) {
						// Handle both subtask-to-subtask and subtask-to-task dependencies
						const formattedDeps = subtask.dependencies
							.map((depId) => {
								// Check if it's a dependency on another subtask
								if (typeof depId === 'number' && depId < 100) {
									const foundSubtask = task.subtasks.find(
										(st) => st.id === depId
									);
									if (foundSubtask) {
										const isDone =
											foundSubtask.status === 'done' ||
											foundSubtask.status === 'completed';
										const isInProgress = foundSubtask.status === 'in-progress';

										// Use consistent color formatting instead of emojis
										if (isDone) {
											return chalk.green.bold(`${task.id}.${depId}`);
										} else if (isInProgress) {
											return chalk.hex('#FFA500').bold(`${task.id}.${depId}`);
										} else {
											return chalk.red.bold(`${task.id}.${depId}`);
										}
									}
								}
								// Default to regular task dependency
								const depTask = data.tasks.find((t) => t.id === depId);
								if (depTask) {
									// Add complexity to depTask before checking status
									addComplexityToTask(depTask, complexityReport);
									const isDone =
										depTask.status === 'done' || depTask.status === 'completed';
									const isInProgress = depTask.status === 'in-progress';
									// Use the same color scheme as in formatDependenciesWithStatus
									if (isDone) {
										return chalk.green.bold(`${depId}`);
									} else if (isInProgress) {
										return chalk.hex('#FFA500').bold(`${depId}`);
									} else {
										return chalk.red.bold(`${depId}`);
									}
								}
								return chalk.cyan(depId.toString());
							})
							.join(', ');

						subtaskDepText = formattedDeps || chalk.gray('None');
					}

					// Add the subtask row without truncating dependencies
					table.push([
						`${task.id}.${subtask.id}`,
						chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`),
						getStatusWithColor(subtask.status, true),
						chalk.dim('-'),
						subtaskDepText,
						subtask.complexityScore
							? chalk.gray(`${subtask.complexityScore}`)
							: chalk.gray('N/A')
					]);
				});
			}
		});

		// Ensure we output the table even if it had to wrap
		try {
			console.log(table.toString());
		} catch (err) {
			log('error', `Error rendering table: ${err.message}`);

			// Fall back to simpler output
			console.log(
				chalk.yellow(
					'\nFalling back to simple task list due to terminal width constraints:'
				)
			);
			filteredTasks.forEach((task) => {
				console.log(
					`${chalk.cyan(task.id)}: ${chalk.white(task.title)} - ${getStatusWithColor(task.status)}`
				);
			});
		}

		// Show filter info if applied
		if (statusFilter) {
			console.log(chalk.yellow(`\nFiltered by status: ${statusFilter}`));
			console.log(
				chalk.yellow(`Showing ${filteredTasks.length} of ${totalTasks} tasks`)
			);
		}

		// Define priority colors
		const priorityColors = {
			high: chalk.red.bold,
			medium: chalk.yellow,
			low: chalk.gray
		};

		// Show next task box in a prominent color
		if (nextItem) {
			// Prepare subtasks section if they exist (Only tasks have .subtasks property)
			let subtasksSection = '';
			// Check if the nextItem is a top-level task before looking for subtasks
			const parentTaskForSubtasks = data.tasks.find(
				(t) => String(t.id) === String(nextItem.id)
			); // Find the original task object
			if (
				parentTaskForSubtasks &&
				parentTaskForSubtasks.subtasks &&
				parentTaskForSubtasks.subtasks.length > 0
			) {
				subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`;
				subtasksSection += parentTaskForSubtasks.subtasks
					.map((subtask) => {
						// Add complexity to subtask before display
						addComplexityToTask(subtask, complexityReport);
						// Using a more simplified format for subtask status display
						const status = subtask.status || 'pending';
						const statusColors = {
							done: chalk.green,
							completed: chalk.green,
							pending: chalk.yellow,
							'in-progress': chalk.blue,
							deferred: chalk.gray,
							blocked: chalk.red,
							cancelled: chalk.gray
						};
						const statusColor =
							statusColors[status.toLowerCase()] || chalk.white;
						// Ensure subtask ID is displayed correctly using parent ID from the original task object
						return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`;
					})
					.join('\n');
			}

			console.log(
				boxen(
					chalk.hex('#FF8800').bold(
						// Use nextItem.id and nextItem.title
						`🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}`
					) +
						'\n\n' +
						// Use nextItem.priority, nextItem.status, nextItem.dependencies
						`${chalk.white('Priority:')} ${priorityColors[nextItem.priority || 'medium'](nextItem.priority || 'medium')}   ${chalk.white('Status:')} ${getStatusWithColor(nextItem.status, true)}\n` +
						`${chalk.white('Dependencies:')} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray('None')}\n\n` +
						// Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this)
						// *** Fetching original item for description and details ***
						`${chalk.white('Description:')} ${getWorkItemDescription(nextItem, data.tasks)}` +
						subtasksSection + // <-- Subtasks are handled above now
						'\n\n' +
						// Use nextItem.id
						`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` +
						// Use nextItem.id
						`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${nextItem.id}`)}`,
					{
						padding: { left: 2, right: 2, top: 1, bottom: 1 },
						borderColor: '#FF8800',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 },
						title: '⚡ RECOMMENDED NEXT TASK ⚡',
						titleAlignment: 'center',
						width: terminalWidth - 4,
						fullscreen: false
					}
				)
			);
		} else {
			console.log(
				boxen(
					chalk.hex('#FF8800').bold('No eligible next task found') +
						'\n\n' +
						'All pending tasks have dependencies that are not yet completed, or all tasks are done.',
					{
						padding: 1,
						borderColor: '#FF8800',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 },
						title: '⚡ NEXT TASK ⚡',
						titleAlignment: 'center',
						width: terminalWidth - 4 // Use full terminal width minus a small margin
					}
				)
			);
		}

		// Show next steps
		console.log(
			boxen(
				chalk.white.bold('Suggested Next Steps:') +
					'\n\n' +
					`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next\n` +
					`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks\n` +
					`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`,
				{
					padding: 1,
					borderColor: 'gray',
					borderStyle: 'round',
					margin: { top: 1 }
				}
			)
		);
	} catch (error) {
		log('error', `Error listing tasks: ${error.message}`);

		if (outputFormat === 'json') {
			// Return structured error for JSON output
			throw {
				code: 'TASK_LIST_ERROR',
				message: error.message,
				details: error.stack
			};
		}

		console.error(chalk.red(`Error: ${error.message}`));
		process.exit(1);
	}
}

// *** Helper function to get description for task or subtask ***
function getWorkItemDescription(item, allTasks) {
	if (!item) return 'N/A';
	if (item.parentId) {
		// It's a subtask
		const parent = allTasks.find((t) => t.id === item.parentId);
		const subtask = parent?.subtasks?.find(
			(st) => `${parent.id}.${st.id}` === item.id
		);
		return subtask?.description || 'No description available.';
	} else {
		// It's a top-level task
		const task = allTasks.find((t) => String(t.id) === String(item.id));
		return task?.description || 'No description available.';
	}
}

/**
 * Generate markdown-formatted output for README files
 * @param {Object} data - Full tasks data
 * @param {Array} filteredTasks - Filtered tasks array
 * @param {Object} stats - Statistics object
 * @returns {string} - Formatted markdown string
 */
function generateMarkdownOutput(data, filteredTasks, stats) {
	const {
		totalTasks,
		completedTasks,
		completionPercentage,
		doneCount,
		inProgressCount,
		pendingCount,
		blockedCount,
		deferredCount,
		cancelledCount,
		totalSubtasks,
		completedSubtasks,
		subtaskCompletionPercentage,
		inProgressSubtasks,
		pendingSubtasks,
		blockedSubtasks,
		deferredSubtasks,
		cancelledSubtasks,
		tasksWithNoDeps,
		tasksReadyToWork,
		tasksWithUnsatisfiedDeps,
		mostDependedOnTask,
		mostDependedOnTaskId,
		maxDependents,
		avgDependenciesPerTask,
		complexityReport,
		withSubtasks,
		nextItem
	} = stats;

	let markdown = '';

	// Create progress bars for markdown (using Unicode block characters)
	const createMarkdownProgressBar = (percentage, width = 20) => {
		const filled = Math.round((percentage / 100) * width);
		const empty = width - filled;
		return '█'.repeat(filled) + '░'.repeat(empty);
	};

	const taskProgressBar = createMarkdownProgressBar(completionPercentage, 20);
	const subtaskProgressBar = createMarkdownProgressBar(
		subtaskCompletionPercentage,
		20
	);

	// Dashboard section
	// markdown += '```\n';
	markdown += '| Project Dashboard |  |\n';
	markdown += '| :-                |:-|\n';
	markdown += `| Task Progress     | ${taskProgressBar} ${Math.round(completionPercentage)}% |\n`;
	markdown += `| Done | ${doneCount} |\n`;
	markdown += `| In Progress | ${inProgressCount} |\n`;
	markdown += `| Pending | ${pendingCount} |\n`;
	markdown += `| Deferred | ${deferredCount} |\n`;
	markdown += `| Cancelled | ${cancelledCount} |\n`;
	markdown += `|-|-|\n`;
	markdown += `| Subtask Progress | ${subtaskProgressBar} ${Math.round(subtaskCompletionPercentage)}% |\n`;
	markdown += `| Completed | ${completedSubtasks} |\n`;
	markdown += `| In Progress | ${inProgressSubtasks} |\n`;
	markdown += `| Pending | ${pendingSubtasks} |\n`;

	markdown += '\n\n';

	// Tasks table
	markdown +=
		'| ID | Title | Status | Priority | Dependencies | Complexity |\n';
	markdown +=
		'| :- | :-    | :-     | :-       | :-           | :-         |\n';

	// Helper function to format status with symbols
	const getStatusSymbol = (status) => {
		switch (status) {
			case 'done':
			case 'completed':
				return '✓ done';
			case 'in-progress':
				return '► in-progress';
			case 'pending':
				return '○ pending';
			case 'blocked':
				return '⭕ blocked';
			case 'deferred':
				return 'x deferred';
			case 'cancelled':
				return 'x cancelled';
			case 'review':
				return '? review';
			default:
				return status || 'pending';
		}
	};

	// Helper function to format dependencies without color codes
	const formatDependenciesForMarkdown = (deps, allTasks) => {
		if (!deps || deps.length === 0) return 'None';
		return deps
			.map((depId) => {
				const depTask = allTasks.find((t) => t.id === depId);
				return depTask ? depId.toString() : depId.toString();
			})
			.join(', ');
	};

	// Process all tasks
	filteredTasks.forEach((task) => {
		const taskTitle = task.title; // No truncation for README
		const statusSymbol = getStatusSymbol(task.status);
		const priority = task.priority || 'medium';
		const deps = formatDependenciesForMarkdown(task.dependencies, data.tasks);
		const complexity = task.complexityScore
			? `● ${task.complexityScore}`
			: 'N/A';

		markdown += `| ${task.id} | ${taskTitle} | ${statusSymbol} | ${priority} | ${deps} | ${complexity} |\n`;

		// Add subtasks if requested
		if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
			task.subtasks.forEach((subtask) => {
				const subtaskTitle = `${subtask.title}`; // No truncation
				const subtaskStatus = getStatusSymbol(subtask.status);
				const subtaskDeps = formatDependenciesForMarkdown(
					subtask.dependencies,
					data.tasks
				);
				const subtaskComplexity = subtask.complexityScore
					? subtask.complexityScore.toString()
					: 'N/A';

				markdown += `| ${task.id}.${subtask.id} | ${subtaskTitle} | ${subtaskStatus} | -            | ${subtaskDeps} | ${subtaskComplexity} |\n`;
			});
		}
	});

	return markdown;
}

export default listTasks;

import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import { z } from 'zod'; // Keep Zod for post-parsing validation

import {
	log as consoleLog,
	readJSON,
	writeJSON,
	truncate,
	isSilentMode
} from '../utils.js';

import {
	getStatusWithColor,
	startLoadingIndicator,
	stopLoadingIndicator,
	displayAiUsageSummary
} from '../ui.js';

import { getDebugFlag } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { generateTextService } from '../ai-services-unified.js';
import { getModelConfiguration } from './models.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';

// Zod schema for validating the structure of tasks AFTER parsing
const updatedTaskSchema = z
	.object({
		id: z.number().int(),
		title: z.string(),
		description: z.string(),
		status: z.string(),
		dependencies: z.array(z.union([z.number().int(), z.string()])),
		priority: z.string().nullable(),
		details: z.string().nullable(),
		testStrategy: z.string().nullable(),
		subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now
	})
	.strip(); // Allow potential extra fields during parsing if needed, then validate structure

// Preprocessing schema that adds defaults before validation
const preprocessTaskSchema = z.preprocess((task) => {
	// Ensure task is an object
	if (typeof task !== 'object' || task === null) {
		return {};
	}

	// Return task with defaults for missing fields
	return {
		...task,
		// Add defaults for required fields if missing
		id: task.id ?? 0,
		title: task.title ?? 'Untitled Task',
		description: task.description ?? '',
		status: task.status ?? 'pending',
		dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
		// Optional fields - preserve undefined/null distinction
		priority: task.hasOwnProperty('priority') ? task.priority : null,
		details: task.hasOwnProperty('details') ? task.details : null,
		testStrategy: task.hasOwnProperty('testStrategy')
			? task.testStrategy
			: null,
		subtasks: Array.isArray(task.subtasks)
			? task.subtasks
			: task.subtasks === null
				? null
				: []
	};
}, updatedTaskSchema);

const updatedTaskArraySchema = z.array(updatedTaskSchema);
const preprocessedTaskArraySchema = z.array(preprocessTaskSchema);

/**
 * Parses an array of task objects from AI's text response.
 * @param {string} text - Response text from AI.
 * @param {number} expectedCount - Expected number of tasks.
 * @param {Function | Object} logFn - The logging function or MCP log object.
 * @param {boolean} isMCP - Flag indicating if logFn is MCP logger.
 * @returns {Array} Parsed and validated tasks array.
 * @throws {Error} If parsing or validation fails.
 */
function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
	const report = (level, ...args) => {
		if (isMCP) {
			if (typeof logFn[level] === 'function') logFn[level](...args);
			else logFn.info(...args);
		} else if (!isSilentMode()) {
			// Check silent mode for consoleLog
			consoleLog(level, ...args);
		}
	};

	report(
		'info',
		'Attempting to parse updated tasks array from text response...'
	);
	if (!text || text.trim() === '')
		throw new Error('AI response text is empty.');

	let cleanedResponse = text.trim();
	const originalResponseForDebug = cleanedResponse;
	let parseMethodUsed = 'raw'; // Track which method worked

	// --- NEW Step 1: Try extracting between [] first ---
	const firstBracketIndex = cleanedResponse.indexOf('[');
	const lastBracketIndex = cleanedResponse.lastIndexOf(']');
	let potentialJsonFromArray = null;

	if (firstBracketIndex !== -1 && lastBracketIndex > firstBracketIndex) {
		potentialJsonFromArray = cleanedResponse.substring(
			firstBracketIndex,
			lastBracketIndex + 1
		);
		// Basic check to ensure it's not just "[]" or malformed
		if (potentialJsonFromArray.length <= 2) {
			potentialJsonFromArray = null; // Ignore empty array
		}
	}

	// If [] extraction yielded something, try parsing it immediately
	if (potentialJsonFromArray) {
		try {
			const testParse = JSON.parse(potentialJsonFromArray);
			// It worked! Use this as the primary cleaned response.
			cleanedResponse = potentialJsonFromArray;
			parseMethodUsed = 'brackets';
		} catch (e) {
			report(
				'info',
				'Content between [] looked promising but failed initial parse. Proceeding to other methods.'
			);
			// Reset cleanedResponse to original if bracket parsing failed
			cleanedResponse = originalResponseForDebug;
		}
	}

	// --- Step 2: If bracket parsing didn't work or wasn't applicable, try code block extraction ---
	if (parseMethodUsed === 'raw') {
		// Only look for ```json blocks now
		const codeBlockMatch = cleanedResponse.match(
			/```json\s*([\s\S]*?)\s*```/i // Only match ```json
		);
		if (codeBlockMatch) {
			cleanedResponse = codeBlockMatch[1].trim();
			parseMethodUsed = 'codeblock';
			report('info', 'Extracted JSON content from JSON Markdown code block.');
		} else {
			report('info', 'No JSON code block found.');
			// --- Step 3: If code block failed, try stripping prefixes ---
			const commonPrefixes = [
				'json\n',
				'javascript\n', // Keep checking common prefixes just in case
				'python\n',
				'here are the updated tasks:',
				'here is the updated json:',
				'updated tasks:',
				'updated json:',
				'response:',
				'output:'
			];
			let prefixFound = false;
			for (const prefix of commonPrefixes) {
				if (cleanedResponse.toLowerCase().startsWith(prefix)) {
					cleanedResponse = cleanedResponse.substring(prefix.length).trim();
					parseMethodUsed = 'prefix';
					report('info', `Stripped prefix: "${prefix.trim()}"`);
					prefixFound = true;
					break;
				}
			}
			if (!prefixFound) {
				report(
					'warn',
					'Response does not appear to contain [], JSON code block, or known prefix. Attempting raw parse.'
				);
			}
		}
	}

	// --- Step 4: Attempt final parse ---
	let parsedTasks;
	try {
		parsedTasks = JSON.parse(cleanedResponse);
	} catch (parseError) {
		report('error', `Failed to parse JSON array: ${parseError.message}`);
		report(
			'error',
			`Extraction method used: ${parseMethodUsed}` // Log which method failed
		);
		report(
			'error',
			`Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}`
		);
		report(
			'error',
			`Original Raw Response (first 500 chars): ${originalResponseForDebug.substring(0, 500)}`
		);
		throw new Error(
			`Failed to parse JSON response array: ${parseError.message}`
		);
	}

	// --- Step 5 & 6: Validate Array structure and Zod schema ---
	if (!Array.isArray(parsedTasks)) {
		report(
			'error',
			`Parsed content is not an array. Type: ${typeof parsedTasks}`
		);
		report(
			'error',
			`Parsed content sample: ${JSON.stringify(parsedTasks).substring(0, 200)}`
		);
		throw new Error('Parsed AI response is not a valid JSON array.');
	}

	report('info', `Successfully parsed ${parsedTasks.length} potential tasks.`);
	if (expectedCount && parsedTasks.length !== expectedCount) {
		report(
			'warn',
			`Expected ${expectedCount} tasks, but parsed ${parsedTasks.length}.`
		);
	}

	// Log missing fields for debugging before preprocessing
	let hasWarnings = false;
	parsedTasks.forEach((task, index) => {
		const missingFields = [];
		if (!task.hasOwnProperty('id')) missingFields.push('id');
		if (!task.hasOwnProperty('status')) missingFields.push('status');
		if (!task.hasOwnProperty('dependencies'))
			missingFields.push('dependencies');

		if (missingFields.length > 0) {
			hasWarnings = true;
			report(
				'warn',
				`Task ${index} is missing fields: ${missingFields.join(', ')} - will use defaults`
			);
		}
	});

	if (hasWarnings) {
		report(
			'warn',
			'Some tasks were missing required fields. Applying defaults...'
		);
	}

	// Use the preprocessing schema to add defaults and validate
	const preprocessResult = preprocessedTaskArraySchema.safeParse(parsedTasks);

	if (!preprocessResult.success) {
		// This should rarely happen now since preprocessing adds defaults
		report('error', 'Failed to validate task array even after preprocessing.');
		preprocessResult.error.errors.forEach((err) => {
			report('error', `  - Path '${err.path.join('.')}': ${err.message}`);
		});

		throw new Error(
			`AI response failed validation: ${preprocessResult.error.message}`
		);
	}

	report('info', 'Successfully validated and transformed task structure.');
	return preprocessResult.data.slice(
		0,
		expectedCount || preprocessResult.data.length
	);
}

/**
 * Update tasks based on new context using the unified AI service.
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {number} fromId - Task ID to start updating from
 * @param {string} prompt - Prompt with new context
 * @param {boolean} [useResearch=false] - Whether to use the research AI role.
 * @param {Object} context - Context object containing session and mcpLog.
 * @param {Object} [context.session] - Session object from MCP server.
 * @param {Object} [context.mcpLog] - MCP logger object.
 * @param {string} [context.tag] - Tag for the task
 * @param {string} [outputFormat='text'] - Output format ('text' or 'json').
 */
async function updateTasks(
	tasksPath,
	fromId,
	prompt,
	useResearch = false,
	context = {},
	outputFormat = 'text' // Default to text for CLI
) {
	const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
	// Use mcpLog if available, otherwise use the imported consoleLog function
	const logFn = mcpLog || consoleLog;
	// Flag to easily check which logger type we have
	const isMCP = !!mcpLog;

	if (isMCP)
		logFn.info(`updateTasks called with context: session=${!!session}`);
	else logFn('info', `updateTasks called`); // CLI log

	try {
		if (isMCP) logFn.info(`Updating tasks from ID ${fromId}`);
		else
			logFn(
				'info',
				`Updating tasks from ID ${fromId} with prompt: "${prompt}"`
			);

		// Determine project root
		const projectRoot = providedProjectRoot || findProjectRoot();
		if (!projectRoot) {
			throw new Error('Could not determine project root directory');
		}

		// --- Task Loading/Filtering (Updated to pass projectRoot and tag) ---
		const data = readJSON(tasksPath, projectRoot, tag);
		if (!data || !data.tasks)
			throw new Error(`No valid tasks found in ${tasksPath}`);
		const tasksToUpdate = data.tasks.filter(
			(task) => task.id >= fromId && task.status !== 'done'
		);
		if (tasksToUpdate.length === 0) {
			if (isMCP)
				logFn.info(`No tasks to update (ID >= ${fromId} and not 'done').`);
			else
				logFn('info', `No tasks to update (ID >= ${fromId} and not 'done').`);
			if (outputFormat === 'text') console.log(/* yellow message */);
			return; // Nothing to do
		}
		// --- End Task Loading/Filtering ---

		// --- Context Gathering ---
		let gatheredContext = '';
		try {
			const contextGatherer = new ContextGatherer(projectRoot, tag);
			const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
			const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update');
			const searchResults = fuzzySearch.findRelevantTasks(prompt, {
				maxResults: 5,
				includeSelf: true
			});
			const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);

			const tasksToUpdateIds = tasksToUpdate.map((t) => t.id.toString());
			const finalTaskIds = [
				...new Set([...tasksToUpdateIds, ...relevantTaskIds])
			];

			if (finalTaskIds.length > 0) {
				const contextResult = await contextGatherer.gather({
					tasks: finalTaskIds,
					format: 'research'
				});
				gatheredContext = contextResult.context || '';
			}
		} catch (contextError) {
			logFn(
				'warn',
				`Could not gather additional context: ${contextError.message}`
			);
		}
		// --- End Context Gathering ---

		// --- Display Tasks to Update (CLI Only - Unchanged) ---
		if (outputFormat === 'text') {
			// Show the tasks that will be updated
			const table = new Table({
				head: [
					chalk.cyan.bold('ID'),
					chalk.cyan.bold('Title'),
					chalk.cyan.bold('Status')
				],
				colWidths: [5, 70, 20]
			});

			tasksToUpdate.forEach((task) => {
				table.push([
					task.id,
					truncate(task.title, 57),
					getStatusWithColor(task.status)
				]);
			});

			console.log(
				boxen(chalk.white.bold(`Updating ${tasksToUpdate.length} tasks`), {
					padding: 1,
					borderColor: 'blue',
					borderStyle: 'round',
					margin: { top: 1, bottom: 0 }
				})
			);

			console.log(table.toString());

			// Display a message about how completed subtasks are handled
			console.log(
				boxen(
					chalk.cyan.bold('How Completed Subtasks Are Handled:') +
						'\n\n' +
						chalk.white(
							'• Subtasks marked as "done" or "completed" will be preserved\n'
						) +
						chalk.white(
							'• New subtasks will build upon what has already been completed\n'
						) +
						chalk.white(
							'• If completed work needs revision, a new subtask will be created instead of modifying done items\n'
						) +
						chalk.white(
							'• This approach maintains a clear record of completed work and new requirements'
						),
					{
						padding: 1,
						borderColor: 'blue',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 }
					}
				)
			);
		}
		// --- End Display Tasks ---

		// --- Build Prompts (Using PromptManager) ---
		// Load prompts using PromptManager
		const promptManager = getPromptManager();
		const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
			'update-tasks',
			{
				tasks: tasksToUpdate,
				updatePrompt: prompt,
				useResearch,
				projectContext: gatheredContext
			}
		);
		// --- End Build Prompts ---

		// --- AI Call ---
		let loadingIndicator = null;
		let aiServiceResponse = null;

		if (!isMCP && outputFormat === 'text') {
			loadingIndicator = startLoadingIndicator('Updating tasks with AI...\n');
		}

		try {
			// Determine role based on research flag
			const serviceRole = useResearch ? 'research' : 'main';

			// Call the unified AI service
			aiServiceResponse = await generateTextService({
				role: serviceRole,
				session: session,
				projectRoot: projectRoot,
				systemPrompt: systemPrompt,
				prompt: userPrompt,
				commandName: 'update-tasks',
				outputType: isMCP ? 'mcp' : 'cli'
			});

			if (loadingIndicator)
				stopLoadingIndicator(loadingIndicator, 'AI update complete.');

			// Use the mainResult (text) for parsing
			const parsedUpdatedTasks = parseUpdatedTasksFromText(
				aiServiceResponse.mainResult,
				tasksToUpdate.length,
				logFn,
				isMCP
			);

			// --- Update Tasks Data (Updated writeJSON call) ---
			if (!Array.isArray(parsedUpdatedTasks)) {
				// Should be caught by parser, but extra check
				throw new Error(
					'Parsed AI response for updated tasks was not an array.'
				);
			}
			if (isMCP)
				logFn.info(
					`Received ${parsedUpdatedTasks.length} updated tasks from AI.`
				);
			else
				logFn(
					'info',
					`Received ${parsedUpdatedTasks.length} updated tasks from AI.`
				);
			// Create a map for efficient lookup
			const updatedTasksMap = new Map(
				parsedUpdatedTasks.map((task) => [task.id, task])
			);

			let actualUpdateCount = 0;
			data.tasks.forEach((task, index) => {
				if (updatedTasksMap.has(task.id)) {
					// Only update if the task was part of the set sent to AI
					const updatedTask = updatedTasksMap.get(task.id);
					// Merge the updated task with the existing one to preserve fields like subtasks
					data.tasks[index] = {
						...task, // Keep all existing fields
						...updatedTask, // Override with updated fields
						// Ensure subtasks field is preserved if not provided by AI
						subtasks:
							updatedTask.subtasks !== undefined
								? updatedTask.subtasks
								: task.subtasks
					};
					actualUpdateCount++;
				}
			});
			if (isMCP)
				logFn.info(
					`Applied updates to ${actualUpdateCount} tasks in the dataset.`
				);
			else
				logFn(
					'info',
					`Applied updates to ${actualUpdateCount} tasks in the dataset.`
				);

			// Fix: Pass projectRoot and currentTag to writeJSON
			writeJSON(tasksPath, data, projectRoot, tag);
			if (isMCP)
				logFn.info(
					`Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
				);
			else
				logFn(
					'success',
					`Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
				);
			// await generateTaskFiles(tasksPath, path.dirname(tasksPath));

			if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
				displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
			}

			return {
				success: true,
				updatedTasks: parsedUpdatedTasks,
				telemetryData: aiServiceResponse.telemetryData,
				tagInfo: aiServiceResponse.tagInfo
			};
		} catch (error) {
			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
			if (isMCP) logFn.error(`Error during AI service call: ${error.message}`);
			else logFn('error', `Error during AI service call: ${error.message}`);
			if (error.message.includes('API key')) {
				if (isMCP)
					logFn.error(
						'Please ensure API keys are configured correctly in .env or mcp.json.'
					);
				else
					logFn(
						'error',
						'Please ensure API keys are configured correctly in .env or mcp.json.'
					);
			}
			throw error;
		} finally {
			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
		}
	} catch (error) {
		// --- General Error Handling (Unchanged) ---
		if (isMCP) logFn.error(`Error updating tasks: ${error.message}`);
		else logFn('error', `Error updating tasks: ${error.message}`);
		if (outputFormat === 'text') {
			console.error(chalk.red(`Error: ${error.message}`));
			if (getDebugFlag(session)) {
				console.error(error);
			}
			process.exit(1);
		} else {
			throw error; // Re-throw for MCP/programmatic callers
		}
		// --- End General Error Handling ---
	}
}

export default updateTasks;

/**
 * commands.js
 * Command-line interface for the Task Master CLI
 */

import { program } from 'commander';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import fs from 'fs';
import https from 'https';
import http from 'http';
import inquirer from 'inquirer';
import search from '@inquirer/search';
import ora from 'ora'; // Import ora

import {
	log,
	readJSON,
	writeJSON,
	getCurrentTag,
	detectCamelCaseFlags,
	toKebabCase
} from './utils.js';
import {
	parsePRD,
	updateTasks,
	generateTaskFiles,
	setTaskStatus,
	listTasks,
	expandTask,
	expandAllTasks,
	clearSubtasks,
	addTask,
	addSubtask,
	removeSubtask,
	analyzeTaskComplexity,
	updateTaskById,
	updateSubtaskById,
	removeTask,
	findTaskById,
	taskExists,
	moveTask,
	migrateProject,
	setResponseLanguage
} from './task-manager.js';

import {
	createTag,
	deleteTag,
	tags,
	useTag,
	renameTag,
	copyTag
} from './task-manager/tag-management.js';

import {
	addDependency,
	removeDependency,
	validateDependenciesCommand,
	fixDependenciesCommand
} from './dependency-manager.js';

import {
	isApiKeySet,
	getDebugFlag,
	getConfig,
	writeConfig,
	ConfigurationError,
	isConfigFilePresent,
	getAvailableModels,
	getBaseUrlForRole,
	getDefaultNumTasks,
	getDefaultSubtasks
} from './config-manager.js';

import { CUSTOM_PROVIDERS } from '../../src/constants/providers.js';

import {
	COMPLEXITY_REPORT_FILE,
	TASKMASTER_TASKS_FILE,
	TASKMASTER_DOCS_DIR
} from '../../src/constants/paths.js';

import { initTaskMaster } from '../../src/task-master.js';

import {
	displayBanner,
	displayHelp,
	displayNextTask,
	displayTaskById,
	displayComplexityReport,
	getStatusWithColor,
	confirmTaskOverwrite,
	startLoadingIndicator,
	stopLoadingIndicator,
	displayModelConfiguration,
	displayAvailableModels,
	displayApiKeyStatus,
	displayAiUsageSummary,
	displayMultipleTasksSummary,
	displayTaggedTasksFYI,
	displayCurrentTagIndicator
} from './ui.js';
import {
	confirmProfilesRemove,
	confirmRemoveAllRemainingProfiles
} from '../../src/ui/confirm.js';
import {
	wouldRemovalLeaveNoProfiles,
	getInstalledProfiles
} from '../../src/utils/profiles.js';

import { initializeProject } from '../init.js';
import {
	getModelConfiguration,
	getAvailableModelsList,
	setModel,
	getApiKeyStatusReport
} from './task-manager/models.js';
import {
	isValidTaskStatus,
	TASK_STATUS_OPTIONS
} from '../../src/constants/task-status.js';
import {
	isValidRulesAction,
	RULES_ACTIONS,
	RULES_SETUP_ACTION
} from '../../src/constants/rules-actions.js';
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
import { syncTasksToReadme } from './sync-readme.js';
import { RULE_PROFILES } from '../../src/constants/profiles.js';
import {
	convertAllRulesToProfileRules,
	removeProfileRules,
	isValidProfile,
	getRulesProfile
} from '../../src/utils/rule-transformer.js';
import {
	runInteractiveProfilesSetup,
	generateProfileSummary,
	categorizeProfileResults,
	generateProfileRemovalSummary,
	categorizeRemovalResults
} from '../../src/utils/profiles.js';

/**
 * Runs the interactive setup process for model configuration.
 * @param {string|null} projectRoot - The resolved project root directory.
 */
async function runInteractiveSetup(projectRoot) {
	if (!projectRoot) {
		console.error(
			chalk.red(
				'Error: Could not determine project root for interactive setup.'
			)
		);
		process.exit(1);
	}

	const currentConfigResult = await getModelConfiguration({ projectRoot });
	const currentModels = currentConfigResult.success
		? currentConfigResult.data.activeModels
		: { main: null, research: null, fallback: null };
	// Handle potential config load failure gracefully for the setup flow
	if (
		!currentConfigResult.success &&
		currentConfigResult.error?.code !== 'CONFIG_MISSING'
	) {
		console.warn(
			chalk.yellow(
				`Warning: Could not load current model configuration: ${currentConfigResult.error?.message || 'Unknown error'}. Proceeding with defaults.`
			)
		);
	}

	// Helper function to fetch OpenRouter models (duplicated for CLI context)
	function fetchOpenRouterModelsCLI() {
		return new Promise((resolve) => {
			const options = {
				hostname: 'openrouter.ai',
				path: '/api/v1/models',
				method: 'GET',
				headers: {
					Accept: 'application/json'
				}
			};

			const req = https.request(options, (res) => {
				let data = '';
				res.on('data', (chunk) => {
					data += chunk;
				});
				res.on('end', () => {
					if (res.statusCode === 200) {
						try {
							const parsedData = JSON.parse(data);
							resolve(parsedData.data || []); // Return the array of models
						} catch (e) {
							console.error('Error parsing OpenRouter response:', e);
							resolve(null); // Indicate failure
						}
					} else {
						console.error(
							`OpenRouter API request failed with status code: ${res.statusCode}`
						);
						resolve(null); // Indicate failure
					}
				});
			});

			req.on('error', (e) => {
				console.error('Error fetching OpenRouter models:', e);
				resolve(null); // Indicate failure
			});
			req.end();
		});
	}

	// Helper function to fetch Ollama models (duplicated for CLI context)
	function fetchOllamaModelsCLI(baseURL = 'http://localhost:11434/api') {
		return new Promise((resolve) => {
			try {
				// Parse the base URL to extract hostname, port, and base path
				const url = new URL(baseURL);
				const isHttps = url.protocol === 'https:';
				const port = url.port || (isHttps ? 443 : 80);
				const basePath = url.pathname.endsWith('/')
					? url.pathname.slice(0, -1)
					: url.pathname;

				const options = {
					hostname: url.hostname,
					port: parseInt(port, 10),
					path: `${basePath}/tags`,
					method: 'GET',
					headers: {
						Accept: 'application/json'
					}
				};

				const requestLib = isHttps ? https : http;
				const req = requestLib.request(options, (res) => {
					let data = '';
					res.on('data', (chunk) => {
						data += chunk;
					});
					res.on('end', () => {
						if (res.statusCode === 200) {
							try {
								const parsedData = JSON.parse(data);
								resolve(parsedData.models || []); // Return the array of models
							} catch (e) {
								console.error('Error parsing Ollama response:', e);
								resolve(null); // Indicate failure
							}
						} else {
							console.error(
								`Ollama API request failed with status code: ${res.statusCode}`
							);
							resolve(null); // Indicate failure
						}
					});
				});

				req.on('error', (e) => {
					console.error('Error fetching Ollama models:', e);
					resolve(null); // Indicate failure
				});
				req.end();
			} catch (e) {
				console.error('Error parsing Ollama base URL:', e);
				resolve(null); // Indicate failure
			}
		});
	}

	// Helper to get choices and default index for a role
	const getPromptData = (role, allowNone = false) => {
		const currentModel = currentModels[role]; // Use the fetched data
		const allModelsRaw = getAvailableModels(); // Get all available models

		// Manually group models by provider
		const modelsByProvider = allModelsRaw.reduce((acc, model) => {
			if (!acc[model.provider]) {
				acc[model.provider] = [];
			}
			acc[model.provider].push(model);
			return acc;
		}, {});

		const cancelOption = { name: '⏹ Cancel Model Setup', value: '__CANCEL__' }; // Symbol updated
		const noChangeOption = currentModel?.modelId
			? {
					name: `✔ No change to current ${role} model (${currentModel.modelId})`, // Symbol updated
					value: '__NO_CHANGE__'
				}
			: null;

		// Define custom provider options
		const customProviderOptions = [
			{ name: '* Custom OpenRouter model', value: '__CUSTOM_OPENROUTER__' },
			{ name: '* Custom Ollama model', value: '__CUSTOM_OLLAMA__' },
			{ name: '* Custom Bedrock model', value: '__CUSTOM_BEDROCK__' },
			{ name: '* Custom Azure model', value: '__CUSTOM_AZURE__' },
			{ name: '* Custom Vertex model', value: '__CUSTOM_VERTEX__' }
		];

		let choices = [];
		let defaultIndex = 0; // Default to 'Cancel'

		// Filter and format models allowed for this role using the manually grouped data
		const roleChoices = Object.entries(modelsByProvider)
			.map(([provider, models]) => {
				const providerModels = models
					.filter((m) => m.allowed_roles.includes(role))
					.map((m) => ({
						name: `${provider} / ${m.id} ${
							m.cost_per_1m_tokens
								? chalk.gray(
										`($${m.cost_per_1m_tokens.input.toFixed(2)} input | $${m.cost_per_1m_tokens.output.toFixed(2)} output)`
									)
								: ''
						}`,
						value: { id: m.id, provider },
						short: `${provider}/${m.id}`
					}));
				if (providerModels.length > 0) {
					return [...providerModels];
				}
				return null;
			})
			.filter(Boolean)
			.flat();

		// Find the index of the currently selected model for setting the default
		let currentChoiceIndex = -1;
		if (currentModel?.modelId && currentModel?.provider) {
			currentChoiceIndex = roleChoices.findIndex(
				(choice) =>
					typeof choice.value === 'object' &&
					choice.value.id === currentModel.modelId &&
					choice.value.provider === currentModel.provider
			);
		}

		// Construct final choices list with custom options moved to bottom
		const systemOptions = [];
		if (noChangeOption) {
			systemOptions.push(noChangeOption);
		}
		systemOptions.push(cancelOption);

		const systemLength = systemOptions.length;

		if (allowNone) {
			choices = [
				...systemOptions,
				new inquirer.Separator('\n── Standard Models ──'),
				{ name: '⚪ None (disable)', value: null },
				...roleChoices,
				new inquirer.Separator('\n── Custom Providers ──'),
				...customProviderOptions
			];
			// Adjust default index: System + Sep1 + None (+2)
			const noneOptionIndex = systemLength + 1;
			defaultIndex =
				currentChoiceIndex !== -1
					? currentChoiceIndex + systemLength + 2 // Offset by system options and separators
					: noneOptionIndex; // Default to 'None' if no current model matched
		} else {
			choices = [
				...systemOptions,
				new inquirer.Separator('\n── Standard Models ──'),
				...roleChoices,
				new inquirer.Separator('\n── Custom Providers ──'),
				...customProviderOptions
			];
			// Adjust default index: System + Sep (+1)
			defaultIndex =
				currentChoiceIndex !== -1
					? currentChoiceIndex + systemLength + 1 // Offset by system options and separator
					: noChangeOption
						? 1
						: 0; // Default to 'No Change' if present, else 'Cancel'
		}

		// Ensure defaultIndex is valid within the final choices array length
		if (defaultIndex < 0 || defaultIndex >= choices.length) {
			// If default calculation failed or pointed outside bounds, reset intelligently
			defaultIndex = 0; // Default to 'Cancel'
			console.warn(
				`Warning: Could not determine default model for role '${role}'. Defaulting to 'Cancel'.`
			); // Add warning
		}

		return { choices, default: defaultIndex };
	};

	// --- Generate choices using the helper ---
	const mainPromptData = getPromptData('main');
	const researchPromptData = getPromptData('research');
	const fallbackPromptData = getPromptData('fallback', true); // Allow 'None' for fallback

	// Display helpful intro message
	console.log(chalk.cyan('\n🎯 Interactive Model Setup'));
	console.log(chalk.gray('━'.repeat(50)));
	console.log(chalk.yellow('💡 Navigation tips:'));
	console.log(chalk.gray('   • Type to search and filter options'));
	console.log(chalk.gray('   • Use ↑↓ arrow keys to navigate results'));
	console.log(
		chalk.gray(
			'   • Standard models are listed first, custom providers at bottom'
		)
	);
	console.log(chalk.gray('   • Press Enter to select\n'));

	// Helper function to create search source for models
	const createSearchSource = (choices, defaultValue) => {
		return (searchTerm = '') => {
			const filteredChoices = choices.filter((choice) => {
				if (choice.type === 'separator') return true; // Always show separators
				const searchText = choice.name || '';
				return searchText.toLowerCase().includes(searchTerm.toLowerCase());
			});
			return Promise.resolve(filteredChoices);
		};
	};

	const answers = {};

	// Main model selection
	answers.mainModel = await search({
		message: 'Select the main model for generation/updates:',
		source: createSearchSource(mainPromptData.choices, mainPromptData.default),
		pageSize: 15
	});

	if (answers.mainModel !== '__CANCEL__') {
		// Research model selection
		answers.researchModel = await search({
			message: 'Select the research model:',
			source: createSearchSource(
				researchPromptData.choices,
				researchPromptData.default
			),
			pageSize: 15
		});

		if (answers.researchModel !== '__CANCEL__') {
			// Fallback model selection
			answers.fallbackModel = await search({
				message: 'Select the fallback model (optional):',
				source: createSearchSource(
					fallbackPromptData.choices,
					fallbackPromptData.default
				),
				pageSize: 15
			});
		}
	}

	let setupSuccess = true;
	let setupConfigModified = false;
	const coreOptionsSetup = { projectRoot }; // Pass root for setup actions

	// Helper to handle setting a model (including custom)
	async function handleSetModel(role, selectedValue, currentModelId) {
		if (selectedValue === '__CANCEL__') {
			console.log(
				chalk.yellow(`\nSetup canceled during ${role} model selection.`)
			);
			setupSuccess = false; // Also mark success as false on cancel
			return false; // Indicate cancellation
		}

		// Handle the new 'No Change' option
		if (selectedValue === '__NO_CHANGE__') {
			console.log(chalk.gray(`No change selected for ${role} model.`));
			return true; // Indicate success, continue setup
		}

		let modelIdToSet = null;
		let providerHint = null;
		let isCustomSelection = false;

		if (selectedValue === '__CUSTOM_OPENROUTER__') {
			isCustomSelection = true;
			const { customId } = await inquirer.prompt([
				{
					type: 'input',
					name: 'customId',
					message: `Enter the custom OpenRouter Model ID for the ${role} role:`
				}
			]);
			if (!customId) {
				console.log(chalk.yellow('No custom ID entered. Skipping role.'));
				return true; // Continue setup, but don't set this role
			}
			modelIdToSet = customId;
			providerHint = CUSTOM_PROVIDERS.OPENROUTER;
			// Validate against live OpenRouter list
			const openRouterModels = await fetchOpenRouterModelsCLI();
			if (
				!openRouterModels ||
				!openRouterModels.some((m) => m.id === modelIdToSet)
			) {
				console.error(
					chalk.red(
						`Error: Model ID "${modelIdToSet}" not found in the live OpenRouter model list. Please check the ID.`
					)
				);
				setupSuccess = false;
				return true; // Continue setup, but mark as failed
			}
		} else if (selectedValue === '__CUSTOM_OLLAMA__') {
			isCustomSelection = true;
			const { customId } = await inquirer.prompt([
				{
					type: 'input',
					name: 'customId',
					message: `Enter the custom Ollama Model ID for the ${role} role:`
				}
			]);
			if (!customId) {
				console.log(chalk.yellow('No custom ID entered. Skipping role.'));
				return true; // Continue setup, but don't set this role
			}
			modelIdToSet = customId;
			providerHint = CUSTOM_PROVIDERS.OLLAMA;
			// Get the Ollama base URL from config for this role
			const ollamaBaseURL = getBaseUrlForRole(role, projectRoot);
			// Validate against live Ollama list
			const ollamaModels = await fetchOllamaModelsCLI(ollamaBaseURL);
			if (ollamaModels === null) {
				console.error(
					chalk.red(
						`Error: Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.`
					)
				);
				setupSuccess = false;
				return true; // Continue setup, but mark as failed
			} else if (!ollamaModels.some((m) => m.model === modelIdToSet)) {
				console.error(
					chalk.red(
						`Error: Model ID "${modelIdToSet}" not found in the Ollama instance. Please verify the model is pulled and available.`
					)
				);
				console.log(
					chalk.yellow(
						`You can check available models with: curl ${ollamaBaseURL}/tags`
					)
				);
				setupSuccess = false;
				return true; // Continue setup, but mark as failed
			}
		} else if (selectedValue === '__CUSTOM_BEDROCK__') {
			isCustomSelection = true;
			const { customId } = await inquirer.prompt([
				{
					type: 'input',
					name: 'customId',
					message: `Enter the custom Bedrock Model ID for the ${role} role (e.g., anthropic.claude-3-sonnet-20240229-v1:0):`
				}
			]);
			if (!customId) {
				console.log(chalk.yellow('No custom ID entered. Skipping role.'));
				return true; // Continue setup, but don't set this role
			}
			modelIdToSet = customId;
			providerHint = CUSTOM_PROVIDERS.BEDROCK;

			// Check if AWS environment variables exist
			if (
				!process.env.AWS_ACCESS_KEY_ID ||
				!process.env.AWS_SECRET_ACCESS_KEY
			) {
				console.warn(
					chalk.yellow(
						'Warning: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Will fallback to system configuration. (ex: aws config files or ec2 instance profiles)'
					)
				);
				setupSuccess = false;
				return true; // Continue setup, but mark as failed
			}

			console.log(
				chalk.blue(
					`Custom Bedrock model "${modelIdToSet}" will be used. No validation performed.`
				)
			);
		} else if (selectedValue === '__CUSTOM_AZURE__') {
			isCustomSelection = true;
			const { customId } = await inquirer.prompt([
				{
					type: 'input',
					name: 'customId',
					message: `Enter the custom Azure OpenAI Model ID for the ${role} role (e.g., gpt-4o):`
				}
			]);
			if (!customId) {
				console.log(chalk.yellow('No custom ID entered. Skipping role.'));
				return true; // Continue setup, but don't set this role
			}
			modelIdToSet = customId;
			providerHint = CUSTOM_PROVIDERS.AZURE;

			// Check if Azure environment variables exist
			if (
				!process.env.AZURE_OPENAI_API_KEY ||
				!process.env.AZURE_OPENAI_ENDPOINT
			) {
				console.error(
					chalk.red(
						'Error: AZURE_OPENAI_API_KEY and/or AZURE_OPENAI_ENDPOINT environment variables are missing. Please set them before using custom Azure models.'
					)
				);
				setupSuccess = false;
				return true; // Continue setup, but mark as failed
			}

			console.log(
				chalk.blue(
					`Custom Azure OpenAI model "${modelIdToSet}" will be used. No validation performed.`
				)
			);
		} else if (selectedValue === '__CUSTOM_VERTEX__') {
			isCustomSelection = true;
			const { customId } = await inquirer.prompt([
				{
					type: 'input',
					name: 'customId',
					message: `Enter the custom Vertex AI Model ID for the ${role} role (e.g., gemini-1.5-pro-002):`
				}
			]);
			if (!customId) {
				console.log(chalk.yellow('No custom ID entered. Skipping role.'));
				return true; // Continue setup, but don't set this role
			}
			modelIdToSet = customId;
			providerHint = CUSTOM_PROVIDERS.VERTEX;

			// Check if Google/Vertex environment variables exist
			if (
				!process.env.GOOGLE_API_KEY &&
				!process.env.GOOGLE_APPLICATION_CREDENTIALS
			) {
				console.error(
					chalk.red(
						'Error: Either GOOGLE_API_KEY or GOOGLE_APPLICATION_CREDENTIALS environment variable is required. Please set one before using custom Vertex models.'
					)
				);
				setupSuccess = false;
				return true; // Continue setup, but mark as failed
			}

			console.log(
				chalk.blue(
					`Custom Vertex AI model "${modelIdToSet}" will be used. No validation performed.`
				)
			);
		} else if (
			selectedValue &&
			typeof selectedValue === 'object' &&
			selectedValue.id
		) {
			// Standard model selected from list
			modelIdToSet = selectedValue.id;
			providerHint = selectedValue.provider; // Provider is known
		} else if (selectedValue === null && role === 'fallback') {
			// Handle disabling fallback
			modelIdToSet = null;
			providerHint = null;
		} else if (selectedValue) {
			console.error(
				chalk.red(
					`Internal Error: Unexpected selection value for ${role}: ${JSON.stringify(selectedValue)}`
				)
			);
			setupSuccess = false;
			return true;
		}

		// Only proceed if there's a change to be made
		if (modelIdToSet !== currentModelId) {
			if (modelIdToSet) {
				// Set a specific model (standard or custom)
				const result = await setModel(role, modelIdToSet, {
					...coreOptionsSetup,
					providerHint // Pass the hint
				});
				if (result.success) {
					console.log(
						chalk.blue(
							`Set ${role} model: ${result.data.provider} / ${result.data.modelId}`
						)
					);
					if (result.data.warning) {
						// Display warning if returned by setModel
						console.log(chalk.yellow(result.data.warning));
					}
					setupConfigModified = true;
				} else {
					console.error(
						chalk.red(
							`Error setting ${role} model: ${result.error?.message || 'Unknown'}`
						)
					);
					setupSuccess = false;
				}
			} else if (role === 'fallback') {
				// Disable fallback model
				const currentCfg = getConfig(projectRoot);
				if (currentCfg?.models?.fallback?.modelId) {
					// Check if it was actually set before clearing
					currentCfg.models.fallback = {
						...currentCfg.models.fallback,
						provider: undefined,
						modelId: undefined
					};
					if (writeConfig(currentCfg, projectRoot)) {
						console.log(chalk.blue('Fallback model disabled.'));
						setupConfigModified = true;
					} else {
						console.error(
							chalk.red('Failed to disable fallback model in config file.')
						);
						setupSuccess = false;
					}
				} else {
					console.log(chalk.blue('Fallback model was already disabled.'));
				}
			}
		}
		return true; // Indicate setup should continue
	}

	// Process answers using the handler
	if (
		!(await handleSetModel(
			'main',
			answers.mainModel,
			currentModels.main?.modelId // <--- Now 'currentModels' is defined
		))
	) {
		return false; // Explicitly return false if cancelled
	}
	if (
		!(await handleSetModel(
			'research',
			answers.researchModel,
			currentModels.research?.modelId // <--- Now 'currentModels' is defined
		))
	) {
		return false; // Explicitly return false if cancelled
	}
	if (
		!(await handleSetModel(
			'fallback',
			answers.fallbackModel,
			currentModels.fallback?.modelId // <--- Now 'currentModels' is defined
		))
	) {
		return false; // Explicitly return false if cancelled
	}

	if (setupSuccess && setupConfigModified) {
		console.log(chalk.green.bold('\nModel setup complete!'));
	} else if (setupSuccess && !setupConfigModified) {
		console.log(chalk.yellow('\nNo changes made to model configuration.'));
	} else if (!setupSuccess) {
		console.error(
			chalk.red(
				'\nErrors occurred during model selection. Please review and try again.'
			)
		);
	}
	return true; // Indicate setup flow completed (not cancelled)
	// Let the main command flow continue to display results
}

/**
 * Configure and register CLI commands
 * @param {Object} program - Commander program instance
 */
function registerCommands(programInstance) {
	// Add global error handler for unknown options
	programInstance.on('option:unknown', function (unknownOption) {
		const commandName = this._name || 'unknown';
		console.error(chalk.red(`Error: Unknown option '${unknownOption}'`));
		console.error(
			chalk.yellow(
				`Run 'task-master ${commandName} --help' to see available options`
			)
		);
		process.exit(1);
	});

	// parse-prd command
	programInstance
		.command('parse-prd')
		.description('Parse a PRD file and generate tasks')
		.argument('[file]', 'Path to the PRD file')
		.option(
			'-i, --input <file>',
			'Path to the PRD file (alternative to positional argument)'
		)
		.option('-o, --output <file>', 'Output file path')
		.option(
			'-n, --num-tasks <number>',
			'Number of tasks to generate',
			getDefaultNumTasks()
		)
		.option('-f, --force', 'Skip confirmation when overwriting existing tasks')
		.option(
			'--append',
			'Append new tasks to existing tasks.json instead of overwriting'
		)
		.option(
			'-r, --research',
			'Use Perplexity AI for research-backed task generation, providing more comprehensive and accurate task breakdown'
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (file, options) => {
			// Initialize TaskMaster
			let taskMaster;
			try {
				const initOptions = {
					prdPath: file || options.input || true,
					tag: options.tag
				};
				// Only include tasksPath if output is explicitly specified
				if (options.output) {
					initOptions.tasksPath = options.output;
				}
				taskMaster = initTaskMaster(initOptions);
			} catch (error) {
				console.log(
					boxen(
						`${chalk.white.bold('Parse PRD Help')}\n\n${chalk.cyan('Usage:')}\n  task-master parse-prd <prd-file.txt> [options]\n\n${chalk.cyan('Options:')}\n  -i, --input <file>       Path to the PRD file (alternative to positional argument)\n  -o, --output <file>      Output file path (default: .taskmaster/tasks/tasks.json)\n  -n, --num-tasks <number> Number of tasks to generate (default: 10)\n  -f, --force              Skip confirmation when overwriting existing tasks\n  --append                 Append new tasks to existing tasks.json instead of overwriting\n  -r, --research           Use Perplexity AI for research-backed task generation\n\n${chalk.cyan('Example:')}\n  task-master parse-prd requirements.txt --num-tasks 15\n  task-master parse-prd --input=requirements.txt\n  task-master parse-prd --force\n  task-master parse-prd requirements_v2.txt --append\n  task-master parse-prd requirements.txt --research\n\n${chalk.yellow('Note: This command will:')}\n  1. Look for a PRD file at ${TASKMASTER_DOCS_DIR}/PRD.md by default\n  2. Use the file specified by --input or positional argument if provided\n  3. Generate tasks from the PRD and either:\n     - Overwrite any existing tasks.json file (default)\n     - Append to existing tasks.json if --append is used`,
						{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
					)
				);
				console.error(chalk.red(`\nError: ${error.message}`));
				process.exit(1);
			}

			const numTasks = parseInt(options.numTasks, 10);
			const force = options.force || false;
			const append = options.append || false;
			const research = options.research || false;
			let useForce = force;
			const useAppend = append;

			// Resolve tag using standard pattern
			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			// Helper function to check if there are existing tasks in the target tag and confirm overwrite
			async function confirmOverwriteIfNeeded() {
				// Check if there are existing tasks in the target tag
				let hasExistingTasksInTag = false;
				const tasksPath = taskMaster.getTasksPath();
				if (fs.existsSync(tasksPath)) {
					try {
						// Read the entire file to check if the tag exists
						const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
						const allData = JSON.parse(existingFileContent);

						// Check if the target tag exists and has tasks
						if (
							allData[tag] &&
							Array.isArray(allData[tag].tasks) &&
							allData[tag].tasks.length > 0
						) {
							hasExistingTasksInTag = true;
						}
					} catch (error) {
						// If we can't read the file or parse it, assume no existing tasks in this tag
						hasExistingTasksInTag = false;
					}
				}

				// Only show confirmation if there are existing tasks in the target tag
				if (hasExistingTasksInTag && !useForce && !useAppend) {
					const overwrite = await confirmTaskOverwrite(tasksPath);
					if (!overwrite) {
						log('info', 'Operation cancelled.');
						return false;
					}
					// If user confirms 'y', we should set useForce = true for the parsePRD call
					// Only overwrite if not appending
					useForce = true;
				}
				return true;
			}

			let spinner;

			try {
				if (!(await confirmOverwriteIfNeeded())) return;

				console.log(chalk.blue(`Parsing PRD file: ${taskMaster.getPrdPath()}`));
				console.log(chalk.blue(`Generating ${numTasks} tasks...`));
				if (append) {
					console.log(chalk.blue('Appending to existing tasks...'));
				}
				if (research) {
					console.log(
						chalk.blue(
							'Using Perplexity AI for research-backed task generation'
						)
					);
				}

				spinner = ora('Parsing PRD and generating tasks...\n').start();
				// Handle case where getTasksPath() returns null
				const outputPath =
					taskMaster.getTasksPath() ||
					path.join(taskMaster.getProjectRoot(), TASKMASTER_TASKS_FILE);
				await parsePRD(taskMaster.getPrdPath(), outputPath, numTasks, {
					append: useAppend,
					force: useForce,
					research: research,
					projectRoot: taskMaster.getProjectRoot(),
					tag: tag
				});
				spinner.succeed('Tasks generated successfully!');
			} catch (error) {
				if (spinner) {
					spinner.fail(`Error parsing PRD: ${error.message}`);
				} else {
					console.error(chalk.red(`Error parsing PRD: ${error.message}`));
				}
				process.exit(1);
			}
		});

	// update command
	programInstance
		.command('update')
		.description(
			'Update multiple tasks with ID >= "from" based on new information or implementation changes'
		)
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'--from <id>',
			'Task ID to start updating from (tasks with ID >= this value will be updated)',
			'1'
		)
		.option(
			'-p, --prompt <text>',
			'Prompt explaining the changes or new context (required)'
		)
		.option(
			'-r, --research',
			'Use Perplexity AI for research-backed task updates'
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag
			});

			const fromId = parseInt(options.from, 10); // Validation happens here
			const prompt = options.prompt;
			const useResearch = options.research || false;

			const tasksPath = taskMaster.getTasksPath();

			// Resolve tag using standard pattern
			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			// Check if there's an 'id' option which is a common mistake (instead of 'from')
			if (
				process.argv.includes('--id') ||
				process.argv.some((arg) => arg.startsWith('--id='))
			) {
				console.error(
					chalk.red('Error: The update command uses --from=<id>, not --id=<id>')
				);
				console.log(chalk.yellow('\nTo update multiple tasks:'));
				console.log(
					`  task-master update --from=${fromId} --prompt="Your prompt here"`
				);
				console.log(
					chalk.yellow(
						'\nTo update a single specific task, use the update-task command instead:'
					)
				);
				console.log(
					`  task-master update-task --id=<id> --prompt="Your prompt here"`
				);
				process.exit(1);
			}

			if (!prompt) {
				console.error(
					chalk.red(
						'Error: --prompt parameter is required. Please provide information about the changes.'
					)
				);
				process.exit(1);
			}

			console.log(
				chalk.blue(
					`Updating tasks from ID >= ${fromId} with prompt: "${prompt}"`
				)
			);
			console.log(chalk.blue(`Tasks file: ${tasksPath}`));

			if (useResearch) {
				console.log(
					chalk.blue('Using Perplexity AI for research-backed task updates')
				);
			}

			// Call core updateTasks, passing context for CLI
			await updateTasks(
				taskMaster.getTasksPath(),
				fromId,
				prompt,
				useResearch,
				{ projectRoot: taskMaster.getProjectRoot(), tag } // Pass context with projectRoot and tag
			);
		});

	// update-task command
	programInstance
		.command('update-task')
		.description(
			'Update a single specific task by ID with new information (use --id parameter)'
		)
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('-i, --id <id>', 'Task ID to update (required)')
		.option(
			'-p, --prompt <text>',
			'Prompt explaining the changes or new context (required)'
		)
		.option(
			'-r, --research',
			'Use Perplexity AI for research-backed task updates'
		)
		.option(
			'--append',
			'Append timestamped information to task details instead of full update'
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			try {
				// Initialize TaskMaster
				const taskMaster = initTaskMaster({
					tasksPath: options.file || true,
					tag: options.tag
				});
				const tasksPath = taskMaster.getTasksPath();

				// Resolve tag using standard pattern
				const tag = taskMaster.getCurrentTag();

				// Show current tag context
				displayCurrentTagIndicator(tag);

				// Validate required parameters
				if (!options.id) {
					console.error(chalk.red('Error: --id parameter is required'));
					console.log(
						chalk.yellow(
							'Usage example: task-master update-task --id=23 --prompt="Update with new information"'
						)
					);
					process.exit(1);
				}

				// Parse the task ID and validate it's a number
				const taskId = parseInt(options.id, 10);
				if (Number.isNaN(taskId) || taskId <= 0) {
					console.error(
						chalk.red(
							`Error: Invalid task ID: ${options.id}. Task ID must be a positive integer.`
						)
					);
					console.log(
						chalk.yellow(
							'Usage example: task-master update-task --id=23 --prompt="Update with new information"'
						)
					);
					process.exit(1);
				}

				if (!options.prompt) {
					console.error(
						chalk.red(
							'Error: --prompt parameter is required. Please provide information about the changes.'
						)
					);
					console.log(
						chalk.yellow(
							'Usage example: task-master update-task --id=23 --prompt="Update with new information"'
						)
					);
					process.exit(1);
				}

				const prompt = options.prompt;
				const useResearch = options.research || false;

				// Validate tasks file exists
				if (!fs.existsSync(tasksPath)) {
					console.error(
						chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
					);
					if (tasksPath === TASKMASTER_TASKS_FILE) {
						console.log(
							chalk.yellow(
								'Hint: Run task-master init or task-master parse-prd to create tasks.json first'
							)
						);
					} else {
						console.log(
							chalk.yellow(
								`Hint: Check if the file path is correct: ${tasksPath}`
							)
						);
					}
					process.exit(1);
				}

				console.log(
					chalk.blue(`Updating task ${taskId} with prompt: "${prompt}"`)
				);
				console.log(chalk.blue(`Tasks file: ${tasksPath}`));

				if (useResearch) {
					// Verify Perplexity API key exists if using research
					if (!isApiKeySet('perplexity')) {
						console.log(
							chalk.yellow(
								'Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available.'
							)
						);
						console.log(
							chalk.yellow('Falling back to Claude AI for task update.')
						);
					} else {
						console.log(
							chalk.blue('Using Perplexity AI for research-backed task update')
						);
					}
				}

				const result = await updateTaskById(
					taskMaster.getTasksPath(),
					taskId,
					prompt,
					useResearch,
					{ projectRoot: taskMaster.getProjectRoot(), tag },
					'text',
					options.append || false
				);

				// If the task wasn't updated (e.g., if it was already marked as done)
				if (!result) {
					console.log(
						chalk.yellow(
							'\nTask update was not completed. Review the messages above for details.'
						)
					);
				}
			} catch (error) {
				console.error(chalk.red(`Error: ${error.message}`));

				// Provide more helpful error messages for common issues
				if (
					error.message.includes('task') &&
					error.message.includes('not found')
				) {
					console.log(chalk.yellow('\nTo fix this issue:'));
					console.log(
						'  1. Run task-master list to see all available task IDs'
					);
					console.log('  2. Use a valid task ID with the --id parameter');
				} else if (error.message.includes('API key')) {
					console.log(
						chalk.yellow(
							'\nThis error is related to API keys. Check your environment variables.'
						)
					);
				}

				// Use getDebugFlag getter instead of CONFIG.debug
				if (getDebugFlag()) {
					console.error(error);
				}

				process.exit(1);
			}
		});

	// update-subtask command
	programInstance
		.command('update-subtask')
		.description(
			'Update a subtask by appending additional timestamped information'
		)
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'-i, --id <id>',
			'Subtask ID to update in format "parentId.subtaskId" (required)'
		)
		.option(
			'-p, --prompt <text>',
			'Prompt explaining what information to add (required)'
		)
		.option('-r, --research', 'Use Perplexity AI for research-backed updates')
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			try {
				// Initialize TaskMaster
				const taskMaster = initTaskMaster({
					tasksPath: options.file || true,
					tag: options.tag
				});
				const tasksPath = taskMaster.getTasksPath();

				// Resolve tag using standard pattern
				const tag = taskMaster.getCurrentTag();

				// Show current tag context
				displayCurrentTagIndicator(tag);

				// Validate required parameters
				if (!options.id) {
					console.error(chalk.red('Error: --id parameter is required'));
					console.log(
						chalk.yellow(
							'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"'
						)
					);
					process.exit(1);
				}

				// Validate subtask ID format (should contain a dot)
				const subtaskId = options.id;
				if (!subtaskId.includes('.')) {
					console.error(
						chalk.red(
							`Error: Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"`
						)
					);
					console.log(
						chalk.yellow(
							'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"'
						)
					);
					process.exit(1);
				}

				if (!options.prompt) {
					console.error(
						chalk.red(
							'Error: --prompt parameter is required. Please provide information to add to the subtask.'
						)
					);
					console.log(
						chalk.yellow(
							'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"'
						)
					);
					process.exit(1);
				}

				const prompt = options.prompt;
				const useResearch = options.research || false;

				// Validate tasks file exists
				if (!fs.existsSync(tasksPath)) {
					console.error(
						chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
					);
					if (tasksPath === TASKMASTER_TASKS_FILE) {
						console.log(
							chalk.yellow(
								'Hint: Run task-master init or task-master parse-prd to create tasks.json first'
							)
						);
					} else {
						console.log(
							chalk.yellow(
								`Hint: Check if the file path is correct: ${tasksPath}`
							)
						);
					}
					process.exit(1);
				}

				console.log(
					chalk.blue(`Updating subtask ${subtaskId} with prompt: "${prompt}"`)
				);
				console.log(chalk.blue(`Tasks file: ${tasksPath}`));

				if (useResearch) {
					// Verify Perplexity API key exists if using research
					if (!isApiKeySet('perplexity')) {
						console.log(
							chalk.yellow(
								'Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available.'
							)
						);
						console.log(
							chalk.yellow('Falling back to Claude AI for subtask update.')
						);
					} else {
						console.log(
							chalk.blue(
								'Using Perplexity AI for research-backed subtask update'
							)
						);
					}
				}

				const result = await updateSubtaskById(
					taskMaster.getTasksPath(),
					subtaskId,
					prompt,
					useResearch,
					{ projectRoot: taskMaster.getProjectRoot(), tag }
				);

				if (!result) {
					console.log(
						chalk.yellow(
							'\nSubtask update was not completed. Review the messages above for details.'
						)
					);
				}
			} catch (error) {
				console.error(chalk.red(`Error: ${error.message}`));

				// Provide more helpful error messages for common issues
				if (
					error.message.includes('subtask') &&
					error.message.includes('not found')
				) {
					console.log(chalk.yellow('\nTo fix this issue:'));
					console.log(
						'  1. Run task-master list --with-subtasks to see all available subtask IDs'
					);
					console.log(
						'  2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"'
					);
				} else if (error.message.includes('API key')) {
					console.log(
						chalk.yellow(
							'\nThis error is related to API keys. Check your environment variables.'
						)
					);
				}

				// Use getDebugFlag getter instead of CONFIG.debug
				if (getDebugFlag()) {
					console.error(error);
				}

				process.exit(1);
			}
		});

	// generate command
	programInstance
		.command('generate')
		.description('Generate task files from tasks.json')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'-o, --output <dir>',
			'Output directory',
			path.dirname(TASKMASTER_TASKS_FILE)
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag
			});

			const outputDir = options.output;
			const tag = taskMaster.getCurrentTag();

			console.log(
				chalk.blue(`Generating task files from: ${taskMaster.getTasksPath()}`)
			);
			console.log(chalk.blue(`Output directory: ${outputDir}`));

			await generateTaskFiles(taskMaster.getTasksPath(), outputDir, {
				projectRoot: taskMaster.getProjectRoot(),
				tag
			});
		});

	// set-status command
	programInstance
		.command('set-status')
		.alias('mark')
		.alias('set')
		.description('Set the status of a task')
		.option(
			'-i, --id <id>',
			'Task ID (can be comma-separated for multiple tasks)'
		)
		.option(
			'-s, --status <status>',
			`New status (one of: ${TASK_STATUS_OPTIONS.join(', ')})`
		)
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag
			});

			const taskId = options.id;
			const status = options.status;

			if (!taskId || !status) {
				console.error(chalk.red('Error: Both --id and --status are required'));
				process.exit(1);
			}

			if (!isValidTaskStatus(status)) {
				console.error(
					chalk.red(
						`Error: Invalid status value: ${status}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}`
					)
				);

				process.exit(1);
			}
			const tag = taskMaster.getCurrentTag();

			displayCurrentTagIndicator(tag);

			console.log(
				chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`)
			);

			await setTaskStatus(taskMaster.getTasksPath(), taskId, status, {
				projectRoot: taskMaster.getProjectRoot(),
				tag
			});
		});

	// list command
	programInstance
		.command('list')
		.description('List all tasks')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'-r, --report <report>',
			'Path to the complexity report file',
			COMPLEXITY_REPORT_FILE
		)
		.option('-s, --status <status>', 'Filter by status')
		.option('--with-subtasks', 'Show subtasks for each task')
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			// Initialize TaskMaster
			const initOptions = {
				tasksPath: options.file || true,
				tag: options.tag
			};

			// Only pass complexityReportPath if user provided a custom path
			if (options.report && options.report !== COMPLEXITY_REPORT_FILE) {
				initOptions.complexityReportPath = options.report;
			}

			const taskMaster = initTaskMaster(initOptions);

			const statusFilter = options.status;
			const withSubtasks = options.withSubtasks || false;
			const tag = taskMaster.getCurrentTag();
			// Show current tag context
			displayCurrentTagIndicator(tag);

			console.log(
				chalk.blue(`Listing tasks from: ${taskMaster.getTasksPath()}`)
			);
			if (statusFilter) {
				console.log(chalk.blue(`Filtering by status: ${statusFilter}`));
			}
			if (withSubtasks) {
				console.log(chalk.blue('Including subtasks in listing'));
			}

			await listTasks(
				taskMaster.getTasksPath(),
				statusFilter,
				taskMaster.getComplexityReportPath(),
				withSubtasks,
				'text',
				{ projectRoot: taskMaster.getProjectRoot(), tag }
			);
		});

	// expand command
	programInstance
		.command('expand')
		.description('Expand a task into subtasks using AI')
		.option('-i, --id <id>', 'ID of the task to expand')
		.option(
			'-a, --all',
			'Expand all pending tasks based on complexity analysis'
		)
		.option(
			'-n, --num <number>',
			'Number of subtasks to generate (uses complexity analysis by default if available)'
		)
		.option(
			'-r, --research',
			'Enable research-backed generation (e.g., using Perplexity)',
			false
		)
		.option('-p, --prompt <text>', 'Additional context for subtask generation')
		.option('-f, --force', 'Force expansion even if subtasks exist', false) // Ensure force option exists
		.option(
			'--file <file>',
			'Path to the tasks file (relative to project root)',
			TASKMASTER_TASKS_FILE // Allow file override
		) // Allow file override
		.option(
			'-cr, --complexity-report <file>',
			'Path to the report file',
			COMPLEXITY_REPORT_FILE
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			// Initialize TaskMaster
			const initOptions = {
				tasksPath: options.file || true,
				tag: options.tag
			};

			if (options.complexityReport) {
				initOptions.complexityReportPath = options.complexityReport;
			}

			const taskMaster = initTaskMaster(initOptions);

			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			if (options.all) {
				// --- Handle expand --all ---
				console.log(chalk.blue('Expanding all pending tasks...'));
				// Updated call to the refactored expandAllTasks
				try {
					const result = await expandAllTasks(
						taskMaster.getTasksPath(),
						options.num, // Pass num
						options.research, // Pass research flag
						options.prompt, // Pass additional context
						options.force, // Pass force flag
						{
							projectRoot: taskMaster.getProjectRoot(),
							tag,
							complexityReportPath: taskMaster.getComplexityReportPath()
						} // Pass context with projectRoot and tag
						// outputFormat defaults to 'text' in expandAllTasks for CLI
					);
				} catch (error) {
					console.error(
						chalk.red(`Error expanding all tasks: ${error.message}`)
					);
					process.exit(1);
				}
			} else if (options.id) {
				// --- Handle expand --id <id> (Should be correct from previous refactor) ---
				if (!options.id) {
					console.error(
						chalk.red('Error: Task ID is required unless using --all.')
					);
					process.exit(1);
				}

				console.log(chalk.blue(`Expanding task ${options.id}...`));
				try {
					// Call the refactored expandTask function
					await expandTask(
						taskMaster.getTasksPath(),
						options.id,
						options.num,
						options.research,
						options.prompt,
						{
							projectRoot: taskMaster.getProjectRoot(),
							tag,
							complexityReportPath: taskMaster.getComplexityReportPath()
						}, // Pass context with projectRoot and tag
						options.force // Pass the force flag down
					);
					// expandTask logs its own success/failure for single task
				} catch (error) {
					console.error(
						chalk.red(`Error expanding task ${options.id}: ${error.message}`)
					);
					process.exit(1);
				}
			} else {
				console.error(
					chalk.red('Error: You must specify either a task ID (--id) or --all.')
				);
				programInstance.help(); // Show help
			}
		});

	// analyze-complexity command
	programInstance
		.command('analyze-complexity')
		.description(
			`Analyze tasks and generate expansion recommendations${chalk.reset('')}`
		)
		.option('-o, --output <file>', 'Output file path for the report')
		.option(
			'-m, --model <model>',
			'LLM model to use for analysis (defaults to configured model)'
		)
		.option(
			'-t, --threshold <number>',
			'Minimum complexity score to recommend expansion (1-10)',
			'5'
		)
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'-r, --research',
			'Use Perplexity AI for research-backed complexity analysis'
		)
		.option(
			'-i, --id <ids>',
			'Comma-separated list of specific task IDs to analyze (e.g., "1,3,5")'
		)
		.option('--from <id>', 'Starting task ID in a range to analyze')
		.option('--to <id>', 'Ending task ID in a range to analyze')
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			// Initialize TaskMaster
			const initOptions = {
				tasksPath: options.file || true, // Tasks file is required to analyze
				tag: options.tag
			};
			// Only include complexityReportPath if output is explicitly specified
			if (options.output) {
				initOptions.complexityReportPath = options.output;
			}

			const taskMaster = initTaskMaster(initOptions);

			const modelOverride = options.model;
			const thresholdScore = parseFloat(options.threshold);
			const useResearch = options.research || false;

			// Use the provided tag, or the current active tag, or default to 'master'
			const targetTag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(targetTag);

			// Use user's explicit output path if provided, otherwise use tag-aware default
			const outputPath = taskMaster.getComplexityReportPath();

			console.log(
				chalk.blue(
					`Analyzing task complexity from: ${taskMaster.getTasksPath()}`
				)
			);
			console.log(chalk.blue(`Output report will be saved to: ${outputPath}`));

			if (options.id) {
				console.log(chalk.blue(`Analyzing specific task IDs: ${options.id}`));
			} else if (options.from || options.to) {
				const fromStr = options.from ? options.from : 'first';
				const toStr = options.to ? options.to : 'last';
				console.log(
					chalk.blue(`Analyzing tasks in range: ${fromStr} to ${toStr}`)
				);
			}

			if (useResearch) {
				console.log(
					chalk.blue(
						'Using Perplexity AI for research-backed complexity analysis'
					)
				);
			}

			// Update options with tag-aware output path and context
			const updatedOptions = {
				...options,
				output: outputPath,
				tag: targetTag,
				projectRoot: taskMaster.getProjectRoot(),
				file: taskMaster.getTasksPath()
			};

			await analyzeTaskComplexity(updatedOptions);
		});

	// research command
	programInstance
		.command('research')
		.description('Perform AI-powered research queries with project context')
		.argument('[prompt]', 'Research prompt to investigate')
		.option('--file <file>', 'Path to the tasks file')
		.option(
			'-i, --id <ids>',
			'Comma-separated task/subtask IDs to include as context (e.g., "15,16.2")'
		)
		.option(
			'-f, --files <paths>',
			'Comma-separated file paths to include as context'
		)
		.option(
			'-c, --context <text>',
			'Additional custom context to include in the research prompt'
		)
		.option(
			'-t, --tree',
			'Include project file tree structure in the research context'
		)
		.option(
			'-s, --save <file>',
			'Save research results to the specified task/subtask(s)'
		)
		.option(
			'-d, --detail <level>',
			'Output detail level: low, medium, high',
			'medium'
		)
		.option(
			'--save-to <id>',
			'Automatically save research results to specified task/subtask ID (e.g., "15" or "15.2")'
		)
		.option(
			'--save-file',
			'Save research results to .taskmaster/docs/research/ directory'
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (prompt, options) => {
			// Initialize TaskMaster
			const initOptions = {
				tasksPath: options.file || true,
				tag: options.tag
			};

			const taskMaster = initTaskMaster(initOptions);

			// Parameter validation
			if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
				console.error(
					chalk.red('Error: Research prompt is required and cannot be empty')
				);
				showResearchHelp();
				process.exit(1);
			}

			// Validate detail level
			const validDetailLevels = ['low', 'medium', 'high'];
			if (
				options.detail &&
				!validDetailLevels.includes(options.detail.toLowerCase())
			) {
				console.error(
					chalk.red(
						`Error: Detail level must be one of: ${validDetailLevels.join(', ')}`
					)
				);
				process.exit(1);
			}

			// Validate and parse task IDs if provided
			let taskIds = [];
			if (options.id) {
				try {
					taskIds = options.id.split(',').map((id) => {
						const trimmedId = id.trim();
						// Support both task IDs (e.g., "15") and subtask IDs (e.g., "15.2")
						if (!/^\d+(\.\d+)?$/.test(trimmedId)) {
							throw new Error(
								`Invalid task ID format: "${trimmedId}". Expected format: "15" or "15.2"`
							);
						}
						return trimmedId;
					});
				} catch (error) {
					console.error(chalk.red(`Error parsing task IDs: ${error.message}`));
					process.exit(1);
				}
			}

			// Validate and parse file paths if provided
			let filePaths = [];
			if (options.files) {
				try {
					filePaths = options.files.split(',').map((filePath) => {
						const trimmedPath = filePath.trim();
						if (trimmedPath.length === 0) {
							throw new Error('Empty file path provided');
						}
						return trimmedPath;
					});
				} catch (error) {
					console.error(
						chalk.red(`Error parsing file paths: ${error.message}`)
					);
					process.exit(1);
				}
			}

			// Validate save-to option if provided
			if (options.saveTo) {
				const saveToId = options.saveTo.trim();
				if (saveToId.length === 0) {
					console.error(chalk.red('Error: Save-to ID cannot be empty'));
					process.exit(1);
				}
				// Validate ID format: number or number.number
				if (!/^\d+(\.\d+)?$/.test(saveToId)) {
					console.error(
						chalk.red(
							'Error: Save-to ID must be in format "15" for task or "15.2" for subtask'
						)
					);
					process.exit(1);
				}
			}

			// Validate save option if provided (legacy file save)
			if (options.save) {
				const saveTarget = options.save.trim();
				if (saveTarget.length === 0) {
					console.error(chalk.red('Error: Save target cannot be empty'));
					process.exit(1);
				}
				// Check if it's a valid file path (basic validation)
				if (saveTarget.includes('..') || saveTarget.startsWith('/')) {
					console.error(
						chalk.red(
							'Error: Save path must be relative and cannot contain ".."'
						)
					);
					process.exit(1);
				}
			}

			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			// Validate tasks file exists if task IDs are specified
			if (taskIds.length > 0) {
				try {
					const tasksData = readJSON(
						taskMaster.getTasksPath(),
						taskMaster.getProjectRoot(),
						tag
					);
					if (!tasksData || !tasksData.tasks) {
						console.error(
							chalk.red(
								`Error: No valid tasks found in ${taskMaster.getTasksPath()} for tag '${tag}'`
							)
						);
						process.exit(1);
					}
				} catch (error) {
					console.error(
						chalk.red(`Error reading tasks file: ${error.message}`)
					);
					process.exit(1);
				}
			}

			// Validate file paths exist if specified
			if (filePaths.length > 0) {
				for (const filePath of filePaths) {
					const fullPath = path.isAbsolute(filePath)
						? filePath
						: path.join(taskMaster.getProjectRoot(), filePath);
					if (!fs.existsSync(fullPath)) {
						console.error(chalk.red(`Error: File not found: ${filePath}`));
						process.exit(1);
					}
				}
			}

			// Create validated parameters object
			const validatedParams = {
				prompt: prompt.trim(),
				taskIds: taskIds,
				filePaths: filePaths,
				customContext: options.context ? options.context.trim() : null,
				includeProjectTree: !!options.tree,
				saveTarget: options.save ? options.save.trim() : null,
				saveToId: options.saveTo ? options.saveTo.trim() : null,
				allowFollowUp: true, // Always allow follow-up in CLI
				detailLevel: options.detail ? options.detail.toLowerCase() : 'medium',
				tasksPath: taskMaster.getTasksPath(),
				projectRoot: taskMaster.getProjectRoot()
			};

			// Display what we're about to do
			console.log(chalk.blue(`Researching: "${validatedParams.prompt}"`));

			if (validatedParams.taskIds.length > 0) {
				console.log(
					chalk.gray(`Task context: ${validatedParams.taskIds.join(', ')}`)
				);
			}

			if (validatedParams.filePaths.length > 0) {
				console.log(
					chalk.gray(`File context: ${validatedParams.filePaths.join(', ')}`)
				);
			}

			if (validatedParams.customContext) {
				console.log(
					chalk.gray(
						`Custom context: ${validatedParams.customContext.substring(0, 50)}${validatedParams.customContext.length > 50 ? '...' : ''}`
					)
				);
			}

			if (validatedParams.includeProjectTree) {
				console.log(chalk.gray('Including project file tree'));
			}

			console.log(chalk.gray(`Detail level: ${validatedParams.detailLevel}`));

			try {
				// Import the research function
				const { performResearch } = await import('./task-manager/research.js');

				// Prepare research options
				const researchOptions = {
					taskIds: validatedParams.taskIds,
					filePaths: validatedParams.filePaths,
					customContext: validatedParams.customContext || '',
					includeProjectTree: validatedParams.includeProjectTree,
					detailLevel: validatedParams.detailLevel,
					projectRoot: validatedParams.projectRoot,
					saveToFile: !!options.saveFile,
					tag: tag
				};

				// Execute research
				const result = await performResearch(
					validatedParams.prompt,
					researchOptions,
					{
						commandName: 'research',
						outputType: 'cli',
						tag: tag
					},
					'text',
					validatedParams.allowFollowUp // Pass follow-up flag
				);

				// Auto-save to task/subtask if requested and no interactive save occurred
				if (validatedParams.saveToId && !result.interactiveSaveOccurred) {
					try {
						const isSubtask = validatedParams.saveToId.includes('.');

						// Format research content for saving
						const researchContent = `## Research Query: ${validatedParams.prompt}

**Detail Level:** ${result.detailLevel}
**Context Size:** ${result.contextSize} characters
**Timestamp:** ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}

### Results

${result.result}`;

						if (isSubtask) {
							// Save to subtask
							const { updateSubtaskById } = await import(
								'./task-manager/update-subtask-by-id.js'
							);

							await updateSubtaskById(
								validatedParams.tasksPath,
								validatedParams.saveToId,
								researchContent,
								false, // useResearch = false for simple append
								{
									commandName: 'research-save',
									outputType: 'cli',
									projectRoot: validatedParams.projectRoot,
									tag: tag
								},
								'text'
							);

							console.log(
								chalk.green(
									`✅ Research saved to subtask ${validatedParams.saveToId}`
								)
							);
						} else {
							// Save to task
							const updateTaskById = (
								await import('./task-manager/update-task-by-id.js')
							).default;

							const taskIdNum = parseInt(validatedParams.saveToId, 10);
							await updateTaskById(
								validatedParams.tasksPath,
								taskIdNum,
								researchContent,
								false, // useResearch = false for simple append
								{
									commandName: 'research-save',
									outputType: 'cli',
									projectRoot: validatedParams.projectRoot,
									tag: tag
								},
								'text',
								true // appendMode = true
							);

							console.log(
								chalk.green(
									`✅ Research saved to task ${validatedParams.saveToId}`
								)
							);
						}
					} catch (saveError) {
						console.log(
							chalk.red(`❌ Error saving to task/subtask: ${saveError.message}`)
						);
					}
				}

				// Save results to file if requested (legacy)
				if (validatedParams.saveTarget) {
					const saveContent = `# Research Query: ${validatedParams.prompt}

**Detail Level:** ${result.detailLevel}
**Context Size:** ${result.contextSize} characters
**Timestamp:** ${new Date().toISOString()}

## Results

${result.result}
`;

					fs.writeFileSync(validatedParams.saveTarget, saveContent, 'utf-8');
					console.log(
						chalk.green(`\n💾 Results saved to: ${validatedParams.saveTarget}`)
					);
				}
			} catch (error) {
				console.error(chalk.red(`\n❌ Research failed: ${error.message}`));
				process.exit(1);
			}
		});

	// clear-subtasks command
	programInstance
		.command('clear-subtasks')
		.description('Clear subtasks from specified tasks')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'-i, --id <ids>',
			'Task IDs (comma-separated) to clear subtasks from'
		)
		.option('--all', 'Clear subtasks from all tasks')
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			const taskIds = options.id;
			const all = options.all;

			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag
			});

			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			if (!taskIds && !all) {
				console.error(
					chalk.red(
						'Error: Please specify task IDs with --id=<ids> or use --all to clear all tasks'
					)
				);
				process.exit(1);
			}

			if (all) {
				// If --all is specified, get all task IDs
				const data = readJSON(
					taskMaster.getTasksPath(),
					taskMaster.getProjectRoot(),
					tag
				);
				if (!data || !data.tasks) {
					console.error(chalk.red('Error: No valid tasks found'));
					process.exit(1);
				}
				const allIds = data.tasks.map((t) => t.id).join(',');
				clearSubtasks(taskMaster.getTasksPath(), allIds, {
					projectRoot: taskMaster.getProjectRoot(),
					tag
				});
			} else {
				clearSubtasks(taskMaster.getTasksPath(), taskIds, {
					projectRoot: taskMaster.getProjectRoot(),
					tag
				});
			}
		});

	// add-task command
	programInstance
		.command('add-task')
		.description('Add a new task using AI, optionally providing manual details')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'-p, --prompt <prompt>',
			'Description of the task to add (required if not using manual fields)'
		)
		.option('-t, --title <title>', 'Task title (for manual task creation)')
		.option(
			'-d, --description <description>',
			'Task description (for manual task creation)'
		)
		.option(
			'--details <details>',
			'Implementation details (for manual task creation)'
		)
		.option(
			'--dependencies <dependencies>',
			'Comma-separated list of task IDs this task depends on'
		)
		.option(
			'--priority <priority>',
			'Task priority (high, medium, low)',
			'medium'
		)
		.option(
			'-r, --research',
			'Whether to use research capabilities for task creation'
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			const isManualCreation = options.title && options.description;

			// Validate that either prompt or title+description are provided
			if (!options.prompt && !isManualCreation) {
				console.error(
					chalk.red(
						'Error: Either --prompt or both --title and --description must be provided'
					)
				);
				process.exit(1);
			}

			const tasksPath = options.file || TASKMASTER_TASKS_FILE;

			if (!fs.existsSync(tasksPath)) {
				console.error(
					`❌ No tasks.json file found. Please run "task-master init" or create a tasks.json file at ${TASKMASTER_TASKS_FILE}`
				);
				process.exit(1);
			}

			// Correctly determine projectRoot
			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag
			});

			const projectRoot = taskMaster.getProjectRoot();

			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			let manualTaskData = null;
			if (isManualCreation) {
				manualTaskData = {
					title: options.title,
					description: options.description,
					details: options.details || '',
					testStrategy: options.testStrategy || ''
				};
				// Restore specific logging for manual creation
				console.log(
					chalk.blue(`Creating task manually with title: "${options.title}"`)
				);
			} else {
				// Restore specific logging for AI creation
				console.log(
					chalk.blue(`Creating task with AI using prompt: "${options.prompt}"`)
				);
			}

			// Log dependencies and priority if provided (restored)
			const dependenciesArray = options.dependencies
				? options.dependencies.split(',').map((id) => id.trim())
				: [];
			if (dependenciesArray.length > 0) {
				console.log(
					chalk.blue(`Dependencies: [${dependenciesArray.join(', ')}]`)
				);
			}
			if (options.priority) {
				console.log(chalk.blue(`Priority: ${options.priority}`));
			}

			const context = {
				projectRoot,
				tag,
				commandName: 'add-task',
				outputType: 'cli'
			};

			try {
				const { newTaskId, telemetryData } = await addTask(
					taskMaster.getTasksPath(),
					options.prompt,
					dependenciesArray,
					options.priority,
					context,
					'text',
					manualTaskData,
					options.research
				);

				// addTask handles detailed CLI success logging AND telemetry display when outputFormat is 'text'
				// No need to call displayAiUsageSummary here anymore.
			} catch (error) {
				console.error(chalk.red(`Error adding task: ${error.message}`));
				if (error.details) {
					console.error(chalk.red(error.details));
				}
				process.exit(1);
			}
		});

	// next command
	programInstance
		.command('next')
		.description(
			`Show the next task to work on based on dependencies and status${chalk.reset('')}`
		)
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'-r, --report <report>',
			'Path to the complexity report file',
			COMPLEXITY_REPORT_FILE
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			const initOptions = {
				tasksPath: options.file || true,
				tag: options.tag
			};

			if (options.report && options.report !== COMPLEXITY_REPORT_FILE) {
				initOptions.complexityReportPath = options.report;
			}

			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag,
				complexityReportPath: options.report || false
			});

			const tag = taskMaster.getCurrentTag();

			const context = {
				projectRoot: taskMaster.getProjectRoot(),
				tag
			};

			// Show current tag context
			displayCurrentTagIndicator(tag);

			await displayNextTask(
				taskMaster.getTasksPath(),
				taskMaster.getComplexityReportPath(),
				context
			);
		});

	// show command
	programInstance
		.command('show')
		.description(
			`Display detailed information about one or more tasks${chalk.reset('')}`
		)
		.argument('[id]', 'Task ID(s) to show (comma-separated for multiple)')
		.option(
			'-i, --id <id>',
			'Task ID(s) to show (comma-separated for multiple)'
		)
		.option('-s, --status <status>', 'Filter subtasks by status')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'-r, --report <report>',
			'Path to the complexity report file',
			COMPLEXITY_REPORT_FILE
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (taskId, options) => {
			// Initialize TaskMaster
			const initOptions = {
				tasksPath: options.file || true,
				tag: options.tag
			};
			// Only pass complexityReportPath if user provided a custom path
			if (options.report && options.report !== COMPLEXITY_REPORT_FILE) {
				initOptions.complexityReportPath = options.report;
			}
			const taskMaster = initTaskMaster(initOptions);

			const idArg = taskId || options.id;
			const statusFilter = options.status;
			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			if (!idArg) {
				console.error(chalk.red('Error: Please provide a task ID'));
				process.exit(1);
			}

			// Check if multiple IDs are provided (comma-separated)
			const taskIds = idArg
				.split(',')
				.map((id) => id.trim())
				.filter((id) => id.length > 0);

			if (taskIds.length > 1) {
				// Multiple tasks - use compact summary view with interactive drill-down
				await displayMultipleTasksSummary(
					taskMaster.getTasksPath(),
					taskIds,
					taskMaster.getComplexityReportPath(),
					statusFilter,
					{ projectRoot: taskMaster.getProjectRoot(), tag }
				);
			} else {
				// Single task - use detailed view
				await displayTaskById(
					taskMaster.getTasksPath(),
					taskIds[0],
					taskMaster.getComplexityReportPath(),
					statusFilter,
					{ projectRoot: taskMaster.getProjectRoot(), tag }
				);
			}
		});

	// add-dependency command
	programInstance
		.command('add-dependency')
		.description('Add a dependency to a task')
		.option('-i, --id <id>', 'Task ID to add dependency to')
		.option('-d, --depends-on <id>', 'Task ID that will become a dependency')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			const initOptions = {
				tasksPath: options.file || true,
				tag: options.tag
			};

			// Initialize TaskMaster
			const taskMaster = initTaskMaster(initOptions);

			const taskId = options.id;
			const dependencyId = options.dependsOn;

			// Resolve tag using standard pattern
			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			if (!taskId || !dependencyId) {
				console.error(
					chalk.red('Error: Both --id and --depends-on are required')
				);
				process.exit(1);
			}

			// Handle subtask IDs correctly by preserving the string format for IDs containing dots
			// Only use parseInt for simple numeric IDs
			const formattedTaskId = taskId.includes('.')
				? taskId
				: parseInt(taskId, 10);
			const formattedDependencyId = dependencyId.includes('.')
				? dependencyId
				: parseInt(dependencyId, 10);

			await addDependency(
				taskMaster.getTasksPath(),
				formattedTaskId,
				formattedDependencyId,
				{
					projectRoot: taskMaster.getProjectRoot(),
					tag
				}
			);
		});

	// remove-dependency command
	programInstance
		.command('remove-dependency')
		.description('Remove a dependency from a task')
		.option('-i, --id <id>', 'Task ID to remove dependency from')
		.option('-d, --depends-on <id>', 'Task ID to remove as a dependency')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			const initOptions = {
				tasksPath: options.file || true,
				tag: options.tag
			};

			// Initialize TaskMaster
			const taskMaster = initTaskMaster(initOptions);

			const taskId = options.id;
			const dependencyId = options.dependsOn;

			// Resolve tag using standard pattern
			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			if (!taskId || !dependencyId) {
				console.error(
					chalk.red('Error: Both --id and --depends-on are required')
				);
				process.exit(1);
			}

			// Handle subtask IDs correctly by preserving the string format for IDs containing dots
			// Only use parseInt for simple numeric IDs
			const formattedTaskId = taskId.includes('.')
				? taskId
				: parseInt(taskId, 10);
			const formattedDependencyId = dependencyId.includes('.')
				? dependencyId
				: parseInt(dependencyId, 10);

			await removeDependency(
				taskMaster.getTasksPath(),
				formattedTaskId,
				formattedDependencyId,
				{
					projectRoot: taskMaster.getProjectRoot(),
					tag
				}
			);
		});

	// validate-dependencies command
	programInstance
		.command('validate-dependencies')
		.description(
			`Identify invalid dependencies without fixing them${chalk.reset('')}`
		)
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			const initOptions = {
				tasksPath: options.file || true,
				tag: options.tag
			};

			// Initialize TaskMaster
			const taskMaster = initTaskMaster(initOptions);

			// Resolve tag using standard pattern
			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			await validateDependenciesCommand(taskMaster.getTasksPath(), {
				context: { projectRoot: taskMaster.getProjectRoot(), tag }
			});
		});

	// fix-dependencies command
	programInstance
		.command('fix-dependencies')
		.description(`Fix invalid dependencies automatically${chalk.reset('')}`)
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			const initOptions = {
				tasksPath: options.file || true,
				tag: options.tag
			};

			// Initialize TaskMaster
			const taskMaster = initTaskMaster(initOptions);

			// Resolve tag using standard pattern
			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			await fixDependenciesCommand(taskMaster.getTasksPath(), {
				context: { projectRoot: taskMaster.getProjectRoot(), tag }
			});
		});

	// complexity-report command
	programInstance
		.command('complexity-report')
		.description(`Display the complexity analysis report${chalk.reset('')}`)
		.option(
			'-f, --file <file>',
			'Path to the report file',
			COMPLEXITY_REPORT_FILE
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			const initOptions = {
				tag: options.tag
			};

			if (options.file && options.file !== COMPLEXITY_REPORT_FILE) {
				initOptions.complexityReportPath = options.file;
			}

			// Initialize TaskMaster
			const taskMaster = initTaskMaster(initOptions);

			// Show current tag context
			displayCurrentTagIndicator(taskMaster.getCurrentTag());

			await displayComplexityReport(taskMaster.getComplexityReportPath());
		});

	// add-subtask command
	programInstance
		.command('add-subtask')
		.description('Add a subtask to an existing task')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('-p, --parent <id>', 'Parent task ID (required)')
		.option('-i, --task-id <id>', 'Existing task ID to convert to subtask')
		.option(
			'-t, --title <title>',
			'Title for the new subtask (when creating a new subtask)'
		)
		.option('-d, --description <text>', 'Description for the new subtask')
		.option('--details <text>', 'Implementation details for the new subtask')
		.option(
			'--dependencies <ids>',
			'Comma-separated list of dependency IDs for the new subtask'
		)
		.option('-s, --status <status>', 'Status for the new subtask', 'pending')
		.option('--generate', 'Regenerate task files after adding subtask')
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag
			});

			const parentId = options.parent;
			const existingTaskId = options.taskId;
			const generateFiles = options.generate || false;

			// Resolve tag using standard pattern
			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			if (!parentId) {
				console.error(
					chalk.red(
						'Error: --parent parameter is required. Please provide a parent task ID.'
					)
				);
				showAddSubtaskHelp();
				process.exit(1);
			}

			// Parse dependencies if provided
			let dependencies = [];
			if (options.dependencies) {
				dependencies = options.dependencies.split(',').map((id) => {
					// Handle both regular IDs and dot notation
					return id.includes('.') ? id.trim() : parseInt(id.trim(), 10);
				});
			}

			try {
				if (existingTaskId) {
					// Convert existing task to subtask
					console.log(
						chalk.blue(
							`Converting task ${existingTaskId} to a subtask of ${parentId}...`
						)
					);
					await addSubtask(
						taskMaster.getTasksPath(),
						parentId,
						existingTaskId,
						null,
						generateFiles,
						{ projectRoot: taskMaster.getProjectRoot(), tag }
					);
					console.log(
						chalk.green(
							`✓ Task ${existingTaskId} successfully converted to a subtask of task ${parentId}`
						)
					);
				} else if (options.title) {
					// Create new subtask with provided data
					console.log(
						chalk.blue(`Creating new subtask for parent task ${parentId}...`)
					);

					const newSubtaskData = {
						title: options.title,
						description: options.description || '',
						details: options.details || '',
						status: options.status || 'pending',
						dependencies: dependencies
					};

					const subtask = await addSubtask(
						taskMaster.getTasksPath(),
						parentId,
						null,
						newSubtaskData,
						generateFiles,
						{ projectRoot: taskMaster.getProjectRoot(), tag }
					);
					console.log(
						chalk.green(
							`✓ New subtask ${parentId}.${subtask.id} successfully created`
						)
					);

					// Display success message and suggested next steps
					console.log(
						boxen(
							chalk.white.bold(
								`Subtask ${parentId}.${subtask.id} Added Successfully`
							) +
								'\n\n' +
								chalk.white(`Title: ${subtask.title}`) +
								'\n' +
								chalk.white(`Status: ${getStatusWithColor(subtask.status)}`) +
								'\n' +
								(dependencies.length > 0
									? chalk.white(`Dependencies: ${dependencies.join(', ')}`) +
										'\n'
									: '') +
								'\n' +
								chalk.white.bold('Next Steps:') +
								'\n' +
								chalk.cyan(
									`1. Run ${chalk.yellow(`task-master show ${parentId}`)} to see the parent task with all subtasks`
								) +
								'\n' +
								chalk.cyan(
									`2. Run ${chalk.yellow(`task-master set-status --id=${parentId}.${subtask.id} --status=in-progress`)} to start working on it`
								),
							{
								padding: 1,
								borderColor: 'green',
								borderStyle: 'round',
								margin: { top: 1 }
							}
						)
					);
				} else {
					console.error(
						chalk.red('Error: Either --task-id or --title must be provided.')
					);
					console.log(
						boxen(
							chalk.white.bold('Usage Examples:') +
								'\n\n' +
								chalk.white('Convert existing task to subtask:') +
								'\n' +
								chalk.yellow(
									`  task-master add-subtask --parent=5 --task-id=8`
								) +
								'\n\n' +
								chalk.white('Create new subtask:') +
								'\n' +
								chalk.yellow(
									`  task-master add-subtask --parent=5 --title="Implement login UI" --description="Create the login form"`
								) +
								'\n\n',
							{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
						)
					);
					process.exit(1);
				}
			} catch (error) {
				console.error(chalk.red(`Error: ${error.message}`));
				showAddSubtaskHelp();
				process.exit(1);
			}
		})
		.on('error', function (err) {
			console.error(chalk.red(`Error: ${err.message}`));
			showAddSubtaskHelp();
			process.exit(1);
		});

	// Helper function to show add-subtask command help
	function showAddSubtaskHelp() {
		console.log(
			boxen(
				`${chalk.white.bold('Add Subtask Command Help')}\n\n${chalk.cyan('Usage:')}\n  task-master add-subtask --parent=<id> [options]\n\n${chalk.cyan('Options:')}\n  -p, --parent <id>         Parent task ID (required)\n  -i, --task-id <id>        Existing task ID to convert to subtask\n  -t, --title <title>       Title for the new subtask\n  -d, --description <text>  Description for the new subtask\n  --details <text>          Implementation details for the new subtask\n  --dependencies <ids>      Comma-separated list of dependency IDs\n  -s, --status <status>     Status for the new subtask (default: "pending")\n  -f, --file <file>         Path to the tasks file (default: "${TASKMASTER_TASKS_FILE}")\n  --generate                Regenerate task files after adding subtask\n\n${chalk.cyan('Examples:')}\n  task-master add-subtask --parent=5 --task-id=8\n  task-master add-subtask -p 5 -t "Implement login UI" -d "Create the login form" --generate`,
				{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
			)
		);
	}

	// remove-subtask command
	programInstance
		.command('remove-subtask')
		.description('Remove a subtask from its parent task')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'-i, --id <id>',
			'Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated for multiple subtasks)'
		)
		.option(
			'-c, --convert',
			'Convert the subtask to a standalone task instead of deleting it'
		)
		.option('--generate', 'Regenerate task files after removing subtask')
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag
			});

			const subtaskIds = options.id;
			const convertToTask = options.convert || false;
			const generateFiles = options.generate || false;
			const tag = taskMaster.getCurrentTag();

			if (!subtaskIds) {
				console.error(
					chalk.red(
						'Error: --id parameter is required. Please provide subtask ID(s) in format "parentId.subtaskId".'
					)
				);
				showRemoveSubtaskHelp();
				process.exit(1);
			}

			try {
				// Split by comma to support multiple subtask IDs
				const subtaskIdArray = subtaskIds.split(',').map((id) => id.trim());

				for (const subtaskId of subtaskIdArray) {
					// Validate subtask ID format
					if (!subtaskId.includes('.')) {
						console.error(
							chalk.red(
								`Error: Subtask ID "${subtaskId}" must be in format "parentId.subtaskId"`
							)
						);
						showRemoveSubtaskHelp();
						process.exit(1);
					}

					console.log(chalk.blue(`Removing subtask ${subtaskId}...`));
					if (convertToTask) {
						console.log(
							chalk.blue('The subtask will be converted to a standalone task')
						);
					}

					const result = await removeSubtask(
						taskMaster.getTasksPath(),
						subtaskId,
						convertToTask,
						generateFiles,
						{ projectRoot: taskMaster.getProjectRoot(), tag }
					);

					if (convertToTask && result) {
						// Display success message and next steps for converted task
						console.log(
							boxen(
								chalk.white.bold(
									`Subtask ${subtaskId} Converted to Task #${result.id}`
								) +
									'\n\n' +
									chalk.white(`Title: ${result.title}`) +
									'\n' +
									chalk.white(`Status: ${getStatusWithColor(result.status)}`) +
									'\n' +
									chalk.white(
										`Dependencies: ${result.dependencies.join(', ')}`
									) +
									'\n\n' +
									chalk.white.bold('Next Steps:') +
									'\n' +
									chalk.cyan(
										`1. Run ${chalk.yellow(`task-master show ${result.id}`)} to see details of the new task`
									) +
									'\n' +
									chalk.cyan(
										`2. Run ${chalk.yellow(`task-master set-status --id=${result.id} --status=in-progress`)} to start working on it`
									),
								{
									padding: 1,
									borderColor: 'green',
									borderStyle: 'round',
									margin: { top: 1 }
								}
							)
						);
					} else {
						// Display success message for deleted subtask
						console.log(
							boxen(
								chalk.white.bold(`Subtask ${subtaskId} Removed`) +
									'\n\n' +
									chalk.white('The subtask has been successfully deleted.'),
								{
									padding: 1,
									borderColor: 'green',
									borderStyle: 'round',
									margin: { top: 1 }
								}
							)
						);
					}
				}
			} catch (error) {
				console.error(chalk.red(`Error: ${error.message}`));
				showRemoveSubtaskHelp();
				process.exit(1);
			}
		})
		.on('error', function (err) {
			console.error(chalk.red(`Error: ${err.message}`));
			showRemoveSubtaskHelp();
			process.exit(1);
		});

	// Helper function to show remove-subtask command help
	function showRemoveSubtaskHelp() {
		console.log(
			boxen(
				chalk.white.bold('Remove Subtask Command Help') +
					'\n\n' +
					chalk.cyan('Usage:') +
					'\n' +
					`  task-master remove-subtask --id=<parentId.subtaskId> [options]\n\n` +
					chalk.cyan('Options:') +
					'\n' +
					'  -i, --id <id>       Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated, required)\n' +
					'  -c, --convert       Convert the subtask to a standalone task instead of deleting it\n' +
					'  -f, --file <file>   Path to the tasks file (default: "' +
					TASKMASTER_TASKS_FILE +
					'")\n' +
					'  --skip-generate     Skip regenerating task files\n\n' +
					chalk.cyan('Examples:') +
					'\n' +
					'  task-master remove-subtask --id=5.2\n' +
					'  task-master remove-subtask --id=5.2,6.3,7.1\n' +
					'  task-master remove-subtask --id=5.2 --convert',
				{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
			)
		);
	}

	// Helper function to show tags command help
	function showTagsHelp() {
		console.log(
			boxen(
				chalk.white.bold('Tags Command Help') +
					'\n\n' +
					chalk.cyan('Usage:') +
					'\n' +
					`  task-master tags [options]\n\n` +
					chalk.cyan('Options:') +
					'\n' +
					'  -f, --file <file>   Path to the tasks file (default: "' +
					TASKMASTER_TASKS_FILE +
					'")\n' +
					'  --show-metadata     Show detailed metadata for each tag\n\n' +
					chalk.cyan('Examples:') +
					'\n' +
					'  task-master tags\n' +
					'  task-master tags --show-metadata\n\n' +
					chalk.cyan('Related Commands:') +
					'\n' +
					'  task-master add-tag <name>      Create a new tag\n' +
					'  task-master use-tag <name>      Switch to a tag\n' +
					'  task-master delete-tag <name>   Delete a tag',
				{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
			)
		);
	}

	// Helper function to show add-tag command help
	function showAddTagHelp() {
		console.log(
			boxen(
				chalk.white.bold('Add Tag Command Help') +
					'\n\n' +
					chalk.cyan('Usage:') +
					'\n' +
					`  task-master add-tag <tagName> [options]\n\n` +
					chalk.cyan('Options:') +
					'\n' +
					'  -f, --file <file>        Path to the tasks file (default: "' +
					TASKMASTER_TASKS_FILE +
					'")\n' +
					'  --copy-from-current      Copy tasks from the current tag to the new tag\n' +
					'  --copy-from <tag>        Copy tasks from the specified tag to the new tag\n' +
					'  -d, --description <text> Optional description for the tag\n\n' +
					chalk.cyan('Examples:') +
					'\n' +
					'  task-master add-tag feature-xyz\n' +
					'  task-master add-tag feature-xyz --copy-from-current\n' +
					'  task-master add-tag feature-xyz --copy-from master\n' +
					'  task-master add-tag feature-xyz -d "Feature XYZ development"',
				{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
			)
		);
	}

	// Helper function to show delete-tag command help
	function showDeleteTagHelp() {
		console.log(
			boxen(
				chalk.white.bold('Delete Tag Command Help') +
					'\n\n' +
					chalk.cyan('Usage:') +
					'\n' +
					`  task-master delete-tag <tagName> [options]\n\n` +
					chalk.cyan('Options:') +
					'\n' +
					'  -f, --file <file>   Path to the tasks file (default: "' +
					TASKMASTER_TASKS_FILE +
					'")\n' +
					'  -y, --yes           Skip confirmation prompts\n\n' +
					chalk.cyan('Examples:') +
					'\n' +
					'  task-master delete-tag feature-xyz\n' +
					'  task-master delete-tag feature-xyz --yes\n\n' +
					chalk.yellow('Warning:') +
					'\n' +
					'  This will permanently delete the tag and all its tasks!',
				{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
			)
		);
	}

	// Helper function to show use-tag command help
	function showUseTagHelp() {
		console.log(
			boxen(
				chalk.white.bold('Use Tag Command Help') +
					'\n\n' +
					chalk.cyan('Usage:') +
					'\n' +
					`  task-master use-tag <tagName> [options]\n\n` +
					chalk.cyan('Options:') +
					'\n' +
					'  -f, --file <file>   Path to the tasks file (default: "' +
					TASKMASTER_TASKS_FILE +
					'")\n\n' +
					chalk.cyan('Examples:') +
					'\n' +
					'  task-master use-tag feature-xyz\n' +
					'  task-master use-tag master\n\n' +
					chalk.cyan('Related Commands:') +
					'\n' +
					'  task-master tags                 List all available tags\n' +
					'  task-master add-tag <name>       Create a new tag',
				{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
			)
		);
	}

	// Helper function to show research command help
	function showResearchHelp() {
		console.log(
			boxen(
				chalk.white.bold('Research Command Help') +
					'\n\n' +
					chalk.cyan('Usage:') +
					'\n' +
					`  task-master research "<query>" [options]\n\n` +
					chalk.cyan('Required:') +
					'\n' +
					'  <query>             Research question or prompt (required)\n\n' +
					chalk.cyan('Context Options:') +
					'\n' +
					'  -i, --id <ids>      Comma-separated task/subtask IDs for context (e.g., "15,23.2")\n' +
					'  -f, --files <paths> Comma-separated file paths for context\n' +
					'  -c, --context <text> Additional custom context text\n' +
					'  --tree              Include project file tree structure\n\n' +
					chalk.cyan('Output Options:') +
					'\n' +
					'  -d, --detail <level> Detail level: low, medium, high (default: medium)\n' +
					'  --save-to <id>      Auto-save results to task/subtask ID (e.g., "15" or "15.2")\n' +
					'  --tag <tag>         Specify tag context for task operations\n\n' +
					chalk.cyan('Examples:') +
					'\n' +
					'  task-master research "How should I implement user authentication?"\n' +
					'  task-master research "What\'s the best approach?" --id=15,23.2\n' +
					'  task-master research "How does auth work?" --files=src/auth.js --tree\n' +
					'  task-master research "Implementation steps?" --save-to=15.2 --detail=high',
				{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
			)
		);
	}

	// remove-task command
	programInstance
		.command('remove-task')
		.description('Remove one or more tasks or subtasks permanently')
		.option(
			'-i, --id <ids>',
			'ID(s) of the task(s) or subtask(s) to remove (e.g., "5", "5.2", or "5,6.1,7")'
		)
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('-y, --yes', 'Skip confirmation prompt', false)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag
			});

			const taskIdsString = options.id;

			// Resolve tag using standard pattern
			const tag = taskMaster.getCurrentTag();

			// Show current tag context
			displayCurrentTagIndicator(tag);

			if (!taskIdsString) {
				console.error(chalk.red('Error: Task ID(s) are required'));
				console.error(
					chalk.yellow(
						'Usage: task-master remove-task --id=<taskId1,taskId2...>'
					)
				);
				process.exit(1);
			}

			const taskIdsToRemove = taskIdsString
				.split(',')
				.map((id) => id.trim())
				.filter(Boolean);

			if (taskIdsToRemove.length === 0) {
				console.error(chalk.red('Error: No valid task IDs provided.'));
				process.exit(1);
			}

			try {
				// Read data once for checks and confirmation
				const data = readJSON(
					taskMaster.getTasksPath(),
					taskMaster.getProjectRoot(),
					tag
				);
				if (!data || !data.tasks) {
					console.error(
						chalk.red(`Error: No valid tasks found in ${tasksPath}`)
					);
					process.exit(1);
				}

				const existingTasksToRemove = [];
				const nonExistentIds = [];
				let totalSubtasksToDelete = 0;
				const dependentTaskMessages = [];

				for (const taskId of taskIdsToRemove) {
					if (!taskExists(data.tasks, taskId)) {
						nonExistentIds.push(taskId);
					} else {
						// Correctly extract the task object from the result of findTaskById
						const findResult = findTaskById(data.tasks, taskId);
						const taskObject = findResult.task; // Get the actual task/subtask object

						if (taskObject) {
							existingTasksToRemove.push({ id: taskId, task: taskObject }); // Push the actual task object

							// If it's a main task, count its subtasks and check dependents
							if (!taskObject.isSubtask) {
								// Check the actual task object
								if (taskObject.subtasks && taskObject.subtasks.length > 0) {
									totalSubtasksToDelete += taskObject.subtasks.length;
								}
								const dependentTasks = data.tasks.filter(
									(t) =>
										t.dependencies &&
										t.dependencies.includes(parseInt(taskId, 10))
								);
								if (dependentTasks.length > 0) {
									dependentTaskMessages.push(
										`  - Task ${taskId}: ${dependentTasks.length} dependent tasks (${dependentTasks.map((t) => t.id).join(', ')})`
									);
								}
							}
						} else {
							// Handle case where findTaskById returned null for the task property (should be rare)
							nonExistentIds.push(`${taskId} (error finding details)`);
						}
					}
				}

				if (nonExistentIds.length > 0) {
					console.warn(
						chalk.yellow(
							`Warning: The following task IDs were not found: ${nonExistentIds.join(', ')}`
						)
					);
				}

				if (existingTasksToRemove.length === 0) {
					console.log(chalk.blue('No existing tasks found to remove.'));
					process.exit(0);
				}

				// Skip confirmation if --yes flag is provided
				if (!options.yes) {
					console.log();
					console.log(
						chalk.red.bold(
							`⚠ WARNING: This will permanently delete the following ${existingTasksToRemove.length} item(s):`
						)
					);
					console.log();

					existingTasksToRemove.forEach(({ id, task }) => {
						if (!task) return; // Should not happen due to taskExists check, but safeguard
						if (task.isSubtask) {
							// Subtask - title is directly on the task object
							console.log(
								chalk.white(`  Subtask ${id}: ${task.title || '(no title)'}`)
							);
							// Optionally show parent context if available
							if (task.parentTask) {
								console.log(
									chalk.gray(
										`    (Parent: ${task.parentTask.id} - ${task.parentTask.title || '(no title)'})`
									)
								);
							}
						} else {
							// Main task - title is directly on the task object
							console.log(
								chalk.white.bold(`  Task ${id}: ${task.title || '(no title)'}`)
							);
						}
					});

					if (totalSubtasksToDelete > 0) {
						console.log(
							chalk.yellow(
								`⚠ This will also delete ${totalSubtasksToDelete} subtasks associated with the selected main tasks!`
							)
						);
					}

					if (dependentTaskMessages.length > 0) {
						console.log(
							chalk.yellow(
								'⚠ Warning: Dependencies on the following tasks will be removed:'
							)
						);
						dependentTaskMessages.forEach((msg) =>
							console.log(chalk.yellow(msg))
						);
					}

					console.log();

					const { confirm } = await inquirer.prompt([
						{
							type: 'confirm',
							name: 'confirm',
							message: chalk.red.bold(
								`Are you sure you want to permanently delete these ${existingTasksToRemove.length} item(s)?`
							),
							default: false
						}
					]);

					if (!confirm) {
						console.log(chalk.blue('Task deletion cancelled.'));
						process.exit(0);
					}
				}

				const indicator = startLoadingIndicator(
					`Removing ${existingTasksToRemove.length} task(s)/subtask(s)...`
				);

				// Use the string of existing IDs for the core function
				const existingIdsString = existingTasksToRemove
					.map(({ id }) => id)
					.join(',');
				const result = await removeTask(
					taskMaster.getTasksPath(),
					existingIdsString,
					{
						projectRoot: taskMaster.getProjectRoot(),
						tag
					}
				);

				stopLoadingIndicator(indicator);

				if (result.success) {
					console.log(
						boxen(
							chalk.green(
								`Successfully removed ${result.removedTasks.length} task(s)/subtask(s).`
							) +
								(result.message ? `\n\nDetails:\n${result.message}` : '') +
								(result.error
									? `\n\nWarnings:\n${chalk.yellow(result.error)}`
									: ''),
							{ padding: 1, borderColor: 'green', borderStyle: 'round' }
						)
					);
				} else {
					console.error(
						boxen(
							chalk.red(
								`Operation completed with errors. Removed ${result.removedTasks.length} task(s)/subtask(s).`
							) +
								(result.message ? `\n\nDetails:\n${result.message}` : '') +
								(result.error ? `\n\nErrors:\n${chalk.red(result.error)}` : ''),
							{
								padding: 1,
								borderColor: 'red',
								borderStyle: 'round'
							}
						)
					);
					process.exit(1); // Exit with error code if any part failed
				}

				// Log any initially non-existent IDs again for clarity
				if (nonExistentIds.length > 0) {
					console.warn(
						chalk.yellow(
							`Note: The following IDs were not found initially and were skipped: ${nonExistentIds.join(', ')}`
						)
					);

					// Exit with error if any removals failed
					if (result.removedTasks.length === 0) {
						process.exit(1);
					}
				}
			} catch (error) {
				console.error(
					chalk.red(`Error: ${error.message || 'An unknown error occurred'}`)
				);
				process.exit(1);
			}
		});

	// init command (Directly calls the implementation from init.js)
	programInstance
		.command('init')
		.description('Initialize a new project with Task Master structure')
		.option('-y, --yes', 'Skip prompts and use default values')
		.option('-n, --name <name>', 'Project name')
		.option('-d, --description <description>', 'Project description')
		.option('-v, --version <version>', 'Project version', '0.1.0') // Set default here
		.option('-a, --author <author>', 'Author name')
		.option(
			'-r, --rules <rules...>',
			'List of rules to add (roo, windsurf, cursor, ...). Accepts comma or space separated values.'
		)
		.option('--skip-install', 'Skip installing dependencies')
		.option('--dry-run', 'Show what would be done without making changes')
		.option('--aliases', 'Add shell aliases (tm, taskmaster)')
		.option('--no-aliases', 'Skip shell aliases (tm, taskmaster)')
		.option('--git', 'Initialize Git repository')
		.option('--no-git', 'Skip Git repository initialization')
		.option('--git-tasks', 'Store tasks in Git')
		.option('--no-git-tasks', 'No Git storage of tasks')
		.action(async (cmdOptions) => {
			// cmdOptions contains parsed arguments
			// Parse rules: accept space or comma separated, default to all available rules
			let selectedProfiles = RULE_PROFILES;
			let rulesExplicitlyProvided = false;

			if (cmdOptions.rules && Array.isArray(cmdOptions.rules)) {
				const userSpecifiedProfiles = cmdOptions.rules
					.flatMap((r) => r.split(','))
					.map((r) => r.trim())
					.filter(Boolean);
				// Only override defaults if user specified valid rules
				if (userSpecifiedProfiles.length > 0) {
					selectedProfiles = userSpecifiedProfiles;
					rulesExplicitlyProvided = true;
				}
			}

			cmdOptions.rules = selectedProfiles;
			cmdOptions.rulesExplicitlyProvided = rulesExplicitlyProvided;

			try {
				// Directly call the initializeProject function, passing the parsed options
				await initializeProject(cmdOptions);
				// initializeProject handles its own flow, including potential process.exit()
			} catch (error) {
				console.error(
					chalk.red(`Error during initialization: ${error.message}`)
				);
				process.exit(1);
			}
		});

	// models command
	programInstance
		.command('models')
		.description('Manage AI model configurations')
		.option(
			'--set-main <model_id>',
			'Set the primary model for task generation/updates'
		)
		.option(
			'--set-research <model_id>',
			'Set the model for research-backed operations'
		)
		.option(
			'--set-fallback <model_id>',
			'Set the model to use if the primary fails'
		)
		.option('--setup', 'Run interactive setup to configure models')
		.option(
			'--openrouter',
			'Allow setting a custom OpenRouter model ID (use with --set-*) '
		)
		.option(
			'--ollama',
			'Allow setting a custom Ollama model ID (use with --set-*) '
		)
		.option(
			'--bedrock',
			'Allow setting a custom Bedrock model ID (use with --set-*) '
		)
		.option(
			'--claude-code',
			'Allow setting a Claude Code model ID (use with --set-*)'
		)
		.option(
			'--azure',
			'Allow setting a custom Azure OpenAI model ID (use with --set-*) '
		)
		.option(
			'--vertex',
			'Allow setting a custom Vertex AI model ID (use with --set-*) '
		)
		.option(
			'--gemini-cli',
			'Allow setting a Gemini CLI model ID (use with --set-*)'
		)
		.addHelpText(
			'after',
			`
Examples:
  $ task-master models                              # View current configuration
  $ task-master models --set-main gpt-4o             # Set main model (provider inferred)
  $ task-master models --set-research sonar-pro       # Set research model
  $ task-master models --set-fallback claude-3-5-sonnet-20241022 # Set fallback
  $ task-master models --set-main my-custom-model --ollama  # Set custom Ollama model for main role
  $ task-master models --set-main anthropic.claude-3-sonnet-20240229-v1:0 --bedrock # Set custom Bedrock model for main role
  $ task-master models --set-main some/other-model --openrouter # Set custom OpenRouter model for main role
  $ task-master models --set-main sonnet --claude-code           # Set Claude Code model for main role
  $ task-master models --set-main gpt-4o --azure # Set custom Azure OpenAI model for main role
  $ task-master models --set-main claude-3-5-sonnet@20241022 --vertex # Set custom Vertex AI model for main role
  $ task-master models --set-main gemini-2.5-pro --gemini-cli # Set Gemini CLI model for main role
  $ task-master models --setup                            # Run interactive setup`
		)
		.action(async (options) => {
			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || false
			});

			const projectRoot = taskMaster.getProjectRoot();

			// Validate flags: cannot use multiple provider flags simultaneously
			const providerFlags = [
				options.openrouter,
				options.ollama,
				options.bedrock,
				options.claudeCode,
				options.geminiCli
			].filter(Boolean).length;
			if (providerFlags > 1) {
				console.error(
					chalk.red(
						'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock, --claude-code, --gemini-cli) simultaneously.'
					)
				);
				process.exit(1);
			}

			// Determine the primary action based on flags
			const isSetup = options.setup;
			const isSetOperation =
				options.setMain || options.setResearch || options.setFallback;

			// --- Execute Action ---

			if (isSetup) {
				// Action 1: Run Interactive Setup
				console.log(chalk.blue('Starting interactive model setup...')); // Added feedback
				try {
					await runInteractiveSetup(taskMaster.getProjectRoot());
					// runInteractiveSetup logs its own completion/error messages
				} catch (setupError) {
					console.error(
						chalk.red('\\nInteractive setup failed unexpectedly:'),
						setupError.message
					);
				}
				// --- IMPORTANT: Exit after setup ---
				return; // Stop execution here
			}

			if (isSetOperation) {
				// Action 2: Perform Direct Set Operations
				let updateOccurred = false; // Track if any update actually happened

				if (options.setMain) {
					const result = await setModel('main', options.setMain, {
						projectRoot,
						providerHint: options.openrouter
							? 'openrouter'
							: options.ollama
								? 'ollama'
								: options.bedrock
									? 'bedrock'
									: options.claudeCode
										? 'claude-code'
										: options.geminiCli
											? 'gemini-cli'
											: undefined
					});
					if (result.success) {
						console.log(chalk.green(`✅ ${result.data.message}`));
						if (result.data.warning)
							console.log(chalk.yellow(result.data.warning));
						updateOccurred = true;
					} else {
						console.error(
							chalk.red(`❌ Error setting main model: ${result.error.message}`)
						);
					}
				}
				if (options.setResearch) {
					const result = await setModel('research', options.setResearch, {
						projectRoot,
						providerHint: options.openrouter
							? 'openrouter'
							: options.ollama
								? 'ollama'
								: options.bedrock
									? 'bedrock'
									: options.claudeCode
										? 'claude-code'
										: options.geminiCli
											? 'gemini-cli'
											: undefined
					});
					if (result.success) {
						console.log(chalk.green(`✅ ${result.data.message}`));
						if (result.data.warning)
							console.log(chalk.yellow(result.data.warning));
						updateOccurred = true;
					} else {
						console.error(
							chalk.red(
								`❌ Error setting research model: ${result.error.message}`
							)
						);
					}
				}
				if (options.setFallback) {
					const result = await setModel('fallback', options.setFallback, {
						projectRoot,
						providerHint: options.openrouter
							? 'openrouter'
							: options.ollama
								? 'ollama'
								: options.bedrock
									? 'bedrock'
									: options.claudeCode
										? 'claude-code'
										: options.geminiCli
											? 'gemini-cli'
											: undefined
					});
					if (result.success) {
						console.log(chalk.green(`✅ ${result.data.message}`));
						if (result.data.warning)
							console.log(chalk.yellow(result.data.warning));
						updateOccurred = true;
					} else {
						console.error(
							chalk.red(
								`❌ Error setting fallback model: ${result.error.message}`
							)
						);
					}
				}

				// Optional: Add a final confirmation if any update occurred
				if (updateOccurred) {
					console.log(chalk.blue('\nModel configuration updated.'));
				} else {
					console.log(
						chalk.yellow(
							'\nNo model configuration changes were made (or errors occurred).'
						)
					);
				}

				// --- IMPORTANT: Exit after set operations ---
				return; // Stop execution here
			}

			// Action 3: Display Full Status (Only runs if no setup and no set flags)
			console.log(chalk.blue('Fetching current model configuration...')); // Added feedback
			const configResult = await getModelConfiguration({ projectRoot });
			const availableResult = await getAvailableModelsList({ projectRoot });
			const apiKeyStatusResult = await getApiKeyStatusReport({ projectRoot });

			// 1. Display Active Models
			if (!configResult.success) {
				console.error(
					chalk.red(
						`❌ Error fetching configuration: ${configResult.error.message}`
					)
				);
			} else {
				displayModelConfiguration(
					configResult.data,
					availableResult.data?.models || []
				);
			}

			// 2. Display API Key Status
			if (apiKeyStatusResult.success) {
				displayApiKeyStatus(apiKeyStatusResult.data.report);
			} else {
				console.error(
					chalk.yellow(
						`⚠ Warning: Could not display API Key status: ${apiKeyStatusResult.error.message}`
					)
				);
			}

			// 3. Display Other Available Models (Filtered)
			if (availableResult.success) {
				const activeIds = configResult.success
					? [
							configResult.data.activeModels.main.modelId,
							configResult.data.activeModels.research.modelId,
							configResult.data.activeModels.fallback?.modelId
						].filter(Boolean)
					: [];
				const displayableAvailable = availableResult.data.models.filter(
					(m) => !activeIds.includes(m.modelId) && !m.modelId.startsWith('[')
				);
				displayAvailableModels(displayableAvailable);
			} else {
				console.error(
					chalk.yellow(
						`⚠ Warning: Could not display available models: ${availableResult.error.message}`
					)
				);
			}

			// 4. Conditional Hint if Config File is Missing
			const configExists = isConfigFilePresent(projectRoot);
			if (!configExists) {
				console.log(
					chalk.yellow(
						"\\nHint: Run 'task-master models --setup' to create or update your configuration."
					)
				);
			}
			// --- IMPORTANT: Exit after displaying status ---
			return; // Stop execution here
		});

	// response-language command
	programInstance
		.command('lang')
		.description('Manage response language settings')
		.option('--response <response_language>', 'Set the response language')
		.option('--setup', 'Run interactive setup to configure response language')
		.action(async (options) => {
			const taskMaster = initTaskMaster({});
			const projectRoot = taskMaster.getProjectRoot(); // Find project root for context
			const { response, setup } = options;
			let responseLanguage = response !== undefined ? response : 'English';
			if (setup) {
				console.log(
					chalk.blue('Starting interactive response language setup...')
				);
				try {
					const userResponse = await inquirer.prompt([
						{
							type: 'input',
							name: 'responseLanguage',
							message: 'Input your preferred response language',
							default: 'English'
						}
					]);

					console.log(
						chalk.blue(
							'Response language set to:',
							userResponse.responseLanguage
						)
					);
					responseLanguage = userResponse.responseLanguage;
				} catch (setupError) {
					console.error(
						chalk.red('\\nInteractive setup failed unexpectedly:'),
						setupError.message
					);
				}
			}

			const result = setResponseLanguage(responseLanguage, {
				projectRoot
			});

			if (result.success) {
				console.log(chalk.green(`✅ ${result.data.message}`));
			} else {
				console.error(
					chalk.red(
						`❌ Error setting response language: ${result.error.message}`
					)
				);
				process.exit(1);
			}
		});

	// move-task command
	programInstance
		.command('move')
		.description('Move a task or subtask to a new position')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'--from <id>',
			'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")'
		)
		.option(
			'--to <id>',
			'ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated'
		)
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag
			});

			const sourceId = options.from;
			const destinationId = options.to;
			const tag = taskMaster.getCurrentTag();

			if (!sourceId || !destinationId) {
				console.error(
					chalk.red('Error: Both --from and --to parameters are required')
				);
				console.log(
					chalk.yellow(
						'Usage: task-master move --from=<sourceId> --to=<destinationId>'
					)
				);
				process.exit(1);
			}

			// Check if we're moving multiple tasks (comma-separated IDs)
			const sourceIds = sourceId.split(',').map((id) => id.trim());
			const destinationIds = destinationId.split(',').map((id) => id.trim());

			// Validate that the number of source and destination IDs match
			if (sourceIds.length !== destinationIds.length) {
				console.error(
					chalk.red(
						'Error: The number of source and destination IDs must match'
					)
				);
				console.log(
					chalk.yellow('Example: task-master move --from=5,6,7 --to=10,11,12')
				);
				process.exit(1);
			}

			// If moving multiple tasks
			if (sourceIds.length > 1) {
				console.log(
					chalk.blue(
						`Moving multiple tasks: ${sourceIds.join(', ')} to ${destinationIds.join(', ')}...`
					)
				);

				try {
					// Read tasks data once to validate destination IDs
					const tasksData = readJSON(
						taskMaster.getTasksPath(),
						taskMaster.getProjectRoot(),
						tag
					);
					if (!tasksData || !tasksData.tasks) {
						console.error(
							chalk.red(`Error: Invalid or missing tasks file at ${tasksPath}`)
						);
						process.exit(1);
					}

					// Move tasks one by one
					for (let i = 0; i < sourceIds.length; i++) {
						const fromId = sourceIds[i];
						const toId = destinationIds[i];

						// Skip if source and destination are the same
						if (fromId === toId) {
							console.log(
								chalk.yellow(`Skipping ${fromId} -> ${toId} (same ID)`)
							);
							continue;
						}

						console.log(
							chalk.blue(`Moving task/subtask ${fromId} to ${toId}...`)
						);
						try {
							await moveTask(
								taskMaster.getTasksPath(),
								fromId,
								toId,
								i === sourceIds.length - 1,
								{ projectRoot: taskMaster.getProjectRoot(), tag }
							);
							console.log(
								chalk.green(
									`✓ Successfully moved task/subtask ${fromId} to ${toId}`
								)
							);
						} catch (error) {
							console.error(
								chalk.red(`Error moving ${fromId} to ${toId}: ${error.message}`)
							);
							// Continue with the next task rather than exiting
						}
					}
				} catch (error) {
					console.error(chalk.red(`Error: ${error.message}`));
					process.exit(1);
				}
			} else {
				// Moving a single task (existing logic)
				console.log(
					chalk.blue(`Moving task/subtask ${sourceId} to ${destinationId}...`)
				);

				try {
					const result = await moveTask(
						taskMaster.getTasksPath(),
						sourceId,
						destinationId,
						true,
						{ projectRoot: taskMaster.getProjectRoot(), tag }
					);
					console.log(
						chalk.green(
							`✓ Successfully moved task/subtask ${sourceId} to ${destinationId}`
						)
					);
				} catch (error) {
					console.error(chalk.red(`Error: ${error.message}`));
					process.exit(1);
				}
			}
		});

	// Add/remove profile rules command
	programInstance
		.command('rules [action] [profiles...]')
		.description(
			`Add or remove rules for one or more profiles. Valid actions: ${Object.values(RULES_ACTIONS).join(', ')} (e.g., task-master rules ${RULES_ACTIONS.ADD} windsurf roo)`
		)
		.option(
			'-f, --force',
			'Skip confirmation prompt when removing rules (dangerous)'
		)
		.option(
			`--${RULES_SETUP_ACTION}`,
			'Run interactive setup to select rule profiles to add'
		)
		.addHelpText(
			'after',
			`
		Examples:
		$ task-master rules ${RULES_ACTIONS.ADD} windsurf roo          # Add Windsurf and Roo rule sets
		$ task-master rules ${RULES_ACTIONS.REMOVE} windsurf          # Remove Windsurf rule set
		$ task-master rules --${RULES_SETUP_ACTION}                  # Interactive setup to select rule profiles`
		)
		.action(async (action, profiles, options) => {
			const taskMaster = initTaskMaster({});
			const projectRoot = taskMaster.getProjectRoot();
			if (!projectRoot) {
				console.error(chalk.red('Error: Could not find project root.'));
				process.exit(1);
			}

			/**
			 * 'task-master rules --setup' action:
			 *
			 * Launches an interactive prompt to select which rule profiles to add to the current project.
			 * This does NOT perform project initialization or ask about shell aliases—only rules selection.
			 *
			 * Example usage:
			 *   $ task-master rules --setup
			 *
			 * Useful for adding rules after project creation.
			 *
			 * The list of profiles is always up-to-date with the available profiles.
			 */
			if (options[RULES_SETUP_ACTION]) {
				// Run interactive rules setup ONLY (no project init)
				const selectedRuleProfiles = await runInteractiveProfilesSetup();

				if (!selectedRuleProfiles || selectedRuleProfiles.length === 0) {
					console.log(chalk.yellow('No profiles selected. Exiting.'));
					return;
				}

				console.log(
					chalk.blue(
						`Installing ${selectedRuleProfiles.length} selected profile(s)...`
					)
				);

				for (let i = 0; i < selectedRuleProfiles.length; i++) {
					const profile = selectedRuleProfiles[i];
					console.log(
						chalk.blue(
							`Processing profile ${i + 1}/${selectedRuleProfiles.length}: ${profile}...`
						)
					);

					if (!isValidProfile(profile)) {
						console.warn(
							`Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
						);
						continue;
					}
					const profileConfig = getRulesProfile(profile);

					const addResult = convertAllRulesToProfileRules(
						projectRoot,
						profileConfig
					);

					console.log(chalk.green(generateProfileSummary(profile, addResult)));
				}

				console.log(
					chalk.green(
						`\nCompleted installation of all ${selectedRuleProfiles.length} profile(s).`
					)
				);
				return;
			}

			// Validate action for non-setup mode
			if (!action || !isValidRulesAction(action)) {
				console.error(
					chalk.red(
						`Error: Invalid or missing action '${action || 'none'}'. Valid actions are: ${Object.values(RULES_ACTIONS).join(', ')}`
					)
				);
				console.error(
					chalk.yellow(
						`For interactive setup, use: task-master rules --${RULES_SETUP_ACTION}`
					)
				);
				process.exit(1);
			}

			if (!profiles || profiles.length === 0) {
				console.error(
					'Please specify at least one rule profile (e.g., windsurf, roo).'
				);
				process.exit(1);
			}

			// Support both space- and comma-separated profile lists
			const expandedProfiles = profiles
				.flatMap((b) => b.split(',').map((s) => s.trim()))
				.filter(Boolean);

			if (action === RULES_ACTIONS.REMOVE) {
				let confirmed = true;
				if (!options.force) {
					// Check if this removal would leave no profiles remaining
					if (wouldRemovalLeaveNoProfiles(projectRoot, expandedProfiles)) {
						const installedProfiles = getInstalledProfiles(projectRoot);
						confirmed = await confirmRemoveAllRemainingProfiles(
							expandedProfiles,
							installedProfiles
						);
					} else {
						confirmed = await confirmProfilesRemove(expandedProfiles);
					}
				}
				if (!confirmed) {
					console.log(chalk.yellow('Aborted: No rules were removed.'));
					return;
				}
			}

			const removalResults = [];
			const addResults = [];

			for (const profile of expandedProfiles) {
				if (!isValidProfile(profile)) {
					console.warn(
						`Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.`
					);
					continue;
				}
				const profileConfig = getRulesProfile(profile);

				if (action === RULES_ACTIONS.ADD) {
					console.log(chalk.blue(`Adding rules for profile: ${profile}...`));
					const addResult = convertAllRulesToProfileRules(
						projectRoot,
						profileConfig
					);
					console.log(
						chalk.blue(`Completed adding rules for profile: ${profile}`)
					);

					// Store result with profile name for summary
					addResults.push({
						profileName: profile,
						success: addResult.success,
						failed: addResult.failed
					});

					console.log(chalk.green(generateProfileSummary(profile, addResult)));
				} else if (action === RULES_ACTIONS.REMOVE) {
					console.log(chalk.blue(`Removing rules for profile: ${profile}...`));
					const result = removeProfileRules(projectRoot, profileConfig);
					removalResults.push(result);
					console.log(
						chalk.green(generateProfileRemovalSummary(profile, result))
					);
				} else {
					console.error(
						`Unknown action. Use "${RULES_ACTIONS.ADD}" or "${RULES_ACTIONS.REMOVE}".`
					);
					process.exit(1);
				}
			}

			// Print summary for additions
			if (action === RULES_ACTIONS.ADD && addResults.length > 0) {
				const { allSuccessfulProfiles, totalSuccess, totalFailed } =
					categorizeProfileResults(addResults);

				if (allSuccessfulProfiles.length > 0) {
					console.log(
						chalk.green(
							`\nSuccessfully processed profiles: ${allSuccessfulProfiles.join(', ')}`
						)
					);

					// Create a descriptive summary
					if (totalSuccess > 0) {
						console.log(
							chalk.green(
								`Total: ${totalSuccess} files processed, ${totalFailed} failed.`
							)
						);
					} else {
						console.log(
							chalk.green(
								`Total: ${allSuccessfulProfiles.length} profile(s) set up successfully.`
							)
						);
					}
				}
			}

			// Print summary for removals
			if (action === RULES_ACTIONS.REMOVE && removalResults.length > 0) {
				const {
					successfulRemovals,
					skippedRemovals,
					failedRemovals,
					removalsWithNotices
				} = categorizeRemovalResults(removalResults);

				if (successfulRemovals.length > 0) {
					console.log(
						chalk.green(
							`\nSuccessfully removed profiles for: ${successfulRemovals.join(', ')}`
						)
					);
				}
				if (skippedRemovals.length > 0) {
					console.log(
						chalk.yellow(
							`Skipped (default or protected): ${skippedRemovals.join(', ')}`
						)
					);
				}
				if (failedRemovals.length > 0) {
					console.log(chalk.red('\nErrors occurred:'));
					failedRemovals.forEach((r) => {
						console.log(chalk.red(`  ${r.profileName}: ${r.error}`));
					});
				}
				// Display notices about preserved files/configurations
				if (removalsWithNotices.length > 0) {
					console.log(chalk.cyan('\nNotices:'));
					removalsWithNotices.forEach((r) => {
						console.log(chalk.cyan(`  ${r.profileName}: ${r.notice}`));
					});
				}

				// Overall summary
				const totalProcessed = removalResults.length;
				const totalSuccessful = successfulRemovals.length;
				const totalSkipped = skippedRemovals.length;
				const totalFailed = failedRemovals.length;

				console.log(
					chalk.blue(
						`\nTotal: ${totalProcessed} profile(s) processed - ${totalSuccessful} removed, ${totalSkipped} skipped, ${totalFailed} failed.`
					)
				);
			}
		});

	programInstance
		.command('migrate')
		.description(
			'Migrate existing project to use the new .taskmaster directory structure'
		)
		.option(
			'-f, --force',
			'Force migration even if .taskmaster directory already exists'
		)
		.option(
			'--backup',
			'Create backup of old files before migration (default: false)',
			false
		)
		.option(
			'--cleanup',
			'Remove old files after successful migration (default: true)',
			true
		)
		.option('-y, --yes', 'Skip confirmation prompts')
		.option(
			'--dry-run',
			'Show what would be migrated without actually moving files'
		)
		.action(async (options) => {
			try {
				await migrateProject(options);
			} catch (error) {
				console.error(chalk.red('Error during migration:'), error.message);
				process.exit(1);
			}
		});

	// sync-readme command
	programInstance
		.command('sync-readme')
		.description('Sync the current task list to README.md in the project root')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('--with-subtasks', 'Include subtasks in the README output')
		.option(
			'-s, --status <status>',
			'Show only tasks matching this status (e.g., pending, done)'
		)
		.option('-t, --tag <tag>', 'Tag to use for the task list (default: master)')
		.action(async (options) => {
			// Initialize TaskMaster
			const taskMaster = initTaskMaster({
				tasksPath: options.file || true,
				tag: options.tag
			});

			const withSubtasks = options.withSubtasks || false;
			const status = options.status || null;

			const tag = taskMaster.getCurrentTag();

			console.log(
				chalk.blue(
					`📝 Syncing tasks to README.md${withSubtasks ? ' (with subtasks)' : ''}${status ? ` (status: ${status})` : ''}...`
				)
			);

			const success = await syncTasksToReadme(taskMaster.getProjectRoot(), {
				withSubtasks,
				status,
				tasksPath: taskMaster.getTasksPath(),
				tag
			});

			if (!success) {
				console.error(chalk.red('❌ Failed to sync tasks to README.md'));
				process.exit(1);
			}
		});

	// ===== TAG MANAGEMENT COMMANDS =====

	// add-tag command
	programInstance
		.command('add-tag')
		.description('Create a new tag context for organizing tasks')
		.argument(
			'[tagName]',
			'Name of the new tag to create (optional when using --from-branch)'
		)
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option(
			'--copy-from-current',
			'Copy tasks from the current tag to the new tag'
		)
		.option(
			'--copy-from <tag>',
			'Copy tasks from the specified tag to the new tag'
		)
		.option(
			'--from-branch',
			'Create tag name from current git branch (ignores tagName argument)'
		)
		.option('-d, --description <text>', 'Optional description for the tag')
		.action(async (tagName, options) => {
			try {
				// Initialize TaskMaster
				const taskMaster = initTaskMaster({
					tasksPath: options.file || true
				});
				const tasksPath = taskMaster.getTasksPath();

				// Validate tasks file exists
				if (!fs.existsSync(tasksPath)) {
					console.error(
						chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
					);
					console.log(
						chalk.yellow(
							'Hint: Run task-master init or task-master parse-prd to create tasks.json first'
						)
					);
					process.exit(1);
				}

				// Validate that either tagName is provided or --from-branch is used
				if (!tagName && !options.fromBranch) {
					console.error(
						chalk.red(
							'Error: Either tagName argument or --from-branch option is required.'
						)
					);
					console.log(chalk.yellow('Usage examples:'));
					console.log(chalk.cyan('  task-master add-tag my-tag'));
					console.log(chalk.cyan('  task-master add-tag --from-branch'));
					process.exit(1);
				}

				const context = {
					projectRoot: taskMaster.getProjectRoot(),
					commandName: 'add-tag',
					outputType: 'cli'
				};

				// Handle --from-branch option
				if (options.fromBranch) {
					const { createTagFromBranch } = await import(
						'./task-manager/tag-management.js'
					);
					const gitUtils = await import('./utils/git-utils.js');

					// Check if we're in a git repository
					if (!(await gitUtils.isGitRepository(projectRoot))) {
						console.error(
							chalk.red(
								'Error: Not in a git repository. Cannot use --from-branch option.'
							)
						);
						process.exit(1);
					}

					// Get current git branch
					const currentBranch = await gitUtils.getCurrentBranch(projectRoot);
					if (!currentBranch) {
						console.error(
							chalk.red('Error: Could not determine current git branch.')
						);
						process.exit(1);
					}

					// Create tag from branch
					const branchOptions = {
						copyFromCurrent: options.copyFromCurrent || false,
						copyFromTag: options.copyFrom,
						description:
							options.description ||
							`Tag created from git branch "${currentBranch}"`
					};

					await createTagFromBranch(
						taskMaster.getTasksPath(),
						currentBranch,
						branchOptions,
						context,
						'text'
					);
				} else {
					// Regular tag creation
					const createOptions = {
						copyFromCurrent: options.copyFromCurrent || false,
						copyFromTag: options.copyFrom,
						description: options.description
					};

					await createTag(
						taskMaster.getTasksPath(),
						tagName,
						createOptions,
						context,
						'text'
					);
				}

				// Handle auto-switch if requested
				if (options.autoSwitch) {
					const { useTag } = await import('./task-manager/tag-management.js');
					const finalTagName = options.fromBranch
						? (await import('./utils/git-utils.js')).sanitizeBranchNameForTag(
								await (await import('./utils/git-utils.js')).getCurrentBranch(
									projectRoot
								)
							)
						: tagName;
					await useTag(
						taskMaster.getTasksPath(),
						finalTagName,
						{},
						context,
						'text'
					);
				}
			} catch (error) {
				console.error(chalk.red(`Error creating tag: ${error.message}`));
				showAddTagHelp();
				process.exit(1);
			}
		})
		.on('error', function (err) {
			console.error(chalk.red(`Error: ${err.message}`));
			showAddTagHelp();
			process.exit(1);
		});

	// delete-tag command
	programInstance
		.command('delete-tag')
		.description('Delete an existing tag and all its tasks')
		.argument('<tagName>', 'Name of the tag to delete')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('-y, --yes', 'Skip confirmation prompts')
		.action(async (tagName, options) => {
			try {
				// Initialize TaskMaster
				const taskMaster = initTaskMaster({
					tasksPath: options.file || true
				});
				const tasksPath = taskMaster.getTasksPath();

				// Validate tasks file exists
				if (!fs.existsSync(tasksPath)) {
					console.error(
						chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
					);
					process.exit(1);
				}

				const deleteOptions = {
					yes: options.yes || false
				};

				const context = {
					projectRoot: taskMaster.getProjectRoot(),
					commandName: 'delete-tag',
					outputType: 'cli'
				};

				await deleteTag(
					taskMaster.getTasksPath(),
					tagName,
					deleteOptions,
					context,
					'text'
				);
			} catch (error) {
				console.error(chalk.red(`Error deleting tag: ${error.message}`));
				showDeleteTagHelp();
				process.exit(1);
			}
		})
		.on('error', function (err) {
			console.error(chalk.red(`Error: ${err.message}`));
			showDeleteTagHelp();
			process.exit(1);
		});

	// tags command
	programInstance
		.command('tags')
		.description('List all available tags with metadata')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('--show-metadata', 'Show detailed metadata for each tag')
		.option('--tag <tag>', 'Specify tag context for task operations')
		.action(async (options) => {
			try {
				// Initialize TaskMaster
				const taskMaster = initTaskMaster({
					tasksPath: options.file || true,
					tag: options.tag
				});
				const tasksPath = taskMaster.getTasksPath();

				// Validate tasks file exists
				if (!fs.existsSync(tasksPath)) {
					console.error(
						chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
					);
					process.exit(1);
				}

				const listOptions = {
					showTaskCounts: true,
					showMetadata: options.showMetadata || false
				};

				const context = {
					projectRoot: taskMaster.getProjectRoot(),
					commandName: 'tags',
					outputType: 'cli'
				};

				await tags(taskMaster.getTasksPath(), listOptions, context, 'text');
			} catch (error) {
				console.error(chalk.red(`Error listing tags: ${error.message}`));
				showTagsHelp();
				process.exit(1);
			}
		})
		.on('error', function (err) {
			console.error(chalk.red(`Error: ${err.message}`));
			showTagsHelp();
			process.exit(1);
		});

	// use-tag command
	programInstance
		.command('use-tag')
		.description('Switch to a different tag context')
		.argument('<tagName>', 'Name of the tag to switch to')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.action(async (tagName, options) => {
			try {
				// Initialize TaskMaster
				const taskMaster = initTaskMaster({
					tasksPath: options.file || true
				});
				const tasksPath = taskMaster.getTasksPath();

				// Validate tasks file exists
				if (!fs.existsSync(tasksPath)) {
					console.error(
						chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
					);
					process.exit(1);
				}

				const context = {
					projectRoot: taskMaster.getProjectRoot(),
					commandName: 'use-tag',
					outputType: 'cli'
				};

				await useTag(taskMaster.getTasksPath(), tagName, {}, context, 'text');
			} catch (error) {
				console.error(chalk.red(`Error switching tag: ${error.message}`));
				showUseTagHelp();
				process.exit(1);
			}
		})
		.on('error', function (err) {
			console.error(chalk.red(`Error: ${err.message}`));
			showUseTagHelp();
			process.exit(1);
		});

	// rename-tag command
	programInstance
		.command('rename-tag')
		.description('Rename an existing tag')
		.argument('<oldName>', 'Current name of the tag')
		.argument('<newName>', 'New name for the tag')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.action(async (oldName, newName, options) => {
			try {
				// Initialize TaskMaster
				const taskMaster = initTaskMaster({
					tasksPath: options.file || true
				});
				const tasksPath = taskMaster.getTasksPath();

				// Validate tasks file exists
				if (!fs.existsSync(tasksPath)) {
					console.error(
						chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
					);
					process.exit(1);
				}

				const context = {
					projectRoot: taskMaster.getProjectRoot(),
					commandName: 'rename-tag',
					outputType: 'cli'
				};

				await renameTag(
					taskMaster.getTasksPath(),
					oldName,
					newName,
					{},
					context,
					'text'
				);
			} catch (error) {
				console.error(chalk.red(`Error renaming tag: ${error.message}`));
				process.exit(1);
			}
		})
		.on('error', function (err) {
			console.error(chalk.red(`Error: ${err.message}`));
			process.exit(1);
		});

	// copy-tag command
	programInstance
		.command('copy-tag')
		.description('Copy an existing tag to create a new tag with the same tasks')
		.argument('<sourceName>', 'Name of the source tag to copy from')
		.argument('<targetName>', 'Name of the new tag to create')
		.option(
			'-f, --file <file>',
			'Path to the tasks file',
			TASKMASTER_TASKS_FILE
		)
		.option('-d, --description <text>', 'Optional description for the new tag')
		.action(async (sourceName, targetName, options) => {
			try {
				// Initialize TaskMaster
				const taskMaster = initTaskMaster({
					tasksPath: options.file || true
				});
				const tasksPath = taskMaster.getTasksPath();

				// Validate tasks file exists
				if (!fs.existsSync(tasksPath)) {
					console.error(
						chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
					);
					process.exit(1);
				}

				const copyOptions = {
					description: options.description
				};

				const context = {
					projectRoot: taskMaster.getProjectRoot(),
					commandName: 'copy-tag',
					outputType: 'cli'
				};

				await copyTag(
					tasksPath,
					sourceName,
					targetName,
					copyOptions,
					context,
					'text'
				);
			} catch (error) {
				console.error(chalk.red(`Error copying tag: ${error.message}`));
				process.exit(1);
			}
		})
		.on('error', function (err) {
			console.error(chalk.red(`Error: ${err.message}`));
			process.exit(1);
		});

	return programInstance;
}

/**
 * Setup the CLI application
 * @returns {Object} Configured Commander program
 */
function setupCLI() {
	// Create a new program instance
	const programInstance = program
		.name('dev')
		.description('AI-driven development task management')
		.version(() => {
			// Read version directly from package.json ONLY
			try {
				const packageJsonPath = path.join(process.cwd(), 'package.json');
				if (fs.existsSync(packageJsonPath)) {
					const packageJson = JSON.parse(
						fs.readFileSync(packageJsonPath, 'utf8')
					);
					return packageJson.version;
				}
			} catch (error) {
				// Silently fall back to 'unknown'
				log(
					'warn',
					'Could not read package.json for version info in .version()'
				);
			}
			return 'unknown'; // Default fallback if package.json fails
		})
		.helpOption('-h, --help', 'Display help')
		.addHelpCommand(false); // Disable default help command

	// Only override help for the main program, not for individual commands
	const originalHelpInformation =
		programInstance.helpInformation.bind(programInstance);
	programInstance.helpInformation = function () {
		// If this is being called for a subcommand, use the default Commander.js help
		if (this.parent && this.parent !== programInstance) {
			return originalHelpInformation();
		}
		// If this is the main program help, use our custom display
		displayHelp();
		return '';
	};

	// Register commands
	registerCommands(programInstance);

	return programInstance;
}

/**
 * Check for newer version of task-master-ai
 * @returns {Promise<{currentVersion: string, latestVersion: string, needsUpdate: boolean}>}
 */
async function checkForUpdate() {
	// Get current version from package.json ONLY
	const currentVersion = getTaskMasterVersion();

	return new Promise((resolve) => {
		// Get the latest version from npm registry
		const options = {
			hostname: 'registry.npmjs.org',
			path: '/task-master-ai',
			method: 'GET',
			headers: {
				Accept: 'application/vnd.npm.install-v1+json' // Lightweight response
			}
		};

		const req = https.request(options, (res) => {
			let data = '';

			res.on('data', (chunk) => {
				data += chunk;
			});

			res.on('end', () => {
				try {
					const npmData = JSON.parse(data);
					const latestVersion = npmData['dist-tags']?.latest || currentVersion;

					// Compare versions
					const needsUpdate =
						compareVersions(currentVersion, latestVersion) < 0;

					resolve({
						currentVersion,
						latestVersion,
						needsUpdate
					});
				} catch (error) {
					log('debug', `Error parsing npm response: ${error.message}`);
					resolve({
						currentVersion,
						latestVersion: currentVersion,
						needsUpdate: false
					});
				}
			});
		});

		req.on('error', (error) => {
			log('debug', `Error checking for updates: ${error.message}`);
			resolve({
				currentVersion,
				latestVersion: currentVersion,
				needsUpdate: false
			});
		});

		// Set a timeout to avoid hanging if npm is slow
		req.setTimeout(3000, () => {
			req.abort();
			log('debug', 'Update check timed out');
			resolve({
				currentVersion,
				latestVersion: currentVersion,
				needsUpdate: false
			});
		});

		req.end();
	});
}

/**
 * Compare semantic versions
 * @param {string} v1 - First version
 * @param {string} v2 - Second version
 * @returns {number} -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
 */
function compareVersions(v1, v2) {
	const v1Parts = v1.split('.').map((p) => parseInt(p, 10));
	const v2Parts = v2.split('.').map((p) => parseInt(p, 10));

	for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
		const v1Part = v1Parts[i] || 0;
		const v2Part = v2Parts[i] || 0;

		if (v1Part < v2Part) return -1;
		if (v1Part > v2Part) return 1;
	}

	return 0;
}

/**
 * Display upgrade notification message
 * @param {string} currentVersion - Current version
 * @param {string} latestVersion - Latest version
 */
function displayUpgradeNotification(currentVersion, latestVersion) {
	const message = boxen(
		`${chalk.blue.bold('Update Available!')} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}\n\n` +
			`Run ${chalk.cyan('npm i task-master-ai@latest -g')} to update to the latest version with new features and bug fixes.`,
		{
			padding: 1,
			margin: { top: 1, bottom: 1 },
			borderColor: 'yellow',
			borderStyle: 'round'
		}
	);

	console.log(message);
}

/**
 * Parse arguments and run the CLI
 * @param {Array} argv - Command-line arguments
 */
async function runCLI(argv = process.argv) {
	try {
		// Display banner if not in a pipe
		if (process.stdout.isTTY) {
			displayBanner();
		}

		// If no arguments provided, show help
		if (argv.length <= 2) {
			displayHelp();
			process.exit(0);
		}

		// Start the update check in the background - don't await yet
		const updateCheckPromise = checkForUpdate();

		// Setup and parse
		// NOTE: getConfig() might be called during setupCLI->registerCommands if commands need config
		// This means the ConfigurationError might be thrown here if configuration file is missing.
		const programInstance = setupCLI();
		await programInstance.parseAsync(argv);

		// After command execution, check if an update is available
		const updateInfo = await updateCheckPromise;
		if (updateInfo.needsUpdate) {
			displayUpgradeNotification(
				updateInfo.currentVersion,
				updateInfo.latestVersion
			);
		}

		// Check if migration has occurred and show FYI notice once
		try {
			// Use initTaskMaster with no required fields - will only fail if no project root
			const taskMaster = initTaskMaster({});

			const tasksPath = taskMaster.getTasksPath();
			const statePath = taskMaster.getStatePath();

			if (tasksPath && fs.existsSync(tasksPath)) {
				// Read raw file to check if it has master key (bypassing tag resolution)
				const rawData = fs.readFileSync(tasksPath, 'utf8');
				const parsedData = JSON.parse(rawData);

				if (parsedData && parsedData.master) {
					// Migration has occurred, check if we've shown the notice
					let stateData = { migrationNoticeShown: false };
					if (statePath && fs.existsSync(statePath)) {
						// Read state.json directly without tag resolution since it's not a tagged file
						const rawStateData = fs.readFileSync(statePath, 'utf8');
						stateData = JSON.parse(rawStateData) || stateData;
					}

					if (!stateData.migrationNoticeShown) {
						displayTaggedTasksFYI({ _migrationHappened: true });

						// Mark as shown
						stateData.migrationNoticeShown = true;
						// Write state.json directly without tag resolution since it's not a tagged file
						if (statePath) {
							fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2));
						}
					}
				}
			}
		} catch (error) {
			// Silently ignore errors checking for migration notice
		}
	} catch (error) {
		// ** Specific catch block for missing configuration file **
		if (error instanceof ConfigurationError) {
			console.error(
				boxen(
					chalk.red.bold('Configuration Update Required!') +
						'\n\n' +
						chalk.white('Taskmaster now uses a ') +
						chalk.yellow.bold('configuration file') +
						chalk.white(
							' in your project for AI model choices and settings.\n\n' +
								'This file appears to be '
						) +
						chalk.red.bold('missing') +
						chalk.white('. No worries though.\n\n') +
						chalk.cyan.bold('To create this file, run the interactive setup:') +
						'\n' +
						chalk.green('   task-master models --setup') +
						'\n\n' +
						chalk.white.bold('Key Points:') +
						'\n' +
						chalk.white('*   ') +
						chalk.yellow.bold('Configuration file') +
						chalk.white(
							': Stores your AI model settings (do not manually edit)\n'
						) +
						chalk.white('*   ') +
						chalk.yellow.bold('.env & .mcp.json') +
						chalk.white(': Still used ') +
						chalk.red.bold('only') +
						chalk.white(' for your AI provider API keys.\n\n') +
						chalk.cyan(
							'`task-master models` to check your config & available models\n'
						) +
						chalk.cyan(
							'`task-master models --setup` to adjust the AI models used by Taskmaster'
						),
					{
						padding: 1,
						margin: { top: 1 },
						borderColor: 'red',
						borderStyle: 'round'
					}
				)
			);
		} else {
			// Generic error handling for other errors
			console.error(chalk.red(`Error: ${error.message}`));
			if (getDebugFlag()) {
				console.error(error);
			}
		}

		process.exit(1);
	}
}

/**
 * Resolve the final complexity-report path.
 * Rules:
 *  1. If caller passes --output, always respect it.
 *  2. If no explicit output AND tag === 'master' → default report file
 *  3. If no explicit output AND tag !== 'master' → append _<tag>.json
 *
 * @param {string|undefined} outputOpt  --output value from CLI (may be undefined)
 * @param {string} targetTag            resolved tag (defaults to 'master')
 * @param {string} projectRoot          absolute project root
 * @returns {string} absolute path for the report
 */
export function resolveComplexityReportPath({
	projectRoot,
	tag = 'master',
	output // may be undefined
}) {
	// 1. user knows best
	if (output) {
		return path.isAbsolute(output) ? output : path.join(projectRoot, output);
	}

	// 2. default naming
	const base = path.join(projectRoot, COMPLEXITY_REPORT_FILE);
	return tag !== 'master' ? base.replace('.json', `_${tag}.json`) : base;
}

export {
	registerCommands,
	setupCLI,
	runCLI,
	checkForUpdate,
	compareVersions,
	displayUpgradeNotification
};

import fs from 'fs';
import path from 'path';
import { z } from 'zod';

import {
	log,
	readJSON,
	writeJSON,
	isSilentMode,
	getTagAwareFilePath
} from '../utils.js';

import {
	startLoadingIndicator,
	stopLoadingIndicator,
	displayAiUsageSummary
} from '../ui.js';

import { generateTextService } from '../ai-services-unified.js';

import { getDefaultSubtasks, getDebugFlag } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';

// --- Zod Schemas (Keep from previous step) ---
const subtaskSchema = z
	.object({
		id: z
			.number()
			.int()
			.positive()
			.describe('Sequential subtask ID starting from 1'),
		title: z.string().min(5).describe('Clear, specific title for the subtask'),
		description: z
			.string()
			.min(10)
			.describe('Detailed description of the subtask'),
		dependencies: z
			.array(z.string())
			.describe(
				'Array of subtask dependencies within the same parent task. Use format ["parentTaskId.1", "parentTaskId.2"]. Subtasks can only depend on siblings, not external tasks.'
			),
		details: z.string().min(20).describe('Implementation details and guidance'),
		status: z
			.string()
			.describe(
				'The current status of the subtask (should be pending initially)'
			),
		testStrategy: z
			.string()
			.nullable()
			.describe('Approach for testing this subtask')
			.default('')
	})
	.strict();
const subtaskArraySchema = z.array(subtaskSchema);
const subtaskWrapperSchema = z.object({
	subtasks: subtaskArraySchema.describe('The array of generated subtasks.')
});
// --- End Zod Schemas ---

/**
 * Parse subtasks from AI's text response. Includes basic cleanup.
 * @param {string} text - Response text from AI.
 * @param {number} startId - Starting subtask ID expected.
 * @param {number} expectedCount - Expected number of subtasks.
 * @param {number} parentTaskId - Parent task ID for context.
 * @param {Object} logger - Logging object (mcpLog or console log).
 * @returns {Array} Parsed and potentially corrected subtasks array.
 * @throws {Error} If parsing fails or JSON is invalid/malformed.
 */
function parseSubtasksFromText(
	text,
	startId,
	expectedCount,
	parentTaskId,
	logger
) {
	if (typeof text !== 'string') {
		logger.error(
			`AI response text is not a string. Received type: ${typeof text}, Value: ${text}`
		);
		throw new Error('AI response text is not a string.');
	}

	if (!text || text.trim() === '') {
		throw new Error('AI response text is empty after trimming.');
	}

	const originalTrimmedResponse = text.trim(); // Store the original trimmed response
	let jsonToParse = originalTrimmedResponse; // Initialize jsonToParse with it

	logger.debug(
		`Original AI Response for parsing (full length: ${jsonToParse.length}): ${jsonToParse.substring(0, 1000)}...`
	);

	// --- Pre-emptive cleanup for known AI JSON issues ---
	// Fix for "dependencies": , or "dependencies":,
	if (jsonToParse.includes('"dependencies":')) {
		const malformedPattern = /"dependencies":\s*,/g;
		if (malformedPattern.test(jsonToParse)) {
			logger.warn('Attempting to fix malformed "dependencies": , issue.');
			jsonToParse = jsonToParse.replace(
				malformedPattern,
				'"dependencies": [],'
			);
			logger.debug(
				`JSON after fixing "dependencies": ${jsonToParse.substring(0, 500)}...`
			);
		}
	}
	// --- End pre-emptive cleanup ---

	let parsedObject;
	let primaryParseAttemptFailed = false;

	// --- Attempt 1: Simple Parse (with optional Markdown cleanup) ---
	logger.debug('Attempting simple parse...');
	try {
		// Check for markdown code block
		const codeBlockMatch = jsonToParse.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
		let contentToParseDirectly = jsonToParse;
		if (codeBlockMatch && codeBlockMatch[1]) {
			contentToParseDirectly = codeBlockMatch[1].trim();
			logger.debug('Simple parse: Extracted content from markdown code block.');
		} else {
			logger.debug(
				'Simple parse: No markdown code block found, using trimmed original.'
			);
		}

		parsedObject = JSON.parse(contentToParseDirectly);
		logger.debug('Simple parse successful!');

		// Quick check if it looks like our target object
		if (
			!parsedObject ||
			typeof parsedObject !== 'object' ||
			!Array.isArray(parsedObject.subtasks)
		) {
			logger.warn(
				'Simple parse succeeded, but result is not the expected {"subtasks": []} structure. Will proceed to advanced extraction.'
			);
			primaryParseAttemptFailed = true;
			parsedObject = null; // Reset parsedObject so we enter the advanced logic
		}
		// If it IS the correct structure, we'll skip advanced extraction.
	} catch (e) {
		logger.warn(
			`Simple parse failed: ${e.message}. Proceeding to advanced extraction logic.`
		);
		primaryParseAttemptFailed = true;
		// jsonToParse is already originalTrimmedResponse if simple parse failed before modifying it for markdown
	}

	// --- Attempt 2: Advanced Extraction (if simple parse failed or produced wrong structure) ---
	if (primaryParseAttemptFailed || !parsedObject) {
		// Ensure we try advanced if simple parse gave wrong structure
		logger.debug('Attempting advanced extraction logic...');
		// Reset jsonToParse to the original full trimmed response for advanced logic
		jsonToParse = originalTrimmedResponse;

		// (Insert the more complex extraction logic here - the one we worked on with:
		//  - targetPattern = '{"subtasks":';
		//  - careful brace counting for that targetPattern
		//  - fallbacks to last '{' and '}' if targetPattern logic fails)
		//  This was the logic from my previous message. Let's assume it's here.
		//  This block should ultimately set `jsonToParse` to the best candidate string.

		// Example snippet of that advanced logic's start:
		const targetPattern = '{"subtasks":';
		const patternStartIndex = jsonToParse.indexOf(targetPattern);

		if (patternStartIndex !== -1) {
			const openBraces = 0;
			const firstBraceFound = false;
			const extractedJsonBlock = '';
			// ... (loop for brace counting as before) ...
			// ... (if successful, jsonToParse = extractedJsonBlock) ...
			// ... (if that fails, fallbacks as before) ...
		} else {
			// ... (fallback to last '{' and '}' if targetPattern not found) ...
		}
		// End of advanced logic excerpt

		logger.debug(
			`Advanced extraction: JSON string that will be parsed: ${jsonToParse.substring(0, 500)}...`
		);
		try {
			parsedObject = JSON.parse(jsonToParse);
			logger.debug('Advanced extraction parse successful!');
		} catch (parseError) {
			logger.error(
				`Advanced extraction: Failed to parse JSON object: ${parseError.message}`
			);
			logger.error(
				`Advanced extraction: Problematic JSON string for parse (first 500 chars): ${jsonToParse.substring(0, 500)}`
			);
			throw new Error(
				// Re-throw a more specific error if advanced also fails
				`Failed to parse JSON response object after both simple and advanced attempts: ${parseError.message}`
			);
		}
	}

	// --- Validation (applies to successfully parsedObject from either attempt) ---
	if (
		!parsedObject ||
		typeof parsedObject !== 'object' ||
		!Array.isArray(parsedObject.subtasks)
	) {
		logger.error(
			`Final parsed content is not an object or missing 'subtasks' array. Content: ${JSON.stringify(parsedObject).substring(0, 200)}`
		);
		throw new Error(
			'Parsed AI response is not a valid object containing a "subtasks" array after all attempts.'
		);
	}
	const parsedSubtasks = parsedObject.subtasks;

	if (expectedCount && parsedSubtasks.length !== expectedCount) {
		logger.warn(
			`Expected ${expectedCount} subtasks, but parsed ${parsedSubtasks.length}.`
		);
	}

	let currentId = startId;
	const validatedSubtasks = [];
	const validationErrors = [];

	for (const rawSubtask of parsedSubtasks) {
		const correctedSubtask = {
			...rawSubtask,
			id: currentId,
			dependencies: Array.isArray(rawSubtask.dependencies)
				? rawSubtask.dependencies.filter(
						(dep) =>
							typeof dep === 'string' && dep.startsWith(`${parentTaskId}.`)
					)
				: [],
			status: 'pending'
		};

		const result = subtaskSchema.safeParse(correctedSubtask);

		if (result.success) {
			validatedSubtasks.push(result.data);
		} else {
			logger.warn(
				`Subtask validation failed for raw data: ${JSON.stringify(rawSubtask).substring(0, 100)}...`
			);
			result.error.errors.forEach((err) => {
				const errorMessage = `  - Field '${err.path.join('.')}': ${err.message}`;
				logger.warn(errorMessage);
				validationErrors.push(`Subtask ${currentId}: ${errorMessage}`);
			});
		}
		currentId++;
	}

	if (validationErrors.length > 0) {
		logger.error(
			`Found ${validationErrors.length} validation errors in the generated subtasks.`
		);
		logger.warn('Proceeding with only the successfully validated subtasks.');
	}

	if (validatedSubtasks.length === 0 && parsedSubtasks.length > 0) {
		throw new Error(
			'AI response contained potential subtasks, but none passed validation.'
		);
	}
	return validatedSubtasks.slice(0, expectedCount || validatedSubtasks.length);
}

/**
 * Expand a task into subtasks using the unified AI service (generateTextService).
 * Appends new subtasks by default. Replaces existing subtasks if force=true.
 * Integrates complexity report to determine subtask count and prompt if available,
 * unless numSubtasks is explicitly provided.
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {number} taskId - Task ID to expand
 * @param {number | null | undefined} [numSubtasks] - Optional: Explicit target number of subtasks. If null/undefined, check complexity report or config default.
 * @param {boolean} [useResearch=false] - Whether to use the research AI role.
 * @param {string} [additionalContext=''] - Optional additional context.
 * @param {Object} context - Context object containing session and mcpLog.
 * @param {Object} [context.session] - Session object from MCP.
 * @param {Object} [context.mcpLog] - MCP logger object.
 * @param {string} [context.projectRoot] - Project root path
 * @param {string} [context.tag] - Tag for the task
 * @param {boolean} [force=false] - If true, replace existing subtasks; otherwise, append.
 * @returns {Promise<Object>} The updated parent task object with new subtasks.
 * @throws {Error} If task not found, AI service fails, or parsing fails.
 */
async function expandTask(
	tasksPath,
	taskId,
	numSubtasks,
	useResearch = false,
	additionalContext = '',
	context = {},
	force = false
) {
	const {
		session,
		mcpLog,
		projectRoot: contextProjectRoot,
		tag,
		complexityReportPath
	} = context;
	const outputFormat = mcpLog ? 'json' : 'text';

	// Determine projectRoot: Use from context if available, otherwise derive from tasksPath
	const projectRoot = contextProjectRoot || findProjectRoot(tasksPath);

	// Use mcpLog if available, otherwise use the default console log wrapper
	const logger = mcpLog || {
		info: (msg) => !isSilentMode() && log('info', msg),
		warn: (msg) => !isSilentMode() && log('warn', msg),
		error: (msg) => !isSilentMode() && log('error', msg),
		debug: (msg) =>
			!isSilentMode() && getDebugFlag(session) && log('debug', msg) // Use getDebugFlag
	};

	if (mcpLog) {
		logger.info(`expandTask called with context: session=${!!session}`);
	}

	try {
		// --- Task Loading/Filtering (Unchanged) ---
		logger.info(`Reading tasks from ${tasksPath}`);
		const data = readJSON(tasksPath, projectRoot, tag);
		if (!data || !data.tasks)
			throw new Error(`Invalid tasks data in ${tasksPath}`);
		const taskIndex = data.tasks.findIndex(
			(t) => t.id === parseInt(taskId, 10)
		);
		if (taskIndex === -1) throw new Error(`Task ${taskId} not found`);
		const task = data.tasks[taskIndex];
		logger.info(
			`Expanding task ${taskId}: ${task.title}${useResearch ? ' with research' : ''}`
		);
		// --- End Task Loading/Filtering ---

		// --- Handle Force Flag: Clear existing subtasks if force=true ---
		if (force && Array.isArray(task.subtasks) && task.subtasks.length > 0) {
			logger.info(
				`Force flag set. Clearing existing ${task.subtasks.length} subtasks for task ${taskId}.`
			);
			task.subtasks = []; // Clear existing subtasks
		}
		// --- End Force Flag Handling ---

		// --- Context Gathering ---
		let gatheredContext = '';
		try {
			const contextGatherer = new ContextGatherer(projectRoot, tag);
			const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
			const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'expand-task');
			const searchQuery = `${task.title} ${task.description}`;
			const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
				maxResults: 5,
				includeSelf: true
			});
			const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);

			const finalTaskIds = [
				...new Set([taskId.toString(), ...relevantTaskIds])
			];

			if (finalTaskIds.length > 0) {
				const contextResult = await contextGatherer.gather({
					tasks: finalTaskIds,
					format: 'research'
				});
				gatheredContext = contextResult.context || '';
			}
		} catch (contextError) {
			logger.warn(`Could not gather context: ${contextError.message}`);
		}
		// --- End Context Gathering ---

		// --- Complexity Report Integration ---
		let finalSubtaskCount;
		let complexityReasoningContext = '';
		let taskAnalysis = null;

		logger.info(
			`Looking for complexity report at: ${complexityReportPath}${tag !== 'master' ? ` (tag-specific for '${tag}')` : ''}`
		);

		try {
			if (fs.existsSync(complexityReportPath)) {
				const complexityReport = readJSON(complexityReportPath);
				taskAnalysis = complexityReport?.complexityAnalysis?.find(
					(a) => a.taskId === task.id
				);
				if (taskAnalysis) {
					logger.info(
						`Found complexity analysis for task ${task.id}: Score ${taskAnalysis.complexityScore}`
					);
					if (taskAnalysis.reasoning) {
						complexityReasoningContext = `\nComplexity Analysis Reasoning: ${taskAnalysis.reasoning}`;
					}
				} else {
					logger.info(
						`No complexity analysis found for task ${task.id} in report.`
					);
				}
			} else {
				logger.info(
					`Complexity report not found at ${complexityReportPath}. Skipping complexity check.`
				);
			}
		} catch (reportError) {
			logger.warn(
				`Could not read or parse complexity report: ${reportError.message}. Proceeding without it.`
			);
		}

		// Determine final subtask count
		const explicitNumSubtasks = parseInt(numSubtasks, 10);
		if (!Number.isNaN(explicitNumSubtasks) && explicitNumSubtasks >= 0) {
			finalSubtaskCount = explicitNumSubtasks;
			logger.info(
				`Using explicitly provided subtask count: ${finalSubtaskCount}`
			);
		} else if (taskAnalysis?.recommendedSubtasks) {
			finalSubtaskCount = parseInt(taskAnalysis.recommendedSubtasks, 10);
			logger.info(
				`Using subtask count from complexity report: ${finalSubtaskCount}`
			);
		} else {
			finalSubtaskCount = getDefaultSubtasks(session);
			logger.info(`Using default number of subtasks: ${finalSubtaskCount}`);
		}
		if (Number.isNaN(finalSubtaskCount) || finalSubtaskCount < 0) {
			logger.warn(
				`Invalid subtask count determined (${finalSubtaskCount}), defaulting to 3.`
			);
			finalSubtaskCount = 3;
		}

		// Determine prompt content AND system prompt
		const nextSubtaskId = (task.subtasks?.length || 0) + 1;

		// Load prompts using PromptManager
		const promptManager = getPromptManager();

		// Combine all context sources into a single additionalContext parameter
		let combinedAdditionalContext = '';
		if (additionalContext || complexityReasoningContext) {
			combinedAdditionalContext =
				`\n\n${additionalContext}${complexityReasoningContext}`.trim();
		}
		if (gatheredContext) {
			combinedAdditionalContext =
				`${combinedAdditionalContext}\n\n# Project Context\n\n${gatheredContext}`.trim();
		}

		// Ensure expansionPrompt is a string (handle both string and object formats)
		let expansionPromptText = undefined;
		if (taskAnalysis?.expansionPrompt) {
			if (typeof taskAnalysis.expansionPrompt === 'string') {
				expansionPromptText = taskAnalysis.expansionPrompt;
			} else if (
				typeof taskAnalysis.expansionPrompt === 'object' &&
				taskAnalysis.expansionPrompt.text
			) {
				expansionPromptText = taskAnalysis.expansionPrompt.text;
			}
		}

		// Ensure gatheredContext is a string (handle both string and object formats)
		let gatheredContextText = gatheredContext;
		if (typeof gatheredContext === 'object' && gatheredContext !== null) {
			if (gatheredContext.data) {
				gatheredContextText = gatheredContext.data;
			} else if (gatheredContext.text) {
				gatheredContextText = gatheredContext.text;
			} else {
				gatheredContextText = JSON.stringify(gatheredContext);
			}
		}

		const promptParams = {
			task: task,
			subtaskCount: finalSubtaskCount,
			nextSubtaskId: nextSubtaskId,
			additionalContext: additionalContext,
			complexityReasoningContext: complexityReasoningContext,
			gatheredContext: gatheredContextText || '',
			useResearch: useResearch,
			expansionPrompt: expansionPromptText || undefined
		};

		let variantKey = 'default';
		if (expansionPromptText) {
			variantKey = 'complexity-report';
			logger.info(
				`Using expansion prompt from complexity report for task ${task.id}.`
			);
		} else if (useResearch) {
			variantKey = 'research';
			logger.info(`Using research variant for task ${task.id}.`);
		} else {
			logger.info(`Using standard prompt generation for task ${task.id}.`);
		}

		const { systemPrompt, userPrompt: promptContent } =
			await promptManager.loadPrompt('expand-task', promptParams, variantKey);
		// --- End Complexity Report / Prompt Logic ---

		// --- AI Subtask Generation using generateTextService ---
		let generatedSubtasks = [];
		let loadingIndicator = null;
		if (outputFormat === 'text') {
			loadingIndicator = startLoadingIndicator(
				`Generating ${finalSubtaskCount || 'appropriate number of'} subtasks...\n`
			);
		}

		let responseText = '';
		let aiServiceResponse = null;

		try {
			const role = useResearch ? 'research' : 'main';

			// Call generateTextService with the determined prompts and telemetry params
			aiServiceResponse = await generateTextService({
				prompt: promptContent,
				systemPrompt: systemPrompt,
				role,
				session,
				projectRoot,
				commandName: 'expand-task',
				outputType: outputFormat
			});
			responseText = aiServiceResponse.mainResult;

			// Parse Subtasks
			generatedSubtasks = parseSubtasksFromText(
				responseText,
				nextSubtaskId,
				finalSubtaskCount,
				task.id,
				logger
			);
			logger.info(
				`Successfully parsed ${generatedSubtasks.length} subtasks from AI response.`
			);
		} catch (error) {
			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
			logger.error(
				`Error during AI call or parsing for task ${taskId}: ${error.message}`, // Added task ID context
				'error'
			);
			// Log raw response in debug mode if parsing failed
			if (
				error.message.includes('Failed to parse valid subtasks') &&
				getDebugFlag(session)
			) {
				logger.error(`Raw AI Response that failed parsing:\n${responseText}`);
			}
			throw error;
		} finally {
			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
		}

		// --- Task Update & File Writing ---
		// Ensure task.subtasks is an array before appending
		if (!Array.isArray(task.subtasks)) {
			task.subtasks = [];
		}
		// Append the newly generated and validated subtasks
		task.subtasks.push(...generatedSubtasks);
		// --- End Change: Append instead of replace ---

		data.tasks[taskIndex] = task; // Assign the modified task back
		writeJSON(tasksPath, data, projectRoot, tag);
		// await generateTaskFiles(tasksPath, path.dirname(tasksPath));

		// Display AI Usage Summary for CLI
		if (
			outputFormat === 'text' &&
			aiServiceResponse &&
			aiServiceResponse.telemetryData
		) {
			displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
		}

		// Return the updated task object AND telemetry data
		return {
			task,
			telemetryData: aiServiceResponse?.telemetryData,
			tagInfo: aiServiceResponse?.tagInfo
		};
	} catch (error) {
		// Catches errors from file reading, parsing, AI call etc.
		logger.error(`Error expanding task ${taskId}: ${error.message}`, 'error');
		if (outputFormat === 'text' && getDebugFlag(session)) {
			console.error(error); // Log full stack in debug CLI mode
		}
		throw error; // Re-throw for the caller
	}
}

export default expandTask;

import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';

import {
	getStatusWithColor,
	startLoadingIndicator,
	stopLoadingIndicator,
	displayAiUsageSummary
} from '../ui.js';
import {
	log as consoleLog,
	readJSON,
	writeJSON,
	truncate,
	isSilentMode,
	findProjectRoot,
	flattenTasksWithSubtasks
} from '../utils.js';
import { generateTextService } from '../ai-services-unified.js';
import { getDebugFlag } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';

/**
 * Update a subtask by appending additional timestamped information using the unified AI service.
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} subtaskId - ID of the subtask to update in format "parentId.subtaskId"
 * @param {string} prompt - Prompt for generating additional information
 * @param {boolean} [useResearch=false] - Whether to use the research AI role.
 * @param {Object} context - Context object containing session and mcpLog.
 * @param {Object} [context.session] - Session object from MCP server.
 * @param {Object} [context.mcpLog] - MCP logger object.
 * @param {string} [context.projectRoot] - Project root path (needed for AI service key resolution).
 * @param {string} [context.tag] - Tag for the task
 * @param {string} [outputFormat='text'] - Output format ('text' or 'json'). Automatically 'json' if mcpLog is present.
 * @returns {Promise<Object|null>} - The updated subtask or null if update failed.
 */
async function updateSubtaskById(
	tasksPath,
	subtaskId,
	prompt,
	useResearch = false,
	context = {},
	outputFormat = context.mcpLog ? 'json' : 'text'
) {
	const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
	const logFn = mcpLog || consoleLog;
	const isMCP = !!mcpLog;

	// Report helper
	const report = (level, ...args) => {
		if (isMCP) {
			if (typeof logFn[level] === 'function') logFn[level](...args);
			else logFn.info(...args);
		} else if (!isSilentMode()) {
			logFn(level, ...args);
		}
	};

	let loadingIndicator = null;

	try {
		report('info', `Updating subtask ${subtaskId} with prompt: "${prompt}"`);

		if (
			!subtaskId ||
			typeof subtaskId !== 'string' ||
			!subtaskId.includes('.')
		) {
			throw new Error(
				`Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"`
			);
		}

		if (!prompt || typeof prompt !== 'string' || prompt.trim() === '') {
			throw new Error(
				'Prompt cannot be empty. Please provide context for the subtask update.'
			);
		}

		if (!fs.existsSync(tasksPath)) {
			throw new Error(`Tasks file not found at path: ${tasksPath}`);
		}

		const projectRoot = providedProjectRoot || findProjectRoot();
		if (!projectRoot) {
			throw new Error('Could not determine project root directory');
		}

		const data = readJSON(tasksPath, projectRoot, tag);
		if (!data || !data.tasks) {
			throw new Error(
				`No valid tasks found in ${tasksPath}. The file may be corrupted or have an invalid format.`
			);
		}

		const [parentIdStr, subtaskIdStr] = subtaskId.split('.');
		const parentId = parseInt(parentIdStr, 10);
		const subtaskIdNum = parseInt(subtaskIdStr, 10);

		if (
			Number.isNaN(parentId) ||
			parentId <= 0 ||
			Number.isNaN(subtaskIdNum) ||
			subtaskIdNum <= 0
		) {
			throw new Error(
				`Invalid subtask ID format: ${subtaskId}. Both parent ID and subtask ID must be positive integers.`
			);
		}

		const parentTask = data.tasks.find((task) => task.id === parentId);
		if (!parentTask) {
			throw new Error(
				`Parent task with ID ${parentId} not found. Please verify the task ID and try again.`
			);
		}

		if (!parentTask.subtasks || !Array.isArray(parentTask.subtasks)) {
			throw new Error(`Parent task ${parentId} has no subtasks.`);
		}

		const subtaskIndex = parentTask.subtasks.findIndex(
			(st) => st.id === subtaskIdNum
		);
		if (subtaskIndex === -1) {
			throw new Error(
				`Subtask with ID ${subtaskId} not found. Please verify the subtask ID and try again.`
			);
		}

		const subtask = parentTask.subtasks[subtaskIndex];

		// --- Context Gathering ---
		let gatheredContext = '';
		try {
			const contextGatherer = new ContextGatherer(projectRoot, tag);
			const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
			const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-subtask');
			const searchQuery = `${parentTask.title} ${subtask.title} ${prompt}`;
			const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
				maxResults: 5,
				includeSelf: true
			});
			const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);

			const finalTaskIds = [
				...new Set([subtaskId.toString(), ...relevantTaskIds])
			];

			if (finalTaskIds.length > 0) {
				const contextResult = await contextGatherer.gather({
					tasks: finalTaskIds,
					format: 'research'
				});
				gatheredContext = contextResult.context || '';
			}
		} catch (contextError) {
			report('warn', `Could not gather context: ${contextError.message}`);
		}
		// --- End Context Gathering ---

		if (outputFormat === 'text') {
			const table = new Table({
				head: [
					chalk.cyan.bold('ID'),
					chalk.cyan.bold('Title'),
					chalk.cyan.bold('Status')
				],
				colWidths: [10, 55, 10]
			});
			table.push([
				subtaskId,
				truncate(subtask.title, 52),
				getStatusWithColor(subtask.status)
			]);
			console.log(
				boxen(chalk.white.bold(`Updating Subtask #${subtaskId}`), {
					padding: 1,
					borderColor: 'blue',
					borderStyle: 'round',
					margin: { top: 1, bottom: 0 }
				})
			);
			console.log(table.toString());
			loadingIndicator = startLoadingIndicator(
				useResearch
					? 'Updating subtask with research...'
					: 'Updating subtask...'
			);
		}

		let generatedContentString = '';
		let newlyAddedSnippet = '';
		let aiServiceResponse = null;

		try {
			const parentContext = {
				id: parentTask.id,
				title: parentTask.title
			};
			const prevSubtask =
				subtaskIndex > 0
					? {
							id: `${parentTask.id}.${parentTask.subtasks[subtaskIndex - 1].id}`,
							title: parentTask.subtasks[subtaskIndex - 1].title,
							status: parentTask.subtasks[subtaskIndex - 1].status
						}
					: undefined;
			const nextSubtask =
				subtaskIndex < parentTask.subtasks.length - 1
					? {
							id: `${parentTask.id}.${parentTask.subtasks[subtaskIndex + 1].id}`,
							title: parentTask.subtasks[subtaskIndex + 1].title,
							status: parentTask.subtasks[subtaskIndex + 1].status
						}
					: undefined;

			// Build prompts using PromptManager
			const promptManager = getPromptManager();

			const promptParams = {
				parentTask: parentContext,
				prevSubtask: prevSubtask,
				nextSubtask: nextSubtask,
				currentDetails: subtask.details || '(No existing details)',
				updatePrompt: prompt,
				useResearch: useResearch,
				gatheredContext: gatheredContext || ''
			};

			const variantKey = useResearch ? 'research' : 'default';
			const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
				'update-subtask',
				promptParams,
				variantKey
			);

			const role = useResearch ? 'research' : 'main';
			report('info', `Using AI text service with role: ${role}`);

			aiServiceResponse = await generateTextService({
				prompt: userPrompt,
				systemPrompt: systemPrompt,
				role,
				session,
				projectRoot,
				maxRetries: 2,
				commandName: 'update-subtask',
				outputType: isMCP ? 'mcp' : 'cli'
			});

			if (
				aiServiceResponse &&
				aiServiceResponse.mainResult &&
				typeof aiServiceResponse.mainResult === 'string'
			) {
				generatedContentString = aiServiceResponse.mainResult;
			} else {
				generatedContentString = '';
				report(
					'warn',
					'AI service response did not contain expected text string.'
				);
			}

			if (outputFormat === 'text' && loadingIndicator) {
				stopLoadingIndicator(loadingIndicator);
				loadingIndicator = null;
			}
		} catch (aiError) {
			report('error', `AI service call failed: ${aiError.message}`);
			if (outputFormat === 'text' && loadingIndicator) {
				stopLoadingIndicator(loadingIndicator);
				loadingIndicator = null;
			}
			throw aiError;
		}

		if (generatedContentString && generatedContentString.trim()) {
			// Check if the string is not empty
			const timestamp = new Date().toISOString();
			const formattedBlock = `<info added on ${timestamp}>\n${generatedContentString.trim()}\n</info added on ${timestamp}>`;
			newlyAddedSnippet = formattedBlock; // <--- ADD THIS LINE: Store for display

			subtask.details =
				(subtask.details ? subtask.details + '\n' : '') + formattedBlock;
		} else {
			report(
				'warn',
				'AI response was empty or whitespace after trimming. Original details remain unchanged.'
			);
			newlyAddedSnippet = 'No new details were added by the AI.';
		}

		const updatedSubtask = parentTask.subtasks[subtaskIndex];

		if (outputFormat === 'text' && getDebugFlag(session)) {
			console.log(
				'>>> DEBUG: Subtask details AFTER AI update:',
				updatedSubtask.details
			);
		}

		if (updatedSubtask.description) {
			if (prompt.length < 100) {
				if (outputFormat === 'text' && getDebugFlag(session)) {
					console.log(
						'>>> DEBUG: Subtask description BEFORE append:',
						updatedSubtask.description
					);
				}
				updatedSubtask.description += ` [Updated: ${new Date().toLocaleDateString()}]`;
				if (outputFormat === 'text' && getDebugFlag(session)) {
					console.log(
						'>>> DEBUG: Subtask description AFTER append:',
						updatedSubtask.description
					);
				}
			}
		}

		if (outputFormat === 'text' && getDebugFlag(session)) {
			console.log('>>> DEBUG: About to call writeJSON with updated data...');
		}
		writeJSON(tasksPath, data, projectRoot, tag);
		if (outputFormat === 'text' && getDebugFlag(session)) {
			console.log('>>> DEBUG: writeJSON call completed.');
		}

		report('success', `Successfully updated subtask ${subtaskId}`);
		// Updated  function call to make sure if uncommented it will generate the task files for the updated subtask based on the tag
		// await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
		// 	tag: tag,
		// 	projectRoot: projectRoot
		// });

		if (outputFormat === 'text') {
			if (loadingIndicator) {
				stopLoadingIndicator(loadingIndicator);
				loadingIndicator = null;
			}
			console.log(
				boxen(
					chalk.green(`Successfully updated subtask #${subtaskId}`) +
						'\n\n' +
						chalk.white.bold('Title:') +
						' ' +
						updatedSubtask.title +
						'\n\n' +
						chalk.white.bold('Newly Added Snippet:') +
						'\n' +
						chalk.white(newlyAddedSnippet),
					{ padding: 1, borderColor: 'green', borderStyle: 'round' }
				)
			);
		}

		if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
			displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
		}

		return {
			updatedSubtask: updatedSubtask,
			telemetryData: aiServiceResponse.telemetryData,
			tagInfo: aiServiceResponse.tagInfo
		};
	} catch (error) {
		if (outputFormat === 'text' && loadingIndicator) {
			stopLoadingIndicator(loadingIndicator);
			loadingIndicator = null;
		}
		report('error', `Error updating subtask: ${error.message}`);
		if (outputFormat === 'text') {
			console.error(chalk.red(`Error: ${error.message}`));
			if (error.message?.includes('ANTHROPIC_API_KEY')) {
				console.log(
					chalk.yellow('\nTo fix this issue, set your Anthropic API key:')
				);
				console.log('  export ANTHROPIC_API_KEY=your_api_key_here');
			} else if (error.message?.includes('PERPLEXITY_API_KEY')) {
				console.log(chalk.yellow('\nTo fix this issue:'));
				console.log(
					'  1. Set your Perplexity API key: export PERPLEXITY_API_KEY=your_api_key_here'
				);
				console.log(
					'  2. Or run without the research flag: task-master update-subtask --id=<id> --prompt="..."'
				);
			} else if (error.message?.includes('overloaded')) {
				console.log(
					chalk.yellow(
						'\nAI model overloaded, and fallback failed or was unavailable:'
					)
				);
				console.log('  1. Try again in a few minutes.');
				console.log('  2. Ensure PERPLEXITY_API_KEY is set for fallback.');
			} else if (error.message?.includes('not found')) {
				console.log(chalk.yellow('\nTo fix this issue:'));
				console.log(
					'  1. Run task-master list --with-subtasks to see all available subtask IDs'
				);
				console.log(
					'  2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"'
				);
			} else if (
				error.message?.includes('empty stream response') ||
				error.message?.includes('AI did not return a valid text string')
			) {
				console.log(
					chalk.yellow(
						'\nThe AI model returned an empty or invalid response. This might be due to the prompt or API issues. Try rephrasing or trying again later.'
					)
				);
			}
			if (getDebugFlag(session)) {
				console.error(error);
			}
		} else {
			throw error;
		}
		return null;
	}
}

export default updateSubtaskById;

import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import { z } from 'zod';
import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search

import {
	displayBanner,
	getStatusWithColor,
	startLoadingIndicator,
	stopLoadingIndicator,
	succeedLoadingIndicator,
	failLoadingIndicator,
	displayAiUsageSummary,
	displayContextAnalysis
} from '../ui.js';
import {
	readJSON,
	writeJSON,
	log as consoleLog,
	truncate,
	ensureTagMetadata,
	performCompleteTagMigration,
	markMigrationForNotice
} from '../utils.js';
import { generateObjectService } from '../ai-services-unified.js';
import { getDefaultPriority } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import ContextGatherer from '../utils/contextGatherer.js';
import generateTaskFiles from './generate-task-files.js';
import {
	TASK_PRIORITY_OPTIONS,
	DEFAULT_TASK_PRIORITY,
	isValidTaskPriority,
	normalizeTaskPriority
} from '../../../src/constants/task-priority.js';

// Define Zod schema for the expected AI output object
const AiTaskDataSchema = z.object({
	title: z.string().describe('Clear, concise title for the task'),
	description: z
		.string()
		.describe('A one or two sentence description of the task'),
	details: z
		.string()
		.describe('In-depth implementation details, considerations, and guidance'),
	testStrategy: z
		.string()
		.describe('Detailed approach for verifying task completion'),
	dependencies: z
		.array(z.number())
		.nullable()
		.describe(
			'Array of task IDs that this task depends on (must be completed before this task can start)'
		)
});

/**
 * Get all tasks from all tags
 * @param {Object} rawData - The raw tagged data object
 * @returns {Array} A flat array of all task objects
 */
function getAllTasks(rawData) {
	let allTasks = [];
	for (const tagName in rawData) {
		if (
			Object.prototype.hasOwnProperty.call(rawData, tagName) &&
			rawData[tagName] &&
			Array.isArray(rawData[tagName].tasks)
		) {
			allTasks = allTasks.concat(rawData[tagName].tasks);
		}
	}
	return allTasks;
}

/**
 * Add a new task using AI
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} prompt - Description of the task to add (required for AI-driven creation)
 * @param {Array} dependencies - Task dependencies
 * @param {string} priority - Task priority
 * @param {function} reportProgress - Function to report progress to MCP server (optional)
 * @param {Object} mcpLog - MCP logger object (optional)
 * @param {Object} session - Session object from MCP server (optional)
 * @param {string} outputFormat - Output format (text or json)
 * @param {Object} customEnv - Custom environment variables (optional) - Note: AI params override deprecated
 * @param {Object} manualTaskData - Manual task data (optional, for direct task creation without AI)
 * @param {boolean} useResearch - Whether to use the research model (passed to unified service)
 * @param {Object} context - Context object containing session and potentially projectRoot
 * @param {string} [context.projectRoot] - Project root path (for MCP/env fallback)
 * @param {string} [context.commandName] - The name of the command being executed (for telemetry)
 * @param {string} [context.outputType] - The output type ('cli' or 'mcp', for telemetry)
 * @param {string} [context.tag] - Tag for the task (optional)
 * @returns {Promise<object>} An object containing newTaskId and telemetryData
 */
async function addTask(
	tasksPath,
	prompt,
	dependencies = [],
	priority = null,
	context = {},
	outputFormat = 'text', // Default to text for CLI
	manualTaskData = null,
	useResearch = false
) {
	const { session, mcpLog, projectRoot, commandName, outputType, tag } =
		context;
	const isMCP = !!mcpLog;

	// Create a consistent logFn object regardless of context
	const logFn = isMCP
		? mcpLog // Use MCP logger if provided
		: {
				// Create a wrapper around consoleLog for CLI
				info: (...args) => consoleLog('info', ...args),
				warn: (...args) => consoleLog('warn', ...args),
				error: (...args) => consoleLog('error', ...args),
				debug: (...args) => consoleLog('debug', ...args),
				success: (...args) => consoleLog('success', ...args)
			};

	// Validate priority - only accept high, medium, or low
	let effectivePriority =
		priority || getDefaultPriority(projectRoot) || DEFAULT_TASK_PRIORITY;

	// If priority is provided, validate and normalize it
	if (priority) {
		const normalizedPriority = normalizeTaskPriority(priority);
		if (normalizedPriority) {
			effectivePriority = normalizedPriority;
		} else {
			if (outputFormat === 'text') {
				consoleLog(
					'warn',
					`Invalid priority "${priority}". Using default priority "${DEFAULT_TASK_PRIORITY}".`
				);
			}
			effectivePriority = DEFAULT_TASK_PRIORITY;
		}
	}

	logFn.info(
		`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
	);
	if (tag) {
		logFn.info(`Using tag context: ${tag}`);
	}

	let loadingIndicator = null;
	let aiServiceResponse = null; // To store the full response from AI service

	// Create custom reporter that checks for MCP log
	const report = (message, level = 'info') => {
		if (mcpLog) {
			mcpLog[level](message);
		} else if (outputFormat === 'text') {
			consoleLog(level, message);
		}
	};

	/**
	 * Recursively builds a dependency graph for a given task
	 * @param {Array} tasks - All tasks from tasks.json
	 * @param {number} taskId - ID of the task to analyze
	 * @param {Set} visited - Set of already visited task IDs
	 * @param {Map} depthMap - Map of task ID to its depth in the graph
	 * @param {number} depth - Current depth in the recursion
	 * @return {Object} Dependency graph data
	 */
	function buildDependencyGraph(
		tasks,
		taskId,
		visited = new Set(),
		depthMap = new Map(),
		depth = 0
	) {
		// Skip if we've already visited this task or it doesn't exist
		if (visited.has(taskId)) {
			return null;
		}

		// Find the task
		const task = tasks.find((t) => t.id === taskId);
		if (!task) {
			return null;
		}

		// Mark as visited
		visited.add(taskId);

		// Update depth if this is a deeper path to this task
		if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) {
			depthMap.set(taskId, depth);
		}

		// Process dependencies
		const dependencyData = [];
		if (task.dependencies && task.dependencies.length > 0) {
			for (const depId of task.dependencies) {
				const depData = buildDependencyGraph(
					tasks,
					depId,
					visited,
					depthMap,
					depth + 1
				);
				if (depData) {
					dependencyData.push(depData);
				}
			}
		}

		return {
			id: task.id,
			title: task.title,
			description: task.description,
			status: task.status,
			dependencies: dependencyData
		};
	}

	try {
		// Read the existing tasks - IMPORTANT: Read the raw data without tag resolution
		let rawData = readJSON(tasksPath, projectRoot, tag); // No tag parameter

		// Handle the case where readJSON returns resolved data with _rawTaggedData
		if (rawData && rawData._rawTaggedData) {
			// Use the raw tagged data and discard the resolved view
			rawData = rawData._rawTaggedData;
		}

		// If file doesn't exist or is invalid, create a new structure in memory
		if (!rawData) {
			report(
				'tasks.json not found or invalid. Initializing new structure.',
				'info'
			);
			rawData = {
				master: {
					tasks: [],
					metadata: {
						created: new Date().toISOString(),
						description: 'Default tasks context'
					}
				}
			};
			// Do not write the file here; it will be written later with the new task.
		}

		// Handle legacy format migration using utilities
		if (rawData && Array.isArray(rawData.tasks) && !rawData._rawTaggedData) {
			report('Legacy format detected. Migrating to tagged format...', 'info');

			// This is legacy format - migrate it to tagged format
			rawData = {
				master: {
					tasks: rawData.tasks,
					metadata: rawData.metadata || {
						created: new Date().toISOString(),
						updated: new Date().toISOString(),
						description: 'Tasks for master context'
					}
				}
			};
			// Ensure proper metadata using utility
			ensureTagMetadata(rawData.master, {
				description: 'Tasks for master context'
			});
			// Do not write the file here; it will be written later with the new task.

			// Perform complete migration (config.json, state.json)
			performCompleteTagMigration(tasksPath);
			markMigrationForNotice(tasksPath);

			report('Successfully migrated to tagged format.', 'success');
		}

		// Use the provided tag, or the current active tag, or default to 'master'
		const targetTag = tag;

		// Ensure the target tag exists
		if (!rawData[targetTag]) {
			report(
				`Tag "${targetTag}" does not exist. Please create it first using the 'add-tag' command.`,
				'error'
			);
			throw new Error(`Tag "${targetTag}" not found.`);
		}

		// Ensure the target tag has a tasks array and metadata object
		if (!rawData[targetTag].tasks) {
			rawData[targetTag].tasks = [];
		}
		if (!rawData[targetTag].metadata) {
			rawData[targetTag].metadata = {
				created: new Date().toISOString(),
				updated: new Date().toISOString(),
				description: ``
			};
		}

		// Get a flat list of ALL tasks across ALL tags to validate dependencies
		const allTasks = getAllTasks(rawData);

		// Find the highest task ID *within the target tag* to determine the next ID
		const tasksInTargetTag = rawData[targetTag].tasks;
		const highestId =
			tasksInTargetTag.length > 0
				? Math.max(...tasksInTargetTag.map((t) => t.id))
				: 0;
		const newTaskId = highestId + 1;

		// Only show UI box for CLI mode
		if (outputFormat === 'text') {
			console.log(
				boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), {
					padding: 1,
					borderColor: 'blue',
					borderStyle: 'round',
					margin: { top: 1, bottom: 1 }
				})
			);
		}

		// Validate dependencies before proceeding
		const invalidDeps = dependencies.filter((depId) => {
			// Ensure depId is parsed as a number for comparison
			const numDepId = parseInt(depId, 10);
			return Number.isNaN(numDepId) || !allTasks.some((t) => t.id === numDepId);
		});

		if (invalidDeps.length > 0) {
			report(
				`The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`,
				'warn'
			);
			report('Removing invalid dependencies...', 'info');
			dependencies = dependencies.filter(
				(depId) => !invalidDeps.includes(depId)
			);
		}
		// Ensure dependencies are numbers
		const numericDependencies = dependencies.map((dep) => parseInt(dep, 10));

		// Build dependency graphs for explicitly specified dependencies
		const dependencyGraphs = [];
		const allRelatedTaskIds = new Set();
		const depthMap = new Map();

		// First pass: build a complete dependency graph for each specified dependency
		for (const depId of numericDependencies) {
			const graph = buildDependencyGraph(allTasks, depId, new Set(), depthMap);
			if (graph) {
				dependencyGraphs.push(graph);
			}
		}

		// Second pass: build a set of all related task IDs for flat analysis
		for (const [taskId, depth] of depthMap.entries()) {
			allRelatedTaskIds.add(taskId);
		}

		let taskData;

		// Check if manual task data is provided
		if (manualTaskData) {
			report('Using manually provided task data', 'info');
			taskData = manualTaskData;
			report('DEBUG: Taking MANUAL task data path.', 'debug');

			// Basic validation for manual data
			if (
				!taskData.title ||
				typeof taskData.title !== 'string' ||
				!taskData.description ||
				typeof taskData.description !== 'string'
			) {
				throw new Error(
					'Manual task data must include at least a title and description.'
				);
			}
		} else {
			report('DEBUG: Taking AI task generation path.', 'debug');
			// --- Refactored AI Interaction ---
			report(`Generating task data with AI with prompt:\n${prompt}`, 'info');

			// --- Use the new ContextGatherer ---
			const contextGatherer = new ContextGatherer(projectRoot, tag);
			const gatherResult = await contextGatherer.gather({
				semanticQuery: prompt,
				dependencyTasks: numericDependencies,
				format: 'research'
			});

			const gatheredContext = gatherResult.context;
			const analysisData = gatherResult.analysisData;

			// Display context analysis if not in silent mode
			if (outputFormat === 'text' && analysisData) {
				displayContextAnalysis(analysisData, prompt, gatheredContext.length);
			}

			// Add any manually provided details to the prompt for context
			let contextFromArgs = '';
			if (manualTaskData?.title)
				contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`;
			if (manualTaskData?.description)
				contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`;
			if (manualTaskData?.details)
				contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`;
			if (manualTaskData?.testStrategy)
				contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`;

			// Load prompts using PromptManager
			const promptManager = getPromptManager();
			const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
				'add-task',
				{
					prompt,
					newTaskId,
					existingTasks: allTasks,
					gatheredContext,
					contextFromArgs,
					useResearch,
					priority: effectivePriority,
					dependencies: numericDependencies
				}
			);

			// Start the loading indicator - only for text mode
			if (outputFormat === 'text') {
				loadingIndicator = startLoadingIndicator(
					`Generating new task with ${useResearch ? 'Research' : 'Main'} AI... \n`
				);
			}

			try {
				const serviceRole = useResearch ? 'research' : 'main';
				report('DEBUG: Calling generateObjectService...', 'debug');

				aiServiceResponse = await generateObjectService({
					// Capture the full response
					role: serviceRole,
					session: session,
					projectRoot: projectRoot,
					schema: AiTaskDataSchema,
					objectName: 'newTaskData',
					systemPrompt: systemPrompt,
					prompt: userPrompt,
					commandName: commandName || 'add-task', // Use passed commandName or default
					outputType: outputType || (isMCP ? 'mcp' : 'cli') // Use passed outputType or derive
				});
				report('DEBUG: generateObjectService returned successfully.', 'debug');

				if (!aiServiceResponse || !aiServiceResponse.mainResult) {
					throw new Error(
						'AI service did not return the expected object structure.'
					);
				}

				// Prefer mainResult if it looks like a valid task object, otherwise try mainResult.object
				if (
					aiServiceResponse.mainResult.title &&
					aiServiceResponse.mainResult.description
				) {
					taskData = aiServiceResponse.mainResult;
				} else if (
					aiServiceResponse.mainResult.object &&
					aiServiceResponse.mainResult.object.title &&
					aiServiceResponse.mainResult.object.description
				) {
					taskData = aiServiceResponse.mainResult.object;
				} else {
					throw new Error('AI service did not return a valid task object.');
				}

				report('Successfully generated task data from AI.', 'success');

				// Success! Show checkmark
				if (loadingIndicator) {
					succeedLoadingIndicator(
						loadingIndicator,
						'Task generated successfully'
					);
					loadingIndicator = null; // Clear it
				}
			} catch (error) {
				// Failure! Show X
				if (loadingIndicator) {
					failLoadingIndicator(loadingIndicator, 'AI generation failed');
					loadingIndicator = null;
				}
				report(
					`DEBUG: generateObjectService caught error: ${error.message}`,
					'debug'
				);
				report(`Error generating task with AI: ${error.message}`, 'error');
				throw error; // Re-throw error after logging
			} finally {
				report('DEBUG: generateObjectService finally block reached.', 'debug');
				// Clean up if somehow still running
				if (loadingIndicator) {
					stopLoadingIndicator(loadingIndicator);
				}
			}
			// --- End Refactored AI Interaction ---
		}

		// Create the new task object
		const newTask = {
			id: newTaskId,
			title: taskData.title,
			description: taskData.description,
			details: taskData.details || '',
			testStrategy: taskData.testStrategy || '',
			status: 'pending',
			dependencies: taskData.dependencies?.length
				? taskData.dependencies
				: numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
			priority: effectivePriority,
			subtasks: [] // Initialize with empty subtasks array
		};

		// Additional check: validate all dependencies in the AI response
		if (taskData.dependencies?.length) {
			const allValidDeps = taskData.dependencies.every((depId) => {
				const numDepId = parseInt(depId, 10);
				return (
					!Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId)
				);
			});

			if (!allValidDeps) {
				report(
					'AI suggested invalid dependencies. Filtering them out...',
					'warn'
				);
				newTask.dependencies = taskData.dependencies.filter((depId) => {
					const numDepId = parseInt(depId, 10);
					return (
						!Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId)
					);
				});
			}
		}

		// Add the task to the tasks array OF THE CORRECT TAG
		rawData[targetTag].tasks.push(newTask);
		// Update the tag's metadata
		ensureTagMetadata(rawData[targetTag], {
			description: `Tasks for ${targetTag} context`
		});

		report('DEBUG: Writing tasks.json...', 'debug');
		// Write the updated raw data back to the file
		// The writeJSON function will automatically filter out _rawTaggedData
		writeJSON(tasksPath, rawData, projectRoot, targetTag);
		report('DEBUG: tasks.json written.', 'debug');

		// Show success message - only for text output (CLI)
		if (outputFormat === 'text') {
			const table = new Table({
				head: [
					chalk.cyan.bold('ID'),
					chalk.cyan.bold('Title'),
					chalk.cyan.bold('Description')
				],
				colWidths: [5, 30, 50] // Adjust widths as needed
			});

			table.push([
				newTask.id,
				truncate(newTask.title, 27),
				truncate(newTask.description, 47)
			]);

			console.log(chalk.green('✓ New task created successfully:'));
			console.log(table.toString());

			// Helper to get priority color
			const getPriorityColor = (p) => {
				switch (p?.toLowerCase()) {
					case 'high':
						return 'red';
					case 'low':
						return 'gray';
					default:
						return 'yellow';
				}
			};

			// Check if AI added new dependencies that weren't explicitly provided
			const aiAddedDeps = newTask.dependencies.filter(
				(dep) => !numericDependencies.includes(dep)
			);

			// Check if AI removed any dependencies that were explicitly provided
			const aiRemovedDeps = numericDependencies.filter(
				(dep) => !newTask.dependencies.includes(dep)
			);

			// Get task titles for dependencies to display
			const depTitles = {};
			newTask.dependencies.forEach((dep) => {
				const depTask = allTasks.find((t) => t.id === dep);
				if (depTask) {
					depTitles[dep] = truncate(depTask.title, 30);
				}
			});

			// Prepare dependency display string
			let dependencyDisplay = '';
			if (newTask.dependencies.length > 0) {
				dependencyDisplay = chalk.white('Dependencies:') + '\n';
				newTask.dependencies.forEach((dep) => {
					const isAiAdded = aiAddedDeps.includes(dep);
					const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : '';
					dependencyDisplay +=
						chalk.white(
							`  - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`
						) + '\n';
				});
			} else {
				dependencyDisplay = chalk.white('Dependencies: None') + '\n';
			}

			// Add info about removed dependencies if any
			if (aiRemovedDeps.length > 0) {
				dependencyDisplay +=
					chalk.gray('\nUser-specified dependencies that were not used:') +
					'\n';
				aiRemovedDeps.forEach((dep) => {
					const depTask = allTasks.find((t) => t.id === dep);
					const title = depTask ? truncate(depTask.title, 30) : 'Unknown task';
					dependencyDisplay += chalk.gray(`  - ${dep}: ${title}`) + '\n';
				});
			}

			// Add dependency analysis summary
			let dependencyAnalysis = '';
			if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
				dependencyAnalysis =
					'\n' + chalk.white.bold('Dependency Analysis:') + '\n';
				if (aiAddedDeps.length > 0) {
					dependencyAnalysis +=
						chalk.green(
							`AI identified ${aiAddedDeps.length} additional dependencies`
						) + '\n';
				}
				if (aiRemovedDeps.length > 0) {
					dependencyAnalysis +=
						chalk.yellow(
							`AI excluded ${aiRemovedDeps.length} user-provided dependencies`
						) + '\n';
				}
			}

			// Show success message box
			console.log(
				boxen(
					chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
						'\n\n' +
						chalk.white(`Title: ${newTask.title}`) +
						'\n' +
						chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) +
						'\n' +
						chalk.white(
							`Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}`
						) +
						'\n\n' +
						dependencyDisplay +
						dependencyAnalysis +
						'\n' +
						chalk.white.bold('Next Steps:') +
						'\n' +
						chalk.cyan(
							`1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`
						) +
						'\n' +
						chalk.cyan(
							`2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`
						) +
						'\n' +
						chalk.cyan(
							`3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`
						),
					{ padding: 1, borderColor: 'green', borderStyle: 'round' }
				)
			);

			// Display AI Usage Summary if telemetryData is available
			if (
				aiServiceResponse &&
				aiServiceResponse.telemetryData &&
				(outputType === 'cli' || outputType === 'text')
			) {
				displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
			}
		}

		report(
			`DEBUG: Returning new task ID: ${newTaskId} and telemetry.`,
			'debug'
		);
		return {
			newTaskId: newTaskId,
			telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null,
			tagInfo: aiServiceResponse ? aiServiceResponse.tagInfo : null
		};
	} catch (error) {
		// Stop any loading indicator on error
		if (loadingIndicator) {
			stopLoadingIndicator(loadingIndicator);
		}

		report(`Error adding task: ${error.message}`, 'error');
		if (outputFormat === 'text') {
			console.error(chalk.red(`Error: ${error.message}`));
		}
		// In MCP mode, we let the direct function handler catch and format
		throw error;
	}
}

export default addTask;

/**
 * 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;
}

// --- Tag-Aware Path Resolution Utility ---

/**
 * Slugifies a tag name to be filesystem-safe
 * @param {string} tagName - The tag name to slugify
 * @returns {string} Slugified tag name safe for filesystem use
 */
function slugifyTagForFilePath(tagName) {
	if (!tagName || typeof tagName !== 'string') {
		return 'unknown-tag';
	}

	// Replace invalid filesystem characters with hyphens and clean up
	return tagName
		.replace(/[^a-zA-Z0-9_-]/g, '-') // Replace invalid chars with hyphens
		.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
		.replace(/-+/g, '-') // Collapse multiple hyphens
		.toLowerCase() // Convert to lowercase
		.substring(0, 50); // Limit length to prevent overly long filenames
}

/**
 * Resolves a file path to be tag-aware, following the pattern used by other commands.
 * For non-master tags, appends _slugified-tagname before the file extension.
 * @param {string} basePath - The base file path (e.g., '.taskmaster/reports/task-complexity-report.json')
 * @param {string|null} tag - The tag name (null, undefined, or 'master' uses base path)
 * @param {string} [projectRoot='.'] - The project root directory
 * @returns {string} The resolved file path
 */
function getTagAwareFilePath(basePath, tag, projectRoot = '.') {
	// Use path.parse and format for clean tag insertion
	const parsedPath = path.parse(basePath);
	if (!tag || tag === 'master') {
		return path.join(projectRoot, basePath);
	}

	// Slugify the tag for filesystem safety
	const slugifiedTag = slugifyTagForFilePath(tag);

	// Append slugified tag before file extension
	parsedPath.base = `${parsedPath.name}_${slugifiedTag}${parsedPath.ext}`;
	const relativePath = path.format(parsedPath);
	return path.join(projectRoot, relativePath);
}

// --- 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', 'pyproject.toml', '.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)}...`;
}

/**
 * Checks if array or object are empty
 * @param {*} value - The value to check
 * @returns {boolean} True if empty, false otherwise
 */
function isEmpty(value) {
	if (Array.isArray(value)) {
		return value.length === 0;
	} else if (typeof value === 'object' && value !== null) {
		return Object.keys(value).length === 0;
	}

	return false; // Not an array or object, or is null
}

/**
 * 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;
}

/**
 * @deprecated Use TaskMaster.getCurrentTag() instead
 * 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,
	isEmpty,
	findCycles,
	toKebabCase,
	detectCamelCaseFlags,
	disableSilentMode,
	enableSilentMode,
	getTaskManager,
	isSilentMode,
	addComplexityToTask,
	resolveEnvVariable,
	findProjectRoot,
	getTagAwareFilePath,
	slugifyTagForFilePath,
	aggregateTelemetry,
	getCurrentTag,
	resolveTag,
	getTasksForTag,
	setTasksForTag,
	performCompleteTagMigration,
	migrateConfigJson,
	createStateJson,
	markMigrationForNotice,
	flattenTasksWithSubtasks,
	ensureTagMetadata
};

/**
 * dependency-manager.js
 * Manages task dependencies and relationships
 */

import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';

import {
	log,
	readJSON,
	writeJSON,
	taskExists,
	formatTaskId,
	findCycles,
	isSilentMode
} from './utils.js';

import { displayBanner } from './ui.js';

import { generateTaskFiles } from './task-manager.js';

/**
 * Add a dependency to a task
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {number|string} taskId - ID of the task to add dependency to
 * @param {number|string} dependencyId - ID of the task to add as dependency
 * @param {Object} context - Context object containing projectRoot and tag information
 * @param {string} [context.projectRoot] - Project root path
 * @param {string} [context.tag] - Tag for the task
 */
async function addDependency(tasksPath, taskId, dependencyId, context = {}) {
	log('info', `Adding dependency ${dependencyId} to task ${taskId}...`);

	const data = readJSON(tasksPath, context.projectRoot, context.tag);
	if (!data || !data.tasks) {
		log('error', 'No valid tasks found in tasks.json');
		process.exit(1);
	}

	// Format the task and dependency IDs correctly
	const formattedTaskId =
		typeof taskId === 'string' && taskId.includes('.')
			? taskId
			: parseInt(taskId, 10);

	const formattedDependencyId = formatTaskId(dependencyId);

	// Check if the dependency task or subtask actually exists
	if (!taskExists(data.tasks, formattedDependencyId)) {
		log(
			'error',
			`Dependency target ${formattedDependencyId} does not exist in tasks.json`
		);
		process.exit(1);
	}

	// Find the task to update
	let targetTask = null;
	let isSubtask = false;

	if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
		// Handle dot notation for subtasks (e.g., "1.2")
		const [parentId, subtaskId] = formattedTaskId
			.split('.')
			.map((id) => parseInt(id, 10));
		const parentTask = data.tasks.find((t) => t.id === parentId);

		if (!parentTask) {
			log('error', `Parent task ${parentId} not found.`);
			process.exit(1);
		}

		if (!parentTask.subtasks) {
			log('error', `Parent task ${parentId} has no subtasks.`);
			process.exit(1);
		}

		targetTask = parentTask.subtasks.find((s) => s.id === subtaskId);
		isSubtask = true;

		if (!targetTask) {
			log('error', `Subtask ${formattedTaskId} not found.`);
			process.exit(1);
		}
	} else {
		// Regular task (not a subtask)
		targetTask = data.tasks.find((t) => t.id === formattedTaskId);

		if (!targetTask) {
			log('error', `Task ${formattedTaskId} not found.`);
			process.exit(1);
		}
	}

	// Initialize dependencies array if it doesn't exist
	if (!targetTask.dependencies) {
		targetTask.dependencies = [];
	}

	// Check if dependency already exists
	if (
		targetTask.dependencies.some((d) => {
			// Convert both to strings for comparison to handle both numeric and string IDs
			return String(d) === String(formattedDependencyId);
		})
	) {
		log(
			'warn',
			`Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.`
		);
		return;
	}

	// Check if the task is trying to depend on itself - compare full IDs (including subtask parts)
	if (String(formattedTaskId) === String(formattedDependencyId)) {
		log('error', `Task ${formattedTaskId} cannot depend on itself.`);
		process.exit(1);
	}

	// For subtasks of the same parent, we need to make sure we're not treating it as a self-dependency
	// Check if we're dealing with subtasks with the same parent task
	let isSelfDependency = false;

	if (
		typeof formattedTaskId === 'string' &&
		typeof formattedDependencyId === 'string' &&
		formattedTaskId.includes('.') &&
		formattedDependencyId.includes('.')
	) {
		const [taskParentId] = formattedTaskId.split('.');
		const [depParentId] = formattedDependencyId.split('.');

		// Only treat it as a self-dependency if both the parent ID and subtask ID are identical
		isSelfDependency = formattedTaskId === formattedDependencyId;

		// Log for debugging
		log(
			'debug',
			`Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}`
		);
		log(
			'debug',
			`Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}`
		);
	}

	if (isSelfDependency) {
		log('error', `Subtask ${formattedTaskId} cannot depend on itself.`);
		process.exit(1);
	}

	// Check for circular dependencies
	const dependencyChain = [formattedTaskId];
	if (
		!isCircularDependency(data.tasks, formattedDependencyId, dependencyChain)
	) {
		// Add the dependency
		targetTask.dependencies.push(formattedDependencyId);

		// Sort dependencies numerically or by parent task ID first, then subtask ID
		targetTask.dependencies.sort((a, b) => {
			if (typeof a === 'number' && typeof b === 'number') {
				return a - b;
			} else if (typeof a === 'string' && typeof b === 'string') {
				const [aParent, aChild] = a.split('.').map(Number);
				const [bParent, bChild] = b.split('.').map(Number);
				return aParent !== bParent ? aParent - bParent : aChild - bChild;
			} else if (typeof a === 'number') {
				return -1; // Numbers come before strings
			} else {
				return 1; // Strings come after numbers
			}
		});

		// Save changes
		writeJSON(tasksPath, data, context.projectRoot, context.tag);
		log(
			'success',
			`Added dependency ${formattedDependencyId} to task ${formattedTaskId}`
		);

		// Display a more visually appealing success message
		if (!isSilentMode()) {
			console.log(
				boxen(
					chalk.green(`Successfully added dependency:\n\n`) +
						`Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`,
					{
						padding: 1,
						borderColor: 'green',
						borderStyle: 'round',
						margin: { top: 1 }
					}
				)
			);
		}

		// Generate updated task files
		// await generateTaskFiles(tasksPath, path.dirname(tasksPath));

		log('info', 'Task files regenerated with updated dependencies.');
	} else {
		log(
			'error',
			`Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.`
		);
		process.exit(1);
	}
}

/**
 * Remove a dependency from a task
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {number|string} taskId - ID of the task to remove dependency from
 * @param {number|string} dependencyId - ID of the task to remove as dependency
 * @param {Object} context - Context object containing projectRoot and tag information
 * @param {string} [context.projectRoot] - Project root path
 * @param {string} [context.tag] - Tag for the task
 */
async function removeDependency(tasksPath, taskId, dependencyId, context = {}) {
	log('info', `Removing dependency ${dependencyId} from task ${taskId}...`);

	// Read tasks file
	const data = readJSON(tasksPath, context.projectRoot, context.tag);
	if (!data || !data.tasks) {
		log('error', 'No valid tasks found.');
		process.exit(1);
	}

	// Format the task and dependency IDs correctly
	const formattedTaskId =
		typeof taskId === 'string' && taskId.includes('.')
			? taskId
			: parseInt(taskId, 10);

	const formattedDependencyId = formatTaskId(dependencyId);

	// Find the task to update
	let targetTask = null;
	let isSubtask = false;

	if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
		// Handle dot notation for subtasks (e.g., "1.2")
		const [parentId, subtaskId] = formattedTaskId
			.split('.')
			.map((id) => parseInt(id, 10));
		const parentTask = data.tasks.find((t) => t.id === parentId);

		if (!parentTask) {
			log('error', `Parent task ${parentId} not found.`);
			process.exit(1);
		}

		if (!parentTask.subtasks) {
			log('error', `Parent task ${parentId} has no subtasks.`);
			process.exit(1);
		}

		targetTask = parentTask.subtasks.find((s) => s.id === subtaskId);
		isSubtask = true;

		if (!targetTask) {
			log('error', `Subtask ${formattedTaskId} not found.`);
			process.exit(1);
		}
	} else {
		// Regular task (not a subtask)
		targetTask = data.tasks.find((t) => t.id === formattedTaskId);

		if (!targetTask) {
			log('error', `Task ${formattedTaskId} not found.`);
			process.exit(1);
		}
	}

	// Check if the task has any dependencies
	if (!targetTask.dependencies || targetTask.dependencies.length === 0) {
		log(
			'info',
			`Task ${formattedTaskId} has no dependencies, nothing to remove.`
		);
		return;
	}

	// Normalize the dependency ID for comparison to handle different formats
	const normalizedDependencyId = String(formattedDependencyId);

	// Check if the dependency exists by comparing string representations
	const dependencyIndex = targetTask.dependencies.findIndex((dep) => {
		// Convert both to strings for comparison
		let depStr = String(dep);

		// Special handling for numeric IDs that might be subtask references
		if (typeof dep === 'number' && dep < 100 && isSubtask) {
			// It's likely a reference to another subtask in the same parent task
			// Convert to full format for comparison (e.g., 2 -> "1.2" for a subtask in task 1)
			const [parentId] = formattedTaskId.split('.');
			depStr = `${parentId}.${dep}`;
		}

		return depStr === normalizedDependencyId;
	});

	if (dependencyIndex === -1) {
		log(
			'info',
			`Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.`
		);
		return;
	}

	// Remove the dependency
	targetTask.dependencies.splice(dependencyIndex, 1);

	// Save the updated tasks
	writeJSON(tasksPath, data, context.projectRoot, context.tag);

	// Success message
	log(
		'success',
		`Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}`
	);

	if (!isSilentMode()) {
		// Display a more visually appealing success message
		console.log(
			boxen(
				chalk.green(`Successfully removed dependency:\n\n`) +
					`Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`,
				{
					padding: 1,
					borderColor: 'green',
					borderStyle: 'round',
					margin: { top: 1 }
				}
			)
		);
	}

	// Regenerate task files
	// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
}

/**
 * Check if adding a dependency would create a circular dependency
 * @param {Array} tasks - Array of all tasks
 * @param {number|string} taskId - ID of task to check
 * @param {Array} chain - Chain of dependencies to check
 * @returns {boolean} True if circular dependency would be created
 */
function isCircularDependency(tasks, taskId, chain = []) {
	// Convert taskId to string for comparison
	const taskIdStr = String(taskId);

	// If we've seen this task before in the chain, we have a circular dependency
	if (chain.some((id) => String(id) === taskIdStr)) {
		return true;
	}

	// Find the task or subtask
	let task = null;
	let parentIdForSubtask = null;

	// Check if this is a subtask reference (e.g., "1.2")
	if (taskIdStr.includes('.')) {
		const [parentId, subtaskId] = taskIdStr.split('.').map(Number);
		const parentTask = tasks.find((t) => t.id === parentId);
		parentIdForSubtask = parentId; // Store parent ID if it's a subtask

		if (parentTask && parentTask.subtasks) {
			task = parentTask.subtasks.find((st) => st.id === subtaskId);
		}
	} else {
		// Regular task
		task = tasks.find((t) => String(t.id) === taskIdStr);
	}

	if (!task) {
		return false; // Task doesn't exist, can't create circular dependency
	}

	// No dependencies, can't create circular dependency
	if (!task.dependencies || task.dependencies.length === 0) {
		return false;
	}

	// Check each dependency recursively
	const newChain = [...chain, taskIdStr]; // Use taskIdStr for consistency
	return task.dependencies.some((depId) => {
		let normalizedDepId = String(depId);
		// Normalize relative subtask dependencies
		if (typeof depId === 'number' && parentIdForSubtask !== null) {
			// If the current task is a subtask AND the dependency is a number,
			// assume it refers to a sibling subtask.
			normalizedDepId = `${parentIdForSubtask}.${depId}`;
		}
		// Pass the normalized ID to the recursive call
		return isCircularDependency(tasks, normalizedDepId, newChain);
	});
}

/**
 * Validate task dependencies
 * @param {Array} tasks - Array of all tasks
 * @returns {Object} Validation result with valid flag and issues array
 */
function validateTaskDependencies(tasks) {
	const issues = [];

	// Check each task's dependencies
	tasks.forEach((task) => {
		if (!task.dependencies) {
			return; // No dependencies to validate
		}

		task.dependencies.forEach((depId) => {
			// Check for self-dependencies
			if (String(depId) === String(task.id)) {
				issues.push({
					type: 'self',
					taskId: task.id,
					message: `Task ${task.id} depends on itself`
				});
				return;
			}

			// Check if dependency exists
			if (!taskExists(tasks, depId)) {
				issues.push({
					type: 'missing',
					taskId: task.id,
					dependencyId: depId,
					message: `Task ${task.id} depends on non-existent task ${depId}`
				});
			}
		});

		// Check for circular dependencies
		if (isCircularDependency(tasks, task.id)) {
			issues.push({
				type: 'circular',
				taskId: task.id,
				message: `Task ${task.id} is part of a circular dependency chain`
			});
		}

		// Check subtask dependencies if they exist
		if (task.subtasks && task.subtasks.length > 0) {
			task.subtasks.forEach((subtask) => {
				if (!subtask.dependencies) {
					return; // No dependencies to validate
				}

				// Create a full subtask ID for reference
				const fullSubtaskId = `${task.id}.${subtask.id}`;

				subtask.dependencies.forEach((depId) => {
					// Check for self-dependencies in subtasks
					if (
						String(depId) === String(fullSubtaskId) ||
						(typeof depId === 'number' && depId === subtask.id)
					) {
						issues.push({
							type: 'self',
							taskId: fullSubtaskId,
							message: `Subtask ${fullSubtaskId} depends on itself`
						});
						return;
					}

					// Check if dependency exists
					if (!taskExists(tasks, depId)) {
						issues.push({
							type: 'missing',
							taskId: fullSubtaskId,
							dependencyId: depId,
							message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`
						});
					}
				});

				// Check for circular dependencies in subtasks
				if (isCircularDependency(tasks, fullSubtaskId)) {
					issues.push({
						type: 'circular',
						taskId: fullSubtaskId,
						message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`
					});
				}
			});
		}
	});

	return {
		valid: issues.length === 0,
		issues
	};
}

/**
 * Remove duplicate dependencies from tasks
 * @param {Object} tasksData - Tasks data object with tasks array
 * @returns {Object} Updated tasks data with duplicates removed
 */
function removeDuplicateDependencies(tasksData) {
	const tasks = tasksData.tasks.map((task) => {
		if (!task.dependencies) {
			return task;
		}

		// Convert to Set and back to array to remove duplicates
		const uniqueDeps = [...new Set(task.dependencies)];
		return {
			...task,
			dependencies: uniqueDeps
		};
	});

	return {
		...tasksData,
		tasks
	};
}

/**
 * Clean up invalid subtask dependencies
 * @param {Object} tasksData - Tasks data object with tasks array
 * @returns {Object} Updated tasks data with invalid subtask dependencies removed
 */
function cleanupSubtaskDependencies(tasksData) {
	const tasks = tasksData.tasks.map((task) => {
		// Handle task's own dependencies
		if (task.dependencies) {
			task.dependencies = task.dependencies.filter((depId) => {
				// Keep only dependencies that exist
				return taskExists(tasksData.tasks, depId);
			});
		}

		// Handle subtask dependencies
		if (task.subtasks) {
			task.subtasks = task.subtasks.map((subtask) => {
				if (!subtask.dependencies) {
					return subtask;
				}

				// Filter out dependencies to non-existent subtasks
				subtask.dependencies = subtask.dependencies.filter((depId) => {
					return taskExists(tasksData.tasks, depId);
				});

				return subtask;
			});
		}

		return task;
	});

	return {
		...tasksData,
		tasks
	};
}

/**
 * Validate dependencies in task files
 * @param {string} tasksPath - Path to tasks.json
 * @param {Object} options - Options object, including context
 */
async function validateDependenciesCommand(tasksPath, options = {}) {
	const { context = {} } = options;
	log('info', 'Checking for invalid dependencies in task files...');

	// Read tasks data
	const data = readJSON(tasksPath, context.projectRoot, context.tag);
	if (!data || !data.tasks) {
		log('error', 'No valid tasks found in tasks.json');
		process.exit(1);
	}

	// Count of tasks and subtasks for reporting
	const taskCount = data.tasks.length;
	let subtaskCount = 0;
	data.tasks.forEach((task) => {
		if (task.subtasks && Array.isArray(task.subtasks)) {
			subtaskCount += task.subtasks.length;
		}
	});

	log(
		'info',
		`Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...`
	);

	try {
		// Directly call the validation function
		const validationResult = validateTaskDependencies(data.tasks);

		if (!validationResult.valid) {
			log(
				'error',
				`Dependency validation failed. Found ${validationResult.issues.length} issue(s):`
			);
			validationResult.issues.forEach((issue) => {
				let errorMsg = `  [${issue.type.toUpperCase()}] Task ${issue.taskId}: ${issue.message}`;
				if (issue.dependencyId) {
					errorMsg += ` (Dependency: ${issue.dependencyId})`;
				}
				log('error', errorMsg); // Log each issue as an error
			});

			// Optionally exit if validation fails, depending on desired behavior
			// process.exit(1); // Uncomment if validation failure should stop the process

			// Display summary box even on failure, showing issues found
			if (!isSilentMode()) {
				console.log(
					boxen(
						chalk.red(`Dependency Validation FAILED\n\n`) +
							`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
							`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
							`${chalk.red('Issues found:')} ${validationResult.issues.length}`, // Display count from result
						{
							padding: 1,
							borderColor: 'red',
							borderStyle: 'round',
							margin: { top: 1, bottom: 1 }
						}
					)
				);
			}
		} else {
			log(
				'success',
				'No invalid dependencies found - all dependencies are valid'
			);

			// Show validation summary - only if not in silent mode
			if (!isSilentMode()) {
				console.log(
					boxen(
						chalk.green(`All Dependencies Are Valid\n\n`) +
							`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
							`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
							`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
						{
							padding: 1,
							borderColor: 'green',
							borderStyle: 'round',
							margin: { top: 1, bottom: 1 }
						}
					)
				);
			}
		}
	} catch (error) {
		log('error', 'Error validating dependencies:', error);
		process.exit(1);
	}
}

/**
 * Helper function to count all dependencies across tasks and subtasks
 * @param {Array} tasks - All tasks
 * @returns {number} - Total number of dependencies
 */
function countAllDependencies(tasks) {
	let count = 0;

	tasks.forEach((task) => {
		// Count main task dependencies
		if (task.dependencies && Array.isArray(task.dependencies)) {
			count += task.dependencies.length;
		}

		// Count subtask dependencies
		if (task.subtasks && Array.isArray(task.subtasks)) {
			task.subtasks.forEach((subtask) => {
				if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
					count += subtask.dependencies.length;
				}
			});
		}
	});

	return count;
}

/**
 * Fixes invalid dependencies in tasks.json
 * @param {string} tasksPath - Path to tasks.json
 * @param {Object} options - Options object, including context
 */
async function fixDependenciesCommand(tasksPath, options = {}) {
	const { context = {} } = options;
	log('info', 'Checking for and fixing invalid dependencies in tasks.json...');

	try {
		// Read tasks data
		const data = readJSON(tasksPath, context.projectRoot, context.tag);
		if (!data || !data.tasks) {
			log('error', 'No valid tasks found in tasks.json');
			process.exit(1);
		}

		// Create a deep copy of the original data for comparison
		const originalData = JSON.parse(JSON.stringify(data));

		// Track fixes for reporting
		const stats = {
			nonExistentDependenciesRemoved: 0,
			selfDependenciesRemoved: 0,
			duplicateDependenciesRemoved: 0,
			circularDependenciesFixed: 0,
			tasksFixed: 0,
			subtasksFixed: 0
		};

		// First phase: Remove duplicate dependencies in tasks
		data.tasks.forEach((task) => {
			if (task.dependencies && Array.isArray(task.dependencies)) {
				const uniqueDeps = new Set();
				const originalLength = task.dependencies.length;
				task.dependencies = task.dependencies.filter((depId) => {
					const depIdStr = String(depId);
					if (uniqueDeps.has(depIdStr)) {
						log(
							'info',
							`Removing duplicate dependency from task ${task.id}: ${depId}`
						);
						stats.duplicateDependenciesRemoved++;
						return false;
					}
					uniqueDeps.add(depIdStr);
					return true;
				});
				if (task.dependencies.length < originalLength) {
					stats.tasksFixed++;
				}
			}

			// Check for duplicates in subtasks
			if (task.subtasks && Array.isArray(task.subtasks)) {
				task.subtasks.forEach((subtask) => {
					if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
						const uniqueDeps = new Set();
						const originalLength = subtask.dependencies.length;
						subtask.dependencies = subtask.dependencies.filter((depId) => {
							let depIdStr = String(depId);
							if (typeof depId === 'number' && depId < 100) {
								depIdStr = `${task.id}.${depId}`;
							}
							if (uniqueDeps.has(depIdStr)) {
								log(
									'info',
									`Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`
								);
								stats.duplicateDependenciesRemoved++;
								return false;
							}
							uniqueDeps.add(depIdStr);
							return true;
						});
						if (subtask.dependencies.length < originalLength) {
							stats.subtasksFixed++;
						}
					}
				});
			}
		});

		// Create validity maps for tasks and subtasks
		const validTaskIds = new Set(data.tasks.map((t) => t.id));
		const validSubtaskIds = new Set();
		data.tasks.forEach((task) => {
			if (task.subtasks && Array.isArray(task.subtasks)) {
				task.subtasks.forEach((subtask) => {
					validSubtaskIds.add(`${task.id}.${subtask.id}`);
				});
			}
		});

		// Second phase: Remove invalid task dependencies (non-existent tasks)
		data.tasks.forEach((task) => {
			if (task.dependencies && Array.isArray(task.dependencies)) {
				const originalLength = task.dependencies.length;
				task.dependencies = task.dependencies.filter((depId) => {
					const isSubtask = typeof depId === 'string' && depId.includes('.');

					if (isSubtask) {
						// Check if the subtask exists
						if (!validSubtaskIds.has(depId)) {
							log(
								'info',
								`Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)`
							);
							stats.nonExistentDependenciesRemoved++;
							return false;
						}
						return true;
					} else {
						// Check if the task exists
						const numericId =
							typeof depId === 'string' ? parseInt(depId, 10) : depId;
						if (!validTaskIds.has(numericId)) {
							log(
								'info',
								`Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`
							);
							stats.nonExistentDependenciesRemoved++;
							return false;
						}
						return true;
					}
				});

				if (task.dependencies.length < originalLength) {
					stats.tasksFixed++;
				}
			}

			// Check subtask dependencies for invalid references
			if (task.subtasks && Array.isArray(task.subtasks)) {
				task.subtasks.forEach((subtask) => {
					if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
						const originalLength = subtask.dependencies.length;
						const subtaskId = `${task.id}.${subtask.id}`;

						// First check for self-dependencies
						const hasSelfDependency = subtask.dependencies.some((depId) => {
							if (typeof depId === 'string' && depId.includes('.')) {
								return depId === subtaskId;
							} else if (typeof depId === 'number' && depId < 100) {
								return depId === subtask.id;
							}
							return false;
						});

						if (hasSelfDependency) {
							subtask.dependencies = subtask.dependencies.filter((depId) => {
								const normalizedDepId =
									typeof depId === 'number' && depId < 100
										? `${task.id}.${depId}`
										: String(depId);

								if (normalizedDepId === subtaskId) {
									log(
										'info',
										`Removing self-dependency from subtask ${subtaskId}`
									);
									stats.selfDependenciesRemoved++;
									return false;
								}
								return true;
							});
						}

						// Then check for non-existent dependencies
						subtask.dependencies = subtask.dependencies.filter((depId) => {
							if (typeof depId === 'string' && depId.includes('.')) {
								if (!validSubtaskIds.has(depId)) {
									log(
										'info',
										`Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)`
									);
									stats.nonExistentDependenciesRemoved++;
									return false;
								}
								return true;
							}

							// Handle numeric dependencies
							const numericId =
								typeof depId === 'number' ? depId : parseInt(depId, 10);

							// Small numbers likely refer to subtasks in the same task
							if (numericId < 100) {
								const fullSubtaskId = `${task.id}.${numericId}`;

								if (!validSubtaskIds.has(fullSubtaskId)) {
									log(
										'info',
										`Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}`
									);
									stats.nonExistentDependenciesRemoved++;
									return false;
								}

								return true;
							}

							// Otherwise it's a task reference
							if (!validTaskIds.has(numericId)) {
								log(
									'info',
									`Removing invalid task dependency from subtask ${subtaskId}: ${numericId}`
								);
								stats.nonExistentDependenciesRemoved++;
								return false;
							}

							return true;
						});

						if (subtask.dependencies.length < originalLength) {
							stats.subtasksFixed++;
						}
					}
				});
			}
		});

		// Third phase: Check for circular dependencies
		log('info', 'Checking for circular dependencies...');

		// Build the dependency map for subtasks
		const subtaskDependencyMap = new Map();
		data.tasks.forEach((task) => {
			if (task.subtasks && Array.isArray(task.subtasks)) {
				task.subtasks.forEach((subtask) => {
					const subtaskId = `${task.id}.${subtask.id}`;

					if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
						const normalizedDeps = subtask.dependencies.map((depId) => {
							if (typeof depId === 'string' && depId.includes('.')) {
								return depId;
							} else if (typeof depId === 'number' && depId < 100) {
								return `${task.id}.${depId}`;
							}
							return String(depId);
						});
						subtaskDependencyMap.set(subtaskId, normalizedDeps);
					} else {
						subtaskDependencyMap.set(subtaskId, []);
					}
				});
			}
		});

		// Check for and fix circular dependencies
		for (const [subtaskId, dependencies] of subtaskDependencyMap.entries()) {
			const visited = new Set();
			const recursionStack = new Set();

			// Detect cycles
			const cycleEdges = findCycles(
				subtaskId,
				subtaskDependencyMap,
				visited,
				recursionStack
			);

			if (cycleEdges.length > 0) {
				const [taskId, subtaskNum] = subtaskId
					.split('.')
					.map((part) => Number(part));
				const task = data.tasks.find((t) => t.id === taskId);

				if (task && task.subtasks) {
					const subtask = task.subtasks.find((st) => st.id === subtaskNum);

					if (subtask && subtask.dependencies) {
						const originalLength = subtask.dependencies.length;

						const edgesToRemove = cycleEdges.map((edge) => {
							if (edge.includes('.')) {
								const [depTaskId, depSubtaskId] = edge
									.split('.')
									.map((part) => Number(part));

								if (depTaskId === taskId) {
									return depSubtaskId;
								}

								return edge;
							}

							return Number(edge);
						});

						subtask.dependencies = subtask.dependencies.filter((depId) => {
							const normalizedDepId =
								typeof depId === 'number' && depId < 100
									? `${taskId}.${depId}`
									: String(depId);

							if (
								edgesToRemove.includes(depId) ||
								edgesToRemove.includes(normalizedDepId)
							) {
								log(
									'info',
									`Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}`
								);
								stats.circularDependenciesFixed++;
								return false;
							}
							return true;
						});

						if (subtask.dependencies.length < originalLength) {
							stats.subtasksFixed++;
						}
					}
				}
			}
		}

		// Check if any changes were made by comparing with original data
		const dataChanged = JSON.stringify(data) !== JSON.stringify(originalData);

		if (dataChanged) {
			// Save the changes
			writeJSON(tasksPath, data, context.projectRoot, context.tag);
			log('success', 'Fixed dependency issues in tasks.json');

			// Regenerate task files
			log('info', 'Regenerating task files to reflect dependency changes...');
			// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
		} else {
			log('info', 'No changes needed to fix dependencies');
		}

		// Show detailed statistics report
		const totalFixedAll =
			stats.nonExistentDependenciesRemoved +
			stats.selfDependenciesRemoved +
			stats.duplicateDependenciesRemoved +
			stats.circularDependenciesFixed;

		if (!isSilentMode()) {
			if (totalFixedAll > 0) {
				log('success', `Fixed ${totalFixedAll} dependency issues in total!`);

				console.log(
					boxen(
						chalk.green(`Dependency Fixes Summary:\n\n`) +
							`${chalk.cyan('Invalid dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` +
							`${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` +
							`${chalk.cyan('Duplicate dependencies removed:')} ${stats.duplicateDependenciesRemoved}\n` +
							`${chalk.cyan('Circular dependencies fixed:')} ${stats.circularDependenciesFixed}\n\n` +
							`${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` +
							`${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}\n`,
						{
							padding: 1,
							borderColor: 'green',
							borderStyle: 'round',
							margin: { top: 1, bottom: 1 }
						}
					)
				);
			} else {
				log(
					'success',
					'No dependency issues found - all dependencies are valid'
				);

				console.log(
					boxen(
						chalk.green(`All Dependencies Are Valid\n\n`) +
							`${chalk.cyan('Tasks checked:')} ${data.tasks.length}\n` +
							`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
						{
							padding: 1,
							borderColor: 'green',
							borderStyle: 'round',
							margin: { top: 1, bottom: 1 }
						}
					)
				);
			}
		}
	} catch (error) {
		log('error', 'Error in fix-dependencies command:', error);
		process.exit(1);
	}
}

/**
 * Ensure at least one subtask in each task has no dependencies
 * @param {Object} tasksData - The tasks data object with tasks array
 * @returns {boolean} - True if any changes were made
 */
function ensureAtLeastOneIndependentSubtask(tasksData) {
	if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
		return false;
	}

	let changesDetected = false;

	tasksData.tasks.forEach((task) => {
		if (
			!task.subtasks ||
			!Array.isArray(task.subtasks) ||
			task.subtasks.length === 0
		) {
			return;
		}

		// Check if any subtask has no dependencies
		const hasIndependentSubtask = task.subtasks.some(
			(st) =>
				!st.dependencies ||
				!Array.isArray(st.dependencies) ||
				st.dependencies.length === 0
		);

		if (!hasIndependentSubtask) {
			// Find the first subtask and clear its dependencies
			if (task.subtasks.length > 0) {
				const firstSubtask = task.subtasks[0];
				log(
					'debug',
					`Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}`
				);
				firstSubtask.dependencies = [];
				changesDetected = true;
			}
		}
	});

	return changesDetected;
}

/**
 * Validate and fix dependencies across all tasks and subtasks
 * This function is designed to be called after any task modification
 * @param {Object} tasksData - The tasks data object with tasks array
 * @param {string} tasksPath - Optional path to save the changes
 * @param {string} projectRoot - Optional project root for tag context
 * @param {string} tag - Optional tag for tag context
 * @returns {boolean} - True if any changes were made
 */
function validateAndFixDependencies(
	tasksData,
	tasksPath = null,
	projectRoot = null,
	tag = null
) {
	if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
		log('error', 'Invalid tasks data');
		return false;
	}

	log('debug', 'Validating and fixing dependencies...');

	// Create a deep copy for comparison
	const originalData = JSON.parse(JSON.stringify(tasksData));

	// 1. Remove duplicate dependencies from tasks and subtasks
	tasksData.tasks = tasksData.tasks.map((task) => {
		// Handle task dependencies
		if (task.dependencies) {
			const uniqueDeps = [...new Set(task.dependencies)];
			task.dependencies = uniqueDeps;
		}

		// Handle subtask dependencies
		if (task.subtasks) {
			task.subtasks = task.subtasks.map((subtask) => {
				if (subtask.dependencies) {
					const uniqueDeps = [...new Set(subtask.dependencies)];
					subtask.dependencies = uniqueDeps;
				}
				return subtask;
			});
		}
		return task;
	});

	// 2. Remove invalid task dependencies (non-existent tasks)
	tasksData.tasks.forEach((task) => {
		// Clean up task dependencies
		if (task.dependencies) {
			task.dependencies = task.dependencies.filter((depId) => {
				// Remove self-dependencies
				if (String(depId) === String(task.id)) {
					return false;
				}
				// Remove non-existent dependencies
				return taskExists(tasksData.tasks, depId);
			});
		}

		// Clean up subtask dependencies
		if (task.subtasks) {
			task.subtasks.forEach((subtask) => {
				if (subtask.dependencies) {
					subtask.dependencies = subtask.dependencies.filter((depId) => {
						// Handle numeric subtask references
						if (typeof depId === 'number' && depId < 100) {
							const fullSubtaskId = `${task.id}.${depId}`;
							return taskExists(tasksData.tasks, fullSubtaskId);
						}
						// Handle full task/subtask references
						return taskExists(tasksData.tasks, depId);
					});
				}
			});
		}
	});

	// 3. Ensure at least one subtask has no dependencies in each task
	tasksData.tasks.forEach((task) => {
		if (task.subtasks && task.subtasks.length > 0) {
			const hasIndependentSubtask = task.subtasks.some(
				(st) =>
					!st.dependencies ||
					!Array.isArray(st.dependencies) ||
					st.dependencies.length === 0
			);

			if (!hasIndependentSubtask) {
				task.subtasks[0].dependencies = [];
			}
		}
	});

	// Check if any changes were made by comparing with original data
	const changesDetected =
		JSON.stringify(tasksData) !== JSON.stringify(originalData);

	// Save changes if needed
	if (tasksPath && changesDetected) {
		try {
			writeJSON(tasksPath, tasksData, projectRoot, tag);
			log('debug', 'Saved dependency fixes to tasks.json');
		} catch (error) {
			log('error', 'Failed to save dependency fixes to tasks.json', error);
		}
	}

	return changesDetected;
}

export {
	addDependency,
	removeDependency,
	isCircularDependency,
	validateTaskDependencies,
	validateDependenciesCommand,
	fixDependenciesCommand,
	removeDuplicateDependencies,
	cleanupSubtaskDependencies,
	ensureAtLeastOneIndependentSubtask,
	validateAndFixDependencies
};

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { log } from './utils.js';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';

/**
 * Manages prompt templates for AI interactions
 */
export class PromptManager {
	constructor() {
		const __filename = fileURLToPath(import.meta.url);
		const __dirname = path.dirname(__filename);
		this.promptsDir = path.join(__dirname, '..', '..', 'src', 'prompts');
		this.cache = new Map();
		this.setupValidation();
	}

	/**
	 * Set up JSON schema validation
	 * @private
	 */
	setupValidation() {
		this.ajv = new Ajv({ allErrors: true, strict: false });
		addFormats(this.ajv);

		try {
			// Load schema from src/prompts/schemas
			const schemaPath = path.join(
				this.promptsDir,
				'schemas',
				'prompt-template.schema.json'
			);
			const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
			const schema = JSON.parse(schemaContent);

			this.validatePrompt = this.ajv.compile(schema);
			log('info', '✓ JSON schema validation enabled');
		} catch (error) {
			log('warn', `⚠ Schema validation disabled: ${error.message}`);
			this.validatePrompt = () => true; // Fallback to no validation
		}
	}

	/**
	 * Load a prompt template and render it with variables
	 * @param {string} promptId - The prompt template ID
	 * @param {Object} variables - Variables to inject into the template
	 * @param {string} [variantKey] - Optional specific variant to use
	 * @returns {{systemPrompt: string, userPrompt: string, metadata: Object}}
	 */
	loadPrompt(promptId, variables = {}, variantKey = null) {
		try {
			// Check cache first
			const cacheKey = `${promptId}-${JSON.stringify(variables)}-${variantKey}`;
			if (this.cache.has(cacheKey)) {
				return this.cache.get(cacheKey);
			}

			// Load template
			const template = this.loadTemplate(promptId);

			// Validate parameters if schema validation is available
			if (this.validatePrompt && this.validatePrompt !== true) {
				this.validateParameters(template, variables);
			}

			// Select the variant - use specified key or select based on conditions
			const variant = variantKey
				? { ...template.prompts[variantKey], name: variantKey }
				: this.selectVariant(template, variables);

			// Render the prompts with variables
			const rendered = {
				systemPrompt: this.renderTemplate(variant.system, variables),
				userPrompt: this.renderTemplate(variant.user, variables),
				metadata: {
					templateId: template.id,
					version: template.version,
					variant: variant.name || 'default',
					parameters: variables
				}
			};

			// Cache the result
			this.cache.set(cacheKey, rendered);

			return rendered;
		} catch (error) {
			log('error', `Failed to load prompt ${promptId}: ${error.message}`);
			throw error;
		}
	}

	/**
	 * Load a prompt template from disk
	 * @private
	 */
	loadTemplate(promptId) {
		const templatePath = path.join(this.promptsDir, `${promptId}.json`);

		try {
			const content = fs.readFileSync(templatePath, 'utf-8');
			const template = JSON.parse(content);

			// Schema validation if available (do this first for detailed errors)
			if (this.validatePrompt && this.validatePrompt !== true) {
				const valid = this.validatePrompt(template);
				if (!valid) {
					const errors = this.validatePrompt.errors
						.map((err) => `${err.instancePath || 'root'}: ${err.message}`)
						.join(', ');
					throw new Error(`Schema validation failed: ${errors}`);
				}
			} else {
				// Fallback basic validation if no schema validation available
				if (!template.id || !template.prompts || !template.prompts.default) {
					throw new Error(
						'Invalid template structure: missing required fields (id, prompts.default)'
					);
				}
			}

			return template;
		} catch (error) {
			if (error.code === 'ENOENT') {
				throw new Error(`Prompt template '${promptId}' not found`);
			}
			throw error;
		}
	}

	/**
	 * Validate parameters against template schema
	 * @private
	 */
	validateParameters(template, variables) {
		if (!template.parameters) return;

		const errors = [];

		for (const [paramName, paramConfig] of Object.entries(
			template.parameters
		)) {
			const value = variables[paramName];

			// Check required parameters
			if (paramConfig.required && value === undefined) {
				errors.push(`Required parameter '${paramName}' missing`);
				continue;
			}

			// Skip validation for undefined optional parameters
			if (value === undefined) continue;

			// Type validation
			if (!this.validateParameterType(value, paramConfig.type)) {
				errors.push(
					`Parameter '${paramName}' expected ${paramConfig.type}, got ${typeof value}`
				);
			}

			// Enum validation
			if (paramConfig.enum && !paramConfig.enum.includes(value)) {
				errors.push(
					`Parameter '${paramName}' must be one of: ${paramConfig.enum.join(', ')}`
				);
			}

			// Pattern validation for strings
			if (paramConfig.pattern && typeof value === 'string') {
				const regex = new RegExp(paramConfig.pattern);
				if (!regex.test(value)) {
					errors.push(
						`Parameter '${paramName}' does not match required pattern: ${paramConfig.pattern}`
					);
				}
			}

			// Range validation for numbers
			if (typeof value === 'number') {
				if (paramConfig.minimum !== undefined && value < paramConfig.minimum) {
					errors.push(
						`Parameter '${paramName}' must be >= ${paramConfig.minimum}`
					);
				}
				if (paramConfig.maximum !== undefined && value > paramConfig.maximum) {
					errors.push(
						`Parameter '${paramName}' must be <= ${paramConfig.maximum}`
					);
				}
			}
		}

		if (errors.length > 0) {
			throw new Error(`Parameter validation failed: ${errors.join('; ')}`);
		}
	}

	/**
	 * Validate parameter type
	 * @private
	 */
	validateParameterType(value, expectedType) {
		switch (expectedType) {
			case 'string':
				return typeof value === 'string';
			case 'number':
				return typeof value === 'number';
			case 'boolean':
				return typeof value === 'boolean';
			case 'array':
				return Array.isArray(value);
			case 'object':
				return (
					typeof value === 'object' && value !== null && !Array.isArray(value)
				);
			default:
				return true;
		}
	}

	/**
	 * Select the best variant based on conditions
	 * @private
	 */
	selectVariant(template, variables) {
		// Check each variant's condition
		for (const [name, variant] of Object.entries(template.prompts)) {
			if (name === 'default') continue;

			if (
				variant.condition &&
				this.evaluateCondition(variant.condition, variables)
			) {
				return { ...variant, name };
			}
		}

		// Fall back to default
		return { ...template.prompts.default, name: 'default' };
	}

	/**
	 * Evaluate a condition string
	 * @private
	 */
	evaluateCondition(condition, variables) {
		try {
			// Create a safe evaluation context
			const context = { ...variables };

			// Simple condition evaluation (can be enhanced)
			// For now, supports basic comparisons
			const func = new Function(...Object.keys(context), `return ${condition}`);
			return func(...Object.values(context));
		} catch (error) {
			log('warn', `Failed to evaluate condition: ${condition}`);
			return false;
		}
	}

	/**
	 * Render a template string with variables
	 * @private
	 */
	renderTemplate(template, variables) {
		let rendered = template;

		// Handle helper functions like (eq variable "value")
		rendered = rendered.replace(
			/\(eq\s+(\w+(?:\.\w+)*)\s+"([^"]+)"\)/g,
			(match, path, compareValue) => {
				const value = this.getNestedValue(variables, path);
				return value === compareValue ? 'true' : 'false';
			}
		);

		// Handle not helper function like (not variable)
		rendered = rendered.replace(/\(not\s+(\w+(?:\.\w+)*)\)/g, (match, path) => {
			const value = this.getNestedValue(variables, path);
			return !value ? 'true' : 'false';
		});

		// Handle gt (greater than) helper function like (gt variable 0)
		rendered = rendered.replace(
			/\(gt\s+(\w+(?:\.\w+)*)\s+(\d+(?:\.\d+)?)\)/g,
			(match, path, compareValue) => {
				const value = this.getNestedValue(variables, path);
				const numValue = parseFloat(compareValue);
				return typeof value === 'number' && value > numValue ? 'true' : 'false';
			}
		);

		// Handle gte (greater than or equal) helper function like (gte variable 0)
		rendered = rendered.replace(
			/\(gte\s+(\w+(?:\.\w+)*)\s+(\d+(?:\.\d+)?)\)/g,
			(match, path, compareValue) => {
				const value = this.getNestedValue(variables, path);
				const numValue = parseFloat(compareValue);
				return typeof value === 'number' && value >= numValue
					? 'true'
					: 'false';
			}
		);

		// Handle conditionals with else {{#if variable}}...{{else}}...{{/if}}
		rendered = rendered.replace(
			/\{\{#if\s+([^}]+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g,
			(match, condition, trueContent, falseContent = '') => {
				// Handle boolean values and helper function results
				let value;
				if (condition === 'true') {
					value = true;
				} else if (condition === 'false') {
					value = false;
				} else {
					value = this.getNestedValue(variables, condition);
				}
				return value ? trueContent : falseContent;
			}
		);

		// Handle each loops {{#each array}}...{{/each}}
		rendered = rendered.replace(
			/\{\{#each\s+(\w+(?:\.\w+)*)\}\}([\s\S]*?)\{\{\/each\}\}/g,
			(match, path, content) => {
				const array = this.getNestedValue(variables, path);
				if (!Array.isArray(array)) return '';

				return array
					.map((item, index) => {
						// Create a context with item properties and special variables
						const itemContext = {
							...variables,
							...item,
							'@index': index,
							'@first': index === 0,
							'@last': index === array.length - 1
						};

						// Recursively render the content with item context
						return this.renderTemplate(content, itemContext);
					})
					.join('');
			}
		);

		// Handle json helper {{{json variable}}} (triple braces for raw output)
		rendered = rendered.replace(
			/\{\{\{json\s+(\w+(?:\.\w+)*)\}\}\}/g,
			(match, path) => {
				const value = this.getNestedValue(variables, path);
				return value !== undefined ? JSON.stringify(value, null, 2) : '';
			}
		);

		// Handle variable substitution {{variable}}
		rendered = rendered.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, path) => {
			const value = this.getNestedValue(variables, path);
			return value !== undefined ? value : '';
		});

		return rendered;
	}

	/**
	 * Get nested value from object using dot notation
	 * @private
	 */
	getNestedValue(obj, path) {
		return path
			.split('.')
			.reduce(
				(current, key) =>
					current && current[key] !== undefined ? current[key] : undefined,
				obj
			);
	}

	/**
	 * Validate all prompt templates
	 */
	validateAllPrompts() {
		const results = { total: 0, errors: [], valid: [] };

		try {
			const files = fs.readdirSync(this.promptsDir);
			const promptFiles = files.filter((file) => file.endsWith('.json'));

			for (const file of promptFiles) {
				const promptId = file.replace('.json', '');
				results.total++;

				try {
					this.loadTemplate(promptId);
					results.valid.push(promptId);
				} catch (error) {
					results.errors.push(`${promptId}: ${error.message}`);
				}
			}
		} catch (error) {
			results.errors.push(
				`Failed to read templates directory: ${error.message}`
			);
		}

		return results;
	}

	/**
	 * List all available prompt templates
	 */
	listPrompts() {
		try {
			const files = fs.readdirSync(this.promptsDir);
			const prompts = [];

			for (const file of files) {
				if (!file.endsWith('.json')) continue;

				const promptId = file.replace('.json', '');
				try {
					const template = this.loadTemplate(promptId);
					prompts.push({
						id: template.id,
						description: template.description,
						version: template.version,
						parameters: template.parameters,
						tags: template.metadata?.tags || []
					});
				} catch (error) {
					log('warn', `Failed to load template ${promptId}: ${error.message}`);
				}
			}

			return prompts;
		} catch (error) {
			if (error.code === 'ENOENT') {
				// Templates directory doesn't exist yet
				return [];
			}
			throw error;
		}
	}

	/**
	 * Validate template structure
	 */
	validateTemplate(templatePath) {
		try {
			const content = fs.readFileSync(templatePath, 'utf-8');
			const template = JSON.parse(content);

			// Check required fields
			const required = ['id', 'version', 'description', 'prompts'];
			for (const field of required) {
				if (!template[field]) {
					return { valid: false, error: `Missing required field: ${field}` };
				}
			}

			// Check default prompt exists
			if (!template.prompts.default) {
				return { valid: false, error: 'Missing default prompt variant' };
			}

			// Check each variant has required fields
			for (const [name, variant] of Object.entries(template.prompts)) {
				if (!variant.system || !variant.user) {
					return {
						valid: false,
						error: `Variant '${name}' missing system or user prompt`
					};
				}
			}

			// Schema validation if available
			if (this.validatePrompt && this.validatePrompt !== true) {
				const valid = this.validatePrompt(template);
				if (!valid) {
					const errors = this.validatePrompt.errors
						.map((err) => `${err.instancePath || 'root'}: ${err.message}`)
						.join(', ');
					return { valid: false, error: `Schema validation failed: ${errors}` };
				}
			}

			return { valid: true };
		} catch (error) {
			return { valid: false, error: error.message };
		}
	}
}

// Singleton instance
let promptManager = null;

/**
 * Get or create the prompt manager instance
 * @returns {PromptManager}
 */
export function getPromptManager() {
	if (!promptManager) {
		promptManager = new PromptManager();
	}
	return promptManager;
}

/**
 * src/ai-providers/gemini-cli.js
 *
 * Implementation for interacting with Gemini models via Gemini CLI
 * using the ai-sdk-provider-gemini-cli package.
 */

import { generateObject, generateText, streamText } from 'ai';
import { parse } from 'jsonc-parser';
import { BaseAIProvider } from './base-provider.js';
import { log } from '../../scripts/modules/utils.js';

let createGeminiProvider;

async function loadGeminiCliModule() {
	if (!createGeminiProvider) {
		try {
			const mod = await import('ai-sdk-provider-gemini-cli');
			createGeminiProvider = mod.createGeminiProvider;
		} catch (err) {
			throw new Error(
				"Gemini CLI SDK is not installed. Please install 'ai-sdk-provider-gemini-cli' to use the gemini-cli provider."
			);
		}
	}
}

export class GeminiCliProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'Gemini CLI';
	}

	/**
	 * Override validateAuth to handle Gemini CLI authentication options
	 * @param {object} params - Parameters to validate
	 */
	validateAuth(params) {
		// Gemini CLI is designed to use pre-configured OAuth authentication
		// Users choose gemini-cli specifically to leverage their existing
		// gemini auth login credentials, not to use API keys.
		// We support API keys for compatibility, but the expected usage
		// is through CLI authentication (no API key required).
		// No validation needed - the SDK will handle auth internally
	}

	/**
	 * Creates and returns a Gemini CLI client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} [params.apiKey] - Optional Gemini API key (rarely used with gemini-cli)
	 * @param {string} [params.baseURL] - Optional custom API endpoint
	 * @returns {Promise<Function>} Gemini CLI client function
	 * @throws {Error} If initialization fails
	 */
	async getClient(params) {
		try {
			// Load the Gemini CLI module dynamically
			await loadGeminiCliModule();
			// Primary use case: Use existing gemini CLI authentication
			// Secondary use case: Direct API key (for compatibility)
			let authOptions = {};

			if (params.apiKey && params.apiKey !== 'gemini-cli-no-key-required') {
				// API key provided - use it for compatibility
				authOptions = {
					authType: 'api-key',
					apiKey: params.apiKey
				};
			} else {
				// Expected case: Use gemini CLI authentication via OAuth
				authOptions = {
					authType: 'oauth-personal'
				};
			}

			// Add baseURL if provided (for custom endpoints)
			if (params.baseURL) {
				authOptions.baseURL = params.baseURL;
			}

			// Create and return the provider
			return createGeminiProvider(authOptions);
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}

	/**
	 * Extracts system messages from the messages array and returns them separately.
	 * This is needed because ai-sdk-provider-gemini-cli expects system prompts as a separate parameter.
	 * @param {Array} messages - Array of message objects
	 * @param {Object} options - Options for system prompt enhancement
	 * @param {boolean} options.enforceJsonOutput - Whether to add JSON enforcement to system prompt
	 * @returns {Object} - {systemPrompt: string|undefined, messages: Array}
	 */
	_extractSystemMessage(messages, options = {}) {
		if (!messages || !Array.isArray(messages)) {
			return { systemPrompt: undefined, messages: messages || [] };
		}

		const systemMessages = messages.filter((msg) => msg.role === 'system');
		const nonSystemMessages = messages.filter((msg) => msg.role !== 'system');

		// Combine multiple system messages if present
		let systemPrompt =
			systemMessages.length > 0
				? systemMessages.map((msg) => msg.content).join('\n\n')
				: undefined;

		// Add Gemini CLI specific JSON enforcement if requested
		if (options.enforceJsonOutput) {
			const jsonEnforcement = this._getJsonEnforcementPrompt();
			systemPrompt = systemPrompt
				? `${systemPrompt}\n\n${jsonEnforcement}`
				: jsonEnforcement;
		}

		return { systemPrompt, messages: nonSystemMessages };
	}

	/**
	 * Gets a Gemini CLI specific system prompt to enforce strict JSON output
	 * @returns {string} JSON enforcement system prompt
	 */
	_getJsonEnforcementPrompt() {
		return `CRITICAL: You MUST respond with ONLY valid JSON. Do not include any explanatory text, markdown formatting, code block markers, or conversational phrases like "Here is" or "Of course". Your entire response must be parseable JSON that starts with { or [ and ends with } or ]. No exceptions.`;
	}

	/**
	 * Checks if a string is valid JSON
	 * @param {string} text - Text to validate
	 * @returns {boolean} True if valid JSON
	 */
	_isValidJson(text) {
		if (!text || typeof text !== 'string') {
			return false;
		}

		try {
			JSON.parse(text.trim());
			return true;
		} catch {
			return false;
		}
	}

	/**
	 * Detects if the user prompt is requesting JSON output
	 * @param {Array} messages - Array of message objects
	 * @returns {boolean} True if JSON output is likely expected
	 */
	_detectJsonRequest(messages) {
		const userMessages = messages.filter((msg) => msg.role === 'user');
		const combinedText = userMessages
			.map((msg) => msg.content)
			.join(' ')
			.toLowerCase();

		// Look for indicators that JSON output is expected
		const jsonIndicators = [
			'json',
			'respond only with',
			'return only',
			'output only',
			'format:',
			'structure:',
			'schema:',
			'{"',
			'[{',
			'subtasks',
			'array',
			'object'
		];

		return jsonIndicators.some((indicator) => combinedText.includes(indicator));
	}

	/**
	 * Simplifies complex prompts for gemini-cli to improve JSON output compliance
	 * @param {Array} messages - Array of message objects
	 * @returns {Array} Simplified messages array
	 */
	_simplifyJsonPrompts(messages) {
		// First, check if this is an expand-task operation by looking at the system message
		const systemMsg = messages.find((m) => m.role === 'system');
		const isExpandTask =
			systemMsg &&
			systemMsg.content.includes(
				'You are an AI assistant helping with task breakdown. Generate exactly'
			);

		if (!isExpandTask) {
			return messages; // Not an expand task, return unchanged
		}

		// Extract subtask count from system message
		const subtaskCountMatch = systemMsg.content.match(
			/Generate exactly (\d+) subtasks/
		);
		const subtaskCount = subtaskCountMatch ? subtaskCountMatch[1] : '10';

		log(
			'debug',
			`${this.name} detected expand-task operation, simplifying for ${subtaskCount} subtasks`
		);

		return messages.map((msg) => {
			if (msg.role !== 'user') {
				return msg;
			}

			// For expand-task user messages, create a much simpler, more direct prompt
			// that doesn't depend on specific task content
			const simplifiedPrompt = `Generate exactly ${subtaskCount} subtasks in the following JSON format.

CRITICAL INSTRUCTION: You must respond with ONLY valid JSON. No explanatory text, no "Here is", no "Of course", no markdown - just the JSON object.

Required JSON structure:
{
  "subtasks": [
    {
      "id": 1,
      "title": "Specific actionable task title",
      "description": "Clear task description",
      "dependencies": [],
      "details": "Implementation details and guidance",
      "testStrategy": "Testing approach"
    }
  ]
}

Generate ${subtaskCount} subtasks based on the original task context. Return ONLY the JSON object.`;

			log(
				'debug',
				`${this.name} simplified user prompt for better JSON compliance`
			);
			return { ...msg, content: simplifiedPrompt };
		});
	}

	/**
	 * Extract JSON from Gemini's response using a tolerant parser.
	 *
	 * Optimized approach that progressively tries different parsing strategies:
	 * 1. Direct parsing after cleanup
	 * 2. Smart boundary detection with single-pass analysis
	 * 3. Limited character-by-character fallback for edge cases
	 *
	 * @param {string} text - Raw text which may contain JSON
	 * @returns {string} A valid JSON string if extraction succeeds, otherwise the original text
	 */
	extractJson(text) {
		if (!text || typeof text !== 'string') {
			return text;
		}

		let content = text.trim();

		// Early exit for very short content
		if (content.length < 2) {
			return text;
		}

		// Strip common wrappers in a single pass
		content = content
			// Remove markdown fences
			.replace(/^.*?```(?:json)?\s*([\s\S]*?)\s*```.*$/i, '$1')
			// Remove variable declarations
			.replace(/^\s*(?:const|let|var)\s+\w+\s*=\s*([\s\S]*?)(?:;|\s*)$/i, '$1')
			// Remove common prefixes
			.replace(/^(?:Here's|The)\s+(?:the\s+)?JSON.*?[:]\s*/i, '')
			.trim();

		// Find the first JSON-like structure
		const firstObj = content.indexOf('{');
		const firstArr = content.indexOf('[');

		if (firstObj === -1 && firstArr === -1) {
			return text;
		}

		const start =
			firstArr === -1
				? firstObj
				: firstObj === -1
					? firstArr
					: Math.min(firstObj, firstArr);
		content = content.slice(start);

		// Optimized parsing function with error collection
		const tryParse = (value) => {
			if (!value || value.length < 2) return undefined;

			const errors = [];
			try {
				const result = parse(value, errors, {
					allowTrailingComma: true,
					allowEmptyContent: false
				});
				if (errors.length === 0 && result !== undefined) {
					return JSON.stringify(result, null, 2);
				}
			} catch {
				// Parsing failed completely
			}
			return undefined;
		};

		// Try parsing the full content first
		const fullParse = tryParse(content);
		if (fullParse !== undefined) {
			return fullParse;
		}

		// Smart boundary detection - single pass with optimizations
		const openChar = content[0];
		const closeChar = openChar === '{' ? '}' : ']';

		let depth = 0;
		let inString = false;
		let escapeNext = false;
		let lastValidEnd = -1;

		// Single-pass boundary detection with early termination
		for (let i = 0; i < content.length && i < 10000; i++) {
			// Limit scan for performance
			const char = content[i];

			if (escapeNext) {
				escapeNext = false;
				continue;
			}

			if (char === '\\') {
				escapeNext = true;
				continue;
			}

			if (char === '"') {
				inString = !inString;
				continue;
			}

			if (inString) continue;

			if (char === openChar) {
				depth++;
			} else if (char === closeChar) {
				depth--;
				if (depth === 0) {
					lastValidEnd = i + 1;
					// Try parsing immediately on first valid boundary
					const candidate = content.slice(0, lastValidEnd);
					const parsed = tryParse(candidate);
					if (parsed !== undefined) {
						return parsed;
					}
				}
			}
		}

		// If we found valid boundaries but parsing failed, try limited fallback
		if (lastValidEnd > 0) {
			const maxAttempts = Math.min(5, Math.floor(lastValidEnd / 100)); // Limit attempts
			for (let i = 0; i < maxAttempts; i++) {
				const testEnd = Math.max(
					lastValidEnd - i * 50,
					Math.floor(lastValidEnd * 0.8)
				);
				const candidate = content.slice(0, testEnd);
				const parsed = tryParse(candidate);
				if (parsed !== undefined) {
					return parsed;
				}
			}
		}

		return text;
	}

	/**
	 * Generates text using Gemini CLI model
	 * Overrides base implementation to properly handle system messages and enforce JSON output when needed
	 */
	async generateText(params) {
		try {
			this.validateParams(params);
			this.validateMessages(params.messages);

			log(
				'debug',
				`Generating ${this.name} text with model: ${params.modelId}`
			);

			// Detect if JSON output is expected and enforce it for better gemini-cli compatibility
			const enforceJsonOutput = this._detectJsonRequest(params.messages);

			// Debug logging to understand what's happening
			log('debug', `${this.name} JSON detection analysis:`, {
				enforceJsonOutput,
				messageCount: params.messages.length,
				messages: params.messages.map((msg) => ({
					role: msg.role,
					contentPreview: msg.content
						? msg.content.substring(0, 200) + '...'
						: 'empty'
				}))
			});

			if (enforceJsonOutput) {
				log(
					'debug',
					`${this.name} detected JSON request - applying strict JSON enforcement system prompt`
				);
			}

			// For gemini-cli, simplify complex prompts before processing
			let processedMessages = params.messages;
			if (enforceJsonOutput) {
				processedMessages = this._simplifyJsonPrompts(params.messages);
			}

			// Extract system messages for separate handling with optional JSON enforcement
			const { systemPrompt, messages } = this._extractSystemMessage(
				processedMessages,
				{ enforceJsonOutput }
			);

			// Debug the final system prompt being sent
			log('debug', `${this.name} final system prompt:`, {
				systemPromptLength: systemPrompt ? systemPrompt.length : 0,
				systemPromptPreview: systemPrompt
					? systemPrompt.substring(0, 300) + '...'
					: 'none',
				finalMessageCount: messages.length
			});

			const client = await this.getClient(params);
			const result = await generateText({
				model: client(params.modelId),
				system: systemPrompt,
				messages: messages,
				maxTokens: params.maxTokens,
				temperature: params.temperature
			});

			// If we detected a JSON request and gemini-cli returned conversational text,
			// attempt to extract JSON from the response
			let finalText = result.text;
			if (enforceJsonOutput && result.text && !this._isValidJson(result.text)) {
				log(
					'debug',
					`${this.name} response appears conversational, attempting JSON extraction`
				);

				// Log first 1000 chars of the response to see what Gemini actually returned
				log('debug', `${this.name} raw response preview:`, {
					responseLength: result.text.length,
					responseStart: result.text.substring(0, 1000)
				});

				const extractedJson = this.extractJson(result.text);
				if (this._isValidJson(extractedJson)) {
					log(
						'debug',
						`${this.name} successfully extracted JSON from conversational response`
					);
					finalText = extractedJson;
				} else {
					log(
						'debug',
						`${this.name} JSON extraction failed, returning original response`
					);

					// Log what extraction returned to debug why it failed
					log('debug', `${this.name} extraction result preview:`, {
						extractedLength: extractedJson ? extractedJson.length : 0,
						extractedStart: extractedJson
							? extractedJson.substring(0, 500)
							: 'null'
					});
				}
			}

			log(
				'debug',
				`${this.name} generateText completed successfully for model: ${params.modelId}`
			);

			return {
				text: finalText,
				usage: {
					inputTokens: result.usage?.promptTokens,
					outputTokens: result.usage?.completionTokens,
					totalTokens: result.usage?.totalTokens
				}
			};
		} catch (error) {
			this.handleError('text generation', error);
		}
	}

	/**
	 * Streams text using Gemini CLI model
	 * Overrides base implementation to properly handle system messages and enforce JSON output when needed
	 */
	async streamText(params) {
		try {
			this.validateParams(params);
			this.validateMessages(params.messages);

			log('debug', `Streaming ${this.name} text with model: ${params.modelId}`);

			// Detect if JSON output is expected and enforce it for better gemini-cli compatibility
			const enforceJsonOutput = this._detectJsonRequest(params.messages);

			// Debug logging to understand what's happening
			log('debug', `${this.name} JSON detection analysis:`, {
				enforceJsonOutput,
				messageCount: params.messages.length,
				messages: params.messages.map((msg) => ({
					role: msg.role,
					contentPreview: msg.content
						? msg.content.substring(0, 200) + '...'
						: 'empty'
				}))
			});

			if (enforceJsonOutput) {
				log(
					'debug',
					`${this.name} detected JSON request - applying strict JSON enforcement system prompt`
				);
			}

			// Extract system messages for separate handling with optional JSON enforcement
			const { systemPrompt, messages } = this._extractSystemMessage(
				params.messages,
				{ enforceJsonOutput }
			);

			const client = await this.getClient(params);
			const stream = await streamText({
				model: client(params.modelId),
				system: systemPrompt,
				messages: messages,
				maxTokens: params.maxTokens,
				temperature: params.temperature
			});

			log(
				'debug',
				`${this.name} streamText initiated successfully for model: ${params.modelId}`
			);

			// Note: For streaming, we can't intercept and modify the response in real-time
			// The JSON extraction would need to happen on the consuming side
			return stream;
		} catch (error) {
			this.handleError('text streaming', error);
		}
	}

	/**
	 * Generates a structured object using Gemini CLI model
	 * Overrides base implementation to handle Gemini-specific JSON formatting issues and system messages
	 */
	async generateObject(params) {
		try {
			// First try the standard generateObject from base class
			return await super.generateObject(params);
		} catch (error) {
			// If it's a JSON parsing error, try to extract and parse JSON manually
			if (error.message?.includes('JSON') || error.message?.includes('parse')) {
				log(
					'debug',
					`Gemini CLI generateObject failed with parsing error, attempting manual extraction`
				);

				try {
					// Validate params first
					this.validateParams(params);
					this.validateMessages(params.messages);

					if (!params.schema) {
						throw new Error('Schema is required for object generation');
					}
					if (!params.objectName) {
						throw new Error('Object name is required for object generation');
					}

					// Extract system messages for separate handling with JSON enforcement
					const { systemPrompt, messages } = this._extractSystemMessage(
						params.messages,
						{ enforceJsonOutput: true }
					);

					// Call generateObject directly with our client
					const client = await this.getClient(params);
					const result = await generateObject({
						model: client(params.modelId),
						system: systemPrompt,
						messages: messages,
						schema: params.schema,
						mode: 'json', // Use json mode instead of auto for Gemini
						maxTokens: params.maxTokens,
						temperature: params.temperature
					});

					// If we get rawResponse text, try to extract JSON from it
					if (result.rawResponse?.text && !result.object) {
						const extractedJson = this.extractJson(result.rawResponse.text);
						try {
							result.object = JSON.parse(extractedJson);
						} catch (parseError) {
							log(
								'error',
								`Failed to parse extracted JSON: ${parseError.message}`
							);
							log(
								'debug',
								`Extracted JSON: ${extractedJson.substring(0, 500)}...`
							);
							throw new Error(
								`Gemini CLI returned invalid JSON that could not be parsed: ${parseError.message}`
							);
						}
					}

					return {
						object: result.object,
						usage: {
							inputTokens: result.usage?.promptTokens,
							outputTokens: result.usage?.completionTokens,
							totalTokens: result.usage?.totalTokens
						}
					};
				} catch (retryError) {
					log(
						'error',
						`Gemini CLI manual JSON extraction failed: ${retryError.message}`
					);
					// Re-throw the original error with more context
					throw new Error(
						`${this.name} failed to generate valid JSON object: ${error.message}`
					);
				}
			}

			// For non-parsing errors, just re-throw
			throw error;
		}
	}

	getRequiredApiKeyName() {
		return 'GEMINI_API_KEY';
	}

	isRequiredApiKey() {
		return false;
	}
}

import path from 'path';
import * as fs from 'fs';
import { readJSON, writeJSON, log, findTaskById } from '../utils.js';
import generateTaskFiles from './generate-task-files.js';
import taskExists from './task-exists.js';

/**
 * Removes one or more tasks or subtasks from the tasks file
 * @param {string} tasksPath - Path to the tasks file
 * @param {string} taskIds - Comma-separated string of task/subtask IDs to remove (e.g., '5,6.1,7')
 * @param {Object} context - Context object containing projectRoot and tag information
 * @param {string} [context.projectRoot] - Project root path
 * @param {string} [context.tag] - Tag for the task
 * @returns {Object} Result object with success status, messages, and removed task info
 */
async function removeTask(tasksPath, taskIds, context = {}) {
	const { projectRoot, tag } = context;
	const results = {
		success: true,
		messages: [],
		errors: [],
		removedTasks: []
	};
	const taskIdsToRemove = taskIds
		.split(',')
		.map((id) => id.trim())
		.filter(Boolean); // Remove empty strings if any

	if (taskIdsToRemove.length === 0) {
		results.success = false;
		results.errors.push('No valid task IDs provided.');
		return results;
	}

	try {
		// Read the tasks file ONCE before the loop, preserving the full tagged structure
		const rawData = readJSON(tasksPath, projectRoot, tag); // Read raw data
		if (!rawData) {
			throw new Error(`Could not read tasks file at ${tasksPath}`);
		}

		// Use the full tagged data if available, otherwise use the data as is
		const fullTaggedData = rawData._rawTaggedData || rawData;

		if (!fullTaggedData[tag] || !fullTaggedData[tag].tasks) {
			throw new Error(`Tag '${tag}' not found or has no tasks.`);
		}

		const tasks = fullTaggedData[tag].tasks; // Work with tasks from the correct tag

		const tasksToDeleteFiles = []; // Collect IDs of main tasks whose files should be deleted

		for (const taskId of taskIdsToRemove) {
			// Check if the task ID exists *before* attempting removal
			if (!taskExists(tasks, taskId)) {
				const errorMsg = `Task with ID ${taskId} in tag '${tag}' not found or already removed.`;
				results.errors.push(errorMsg);
				results.success = false; // Mark overall success as false if any error occurs
				continue; // Skip to the next ID
			}

			try {
				// Handle subtask removal (e.g., '5.2')
				if (typeof taskId === 'string' && taskId.includes('.')) {
					const [parentTaskId, subtaskId] = taskId
						.split('.')
						.map((id) => parseInt(id, 10));

					// Find the parent task
					const parentTask = tasks.find((t) => t.id === parentTaskId);
					if (!parentTask || !parentTask.subtasks) {
						throw new Error(
							`Parent task ${parentTaskId} or its subtasks not found for subtask ${taskId}`
						);
					}

					// Find the subtask to remove
					const subtaskIndex = parentTask.subtasks.findIndex(
						(st) => st.id === subtaskId
					);
					if (subtaskIndex === -1) {
						throw new Error(
							`Subtask ${subtaskId} not found in parent task ${parentTaskId}`
						);
					}

					// Store the subtask info before removal
					const removedSubtask = {
						...parentTask.subtasks[subtaskIndex],
						parentTaskId: parentTaskId
					};
					results.removedTasks.push(removedSubtask);

					// Remove the subtask from the parent
					parentTask.subtasks.splice(subtaskIndex, 1);

					results.messages.push(
						`Successfully removed subtask ${taskId} from tag '${tag}'`
					);
				}
				// Handle main task removal
				else {
					const taskIdNum = parseInt(taskId, 10);
					const taskIndex = tasks.findIndex((t) => t.id === taskIdNum);
					if (taskIndex === -1) {
						throw new Error(`Task with ID ${taskId} not found in tag '${tag}'`);
					}

					// Store the task info before removal
					const removedTask = tasks[taskIndex];
					results.removedTasks.push(removedTask);
					tasksToDeleteFiles.push(taskIdNum); // Add to list for file deletion

					// Remove the task from the main array
					tasks.splice(taskIndex, 1);

					results.messages.push(
						`Successfully removed task ${taskId} from tag '${tag}'`
					);
				}
			} catch (innerError) {
				// Catch errors specific to processing *this* ID
				const errorMsg = `Error processing ID ${taskId}: ${innerError.message}`;
				results.errors.push(errorMsg);
				results.success = false;
				log('warn', errorMsg); // Log as warning and continue with next ID
			}
		} // End of loop through taskIdsToRemove

		// --- Post-Loop Operations ---

		// Only proceed with cleanup and saving if at least one task was potentially removed
		if (results.removedTasks.length > 0) {
			const allRemovedIds = new Set(
				taskIdsToRemove.map((id) =>
					typeof id === 'string' && id.includes('.') ? id : parseInt(id, 10)
				)
			);

			// Update the tasks in the current tag of the full data structure
			fullTaggedData[tag].tasks = tasks;

			// Remove dependencies from all tags
			for (const tagName in fullTaggedData) {
				if (
					Object.prototype.hasOwnProperty.call(fullTaggedData, tagName) &&
					fullTaggedData[tagName] &&
					fullTaggedData[tagName].tasks
				) {
					const currentTagTasks = fullTaggedData[tagName].tasks;
					currentTagTasks.forEach((task) => {
						if (task.dependencies) {
							task.dependencies = task.dependencies.filter(
								(depId) => !allRemovedIds.has(depId)
							);
						}
						if (task.subtasks) {
							task.subtasks.forEach((subtask) => {
								if (subtask.dependencies) {
									subtask.dependencies = subtask.dependencies.filter(
										(depId) =>
											!allRemovedIds.has(`${task.id}.${depId}`) &&
											!allRemovedIds.has(depId)
									);
								}
							});
						}
					});
				}
			}

			// Save the updated raw data structure
			writeJSON(tasksPath, fullTaggedData, projectRoot, tag);

			// Delete task files AFTER saving tasks.json
			for (const taskIdNum of tasksToDeleteFiles) {
				const taskFileName = path.join(
					path.dirname(tasksPath),
					`task_${taskIdNum.toString().padStart(3, '0')}.txt`
				);
				if (fs.existsSync(taskFileName)) {
					try {
						fs.unlinkSync(taskFileName);
						results.messages.push(`Deleted task file: ${taskFileName}`);
					} catch (unlinkError) {
						const unlinkMsg = `Failed to delete task file ${taskFileName}: ${unlinkError.message}`;
						results.errors.push(unlinkMsg);
						results.success = false;
						log('warn', unlinkMsg);
					}
				}
			}

			// Generate updated task files ONCE, with context
			// try {
			// 	await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
			// 		projectRoot,
			// 		tag
			// 	});
			// 	results.messages.push('Task files regenerated successfully.');
			// } catch (genError) {
			// 	const genErrMsg = `Failed to regenerate task files: ${genError.message}`;
			// 	results.errors.push(genErrMsg);
			// 	results.success = false;
			// 	log('warn', genErrMsg);
			// }
		} else if (results.errors.length === 0) {
			results.messages.push('No tasks found matching the provided IDs.');
		}

		// Consolidate messages for final output
		const finalMessage = results.messages.join('\n');
		const finalError = results.errors.join('\n');

		return {
			success: results.success,
			message: finalMessage || 'No tasks were removed.',
			error: finalError || null,
			removedTasks: results.removedTasks
		};
	} catch (error) {
		// Catch errors from reading file or other initial setup
		log('error', `Error removing tasks: ${error.message}`);
		return {
			success: false,
			message: '',
			error: `Operation failed: ${error.message}`,
			removedTasks: []
		};
	}
}

export default removeTask;

import path from 'path';
import { log, readJSON, writeJSON, setTasksForTag } from '../utils.js';
import { isTaskDependentOn } from '../task-manager.js';
import generateTaskFiles from './generate-task-files.js';

/**
 * Move one or more tasks/subtasks to new positions
 * @param {string} tasksPath - Path to tasks.json file
 * @param {string} sourceId - ID(s) of the task/subtask to move (e.g., '5' or '5.2' or '5,6,7')
 * @param {string} destinationId - ID(s) of the destination (e.g., '7' or '7.3' or '7,8,9')
 * @param {boolean} generateFiles - Whether to regenerate task files after moving
 * @param {Object} options - Additional options
 * @param {string} options.projectRoot - Project root directory for tag resolution
 * @param {string} options.tag - Explicit tag to use (optional)
 * @returns {Object} Result object with moved task details
 */
async function moveTask(
	tasksPath,
	sourceId,
	destinationId,
	generateFiles = false,
	options = {}
) {
	const { projectRoot, tag } = options;
	// Check if we have comma-separated IDs (batch move)
	const sourceIds = sourceId.split(',').map((id) => id.trim());
	const destinationIds = destinationId.split(',').map((id) => id.trim());

	if (sourceIds.length !== destinationIds.length) {
		throw new Error(
			`Number of source IDs (${sourceIds.length}) must match number of destination IDs (${destinationIds.length})`
		);
	}

	// For batch moves, process each pair sequentially
	if (sourceIds.length > 1) {
		const results = [];
		for (let i = 0; i < sourceIds.length; i++) {
			const result = await moveTask(
				tasksPath,
				sourceIds[i],
				destinationIds[i],
				false, // Don't generate files for each individual move
				options
			);
			results.push(result);
		}

		// Generate files once at the end if requested
		if (generateFiles) {
			await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
				tag: tag,
				projectRoot: projectRoot
			});
		}

		return {
			message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
			moves: results
		};
	}

	// Single move logic
	// Read the raw data without tag resolution to preserve tagged structure
	let rawData = readJSON(tasksPath, projectRoot, tag);

	// Handle the case where readJSON returns resolved data with _rawTaggedData
	if (rawData && rawData._rawTaggedData) {
		// Use the raw tagged data and discard the resolved view
		rawData = rawData._rawTaggedData;
	}

	// Ensure the tag exists in the raw data
	if (!rawData || !rawData[tag] || !Array.isArray(rawData[tag].tasks)) {
		throw new Error(
			`Invalid tasks file or tag "${tag}" not found at ${tasksPath}`
		);
	}

	// Get the tasks for the current tag
	const tasks = rawData[tag].tasks;

	log(
		'info',
		`Moving task/subtask ${sourceId} to ${destinationId} (tag: ${tag})`
	);

	// Parse source and destination IDs
	const isSourceSubtask = sourceId.includes('.');
	const isDestSubtask = destinationId.includes('.');

	let result;

	if (isSourceSubtask && isDestSubtask) {
		// Subtask to subtask
		result = moveSubtaskToSubtask(tasks, sourceId, destinationId);
	} else if (isSourceSubtask && !isDestSubtask) {
		// Subtask to task
		result = moveSubtaskToTask(tasks, sourceId, destinationId);
	} else if (!isSourceSubtask && isDestSubtask) {
		// Task to subtask
		result = moveTaskToSubtask(tasks, sourceId, destinationId);
	} else {
		// Task to task
		result = moveTaskToTask(tasks, sourceId, destinationId);
	}

	// Update the data structure with the modified tasks
	rawData[tag].tasks = tasks;

	// Always write the data object, never the _rawTaggedData directly
	// The writeJSON function will filter out _rawTaggedData automatically
	writeJSON(tasksPath, rawData, options.projectRoot, tag);

	if (generateFiles) {
		await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
			tag: tag,
			projectRoot: projectRoot
		});
	}

	return result;
}

// Helper functions for different move scenarios
function moveSubtaskToSubtask(tasks, sourceId, destinationId) {
	// Parse IDs
	const [sourceParentId, sourceSubtaskId] = sourceId
		.split('.')
		.map((id) => parseInt(id, 10));
	const [destParentId, destSubtaskId] = destinationId
		.split('.')
		.map((id) => parseInt(id, 10));

	// Find source and destination parent tasks
	const sourceParentTask = tasks.find((t) => t.id === sourceParentId);
	const destParentTask = tasks.find((t) => t.id === destParentId);

	if (!sourceParentTask) {
		throw new Error(`Source parent task with ID ${sourceParentId} not found`);
	}
	if (!destParentTask) {
		throw new Error(
			`Destination parent task with ID ${destParentId} not found`
		);
	}

	// Initialize subtasks arrays if they don't exist (based on commit fixes)
	if (!sourceParentTask.subtasks) {
		sourceParentTask.subtasks = [];
	}
	if (!destParentTask.subtasks) {
		destParentTask.subtasks = [];
	}

	// Find source subtask
	const sourceSubtaskIndex = sourceParentTask.subtasks.findIndex(
		(st) => st.id === sourceSubtaskId
	);
	if (sourceSubtaskIndex === -1) {
		throw new Error(`Source subtask ${sourceId} not found`);
	}

	const sourceSubtask = sourceParentTask.subtasks[sourceSubtaskIndex];

	if (sourceParentId === destParentId) {
		// Moving within the same parent
		if (destParentTask.subtasks.length > 0) {
			const destSubtaskIndex = destParentTask.subtasks.findIndex(
				(st) => st.id === destSubtaskId
			);
			if (destSubtaskIndex !== -1) {
				// Remove from old position
				sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
				// Insert at new position (adjust index if moving within same array)
				const adjustedIndex =
					sourceSubtaskIndex < destSubtaskIndex
						? destSubtaskIndex - 1
						: destSubtaskIndex;
				destParentTask.subtasks.splice(adjustedIndex + 1, 0, sourceSubtask);
			} else {
				// Destination subtask doesn't exist, insert at end
				sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
				destParentTask.subtasks.push(sourceSubtask);
			}
		} else {
			// No existing subtasks, this will be the first one
			sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);
			destParentTask.subtasks.push(sourceSubtask);
		}
	} else {
		// Moving between different parents
		moveSubtaskToAnotherParent(
			sourceSubtask,
			sourceParentTask,
			sourceSubtaskIndex,
			destParentTask,
			destSubtaskId
		);
	}

	return {
		message: `Moved subtask ${sourceId} to ${destinationId}`,
		movedItem: sourceSubtask
	};
}

function moveSubtaskToTask(tasks, sourceId, destinationId) {
	// Parse source ID
	const [sourceParentId, sourceSubtaskId] = sourceId
		.split('.')
		.map((id) => parseInt(id, 10));
	const destTaskId = parseInt(destinationId, 10);

	// Find source parent and destination task
	const sourceParentTask = tasks.find((t) => t.id === sourceParentId);

	if (!sourceParentTask) {
		throw new Error(`Source parent task with ID ${sourceParentId} not found`);
	}
	if (!sourceParentTask.subtasks) {
		throw new Error(`Source parent task ${sourceParentId} has no subtasks`);
	}

	// Find source subtask
	const sourceSubtaskIndex = sourceParentTask.subtasks.findIndex(
		(st) => st.id === sourceSubtaskId
	);
	if (sourceSubtaskIndex === -1) {
		throw new Error(`Source subtask ${sourceId} not found`);
	}

	const sourceSubtask = sourceParentTask.subtasks[sourceSubtaskIndex];

	// Check if destination task exists
	const existingDestTask = tasks.find((t) => t.id === destTaskId);
	if (existingDestTask) {
		throw new Error(
			`Cannot move to existing task ID ${destTaskId}. Choose a different ID or use subtask destination.`
		);
	}

	// Create new task from subtask
	const newTask = {
		id: destTaskId,
		title: sourceSubtask.title,
		description: sourceSubtask.description,
		status: sourceSubtask.status || 'pending',
		dependencies: sourceSubtask.dependencies || [],
		priority: sourceSubtask.priority || 'medium',
		details: sourceSubtask.details || '',
		testStrategy: sourceSubtask.testStrategy || '',
		subtasks: []
	};

	// Remove subtask from source parent
	sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);

	// Insert new task in correct position
	const insertIndex = tasks.findIndex((t) => t.id > destTaskId);
	if (insertIndex === -1) {
		tasks.push(newTask);
	} else {
		tasks.splice(insertIndex, 0, newTask);
	}

	return {
		message: `Converted subtask ${sourceId} to task ${destinationId}`,
		movedItem: newTask
	};
}

function moveTaskToSubtask(tasks, sourceId, destinationId) {
	// Parse IDs
	const sourceTaskId = parseInt(sourceId, 10);
	const [destParentId, destSubtaskId] = destinationId
		.split('.')
		.map((id) => parseInt(id, 10));

	// Find source task and destination parent
	const sourceTaskIndex = tasks.findIndex((t) => t.id === sourceTaskId);
	const destParentTask = tasks.find((t) => t.id === destParentId);

	if (sourceTaskIndex === -1) {
		throw new Error(`Source task with ID ${sourceTaskId} not found`);
	}
	if (!destParentTask) {
		throw new Error(
			`Destination parent task with ID ${destParentId} not found`
		);
	}

	const sourceTask = tasks[sourceTaskIndex];

	// Initialize subtasks array if it doesn't exist (based on commit fixes)
	if (!destParentTask.subtasks) {
		destParentTask.subtasks = [];
	}

	// Create new subtask from task
	const newSubtask = {
		id: destSubtaskId,
		title: sourceTask.title,
		description: sourceTask.description,
		status: sourceTask.status || 'pending',
		dependencies: sourceTask.dependencies || [],
		details: sourceTask.details || '',
		testStrategy: sourceTask.testStrategy || ''
	};

	// Find insertion position (based on commit fixes)
	let destSubtaskIndex = -1;
	if (destParentTask.subtasks.length > 0) {
		destSubtaskIndex = destParentTask.subtasks.findIndex(
			(st) => st.id === destSubtaskId
		);
		if (destSubtaskIndex === -1) {
			// Subtask doesn't exist, we'll insert at the end
			destSubtaskIndex = destParentTask.subtasks.length - 1;
		}
	}

	// Insert at specific position (based on commit fixes)
	const insertPosition = destSubtaskIndex === -1 ? 0 : destSubtaskIndex + 1;
	destParentTask.subtasks.splice(insertPosition, 0, newSubtask);

	// Remove the original task from the tasks array
	tasks.splice(sourceTaskIndex, 1);

	return {
		message: `Converted task ${sourceId} to subtask ${destinationId}`,
		movedItem: newSubtask
	};
}

function moveTaskToTask(tasks, sourceId, destinationId) {
	const sourceTaskId = parseInt(sourceId, 10);
	const destTaskId = parseInt(destinationId, 10);

	// Find source task
	const sourceTaskIndex = tasks.findIndex((t) => t.id === sourceTaskId);
	if (sourceTaskIndex === -1) {
		throw new Error(`Source task with ID ${sourceTaskId} not found`);
	}

	const sourceTask = tasks[sourceTaskIndex];

	// Check if destination exists
	const destTaskIndex = tasks.findIndex((t) => t.id === destTaskId);

	if (destTaskIndex !== -1) {
		// Destination exists - this could be overwriting or swapping
		const destTask = tasks[destTaskIndex];

		// For now, throw an error to avoid accidental overwrites
		throw new Error(
			`Task with ID ${destTaskId} already exists. Use a different destination ID.`
		);
	} else {
		// Destination doesn't exist - create new task ID
		return moveTaskToNewId(tasks, sourceTaskIndex, sourceTask, destTaskId);
	}
}

function moveSubtaskToAnotherParent(
	sourceSubtask,
	sourceParentTask,
	sourceSubtaskIndex,
	destParentTask,
	destSubtaskId
) {
	const destSubtaskId_num = parseInt(destSubtaskId, 10);

	// Create new subtask with destination ID
	const newSubtask = {
		...sourceSubtask,
		id: destSubtaskId_num
	};

	// Initialize subtasks array if it doesn't exist (based on commit fixes)
	if (!destParentTask.subtasks) {
		destParentTask.subtasks = [];
	}

	// Find insertion position
	let destSubtaskIndex = -1;
	if (destParentTask.subtasks.length > 0) {
		destSubtaskIndex = destParentTask.subtasks.findIndex(
			(st) => st.id === destSubtaskId_num
		);
		if (destSubtaskIndex === -1) {
			// Subtask doesn't exist, we'll insert at the end
			destSubtaskIndex = destParentTask.subtasks.length - 1;
		}
	}

	// Insert at the destination position (based on commit fixes)
	const insertPosition = destSubtaskIndex === -1 ? 0 : destSubtaskIndex + 1;
	destParentTask.subtasks.splice(insertPosition, 0, newSubtask);

	// Remove the subtask from the original parent
	sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1);

	return newSubtask;
}

function moveTaskToNewId(tasks, sourceTaskIndex, sourceTask, destTaskId) {
	const destTaskIndex = tasks.findIndex((t) => t.id === destTaskId);

	// Create moved task with new ID
	const movedTask = {
		...sourceTask,
		id: destTaskId
	};

	// Update any dependencies that reference the old task ID
	tasks.forEach((task) => {
		if (task.dependencies && task.dependencies.includes(sourceTask.id)) {
			const depIndex = task.dependencies.indexOf(sourceTask.id);
			task.dependencies[depIndex] = destTaskId;
		}
		if (task.subtasks) {
			task.subtasks.forEach((subtask) => {
				if (
					subtask.dependencies &&
					subtask.dependencies.includes(sourceTask.id)
				) {
					const depIndex = subtask.dependencies.indexOf(sourceTask.id);
					subtask.dependencies[depIndex] = destTaskId;
				}
			});
		}
	});

	// Update dependencies within movedTask's subtasks that reference sibling subtasks
	if (Array.isArray(movedTask.subtasks)) {
		movedTask.subtasks.forEach((subtask) => {
			if (Array.isArray(subtask.dependencies)) {
				subtask.dependencies = subtask.dependencies.map((dep) => {
					// If dependency is a string like "oldParent.subId", update to "newParent.subId"
					if (typeof dep === 'string' && dep.includes('.')) {
						const [depParent, depSub] = dep.split('.');
						if (parseInt(depParent, 10) === sourceTask.id) {
							return `${destTaskId}.${depSub}`;
						}
					}
					// If dependency is a number, and matches a subtask ID in the moved task, leave as is (context is implied)
					return dep;
				});
			}
		});
	}

	// Strategy based on commit fixes: remove source first, then replace destination
	// This avoids index shifting problems

	// Remove the source task first
	tasks.splice(sourceTaskIndex, 1);

	// Adjust the destination index if the source was before the destination
	// Since we removed the source, indices after it shift down by 1
	const adjustedDestIndex =
		sourceTaskIndex < destTaskIndex ? destTaskIndex - 1 : destTaskIndex;

	// Replace the placeholder destination task with the moved task (based on commit fixes)
	if (adjustedDestIndex >= 0 && adjustedDestIndex < tasks.length) {
		tasks[adjustedDestIndex] = movedTask;
	} else {
		// Insert at the end if index is out of bounds
		tasks.push(movedTask);
	}

	log('info', `Moved task ${sourceTask.id} to new ID ${destTaskId}`);

	return {
		message: `Moved task ${sourceTask.id} to new ID ${destTaskId}`,
		movedItem: movedTask
	};
}

export default moveTask;

import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import { z } from 'zod';

import {
	log,
	writeJSON,
	enableSilentMode,
	disableSilentMode,
	isSilentMode,
	readJSON,
	findTaskById,
	ensureTagMetadata,
	getCurrentTag
} from '../utils.js';

import { generateObjectService } from '../ai-services-unified.js';
import { getDebugFlag } from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import { displayAiUsageSummary } from '../ui.js';

// Define the Zod schema for a SINGLE task object
const prdSingleTaskSchema = z.object({
	id: z.number().int().positive(),
	title: z.string().min(1),
	description: z.string().min(1),
	details: z.string().nullable(),
	testStrategy: z.string().nullable(),
	priority: z.enum(['high', 'medium', 'low']).nullable(),
	dependencies: z.array(z.number().int().positive()).nullable(),
	status: z.string().nullable()
});

// Define the Zod schema for the ENTIRE expected AI response object
const prdResponseSchema = z.object({
	tasks: z.array(prdSingleTaskSchema),
	metadata: z.object({
		projectName: z.string(),
		totalTasks: z.number(),
		sourceFile: z.string(),
		generatedAt: z.string()
	})
});

/**
 * Parse a PRD file and generate tasks
 * @param {string} prdPath - Path to the PRD file
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {number} numTasks - Number of tasks to generate
 * @param {Object} options - Additional options
 * @param {boolean} [options.force=false] - Whether to overwrite existing tasks.json.
 * @param {boolean} [options.append=false] - Append to existing tasks file.
 * @param {boolean} [options.research=false] - Use research model for enhanced PRD analysis.
 * @param {Object} [options.reportProgress] - Function to report progress (optional, likely unused).
 * @param {Object} [options.mcpLog] - MCP logger object (optional).
 * @param {Object} [options.session] - Session object from MCP server (optional).
 * @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
 * @param {string} [options.tag] - Target tag for task generation.
 * @param {string} [outputFormat='text'] - Output format ('text' or 'json').
 */
async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
	const {
		reportProgress,
		mcpLog,
		session,
		projectRoot,
		force = false,
		append = false,
		research = false,
		tag
	} = options;
	const isMCP = !!mcpLog;
	const outputFormat = isMCP ? 'json' : 'text';

	// Use the provided tag, or the current active tag, or default to 'master'
	const targetTag = tag;

	const logFn = mcpLog
		? mcpLog
		: {
				// Wrapper for CLI
				info: (...args) => log('info', ...args),
				warn: (...args) => log('warn', ...args),
				error: (...args) => log('error', ...args),
				debug: (...args) => log('debug', ...args),
				success: (...args) => log('success', ...args)
			};

	// Create custom reporter using logFn
	const report = (message, level = 'info') => {
		// Check logFn directly
		if (logFn && typeof logFn[level] === 'function') {
			logFn[level](message);
		} else if (!isSilentMode() && outputFormat === 'text') {
			// Fallback to original log only if necessary and in CLI text mode
			log(level, message);
		}
	};

	report(
		`Parsing PRD file: ${prdPath}, Force: ${force}, Append: ${append}, Research: ${research}`
	);

	let existingTasks = [];
	let nextId = 1;
	let aiServiceResponse = null;

	try {
		// Check if there are existing tasks in the target tag
		let hasExistingTasksInTag = false;
		if (fs.existsSync(tasksPath)) {
			try {
				// Read the entire file to check if the tag exists
				const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
				const allData = JSON.parse(existingFileContent);

				// Check if the target tag exists and has tasks
				if (
					allData[targetTag] &&
					Array.isArray(allData[targetTag].tasks) &&
					allData[targetTag].tasks.length > 0
				) {
					hasExistingTasksInTag = true;
					existingTasks = allData[targetTag].tasks;
					nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
				}
			} catch (error) {
				// If we can't read the file or parse it, assume no existing tasks in this tag
				hasExistingTasksInTag = false;
			}
		}

		// Handle file existence and overwrite/append logic based on target tag
		if (hasExistingTasksInTag) {
			if (append) {
				report(
					`Append mode enabled. Found ${existingTasks.length} existing tasks in tag '${targetTag}'. Next ID will be ${nextId}.`,
					'info'
				);
			} else if (!force) {
				// Not appending and not forcing overwrite, and there are existing tasks in the target tag
				const overwriteError = new Error(
					`Tag '${targetTag}' already contains ${existingTasks.length} tasks. Use --force to overwrite or --append to add to existing tasks.`
				);
				report(overwriteError.message, 'error');
				if (outputFormat === 'text') {
					console.error(chalk.red(overwriteError.message));
				}
				throw overwriteError;
			} else {
				// Force overwrite is true
				report(
					`Force flag enabled. Overwriting existing tasks in tag '${targetTag}'.`,
					'info'
				);
			}
		} else {
			// No existing tasks in target tag, proceed without confirmation
			report(
				`Tag '${targetTag}' is empty or doesn't exist. Creating/updating tag with new tasks.`,
				'info'
			);
		}

		report(`Reading PRD content from ${prdPath}`, 'info');
		const prdContent = fs.readFileSync(prdPath, 'utf8');
		if (!prdContent) {
			throw new Error(`Input file ${prdPath} is empty or could not be read.`);
		}

		// Load prompts using PromptManager
		const promptManager = getPromptManager();

		// Get defaultTaskPriority from config
		const { getDefaultPriority } = await import('../config-manager.js');
		const defaultTaskPriority = getDefaultPriority(projectRoot) || 'medium';

		const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
			'parse-prd',
			{
				research,
				numTasks,
				nextId,
				prdContent,
				prdPath,
				defaultTaskPriority
			}
		);

		// Call the unified AI service
		report(
			`Calling AI service to generate tasks from PRD${research ? ' with research-backed analysis' : ''}...`,
			'info'
		);

		// Call generateObjectService with the CORRECT schema and additional telemetry params
		aiServiceResponse = await generateObjectService({
			role: research ? 'research' : 'main', // Use research role if flag is set
			session: session,
			projectRoot: projectRoot,
			schema: prdResponseSchema,
			objectName: 'tasks_data',
			systemPrompt: systemPrompt,
			prompt: userPrompt,
			commandName: 'parse-prd',
			outputType: isMCP ? 'mcp' : 'cli'
		});

		// Create the directory if it doesn't exist
		const tasksDir = path.dirname(tasksPath);
		if (!fs.existsSync(tasksDir)) {
			fs.mkdirSync(tasksDir, { recursive: true });
		}
		logFn.success(
			`Successfully parsed PRD via AI service${research ? ' with research-backed analysis' : ''}.`
		);

		// Validate and Process Tasks
		// const generatedData = aiServiceResponse?.mainResult?.object;

		// Robustly get the actual AI-generated object
		let generatedData = null;
		if (aiServiceResponse?.mainResult) {
			if (
				typeof aiServiceResponse.mainResult === 'object' &&
				aiServiceResponse.mainResult !== null &&
				'tasks' in aiServiceResponse.mainResult
			) {
				// If mainResult itself is the object with a 'tasks' property
				generatedData = aiServiceResponse.mainResult;
			} else if (
				typeof aiServiceResponse.mainResult.object === 'object' &&
				aiServiceResponse.mainResult.object !== null &&
				'tasks' in aiServiceResponse.mainResult.object
			) {
				// If mainResult.object is the object with a 'tasks' property
				generatedData = aiServiceResponse.mainResult.object;
			}
		}

		if (!generatedData || !Array.isArray(generatedData.tasks)) {
			logFn.error(
				`Internal Error: generateObjectService returned unexpected data structure: ${JSON.stringify(generatedData)}`
			);
			throw new Error(
				'AI service returned unexpected data structure after validation.'
			);
		}

		let currentId = nextId;
		const taskMap = new Map();
		const processedNewTasks = generatedData.tasks.map((task) => {
			const newId = currentId++;
			taskMap.set(task.id, newId);
			return {
				...task,
				id: newId,
				status: 'pending',
				priority: task.priority || 'medium',
				dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
				subtasks: []
			};
		});

		// Remap dependencies for the NEWLY processed tasks
		processedNewTasks.forEach((task) => {
			task.dependencies = task.dependencies
				.map((depId) => taskMap.get(depId)) // Map old AI ID to new sequential ID
				.filter(
					(newDepId) =>
						newDepId != null && // Must exist
						newDepId < task.id && // Must be a lower ID (could be existing or newly generated)
						(findTaskById(existingTasks, newDepId) || // Check if it exists in old tasks OR
							processedNewTasks.some((t) => t.id === newDepId)) // check if it exists in new tasks
				);
		});

		const finalTasks = append
			? [...existingTasks, ...processedNewTasks]
			: processedNewTasks;

		// Read the existing file to preserve other tags
		let outputData = {};
		if (fs.existsSync(tasksPath)) {
			try {
				const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
				outputData = JSON.parse(existingFileContent);
			} catch (error) {
				// If we can't read the existing file, start with empty object
				outputData = {};
			}
		}

		// Update only the target tag, preserving other tags
		outputData[targetTag] = {
			tasks: finalTasks,
			metadata: {
				created:
					outputData[targetTag]?.metadata?.created || new Date().toISOString(),
				updated: new Date().toISOString(),
				description: `Tasks for ${targetTag} context`
			}
		};

		// Ensure the target tag has proper metadata
		ensureTagMetadata(outputData[targetTag], {
			description: `Tasks for ${targetTag} context`
		});

		// Write the complete data structure back to the file
		fs.writeFileSync(tasksPath, JSON.stringify(outputData, null, 2));
		report(
			`Successfully ${append ? 'appended' : 'generated'} ${processedNewTasks.length} tasks in ${tasksPath}${research ? ' with research-backed analysis' : ''}`,
			'success'
		);

		// Generate markdown task files after writing tasks.json
		// await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });

		// Handle CLI output (e.g., success message)
		if (outputFormat === 'text') {
			console.log(
				boxen(
					chalk.green(
						`Successfully generated ${processedNewTasks.length} new tasks${research ? ' with research-backed analysis' : ''}. Total tasks in ${tasksPath}: ${finalTasks.length}`
					),
					{ padding: 1, borderColor: 'green', borderStyle: 'round' }
				)
			);

			console.log(
				boxen(
					chalk.white.bold('Next Steps:') +
						'\n\n' +
						`${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\n` +
						`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
					{
						padding: 1,
						borderColor: 'cyan',
						borderStyle: 'round',
						margin: { top: 1 }
					}
				)
			);

			if (aiServiceResponse && aiServiceResponse.telemetryData) {
				displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
			}
		}

		// Return telemetry data
		return {
			success: true,
			tasksPath,
			telemetryData: aiServiceResponse?.telemetryData,
			tagInfo: aiServiceResponse?.tagInfo
		};
	} catch (error) {
		report(`Error parsing PRD: ${error.message}`, 'error');

		// Only show error UI for text output (CLI)
		if (outputFormat === 'text') {
			console.error(chalk.red(`Error: ${error.message}`));

			if (getDebugFlag(projectRoot)) {
				// Use projectRoot for debug flag check
				console.error(error);
			}
		}

		throw error; // Always re-throw for proper error handling
	}
}

export default parsePRD;

import { log } from '../utils.js';
import { addComplexityToTask } from '../utils.js';

/**
 * Return the next work item:
 *   •  Prefer an eligible SUBTASK that belongs to any parent task
 *      whose own status is `in-progress`.
 *   •  If no such subtask exists, fall back to the best top-level task
 *      (previous behaviour).
 *
 * The function still exports the same name (`findNextTask`) so callers
 * don't need to change.  It now always returns an object with
 *  ─ id            →  number  (task)  or  "parentId.subId"  (subtask)
 *  ─ title         →  string
 *  ─ status        →  string
 *  ─ priority      →  string  ("high" | "medium" | "low")
 *  ─ dependencies  →  array   (all IDs expressed in the same dotted form)
 *  ─ parentId      →  number  (present only when it's a subtask)
 *
 * @param {Object[]} tasks  – full array of top-level tasks, each may contain .subtasks[]
 * @param {Object} [complexityReport=null] - Optional complexity report object
 * @returns {Object|null}   – next work item or null if nothing is eligible
 */
function findNextTask(tasks, complexityReport = null) {
	// ---------- helpers ----------------------------------------------------
	const priorityValues = { high: 3, medium: 2, low: 1 };

	const toFullSubId = (parentId, maybeDotId) => {
		//  "12.3"  ->  "12.3"
		//        4 ->  "12.4"   (numeric / short form)
		if (typeof maybeDotId === 'string' && maybeDotId.includes('.')) {
			return maybeDotId;
		}
		return `${parentId}.${maybeDotId}`;
	};

	// ---------- build completed-ID set (tasks *and* subtasks) --------------
	const completedIds = new Set();
	tasks.forEach((t) => {
		if (t.status === 'done' || t.status === 'completed') {
			completedIds.add(String(t.id));
		}
		if (Array.isArray(t.subtasks)) {
			t.subtasks.forEach((st) => {
				if (st.status === 'done' || st.status === 'completed') {
					completedIds.add(`${t.id}.${st.id}`);
				}
			});
		}
	});

	// ---------- 1) look for eligible subtasks ------------------------------
	const candidateSubtasks = [];

	tasks
		.filter((t) => t.status === 'in-progress' && Array.isArray(t.subtasks))
		.forEach((parent) => {
			parent.subtasks.forEach((st) => {
				const stStatus = (st.status || 'pending').toLowerCase();
				if (stStatus !== 'pending' && stStatus !== 'in-progress') return;

				const fullDeps =
					st.dependencies?.map((d) => toFullSubId(parent.id, d)) ?? [];

				const depsSatisfied =
					fullDeps.length === 0 ||
					fullDeps.every((depId) => completedIds.has(String(depId)));

				if (depsSatisfied) {
					candidateSubtasks.push({
						id: `${parent.id}.${st.id}`,
						title: st.title || `Subtask ${st.id}`,
						status: st.status || 'pending',
						priority: st.priority || parent.priority || 'medium',
						dependencies: fullDeps,
						parentId: parent.id
					});
				}
			});
		});

	if (candidateSubtasks.length > 0) {
		// sort by priority → dep-count → parent-id → sub-id
		candidateSubtasks.sort((a, b) => {
			const pa = priorityValues[a.priority] ?? 2;
			const pb = priorityValues[b.priority] ?? 2;
			if (pb !== pa) return pb - pa;

			if (a.dependencies.length !== b.dependencies.length)
				return a.dependencies.length - b.dependencies.length;

			// compare parent then sub-id numerically
			const [aPar, aSub] = a.id.split('.').map(Number);
			const [bPar, bSub] = b.id.split('.').map(Number);
			if (aPar !== bPar) return aPar - bPar;
			return aSub - bSub;
		});
		const nextTask = candidateSubtasks[0];

		// Add complexity to the task before returning
		if (nextTask && complexityReport) {
			addComplexityToTask(nextTask, complexityReport);
		}

		return nextTask;
	}

	// ---------- 2) fall back to top-level tasks (original logic) ------------
	const eligibleTasks = tasks.filter((task) => {
		const status = (task.status || 'pending').toLowerCase();
		if (status !== 'pending' && status !== 'in-progress') return false;
		const deps = task.dependencies ?? [];
		return deps.every((depId) => completedIds.has(String(depId)));
	});

	if (eligibleTasks.length === 0) return null;

	const nextTask = eligibleTasks.sort((a, b) => {
		const pa = priorityValues[a.priority || 'medium'] ?? 2;
		const pb = priorityValues[b.priority || 'medium'] ?? 2;
		if (pb !== pa) return pb - pa;

		const da = (a.dependencies ?? []).length;
		const db = (b.dependencies ?? []).length;
		if (da !== db) return da - db;

		return a.id - b.id;
	})[0];

	// Add complexity to the task before returning
	if (nextTask && complexityReport) {
		addComplexityToTask(nextTask, complexityReport);
	}

	return nextTask;
}

export default findNextTask;

import path from 'path';
import fs from 'fs';
import chalk from 'chalk';

import { log, readJSON } from '../utils.js';
import { formatDependenciesWithStatus } from '../ui.js';
import { validateAndFixDependencies } from '../dependency-manager.js';
import { getDebugFlag } from '../config-manager.js';

/**
 * Generate individual task files from tasks.json
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} outputDir - Output directory for task files
 * @param {Object} options - Additional options (mcpLog for MCP mode, projectRoot, tag)
 * @param {string} [options.projectRoot] - Project root path
 * @param {string} [options.tag] - Tag for the task
 * @param {Object} [options.mcpLog] - MCP logger object
 * @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
 */
function generateTaskFiles(tasksPath, outputDir, options = {}) {
	try {
		const isMcpMode = !!options?.mcpLog;
		const { projectRoot, tag } = options;

		// 1. Read the raw data structure, ensuring we have all tags.
		// We call readJSON without a specific tag to get the resolved default view,
		// which correctly contains the full structure in `_rawTaggedData`.
		const resolvedData = readJSON(tasksPath, projectRoot, tag);
		if (!resolvedData) {
			throw new Error(`Could not read or parse tasks file: ${tasksPath}`);
		}
		// Prioritize the _rawTaggedData if it exists, otherwise use the data as is.
		const rawData = resolvedData._rawTaggedData || resolvedData;

		// 2. Determine the target tag we need to generate files for.
		const tagData = rawData[tag];

		if (!tagData || !tagData.tasks) {
			throw new Error(`Tag '${tag}' not found or has no tasks in the data.`);
		}
		const tasksForGeneration = tagData.tasks;

		// Create the output directory if it doesn't exist
		if (!fs.existsSync(outputDir)) {
			fs.mkdirSync(outputDir, { recursive: true });
		}

		log(
			'info',
			`Preparing to regenerate ${tasksForGeneration.length} task files for tag '${tag}'`
		);

		// 3. Validate dependencies using the FULL, raw data structure to prevent data loss.
		validateAndFixDependencies(
			rawData, // Pass the entire object with all tags
			tasksPath,
			projectRoot,
			tag // Provide the current tag context for the operation
		);

		const allTasksInTag = tagData.tasks;
		const validTaskIds = allTasksInTag.map((task) => task.id);

		// Cleanup orphaned task files
		log('info', 'Checking for orphaned task files to clean up...');
		try {
			const files = fs.readdirSync(outputDir);
			// Tag-aware file patterns: master -> task_001.txt, other tags -> task_001_tagname.txt
			const masterFilePattern = /^task_(\d+)\.txt$/;
			const taggedFilePattern = new RegExp(`^task_(\\d+)_${tag}\\.txt$`);

			const orphanedFiles = files.filter((file) => {
				let match = null;
				let fileTaskId = null;

				// Check if file belongs to current tag
				if (tag === 'master') {
					match = file.match(masterFilePattern);
					if (match) {
						fileTaskId = parseInt(match[1], 10);
						// Only clean up master files when processing master tag
						return !validTaskIds.includes(fileTaskId);
					}
				} else {
					match = file.match(taggedFilePattern);
					if (match) {
						fileTaskId = parseInt(match[1], 10);
						// Only clean up files for the current tag
						return !validTaskIds.includes(fileTaskId);
					}
				}
				return false;
			});

			if (orphanedFiles.length > 0) {
				log(
					'info',
					`Found ${orphanedFiles.length} orphaned task files to remove for tag '${tag}'`
				);
				orphanedFiles.forEach((file) => {
					const filePath = path.join(outputDir, file);
					fs.unlinkSync(filePath);
				});
			} else {
				log('info', 'No orphaned task files found.');
			}
		} catch (err) {
			log('warn', `Error cleaning up orphaned task files: ${err.message}`);
		}

		// Generate task files for the target tag
		log('info', `Generating individual task files for tag '${tag}'...`);
		tasksForGeneration.forEach((task) => {
			// Tag-aware file naming: master -> task_001.txt, other tags -> task_001_tagname.txt
			const taskFileName =
				tag === 'master'
					? `task_${task.id.toString().padStart(3, '0')}.txt`
					: `task_${task.id.toString().padStart(3, '0')}_${tag}.txt`;

			const taskPath = path.join(outputDir, taskFileName);

			let content = `# Task ID: ${task.id}\n`;
			content += `# Title: ${task.title}\n`;
			content += `# Status: ${task.status || 'pending'}\n`;

			if (task.dependencies && task.dependencies.length > 0) {
				content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, allTasksInTag, false)}\n`;
			} else {
				content += '# Dependencies: None\n';
			}

			content += `# Priority: ${task.priority || 'medium'}\n`;
			content += `# Description: ${task.description || ''}\n`;
			content += '# Details:\n';
			content += (task.details || '')
				.split('\n')
				.map((line) => line)
				.join('\n');
			content += '\n\n';
			content += '# Test Strategy:\n';
			content += (task.testStrategy || '')
				.split('\n')
				.map((line) => line)
				.join('\n');
			content += '\n';

			if (task.subtasks && task.subtasks.length > 0) {
				content += '\n# Subtasks:\n';
				task.subtasks.forEach((subtask) => {
					content += `## ${subtask.id}. ${subtask.title} [${subtask.status || 'pending'}]\n`;
					if (subtask.dependencies && subtask.dependencies.length > 0) {
						const subtaskDeps = subtask.dependencies
							.map((depId) =>
								typeof depId === 'number'
									? `${task.id}.${depId}`
									: depId.toString()
							)
							.join(', ');
						content += `### Dependencies: ${subtaskDeps}\n`;
					} else {
						content += '### Dependencies: None\n';
					}
					content += `### Description: ${subtask.description || ''}\n`;
					content += '### Details:\n';
					content += (subtask.details || '')
						.split('\n')
						.map((line) => line)
						.join('\n');
					content += '\n\n';
				});
			}

			fs.writeFileSync(taskPath, content);
		});

		log(
			'success',
			`All ${tasksForGeneration.length} tasks for tag '${tag}' have been generated into '${outputDir}'.`
		);

		if (isMcpMode) {
			return {
				success: true,
				count: tasksForGeneration.length,
				directory: outputDir
			};
		}
	} catch (error) {
		log('error', `Error generating task files: ${error.message}`);
		if (!options?.mcpLog) {
			console.error(chalk.red(`Error generating task files: ${error.message}`));
			if (getDebugFlag()) {
				console.error(error);
			}
			process.exit(1);
		} else {
			throw error;
		}
	}
}

export default generateTaskFiles;

// Base profile factory for rule-transformer
import path from 'path';

/**
 * Creates a standardized profile configuration for different editors
 * @param {Object} editorConfig - Editor-specific configuration
 * @param {string} editorConfig.name - Profile name (e.g., 'cursor', 'vscode')
 * @param {string} [editorConfig.displayName] - Display name for the editor (defaults to name)
 * @param {string} editorConfig.url - Editor website URL
 * @param {string} editorConfig.docsUrl - Editor documentation URL
 * @param {string} editorConfig.profileDir - Directory for profile configuration
 * @param {string} [editorConfig.rulesDir] - Directory for rules files (defaults to profileDir/rules)
 * @param {boolean} [editorConfig.mcpConfig=true] - Whether to create MCP configuration
 * @param {string} [editorConfig.mcpConfigName='mcp.json'] - Name of MCP config file
 * @param {string} [editorConfig.fileExtension='.mdc'] - Source file extension
 * @param {string} [editorConfig.targetExtension='.md'] - Target file extension
 * @param {Object} [editorConfig.toolMappings={}] - Tool name mappings
 * @param {Array} [editorConfig.customReplacements=[]] - Custom text replacements
 * @param {Object} [editorConfig.fileMap={}] - Custom file name mappings
 * @param {boolean} [editorConfig.supportsRulesSubdirectories=false] - Whether to use taskmaster/ subdirectory for taskmaster-specific rules (only Cursor uses this by default)
 * @param {boolean} [editorConfig.includeDefaultRules=true] - Whether to include default rule files
 * @param {Function} [editorConfig.onAdd] - Lifecycle hook for profile addition
 * @param {Function} [editorConfig.onRemove] - Lifecycle hook for profile removal
 * @param {Function} [editorConfig.onPostConvert] - Lifecycle hook for post-conversion
 * @returns {Object} - Complete profile configuration
 */
export function createProfile(editorConfig) {
	const {
		name,
		displayName = name,
		url,
		docsUrl,
		profileDir = `.${name.toLowerCase()}`,
		rulesDir = `${profileDir}/rules`,
		mcpConfig = true,
		mcpConfigName = mcpConfig ? 'mcp.json' : null,
		fileExtension = '.mdc',
		targetExtension = '.md',
		toolMappings = {},
		customReplacements = [],
		fileMap = {},
		supportsRulesSubdirectories = false,
		includeDefaultRules = true,
		onAdd,
		onRemove,
		onPostConvert
	} = editorConfig;

	const mcpConfigPath = mcpConfigName
		? path.join(profileDir, mcpConfigName)
		: null;

	// Standard file mapping with custom overrides
	// Use taskmaster subdirectory only if profile supports it
	const taskmasterPrefix = supportsRulesSubdirectories ? 'taskmaster/' : '';
	const defaultFileMap = {
		'rules/cursor_rules.mdc': `${name.toLowerCase()}_rules${targetExtension}`,
		'rules/dev_workflow.mdc': `${taskmasterPrefix}dev_workflow${targetExtension}`,
		'rules/self_improve.mdc': `self_improve${targetExtension}`,
		'rules/taskmaster.mdc': `${taskmasterPrefix}taskmaster${targetExtension}`
	};

	// Build final fileMap - merge defaults with custom entries when includeDefaultRules is true
	const finalFileMap = includeDefaultRules
		? { ...defaultFileMap, ...fileMap }
		: fileMap;

	// Base global replacements that work for all editors
	const baseGlobalReplacements = [
		// Handle URLs in any context
		{ from: /cursor\.so/gi, to: url },
		{ from: /cursor\s*\.\s*so/gi, to: url },
		{ from: /https?:\/\/cursor\.so/gi, to: `https://${url}` },
		{ from: /https?:\/\/www\.cursor\.so/gi, to: `https://www.${url}` },

		// Handle tool references
		{ from: /\bedit_file\b/gi, to: toolMappings.edit_file || 'edit_file' },
		{
			from: /\bsearch tool\b/gi,
			to: `${toolMappings.search || 'search'} tool`
		},
		{ from: /\bSearch Tool\b/g, to: `${toolMappings.search || 'Search'} Tool` },

		// Handle basic terms with proper case handling
		{
			from: /\bcursor\b/gi,
			to: (match) =>
				match.charAt(0) === 'C' ? displayName : name.toLowerCase()
		},
		{ from: /Cursor/g, to: displayName },
		{ from: /CURSOR/g, to: displayName.toUpperCase() },

		// Handle file extensions if different
		...(targetExtension !== fileExtension
			? [
					{
						from: new RegExp(`\\${fileExtension}(?!\\])\\b`, 'g'),
						to: targetExtension
					}
				]
			: []),

		// Handle documentation URLs
		{ from: /docs\.cursor\.com/gi, to: docsUrl },

		// Custom editor-specific replacements
		...customReplacements
	];

	// Standard tool mappings
	const defaultToolMappings = {
		search: 'search',
		read_file: 'read_file',
		edit_file: 'edit_file',
		create_file: 'create_file',
		run_command: 'run_command',
		terminal_command: 'terminal_command',
		use_mcp: 'use_mcp',
		switch_mode: 'switch_mode',
		...toolMappings
	};

	// Create conversion config
	const conversionConfig = {
		// Profile name replacements
		profileTerms: [
			{ from: /cursor\.so/g, to: url },
			{ from: /\[cursor\.so\]/g, to: `[${url}]` },
			{ from: /href="https:\/\/cursor\.so/g, to: `href="https://${url}` },
			{ from: /\(https:\/\/cursor\.so/g, to: `(https://${url}` },
			{
				from: /\bcursor\b/gi,
				to: (match) => (match === 'Cursor' ? displayName : name.toLowerCase())
			},
			{ from: /Cursor/g, to: displayName }
		],

		// File extension replacements
		fileExtensions:
			targetExtension !== fileExtension
				? [
						{
							from: new RegExp(`\\${fileExtension}\\b`, 'g'),
							to: targetExtension
						}
					]
				: [],

		// Documentation URL replacements
		docUrls: [
			{
				from: new RegExp(`https:\\/\\/docs\\.cursor\\.com\\/[^\\s)'\"]+`, 'g'),
				to: (match) => match.replace('docs.cursor.com', docsUrl)
			},
			{
				from: new RegExp(`https:\\/\\/${docsUrl}\\/`, 'g'),
				to: `https://${docsUrl}/`
			}
		],

		// Tool references - direct replacements
		toolNames: defaultToolMappings,

		// Tool references in context - more specific replacements
		toolContexts: Object.entries(defaultToolMappings).flatMap(
			([original, mapped]) => [
				{
					from: new RegExp(`\\b${original} tool\\b`, 'g'),
					to: `${mapped} tool`
				},
				{ from: new RegExp(`\\bthe ${original}\\b`, 'g'), to: `the ${mapped}` },
				{ from: new RegExp(`\\bThe ${original}\\b`, 'g'), to: `The ${mapped}` },
				{
					from: new RegExp(`\\bCursor ${original}\\b`, 'g'),
					to: `${displayName} ${mapped}`
				}
			]
		),

		// Tool group and category names
		toolGroups: [
			{ from: /\bSearch tools\b/g, to: 'Read Group tools' },
			{ from: /\bEdit tools\b/g, to: 'Edit Group tools' },
			{ from: /\bRun tools\b/g, to: 'Command Group tools' },
			{ from: /\bMCP servers\b/g, to: 'MCP Group tools' },
			{ from: /\bSearch Group\b/g, to: 'Read Group' },
			{ from: /\bEdit Group\b/g, to: 'Edit Group' },
			{ from: /\bRun Group\b/g, to: 'Command Group' }
		],

		// File references in markdown links
		fileReferences: {
			pathPattern: /\[(.+?)\]\(mdc:\.cursor\/rules\/(.+?)\.mdc\)/g,
			replacement: (match, text, filePath) => {
				const baseName = path.basename(filePath, '.mdc');
				const newFileName =
					finalFileMap[`rules/${baseName}.mdc`] ||
					`${baseName}${targetExtension}`;
				// Update the link text to match the new filename (strip directory path for display)
				const newLinkText = path.basename(newFileName);
				// For Cursor, keep the mdc: protocol; for others, use standard relative paths
				if (name.toLowerCase() === 'cursor') {
					return `[${newLinkText}](mdc:${rulesDir}/${newFileName})`;
				} else {
					return `[${newLinkText}](${rulesDir}/${newFileName})`;
				}
			}
		}
	};

	function getTargetRuleFilename(sourceFilename) {
		if (finalFileMap[sourceFilename]) {
			return finalFileMap[sourceFilename];
		}
		return targetExtension !== fileExtension
			? sourceFilename.replace(
					new RegExp(`\\${fileExtension}$`),
					targetExtension
				)
			: sourceFilename;
	}

	return {
		profileName: name, // Use name for programmatic access (tests expect this)
		displayName: displayName, // Keep displayName for UI purposes
		profileDir,
		rulesDir,
		mcpConfig,
		mcpConfigName,
		mcpConfigPath,
		supportsRulesSubdirectories,
		includeDefaultRules,
		fileMap: finalFileMap,
		globalReplacements: baseGlobalReplacements,
		conversionConfig,
		getTargetRuleFilename,
		targetExtension,
		// Optional lifecycle hooks
		...(onAdd && { onAddRulesProfile: onAdd }),
		...(onRemove && { onRemoveRulesProfile: onRemove }),
		...(onPostConvert && { onPostConvertRulesProfile: onPostConvert })
	};
}

// Common tool mappings for editors that share similar tool sets
export const COMMON_TOOL_MAPPINGS = {
	// Most editors (Cursor, Cline, Windsurf) keep original tool names
	STANDARD: {},

	// Roo Code uses different tool names
	ROO_STYLE: {
		edit_file: 'apply_diff',
		search: 'search_files',
		create_file: 'write_to_file',
		run_command: 'execute_command',
		terminal_command: 'execute_command',
		use_mcp: 'use_mcp_tool'
	}
};

/**
 * Rule Transformer Module
 * Handles conversion of Cursor rules to profile rules
 *
 * This module procedurally generates .{profile}/rules files from assets/rules files,
 * eliminating the need to maintain both sets of files manually.
 */
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { log } from '../../scripts/modules/utils.js';

// Import the shared MCP configuration helper
import {
	setupMCPConfiguration,
	removeTaskMasterMCPConfiguration
} from './create-mcp-config.js';

// Import profile constants (single source of truth)
import { RULE_PROFILES } from '../constants/profiles.js';

// --- Profile Imports ---
import * as profilesModule from '../profiles/index.js';

export function isValidProfile(profile) {
	return RULE_PROFILES.includes(profile);
}

/**
 * Get rule profile by name
 * @param {string} name - Profile name
 * @returns {Object|null} Profile object or null if not found
 */
export function getRulesProfile(name) {
	if (!isValidProfile(name)) {
		return null;
	}

	// Get the profile from the imported profiles module
	const profileKey = `${name}Profile`;
	const profile = profilesModule[profileKey];

	if (!profile) {
		throw new Error(
			`Profile not found: static import missing for '${name}'. Valid profiles: ${RULE_PROFILES.join(', ')}`
		);
	}

	return profile;
}

/**
 * Replace basic Cursor terms with profile equivalents
 */
function replaceBasicTerms(content, conversionConfig) {
	let result = content;

	// Apply profile term replacements
	conversionConfig.profileTerms.forEach((pattern) => {
		if (typeof pattern.to === 'function') {
			result = result.replace(pattern.from, pattern.to);
		} else {
			result = result.replace(pattern.from, pattern.to);
		}
	});

	// Apply file extension replacements
	conversionConfig.fileExtensions.forEach((pattern) => {
		result = result.replace(pattern.from, pattern.to);
	});

	return result;
}

/**
 * Replace Cursor tool references with profile tool equivalents
 */
function replaceToolReferences(content, conversionConfig) {
	let result = content;

	// Basic pattern for direct tool name replacements
	const toolNames = conversionConfig.toolNames;
	const toolReferencePattern = new RegExp(
		`\\b(${Object.keys(toolNames).join('|')})\\b`,
		'g'
	);

	// Apply direct tool name replacements
	result = result.replace(toolReferencePattern, (match, toolName) => {
		return toolNames[toolName] || toolName;
	});

	// Apply contextual tool replacements
	conversionConfig.toolContexts.forEach((pattern) => {
		result = result.replace(pattern.from, pattern.to);
	});

	// Apply tool group replacements
	conversionConfig.toolGroups.forEach((pattern) => {
		result = result.replace(pattern.from, pattern.to);
	});

	return result;
}

/**
 * Update documentation URLs to point to profile documentation
 */
function updateDocReferences(content, conversionConfig) {
	let result = content;

	// Apply documentation URL replacements
	conversionConfig.docUrls.forEach((pattern) => {
		if (typeof pattern.to === 'function') {
			result = result.replace(pattern.from, pattern.to);
		} else {
			result = result.replace(pattern.from, pattern.to);
		}
	});

	return result;
}

/**
 * Update file references in markdown links
 */
function updateFileReferences(content, conversionConfig) {
	const { pathPattern, replacement } = conversionConfig.fileReferences;
	return content.replace(pathPattern, replacement);
}

/**
 * Transform rule content to profile-specific rules
 * @param {string} content - The content to transform
 * @param {Object} conversionConfig - The conversion configuration
 * @param {Object} globalReplacements - Global text replacements
 * @returns {string} - The transformed content
 */
function transformRuleContent(content, conversionConfig, globalReplacements) {
	let result = content;

	// Apply all transformations in appropriate order
	result = updateFileReferences(result, conversionConfig);
	result = replaceBasicTerms(result, conversionConfig);
	result = replaceToolReferences(result, conversionConfig);
	result = updateDocReferences(result, conversionConfig);

	// Apply any global/catch-all replacements from the profile
	// Super aggressive failsafe pass to catch any variations we might have missed
	// This ensures critical transformations are applied even in contexts we didn't anticipate
	globalReplacements.forEach((pattern) => {
		if (typeof pattern.to === 'function') {
			result = result.replace(pattern.from, pattern.to);
		} else {
			result = result.replace(pattern.from, pattern.to);
		}
	});

	return result;
}

/**
 * Convert a Cursor rule file to a profile-specific rule file
 * @param {string} sourcePath - Path to the source .mdc file
 * @param {string} targetPath - Path to the target file
 * @param {Object} profile - The profile configuration
 * @returns {boolean} - Success status
 */
export function convertRuleToProfileRule(sourcePath, targetPath, profile) {
	const { conversionConfig, globalReplacements } = profile;
	try {
		// Read source content
		const content = fs.readFileSync(sourcePath, 'utf8');

		// Transform content
		const transformedContent = transformRuleContent(
			content,
			conversionConfig,
			globalReplacements
		);

		// Ensure target directory exists
		const targetDir = path.dirname(targetPath);
		if (!fs.existsSync(targetDir)) {
			fs.mkdirSync(targetDir, { recursive: true });
		}

		// Write transformed content
		fs.writeFileSync(targetPath, transformedContent);

		return true;
	} catch (error) {
		console.error(`Error converting rule file: ${error.message}`);
		return false;
	}
}

/**
 * Convert all Cursor rules to profile rules for a specific profile
 */
export function convertAllRulesToProfileRules(projectRoot, profile) {
	const __filename = fileURLToPath(import.meta.url);
	const __dirname = path.dirname(__filename);
	const sourceDir = path.join(__dirname, '..', '..', 'assets', 'rules');
	const targetDir = path.join(projectRoot, profile.rulesDir);
	const assetsDir = path.join(__dirname, '..', '..', 'assets');

	let success = 0;
	let failed = 0;

	// 1. Call onAddRulesProfile first (for pre-processing like copying assets)
	if (typeof profile.onAddRulesProfile === 'function') {
		try {
			profile.onAddRulesProfile(projectRoot, assetsDir);
			log(
				'debug',
				`[Rule Transformer] Called onAddRulesProfile for ${profile.profileName}`
			);
		} catch (error) {
			log(
				'error',
				`[Rule Transformer] onAddRulesProfile failed for ${profile.profileName}: ${error.message}`
			);
			failed++;
		}
	}

	// 2. Handle fileMap-based rule conversion (if any)
	const sourceFiles = Object.keys(profile.fileMap);
	if (sourceFiles.length > 0) {
		// Only create rules directory if we have files to copy
		if (!fs.existsSync(targetDir)) {
			fs.mkdirSync(targetDir, { recursive: true });
		}

		for (const sourceFile of sourceFiles) {
			// Determine if this is an asset file (not a rule file)
			const isAssetFile = !sourceFile.startsWith('rules/');

			try {
				// Use explicit path from fileMap - assets/ is the base directory
				const sourcePath = path.join(assetsDir, sourceFile);

				// Check if source file exists
				if (!fs.existsSync(sourcePath)) {
					log(
						'warn',
						`[Rule Transformer] Source file not found: ${sourcePath}, skipping`
					);
					continue;
				}

				const targetFilename = profile.fileMap[sourceFile];
				const targetPath = path.join(targetDir, targetFilename);

				// Ensure target subdirectory exists (for rules like taskmaster/dev_workflow.md)
				const targetFileDir = path.dirname(targetPath);
				if (!fs.existsSync(targetFileDir)) {
					fs.mkdirSync(targetFileDir, { recursive: true });
				}

				// Read source content
				let content = fs.readFileSync(sourcePath, 'utf8');

				// Apply transformations (only if this is a rule file, not an asset file)
				if (!isAssetFile) {
					content = transformRuleContent(
						content,
						profile.conversionConfig,
						profile.globalReplacements
					);
				}

				// Write to target
				fs.writeFileSync(targetPath, content, 'utf8');
				success++;

				log(
					'debug',
					`[Rule Transformer] ${isAssetFile ? 'Copied' : 'Converted'} ${sourceFile} -> ${targetFilename} for ${profile.profileName}`
				);
			} catch (error) {
				failed++;
				log(
					'error',
					`[Rule Transformer] Failed to ${isAssetFile ? 'copy' : 'convert'} ${sourceFile} for ${profile.profileName}: ${error.message}`
				);
			}
		}
	}

	// 3. Setup MCP configuration (if enabled)
	if (profile.mcpConfig !== false) {
		try {
			setupMCPConfiguration(projectRoot, profile.mcpConfigPath);
			log(
				'debug',
				`[Rule Transformer] Setup MCP configuration for ${profile.profileName}`
			);
		} catch (error) {
			log(
				'error',
				`[Rule Transformer] MCP setup failed for ${profile.profileName}: ${error.message}`
			);
		}
	}

	// 4. Call post-conversion hook (for finalization)
	if (typeof profile.onPostConvertRulesProfile === 'function') {
		try {
			profile.onPostConvertRulesProfile(projectRoot, assetsDir);
			log(
				'debug',
				`[Rule Transformer] Called onPostConvertRulesProfile for ${profile.profileName}`
			);
		} catch (error) {
			log(
				'error',
				`[Rule Transformer] onPostConvertRulesProfile failed for ${profile.profileName}: ${error.message}`
			);
		}
	}

	// Ensure we return at least 1 success for profiles that only use lifecycle functions
	return { success: Math.max(success, 1), failed };
}

/**
 * Remove only Task Master specific files from a profile, leaving other existing rules intact
 * @param {string} projectRoot - Target project directory
 * @param {Object} profile - Profile configuration
 * @returns {Object} Result object
 */
export function removeProfileRules(projectRoot, profile) {
	const targetDir = path.join(projectRoot, profile.rulesDir);
	const profileDir = path.join(projectRoot, profile.profileDir);

	const result = {
		profileName: profile.profileName,
		success: false,
		skipped: false,
		error: null,
		filesRemoved: [],
		mcpResult: null,
		profileDirRemoved: false,
		notice: null
	};

	try {
		// 1. Call onRemoveRulesProfile first (for custom cleanup like removing assets)
		if (typeof profile.onRemoveRulesProfile === 'function') {
			try {
				profile.onRemoveRulesProfile(projectRoot);
				log(
					'debug',
					`[Rule Transformer] Called onRemoveRulesProfile for ${profile.profileName}`
				);
			} catch (error) {
				log(
					'error',
					`[Rule Transformer] onRemoveRulesProfile failed for ${profile.profileName}: ${error.message}`
				);
			}
		}

		// 2. Remove fileMap-based files (if any)
		const sourceFiles = Object.keys(profile.fileMap);
		if (sourceFiles.length > 0) {
			// Check if profile directory exists at all (for full profiles)
			if (!fs.existsSync(profileDir)) {
				result.success = true;
				result.skipped = true;
				log(
					'debug',
					`[Rule Transformer] Profile directory does not exist: ${profileDir}`
				);
				return result;
			}

			let hasOtherRulesFiles = false;

			if (fs.existsSync(targetDir)) {
				// Get list of files we're responsible for
				const taskMasterFiles = sourceFiles.map(
					(sourceFile) => profile.fileMap[sourceFile]
				);

				// Get all files in the rules directory
				const allFiles = fs.readdirSync(targetDir, { recursive: true });
				const allFilePaths = allFiles
					.filter((file) => {
						const fullPath = path.join(targetDir, file);
						return fs.statSync(fullPath).isFile();
					})
					.map((file) => file.toString()); // Ensure it's a string

				// Remove only Task Master files
				for (const taskMasterFile of taskMasterFiles) {
					const filePath = path.join(targetDir, taskMasterFile);
					if (fs.existsSync(filePath)) {
						try {
							fs.rmSync(filePath, { force: true });
							result.filesRemoved.push(taskMasterFile);
							log(
								'debug',
								`[Rule Transformer] Removed Task Master file: ${taskMasterFile}`
							);
						} catch (error) {
							log(
								'error',
								`[Rule Transformer] Failed to remove ${taskMasterFile}: ${error.message}`
							);
						}
					}
				}

				// Check for other (non-Task Master) files
				const remainingFiles = allFilePaths.filter(
					(file) => !taskMasterFiles.includes(file)
				);

				hasOtherRulesFiles = remainingFiles.length > 0;

				// Remove empty directories or note preserved files
				if (remainingFiles.length === 0) {
					fs.rmSync(targetDir, { recursive: true, force: true });
					log(
						'debug',
						`[Rule Transformer] Removed empty rules directory: ${targetDir}`
					);
				} else if (hasOtherRulesFiles) {
					result.notice = `Preserved ${remainingFiles.length} existing rule files in ${profile.rulesDir}`;
					log('info', `[Rule Transformer] ${result.notice}`);
				}
			}
		}

		// 3. Handle MCP configuration - only remove Task Master, preserve other servers
		if (profile.mcpConfig !== false) {
			try {
				result.mcpResult = removeTaskMasterMCPConfiguration(
					projectRoot,
					profile.mcpConfigPath
				);
				if (result.mcpResult.hasOtherServers) {
					if (!result.notice) {
						result.notice = 'Preserved other MCP server configurations';
					} else {
						result.notice += '; preserved other MCP server configurations';
					}
				}
				log(
					'debug',
					`[Rule Transformer] Processed MCP configuration for ${profile.profileName}`
				);
			} catch (error) {
				log(
					'error',
					`[Rule Transformer] MCP cleanup failed for ${profile.profileName}: ${error.message}`
				);
			}
		}

		// 4. Check if we should remove the entire profile directory
		if (fs.existsSync(profileDir)) {
			const remainingContents = fs.readdirSync(profileDir);
			if (remainingContents.length === 0 && profile.profileDir !== '.') {
				// Only remove profile directory if it's empty and not root directory
				try {
					fs.rmSync(profileDir, { recursive: true, force: true });
					result.profileDirRemoved = true;
					log(
						'debug',
						`[Rule Transformer] Removed empty profile directory: ${profileDir}`
					);
				} catch (error) {
					log(
						'error',
						`[Rule Transformer] Failed to remove profile directory ${profileDir}: ${error.message}`
					);
				}
			} else if (remainingContents.length > 0) {
				// Profile directory has remaining files/folders, add notice
				const preservedNotice = `Preserved ${remainingContents.length} existing files/folders in ${profile.profileDir}`;
				if (!result.notice) {
					result.notice = preservedNotice;
				} else {
					result.notice += `; ${preservedNotice.toLowerCase()}`;
				}
				log('info', `[Rule Transformer] ${preservedNotice}`);
			}
		}

		result.success = true;
		log(
			'debug',
			`[Rule Transformer] Successfully removed ${profile.profileName} Task Master files from ${projectRoot}`
		);
	} catch (error) {
		result.error = error.message;
		log(
			'error',
			`[Rule Transformer] Failed to remove ${profile.profileName} rules: ${error.message}`
		);
	}

	return result;
}

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import chalk from 'chalk';
import { z } from 'zod';
import { AI_COMMAND_NAMES } from '../../src/constants/commands.js';
import {
	LEGACY_CONFIG_FILE,
	TASKMASTER_DIR
} from '../../src/constants/paths.js';
import {
	ALL_PROVIDERS,
	CUSTOM_PROVIDERS,
	CUSTOM_PROVIDERS_ARRAY,
	VALIDATED_PROVIDERS
} from '../../src/constants/providers.js';
import { findConfigPath } from '../../src/utils/path-utils.js';
import { findProjectRoot, isEmpty, log, resolveEnvVariable } from './utils.js';

// Calculate __dirname in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Load supported models from JSON file using the calculated __dirname
let MODEL_MAP;
try {
	const supportedModelsRaw = fs.readFileSync(
		path.join(__dirname, 'supported-models.json'),
		'utf-8'
	);
	MODEL_MAP = JSON.parse(supportedModelsRaw);
} catch (error) {
	console.error(
		chalk.red(
			'FATAL ERROR: Could not load supported-models.json. Please ensure the file exists and is valid JSON.'
		),
		error
	);
	MODEL_MAP = {}; // Default to empty map on error to avoid crashing, though functionality will be limited
	process.exit(1); // Exit if models can't be loaded
}

// Default configuration values (used if config file is missing or incomplete)
const DEFAULTS = {
	models: {
		main: {
			provider: 'anthropic',
			modelId: 'claude-3-7-sonnet-20250219',
			maxTokens: 64000,
			temperature: 0.2
		},
		research: {
			provider: 'perplexity',
			modelId: 'sonar-pro',
			maxTokens: 8700,
			temperature: 0.1
		},
		fallback: {
			// No default fallback provider/model initially
			provider: 'anthropic',
			modelId: 'claude-3-5-sonnet',
			maxTokens: 8192, // Default parameters if fallback IS configured
			temperature: 0.2
		}
	},
	global: {
		logLevel: 'info',
		debug: false,
		defaultNumTasks: 10,
		defaultSubtasks: 5,
		defaultPriority: 'medium',
		projectName: 'Task Master',
		ollamaBaseURL: 'http://localhost:11434/api',
		bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
		responseLanguage: 'English'
	},
	claudeCode: {}
};

// --- Internal Config Loading ---
let loadedConfig = null;
let loadedConfigRoot = null; // Track which root loaded the config

// Custom Error for configuration issues
class ConfigurationError extends Error {
	constructor(message) {
		super(message);
		this.name = 'ConfigurationError';
	}
}

function _loadAndValidateConfig(explicitRoot = null) {
	const defaults = DEFAULTS; // Use the defined defaults
	let rootToUse = explicitRoot;
	let configSource = explicitRoot
		? `explicit root (${explicitRoot})`
		: 'defaults (no root provided yet)';

	// ---> If no explicit root, TRY to find it <---
	if (!rootToUse) {
		rootToUse = findProjectRoot();
		if (rootToUse) {
			configSource = `found root (${rootToUse})`;
		} else {
			// No root found, use current working directory as fallback
			// This prevents infinite loops during initialization
			rootToUse = process.cwd();
			configSource = `current directory (${rootToUse}) - no project markers found`;
		}
	}
	// ---> End find project root logic <---

	// --- Find configuration file ---
	let configPath = null;
	let config = { ...defaults }; // Start with a deep copy of defaults
	let configExists = false;

	// During initialization (no project markers), skip config file search entirely
	const hasProjectMarkers =
		fs.existsSync(path.join(rootToUse, TASKMASTER_DIR)) ||
		fs.existsSync(path.join(rootToUse, LEGACY_CONFIG_FILE));

	if (hasProjectMarkers) {
		// Only try to find config if we have project markers
		// This prevents the repeated warnings during init
		configPath = findConfigPath(null, { projectRoot: rootToUse });
	}

	if (configPath) {
		configExists = true;
		const isLegacy = configPath.endsWith(LEGACY_CONFIG_FILE);

		try {
			const rawData = fs.readFileSync(configPath, 'utf-8');
			const parsedConfig = JSON.parse(rawData);

			// Deep merge parsed config onto defaults
			config = {
				models: {
					main: { ...defaults.models.main, ...parsedConfig?.models?.main },
					research: {
						...defaults.models.research,
						...parsedConfig?.models?.research
					},
					fallback:
						parsedConfig?.models?.fallback?.provider &&
						parsedConfig?.models?.fallback?.modelId
							? { ...defaults.models.fallback, ...parsedConfig.models.fallback }
							: { ...defaults.models.fallback }
				},
				global: { ...defaults.global, ...parsedConfig?.global },
				claudeCode: { ...defaults.claudeCode, ...parsedConfig?.claudeCode }
			};
			configSource = `file (${configPath})`; // Update source info

			// Issue deprecation warning if using legacy config file
			if (isLegacy) {
				console.warn(
					chalk.yellow(
						`⚠  DEPRECATION WARNING: Found configuration in legacy location '${configPath}'. Please migrate to .taskmaster/config.json. Run 'task-master migrate' to automatically migrate your project.`
					)
				);
			}

			// --- Validation (Warn if file content is invalid) ---
			// Use log.warn for consistency
			if (!validateProvider(config.models.main.provider)) {
				console.warn(
					chalk.yellow(
						`Warning: Invalid main provider "${config.models.main.provider}" in ${configPath}. Falling back to default.`
					)
				);
				config.models.main = { ...defaults.models.main };
			}
			if (!validateProvider(config.models.research.provider)) {
				console.warn(
					chalk.yellow(
						`Warning: Invalid research provider "${config.models.research.provider}" in ${configPath}. Falling back to default.`
					)
				);
				config.models.research = { ...defaults.models.research };
			}
			if (
				config.models.fallback?.provider &&
				!validateProvider(config.models.fallback.provider)
			) {
				console.warn(
					chalk.yellow(
						`Warning: Invalid fallback provider "${config.models.fallback.provider}" in ${configPath}. Fallback model configuration will be ignored.`
					)
				);
				config.models.fallback.provider = undefined;
				config.models.fallback.modelId = undefined;
			}
			if (config.claudeCode && !isEmpty(config.claudeCode)) {
				config.claudeCode = validateClaudeCodeSettings(config.claudeCode);
			}
		} catch (error) {
			// Use console.error for actual errors during parsing
			console.error(
				chalk.red(
					`Error reading or parsing ${configPath}: ${error.message}. Using default configuration.`
				)
			);
			config = { ...defaults }; // Reset to defaults on parse error
			configSource = `defaults (parse error at ${configPath})`;
		}
	} else {
		// Config file doesn't exist at the determined rootToUse.
		if (explicitRoot) {
			// Only warn if an explicit root was *expected*.
			console.warn(
				chalk.yellow(
					`Warning: Configuration file not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.`
				)
			);
		} else {
			// Don't warn about missing config during initialization
			// Only warn if this looks like an existing project (has .taskmaster dir or legacy config marker)
			const hasTaskmasterDir = fs.existsSync(
				path.join(rootToUse, TASKMASTER_DIR)
			);
			const hasLegacyMarker = fs.existsSync(
				path.join(rootToUse, LEGACY_CONFIG_FILE)
			);

			if (hasTaskmasterDir || hasLegacyMarker) {
				console.warn(
					chalk.yellow(
						`Warning: Configuration file not found at derived root (${rootToUse}). Using defaults.`
					)
				);
			}
		}
		// Keep config as defaults
		config = { ...defaults };
		configSource = `defaults (no config file found at ${rootToUse})`;
	}

	return config;
}

/**
 * Gets the current configuration, loading it if necessary.
 * Handles MCP initialization context gracefully.
 * @param {string|null} explicitRoot - Optional explicit path to the project root.
 * @param {boolean} forceReload - Force reloading the config file.
 * @returns {object} The loaded configuration object.
 */
function getConfig(explicitRoot = null, forceReload = false) {
	// Determine if a reload is necessary
	const needsLoad =
		!loadedConfig ||
		forceReload ||
		(explicitRoot && explicitRoot !== loadedConfigRoot);

	if (needsLoad) {
		const newConfig = _loadAndValidateConfig(explicitRoot); // _load handles null explicitRoot

		// Only update the global cache if loading was forced or if an explicit root
		// was provided (meaning we attempted to load a specific project's config).
		// We avoid caching the initial default load triggered without an explicitRoot.
		if (forceReload || explicitRoot) {
			loadedConfig = newConfig;
			loadedConfigRoot = explicitRoot; // Store the root used for this loaded config
		}
		return newConfig; // Return the newly loaded/default config
	}

	// If no load was needed, return the cached config
	return loadedConfig;
}

/**
 * Validates if a provider name is supported.
 * Custom providers (azure, vertex, bedrock, openrouter, ollama) are always allowed.
 * Validated providers must exist in the MODEL_MAP from supported-models.json.
 * @param {string} providerName The name of the provider.
 * @returns {boolean} True if the provider is valid, false otherwise.
 */
function validateProvider(providerName) {
	// Custom providers are always allowed
	if (CUSTOM_PROVIDERS_ARRAY.includes(providerName)) {
		return true;
	}

	// Validated providers must exist in MODEL_MAP
	if (VALIDATED_PROVIDERS.includes(providerName)) {
		return !!(MODEL_MAP && MODEL_MAP[providerName]);
	}

	// Unknown providers are not allowed
	return false;
}

/**
 * Optional: Validates if a modelId is known for a given provider based on MODEL_MAP.
 * This is a non-strict validation; an unknown model might still be valid.
 * @param {string} providerName The name of the provider.
 * @param {string} modelId The model ID.
 * @returns {boolean} True if the modelId is in the map for the provider, false otherwise.
 */
function validateProviderModelCombination(providerName, modelId) {
	// If provider isn't even in our map, we can't validate the model
	if (!MODEL_MAP[providerName]) {
		return true; // Allow unknown providers or those without specific model lists
	}
	// If the provider is known, check if the model is in its list OR if the list is empty (meaning accept any)
	return (
		MODEL_MAP[providerName].length === 0 ||
		// Use .some() to check the 'id' property of objects in the array
		MODEL_MAP[providerName].some((modelObj) => modelObj.id === modelId)
	);
}

/**
 * Validates Claude Code AI provider custom settings
 * @param {object} settings The settings to validate
 * @returns {object} The validated settings
 */
function validateClaudeCodeSettings(settings) {
	// Define the base settings schema without commandSpecific first
	const BaseSettingsSchema = z.object({
		maxTurns: z.number().int().positive().optional(),
		customSystemPrompt: z.string().optional(),
		appendSystemPrompt: z.string().optional(),
		permissionMode: z
			.enum(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
			.optional(),
		allowedTools: z.array(z.string()).optional(),
		disallowedTools: z.array(z.string()).optional(),
		mcpServers: z
			.record(
				z.string(),
				z.object({
					type: z.enum(['stdio', 'sse']).optional(),
					command: z.string(),
					args: z.array(z.string()).optional(),
					env: z.record(z.string()).optional(),
					url: z.string().url().optional(),
					headers: z.record(z.string()).optional()
				})
			)
			.optional()
	});

	// Define CommandSpecificSchema using the base schema
	const CommandSpecificSchema = z.record(
		z.enum(AI_COMMAND_NAMES),
		BaseSettingsSchema
	);

	// Define the full settings schema with commandSpecific
	const SettingsSchema = BaseSettingsSchema.extend({
		commandSpecific: CommandSpecificSchema.optional()
	});

	let validatedSettings = {};

	try {
		validatedSettings = SettingsSchema.parse(settings);
	} catch (error) {
		console.warn(
			chalk.yellow(
				`Warning: Invalid Claude Code settings in config: ${error.message}. Falling back to default.`
			)
		);

		validatedSettings = {};
	}

	return validatedSettings;
}

// --- Claude Code Settings Getters ---

function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
	const config = getConfig(explicitRoot, forceReload);
	// Ensure Claude Code defaults are applied if Claude Code section is missing
	return { ...DEFAULTS.claudeCode, ...(config?.claudeCode || {}) };
}

function getClaudeCodeSettingsForCommand(
	commandName,
	explicitRoot = null,
	forceReload = false
) {
	const settings = getClaudeCodeSettings(explicitRoot, forceReload);
	const commandSpecific = settings?.commandSpecific || {};
	return { ...settings, ...commandSpecific[commandName] };
}

// --- Role-Specific Getters ---

function getModelConfigForRole(role, explicitRoot = null) {
	const config = getConfig(explicitRoot);
	const roleConfig = config?.models?.[role];
	if (!roleConfig) {
		log(
			'warn',
			`No model configuration found for role: ${role}. Returning default.`
		);
		return DEFAULTS.models[role] || {};
	}
	return roleConfig;
}

function getMainProvider(explicitRoot = null) {
	return getModelConfigForRole('main', explicitRoot).provider;
}

function getMainModelId(explicitRoot = null) {
	return getModelConfigForRole('main', explicitRoot).modelId;
}

function getMainMaxTokens(explicitRoot = null) {
	// Directly return value from config (which includes defaults)
	return getModelConfigForRole('main', explicitRoot).maxTokens;
}

function getMainTemperature(explicitRoot = null) {
	// Directly return value from config
	return getModelConfigForRole('main', explicitRoot).temperature;
}

function getResearchProvider(explicitRoot = null) {
	return getModelConfigForRole('research', explicitRoot).provider;
}

function getResearchModelId(explicitRoot = null) {
	return getModelConfigForRole('research', explicitRoot).modelId;
}

function getResearchMaxTokens(explicitRoot = null) {
	// Directly return value from config
	return getModelConfigForRole('research', explicitRoot).maxTokens;
}

function getResearchTemperature(explicitRoot = null) {
	// Directly return value from config
	return getModelConfigForRole('research', explicitRoot).temperature;
}

function getFallbackProvider(explicitRoot = null) {
	// Directly return value from config (will be undefined if not set)
	return getModelConfigForRole('fallback', explicitRoot).provider;
}

function getFallbackModelId(explicitRoot = null) {
	// Directly return value from config
	return getModelConfigForRole('fallback', explicitRoot).modelId;
}

function getFallbackMaxTokens(explicitRoot = null) {
	// Directly return value from config
	return getModelConfigForRole('fallback', explicitRoot).maxTokens;
}

function getFallbackTemperature(explicitRoot = null) {
	// Directly return value from config
	return getModelConfigForRole('fallback', explicitRoot).temperature;
}

// --- Global Settings Getters ---

function getGlobalConfig(explicitRoot = null) {
	const config = getConfig(explicitRoot);
	// Ensure global defaults are applied if global section is missing
	return { ...DEFAULTS.global, ...(config?.global || {}) };
}

function getLogLevel(explicitRoot = null) {
	// Directly return value from config
	return getGlobalConfig(explicitRoot).logLevel.toLowerCase();
}

function getDebugFlag(explicitRoot = null) {
	// Directly return value from config, ensure boolean
	return getGlobalConfig(explicitRoot).debug === true;
}

function getDefaultSubtasks(explicitRoot = null) {
	// Directly return value from config, ensure integer
	const val = getGlobalConfig(explicitRoot).defaultSubtasks;
	const parsedVal = parseInt(val, 10);
	return Number.isNaN(parsedVal) ? DEFAULTS.global.defaultSubtasks : parsedVal;
}

function getDefaultNumTasks(explicitRoot = null) {
	const val = getGlobalConfig(explicitRoot).defaultNumTasks;
	const parsedVal = parseInt(val, 10);
	return Number.isNaN(parsedVal) ? DEFAULTS.global.defaultNumTasks : parsedVal;
}

function getDefaultPriority(explicitRoot = null) {
	// Directly return value from config
	return getGlobalConfig(explicitRoot).defaultPriority;
}

function getProjectName(explicitRoot = null) {
	// Directly return value from config
	return getGlobalConfig(explicitRoot).projectName;
}

function getOllamaBaseURL(explicitRoot = null) {
	// Directly return value from config
	return getGlobalConfig(explicitRoot).ollamaBaseURL;
}

function getAzureBaseURL(explicitRoot = null) {
	// Directly return value from config
	return getGlobalConfig(explicitRoot).azureBaseURL;
}

function getBedrockBaseURL(explicitRoot = null) {
	// Directly return value from config
	return getGlobalConfig(explicitRoot).bedrockBaseURL;
}

/**
 * Gets the Google Cloud project ID for Vertex AI from configuration
 * @param {string|null} explicitRoot - Optional explicit path to the project root.
 * @returns {string|null} The project ID or null if not configured
 */
function getVertexProjectId(explicitRoot = null) {
	// Return value from config
	return getGlobalConfig(explicitRoot).vertexProjectId;
}

/**
 * Gets the Google Cloud location for Vertex AI from configuration
 * @param {string|null} explicitRoot - Optional explicit path to the project root.
 * @returns {string} The location or default value of "us-central1"
 */
function getVertexLocation(explicitRoot = null) {
	// Return value from config or default
	return getGlobalConfig(explicitRoot).vertexLocation || 'us-central1';
}

function getResponseLanguage(explicitRoot = null) {
	// Directly return value from config
	return getGlobalConfig(explicitRoot).responseLanguage;
}

/**
 * Gets model parameters (maxTokens, temperature) for a specific role,
 * considering model-specific overrides from supported-models.json.
 * @param {string} role - The role ('main', 'research', 'fallback').
 * @param {string|null} explicitRoot - Optional explicit path to the project root.
 * @returns {{maxTokens: number, temperature: number}}
 */
function getParametersForRole(role, explicitRoot = null) {
	const roleConfig = getModelConfigForRole(role, explicitRoot);
	const roleMaxTokens = roleConfig.maxTokens;
	const roleTemperature = roleConfig.temperature;
	const modelId = roleConfig.modelId;
	const providerName = roleConfig.provider;

	let effectiveMaxTokens = roleMaxTokens; // Start with the role's default

	try {
		// Find the model definition in MODEL_MAP
		const providerModels = MODEL_MAP[providerName];
		if (providerModels && Array.isArray(providerModels)) {
			const modelDefinition = providerModels.find((m) => m.id === modelId);

			// Check if a model-specific max_tokens is defined and valid
			if (
				modelDefinition &&
				typeof modelDefinition.max_tokens === 'number' &&
				modelDefinition.max_tokens > 0
			) {
				const modelSpecificMaxTokens = modelDefinition.max_tokens;
				// Use the minimum of the role default and the model specific limit
				effectiveMaxTokens = Math.min(roleMaxTokens, modelSpecificMaxTokens);
				log(
					'debug',
					`Applying model-specific max_tokens (${modelSpecificMaxTokens}) for ${modelId}. Effective limit: ${effectiveMaxTokens}`
				);
			} else {
				log(
					'debug',
					`No valid model-specific max_tokens override found for ${modelId}. Using role default: ${roleMaxTokens}`
				);
			}
		} else {
			// Special handling for custom OpenRouter models
			if (providerName === CUSTOM_PROVIDERS.OPENROUTER) {
				// Use a conservative default for OpenRouter models not in our list
				const openrouterDefault = 32768;
				effectiveMaxTokens = Math.min(roleMaxTokens, openrouterDefault);
				log(
					'debug',
					`Custom OpenRouter model ${modelId} detected. Using conservative max_tokens: ${effectiveMaxTokens}`
				);
			} else {
				log(
					'debug',
					`No model definitions found for provider ${providerName} in MODEL_MAP. Using role default maxTokens: ${roleMaxTokens}`
				);
			}
		}
	} catch (lookupError) {
		log(
			'warn',
			`Error looking up model-specific max_tokens for ${modelId}: ${lookupError.message}. Using role default: ${roleMaxTokens}`
		);
		// Fallback to role default on error
		effectiveMaxTokens = roleMaxTokens;
	}

	return {
		maxTokens: effectiveMaxTokens,
		temperature: roleTemperature
	};
}

/**
 * Checks if the API key for a given provider is set in the environment.
 * Checks process.env first, then session.env if session is provided, then .env file if projectRoot provided.
 * @param {string} providerName - The name of the provider (e.g., 'openai', 'anthropic').
 * @param {object|null} [session=null] - The MCP session object (optional).
 * @param {string|null} [projectRoot=null] - The project root directory (optional, for .env file check).
 * @returns {boolean} True if the API key is set, false otherwise.
 */
function isApiKeySet(providerName, session = null, projectRoot = null) {
	// Define the expected environment variable name for each provider

	// Providers that don't require API keys for authentication
	const providersWithoutApiKeys = [
		CUSTOM_PROVIDERS.OLLAMA,
		CUSTOM_PROVIDERS.BEDROCK,
		CUSTOM_PROVIDERS.MCP,
		CUSTOM_PROVIDERS.GEMINI_CLI
	];

	if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
		return true; // Indicate key status is effectively "OK"
	}

	// Claude Code doesn't require an API key
	if (providerName?.toLowerCase() === 'claude-code') {
		return true; // No API key needed
	}

	const keyMap = {
		openai: 'OPENAI_API_KEY',
		anthropic: 'ANTHROPIC_API_KEY',
		google: 'GOOGLE_API_KEY',
		perplexity: 'PERPLEXITY_API_KEY',
		mistral: 'MISTRAL_API_KEY',
		azure: 'AZURE_OPENAI_API_KEY',
		openrouter: 'OPENROUTER_API_KEY',
		xai: 'XAI_API_KEY',
		groq: 'GROQ_API_KEY',
		vertex: 'GOOGLE_API_KEY', // Vertex uses the same key as Google
		'claude-code': 'CLAUDE_CODE_API_KEY', // Not actually used, but included for consistency
		bedrock: 'AWS_ACCESS_KEY_ID' // Bedrock uses AWS credentials
		// Add other providers as needed
	};

	const providerKey = providerName?.toLowerCase();
	if (!providerKey || !keyMap[providerKey]) {
		log('warn', `Unknown provider name: ${providerName} in isApiKeySet check.`);
		return false;
	}

	const envVarName = keyMap[providerKey];
	const apiKeyValue = resolveEnvVariable(envVarName, session, projectRoot);

	// Check if the key exists, is not empty, and is not a placeholder
	return (
		apiKeyValue &&
		apiKeyValue.trim() !== '' &&
		!/YOUR_.*_API_KEY_HERE/.test(apiKeyValue) && // General placeholder check
		!apiKeyValue.includes('KEY_HERE')
	); // Another common placeholder pattern
}

/**
 * Checks the API key status within .cursor/mcp.json for a given provider.
 * Reads the mcp.json file, finds the taskmaster-ai server config, and checks the relevant env var.
 * @param {string} providerName The name of the provider.
 * @param {string|null} projectRoot - Optional explicit path to the project root.
 * @returns {boolean} True if the key exists and is not a placeholder, false otherwise.
 */
function getMcpApiKeyStatus(providerName, projectRoot = null) {
	const rootDir = projectRoot || findProjectRoot(); // Use existing root finding
	if (!rootDir) {
		console.warn(
			chalk.yellow('Warning: Could not find project root to check mcp.json.')
		);
		return false; // Cannot check without root
	}
	const mcpConfigPath = path.join(rootDir, '.cursor', 'mcp.json');

	if (!fs.existsSync(mcpConfigPath)) {
		// console.warn(chalk.yellow('Warning: .cursor/mcp.json not found.'));
		return false; // File doesn't exist
	}

	try {
		const mcpConfigRaw = fs.readFileSync(mcpConfigPath, 'utf-8');
		const mcpConfig = JSON.parse(mcpConfigRaw);

		const mcpEnv =
			mcpConfig?.mcpServers?.['task-master-ai']?.env ||
			mcpConfig?.mcpServers?.['taskmaster-ai']?.env;
		if (!mcpEnv) {
			return false;
		}

		let apiKeyToCheck = null;
		let placeholderValue = null;

		switch (providerName) {
			case 'anthropic':
				apiKeyToCheck = mcpEnv.ANTHROPIC_API_KEY;
				placeholderValue = 'YOUR_ANTHROPIC_API_KEY_HERE';
				break;
			case 'openai':
				apiKeyToCheck = mcpEnv.OPENAI_API_KEY;
				placeholderValue = 'YOUR_OPENAI_API_KEY_HERE'; // Assuming placeholder matches OPENAI
				break;
			case 'openrouter':
				apiKeyToCheck = mcpEnv.OPENROUTER_API_KEY;
				placeholderValue = 'YOUR_OPENROUTER_API_KEY_HERE';
				break;
			case 'google':
				apiKeyToCheck = mcpEnv.GOOGLE_API_KEY;
				placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE';
				break;
			case 'perplexity':
				apiKeyToCheck = mcpEnv.PERPLEXITY_API_KEY;
				placeholderValue = 'YOUR_PERPLEXITY_API_KEY_HERE';
				break;
			case 'xai':
				apiKeyToCheck = mcpEnv.XAI_API_KEY;
				placeholderValue = 'YOUR_XAI_API_KEY_HERE';
				break;
			case 'groq':
				apiKeyToCheck = mcpEnv.GROQ_API_KEY;
				placeholderValue = 'YOUR_GROQ_API_KEY_HERE';
				break;
			case 'ollama':
				return true; // No key needed
			case 'claude-code':
				return true; // No key needed
			case 'mistral':
				apiKeyToCheck = mcpEnv.MISTRAL_API_KEY;
				placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE';
				break;
			case 'azure':
				apiKeyToCheck = mcpEnv.AZURE_OPENAI_API_KEY;
				placeholderValue = 'YOUR_AZURE_OPENAI_API_KEY_HERE';
				break;
			case 'vertex':
				apiKeyToCheck = mcpEnv.GOOGLE_API_KEY; // Vertex uses Google API key
				placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE';
				break;
			case 'bedrock':
				apiKeyToCheck = mcpEnv.AWS_ACCESS_KEY_ID; // Bedrock uses AWS credentials
				placeholderValue = 'YOUR_AWS_ACCESS_KEY_ID_HERE';
				break;
			default:
				return false; // Unknown provider
		}

		return !!apiKeyToCheck && !/KEY_HERE$/.test(apiKeyToCheck);
	} catch (error) {
		console.error(
			chalk.red(`Error reading or parsing .cursor/mcp.json: ${error.message}`)
		);
		return false;
	}
}

/**
 * Gets a list of available models based on the MODEL_MAP.
 * @returns {Array<{id: string, name: string, provider: string, swe_score: number|null, cost_per_1m_tokens: {input: number|null, output: number|null}|null, allowed_roles: string[]}>}
 */
function getAvailableModels() {
	const available = [];
	for (const [provider, models] of Object.entries(MODEL_MAP)) {
		if (models.length > 0) {
			models
				.filter((modelObj) => Boolean(modelObj.supported))
				.forEach((modelObj) => {
					// Basic name generation - can be improved
					const modelId = modelObj.id;
					const sweScore = modelObj.swe_score;
					const cost = modelObj.cost_per_1m_tokens;
					const allowedRoles = modelObj.allowed_roles || ['main', 'fallback'];
					const nameParts = modelId
						.split('-')
						.map((p) => p.charAt(0).toUpperCase() + p.slice(1));
					// Handle specific known names better if needed
					let name = nameParts.join(' ');
					if (modelId === 'claude-3.5-sonnet-20240620')
						name = 'Claude 3.5 Sonnet';
					if (modelId === 'claude-3-7-sonnet-20250219')
						name = 'Claude 3.7 Sonnet';
					if (modelId === 'gpt-4o') name = 'GPT-4o';
					if (modelId === 'gpt-4-turbo') name = 'GPT-4 Turbo';
					if (modelId === 'sonar-pro') name = 'Perplexity Sonar Pro';
					if (modelId === 'sonar-mini') name = 'Perplexity Sonar Mini';

					available.push({
						id: modelId,
						name: name,
						provider: provider,
						swe_score: sweScore,
						cost_per_1m_tokens: cost,
						allowed_roles: allowedRoles,
						max_tokens: modelObj.max_tokens
					});
				});
		} else {
			// For providers with empty lists (like ollama), maybe add a placeholder or skip
			available.push({
				id: `[${provider}-any]`,
				name: `Any (${provider})`,
				provider: provider
			});
		}
	}
	return available;
}

/**
 * Writes the configuration object to the file.
 * @param {Object} config The configuration object to write.
 * @param {string|null} explicitRoot - Optional explicit path to the project root.
 * @returns {boolean} True if successful, false otherwise.
 */
function writeConfig(config, explicitRoot = null) {
	// ---> Determine root path reliably <---
	let rootPath = explicitRoot;
	if (explicitRoot === null || explicitRoot === undefined) {
		// Logic matching _loadAndValidateConfig
		const foundRoot = findProjectRoot(); // *** Explicitly call findProjectRoot ***
		if (!foundRoot) {
			console.error(
				chalk.red(
					'Error: Could not determine project root. Configuration not saved.'
				)
			);
			return false;
		}
		rootPath = foundRoot;
	}
	// ---> End determine root path logic <---

	// Use new config location: .taskmaster/config.json
	const taskmasterDir = path.join(rootPath, '.taskmaster');
	const configPath = path.join(taskmasterDir, 'config.json');

	try {
		// Ensure .taskmaster directory exists
		if (!fs.existsSync(taskmasterDir)) {
			fs.mkdirSync(taskmasterDir, { recursive: true });
		}

		fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
		loadedConfig = config; // Update the cache after successful write
		return true;
	} catch (error) {
		console.error(
			chalk.red(
				`Error writing configuration to ${configPath}: ${error.message}`
			)
		);
		return false;
	}
}

/**
 * Checks if a configuration file exists at the project root (new or legacy location)
 * @param {string|null} explicitRoot - Optional explicit path to the project root
 * @returns {boolean} True if the file exists, false otherwise
 */
function isConfigFilePresent(explicitRoot = null) {
	return findConfigPath(null, { projectRoot: explicitRoot }) !== null;
}

/**
 * Gets the user ID from the configuration.
 * @param {string|null} explicitRoot - Optional explicit path to the project root.
 * @returns {string|null} The user ID or null if not found.
 */
function getUserId(explicitRoot = null) {
	const config = getConfig(explicitRoot);
	if (!config.global) {
		config.global = {}; // Ensure global object exists
	}
	if (!config.global.userId) {
		config.global.userId = '1234567890';
		// Attempt to write the updated config.
		// It's important that writeConfig correctly resolves the path
		// using explicitRoot, similar to how getConfig does.
		const success = writeConfig(config, explicitRoot);
		if (!success) {
			// Log an error or handle the failure to write,
			// though for now, we'll proceed with the in-memory default.
			log(
				'warning',
				'Failed to write updated configuration with new userId. Please let the developers know.'
			);
		}
	}
	return config.global.userId;
}

/**
 * Gets a list of all known provider names (both validated and custom).
 * @returns {string[]} An array of all provider names.
 */
function getAllProviders() {
	return ALL_PROVIDERS;
}

function getBaseUrlForRole(role, explicitRoot = null) {
	const roleConfig = getModelConfigForRole(role, explicitRoot);
	if (roleConfig && typeof roleConfig.baseURL === 'string') {
		return roleConfig.baseURL;
	}
	const provider = roleConfig?.provider;
	if (provider) {
		const envVarName = `${provider.toUpperCase()}_BASE_URL`;
		return resolveEnvVariable(envVarName, null, explicitRoot);
	}
	return undefined;
}

// Export the providers without API keys array for use in other modules
export const providersWithoutApiKeys = [
	CUSTOM_PROVIDERS.OLLAMA,
	CUSTOM_PROVIDERS.BEDROCK,
	CUSTOM_PROVIDERS.GEMINI_CLI,
	CUSTOM_PROVIDERS.MCP
];

export {
	// Core config access
	getConfig,
	writeConfig,
	ConfigurationError,
	isConfigFilePresent,
	// Claude Code settings
	getClaudeCodeSettings,
	getClaudeCodeSettingsForCommand,
	// Validation
	validateProvider,
	validateProviderModelCombination,
	validateClaudeCodeSettings,
	VALIDATED_PROVIDERS,
	CUSTOM_PROVIDERS,
	ALL_PROVIDERS,
	MODEL_MAP,
	getAvailableModels,
	// Role-specific getters (No env var overrides)
	getMainProvider,
	getMainModelId,
	getMainMaxTokens,
	getMainTemperature,
	getResearchProvider,
	getResearchModelId,
	getResearchMaxTokens,
	getResearchTemperature,
	getFallbackProvider,
	getFallbackModelId,
	getFallbackMaxTokens,
	getFallbackTemperature,
	getBaseUrlForRole,
	// Global setting getters (No env var overrides)
	getLogLevel,
	getDebugFlag,
	getDefaultNumTasks,
	getDefaultSubtasks,
	getDefaultPriority,
	getProjectName,
	getOllamaBaseURL,
	getAzureBaseURL,
	getBedrockBaseURL,
	getResponseLanguage,
	getParametersForRole,
	getUserId,
	// API Key Checkers (still relevant)
	isApiKeySet,
	getMcpApiKeyStatus,
	// ADD: Function to get all provider names
	getAllProviders,
	getVertexProjectId,
	getVertexLocation
};

/**
 * tools/utils.js
 * Utility functions for Task Master CLI integration
 */

import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { contextManager } from '../core/context-manager.js'; // Import the singleton
import { fileURLToPath } from 'url';
import { getCurrentTag } from '../../../scripts/modules/utils.js';

// Import path utilities to ensure consistent path resolution
import {
	lastFoundProjectRoot,
	PROJECT_MARKERS
} from '../core/utils/path-utils.js';

const __filename = fileURLToPath(import.meta.url);

// Cache for version info to avoid repeated file reads
let cachedVersionInfo = null;

/**
 * Get version information from package.json
 * @returns {Object} Version information
 */
function getVersionInfo() {
	// Return cached version if available
	if (cachedVersionInfo) {
		return cachedVersionInfo;
	}

	try {
		// Navigate to the project root from the tools directory
		const packageJsonPath = path.join(
			path.dirname(__filename),
			'../../../package.json'
		);
		if (fs.existsSync(packageJsonPath)) {
			const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
			cachedVersionInfo = {
				version: packageJson.version,
				name: packageJson.name
			};
			return cachedVersionInfo;
		}
		cachedVersionInfo = {
			version: 'unknown',
			name: 'task-master-ai'
		};
		return cachedVersionInfo;
	} catch (error) {
		// Fallback version info if package.json can't be read
		cachedVersionInfo = {
			version: 'unknown',
			name: 'task-master-ai'
		};
		return cachedVersionInfo;
	}
}

/**
 * Get current tag information for MCP responses
 * @param {string} projectRoot - The project root directory
 * @param {Object} log - Logger object
 * @returns {Object} Tag information object
 */
function getTagInfo(projectRoot, log) {
	try {
		if (!projectRoot) {
			log.warn('No project root provided for tag information');
			return { currentTag: 'master', availableTags: ['master'] };
		}

		const currentTag = getCurrentTag(projectRoot);

		// Read available tags from tasks.json
		let availableTags = ['master']; // Default fallback
		try {
			const tasksJsonPath = path.join(
				projectRoot,
				'.taskmaster',
				'tasks',
				'tasks.json'
			);
			if (fs.existsSync(tasksJsonPath)) {
				const tasksData = JSON.parse(fs.readFileSync(tasksJsonPath, 'utf-8'));

				// If it's the new tagged format, extract tag keys
				if (
					tasksData &&
					typeof tasksData === 'object' &&
					!Array.isArray(tasksData.tasks)
				) {
					const tagKeys = Object.keys(tasksData).filter(
						(key) =>
							tasksData[key] &&
							typeof tasksData[key] === 'object' &&
							Array.isArray(tasksData[key].tasks)
					);
					if (tagKeys.length > 0) {
						availableTags = tagKeys;
					}
				}
			}
		} catch (tagError) {
			log.debug(`Could not read available tags: ${tagError.message}`);
		}

		return {
			currentTag: currentTag || 'master',
			availableTags: availableTags
		};
	} catch (error) {
		log.warn(`Error getting tag information: ${error.message}`);
		return { currentTag: 'master', availableTags: ['master'] };
	}
}

/**
 * Get normalized project root path
 * @param {string|undefined} projectRootRaw - Raw project root from arguments
 * @param {Object} log - Logger object
 * @returns {string} - Normalized absolute path to project root
 */
function getProjectRoot(projectRootRaw, log) {
	// PRECEDENCE ORDER:
	// 1. Environment variable override (TASK_MASTER_PROJECT_ROOT)
	// 2. Explicitly provided projectRoot in args
	// 3. Previously found/cached project root
	// 4. Current directory if it has project markers
	// 5. Current directory with warning

	// 1. Check for environment variable override
	if (process.env.TASK_MASTER_PROJECT_ROOT) {
		const envRoot = process.env.TASK_MASTER_PROJECT_ROOT;
		const absolutePath = path.isAbsolute(envRoot)
			? envRoot
			: path.resolve(process.cwd(), envRoot);
		log.info(
			`Using project root from TASK_MASTER_PROJECT_ROOT environment variable: ${absolutePath}`
		);
		return absolutePath;
	}

	// 2. If project root is explicitly provided, use it
	if (projectRootRaw) {
		const absolutePath = path.isAbsolute(projectRootRaw)
			? projectRootRaw
			: path.resolve(process.cwd(), projectRootRaw);

		log.info(`Using explicitly provided project root: ${absolutePath}`);
		return absolutePath;
	}

	// 3. If we have a last found project root from a tasks.json search, use that for consistency
	if (lastFoundProjectRoot) {
		log.info(
			`Using last known project root where tasks.json was found: ${lastFoundProjectRoot}`
		);
		return lastFoundProjectRoot;
	}

	// 4. Check if the current directory has any indicators of being a task-master project
	const currentDir = process.cwd();
	if (
		PROJECT_MARKERS.some((marker) => {
			const markerPath = path.join(currentDir, marker);
			return fs.existsSync(markerPath);
		})
	) {
		log.info(
			`Using current directory as project root (found project markers): ${currentDir}`
		);
		return currentDir;
	}

	// 5. Default to current working directory but warn the user
	log.warn(
		`No task-master project detected in current directory. Using ${currentDir} as project root.`
	);
	log.warn(
		'Consider using --project-root to specify the correct project location or set TASK_MASTER_PROJECT_ROOT environment variable.'
	);
	return currentDir;
}

/**
 * Extracts and normalizes the project root path from the MCP session object.
 * @param {Object} session - The MCP session object.
 * @param {Object} log - The MCP logger object.
 * @returns {string|null} - The normalized absolute project root path or null if not found/invalid.
 */
function getProjectRootFromSession(session, log) {
	try {
		// Add detailed logging of session structure
		log.info(
			`Session object: ${JSON.stringify({
				hasSession: !!session,
				hasRoots: !!session?.roots,
				rootsType: typeof session?.roots,
				isRootsArray: Array.isArray(session?.roots),
				rootsLength: session?.roots?.length,
				firstRoot: session?.roots?.[0],
				hasRootsRoots: !!session?.roots?.roots,
				rootsRootsType: typeof session?.roots?.roots,
				isRootsRootsArray: Array.isArray(session?.roots?.roots),
				rootsRootsLength: session?.roots?.roots?.length,
				firstRootsRoot: session?.roots?.roots?.[0]
			})}`
		);

		let rawRootPath = null;
		let decodedPath = null;
		let finalPath = null;

		// Check primary location
		if (session?.roots?.[0]?.uri) {
			rawRootPath = session.roots[0].uri;
			log.info(`Found raw root URI in session.roots[0].uri: ${rawRootPath}`);
		}
		// Check alternate location
		else if (session?.roots?.roots?.[0]?.uri) {
			rawRootPath = session.roots.roots[0].uri;
			log.info(
				`Found raw root URI in session.roots.roots[0].uri: ${rawRootPath}`
			);
		}

		if (rawRootPath) {
			// Decode URI and strip file:// protocol
			decodedPath = rawRootPath.startsWith('file://')
				? decodeURIComponent(rawRootPath.slice(7))
				: rawRootPath; // Assume non-file URI is already decoded? Or decode anyway? Let's decode.
			if (!rawRootPath.startsWith('file://')) {
				decodedPath = decodeURIComponent(rawRootPath); // Decode even if no file://
			}

			// Handle potential Windows drive prefix after stripping protocol (e.g., /C:/...)
			if (
				decodedPath.startsWith('/') &&
				/[A-Za-z]:/.test(decodedPath.substring(1, 3))
			) {
				decodedPath = decodedPath.substring(1); // Remove leading slash if it's like /C:/...
			}

			log.info(`Decoded path: ${decodedPath}`);

			// Normalize slashes and resolve
			const normalizedSlashes = decodedPath.replace(/\\/g, '/');
			finalPath = path.resolve(normalizedSlashes); // Resolve to absolute path for current OS

			log.info(`Normalized and resolved session path: ${finalPath}`);
			return finalPath;
		}

		// Fallback Logic (remains the same)
		log.warn('No project root URI found in session. Attempting fallbacks...');
		const cwd = process.cwd();

		// Fallback 1: Use server path deduction (Cursor IDE)
		const serverPath = process.argv[1];
		if (serverPath && serverPath.includes('mcp-server')) {
			const mcpServerIndex = serverPath.indexOf('mcp-server');
			if (mcpServerIndex !== -1) {
				const projectRoot = path.dirname(
					serverPath.substring(0, mcpServerIndex)
				); // Go up one level

				if (
					fs.existsSync(path.join(projectRoot, '.cursor')) ||
					fs.existsSync(path.join(projectRoot, 'mcp-server')) ||
					fs.existsSync(path.join(projectRoot, 'package.json'))
				) {
					log.info(
						`Using project root derived from server path: ${projectRoot}`
					);
					return projectRoot; // Already absolute
				}
			}
		}

		// Fallback 2: Use CWD
		log.info(`Using current working directory as ultimate fallback: ${cwd}`);
		return cwd; // Already absolute
	} catch (e) {
		log.error(`Error in getProjectRootFromSession: ${e.message}`);
		// Attempt final fallback to CWD on error
		const cwd = process.cwd();
		log.warn(
			`Returning CWD (${cwd}) due to error during session root processing.`
		);
		return cwd;
	}
}

/**
 * Handle API result with standardized error handling and response formatting
 * @param {Object} result - Result object from API call with success, data, and error properties
 * @param {Object} log - Logger object
 * @param {string} errorPrefix - Prefix for error messages
 * @param {Function} processFunction - Optional function to process successful result data
 * @param {string} [projectRoot] - Optional project root for tag information
 * @returns {Object} - Standardized MCP response object
 */
async function handleApiResult(
	result,
	log,
	errorPrefix = 'API error',
	processFunction = processMCPResponseData,
	projectRoot = null
) {
	// Get version info for every response
	const versionInfo = getVersionInfo();

	// Get tag info if project root is provided
	const tagInfo = projectRoot ? getTagInfo(projectRoot, log) : null;

	if (!result.success) {
		const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
		log.error(`${errorPrefix}: ${errorMsg}`);
		return createErrorResponse(errorMsg, versionInfo, tagInfo);
	}

	// Process the result data if needed
	const processedData = processFunction
		? processFunction(result.data)
		: result.data;

	log.info('Successfully completed operation');

	// Create the response payload including version info and tag info
	const responsePayload = {
		data: processedData,
		version: versionInfo
	};

	// Add tag information if available
	if (tagInfo) {
		responsePayload.tag = tagInfo;
	}

	return createContentResponse(responsePayload);
}

/**
 * Executes a task-master CLI command synchronously.
 * @param {string} command - The command to execute (e.g., 'add-task')
 * @param {Object} log - Logger instance
 * @param {Array} args - Arguments for the command
 * @param {string|undefined} projectRootRaw - Optional raw project root path (will be normalized internally)
 * @param {Object|null} customEnv - Optional object containing environment variables to pass to the child process
 * @returns {Object} - The result of the command execution
 */
function executeTaskMasterCommand(
	command,
	log,
	args = [],
	projectRootRaw = null,
	customEnv = null // Changed from session to customEnv
) {
	try {
		// Normalize project root internally using the getProjectRoot utility
		const cwd = getProjectRoot(projectRootRaw, log);

		log.info(
			`Executing task-master ${command} with args: ${JSON.stringify(
				args
			)} in directory: ${cwd}`
		);

		// Prepare full arguments array
		const fullArgs = [command, ...args];

		// Common options for spawn
		const spawnOptions = {
			encoding: 'utf8',
			cwd: cwd,
			// Merge process.env with customEnv, giving precedence to customEnv
			env: { ...process.env, ...(customEnv || {}) }
		};

		// Log the environment being passed (optional, for debugging)
		// log.info(`Spawn options env: ${JSON.stringify(spawnOptions.env)}`);

		// Execute the command using the global task-master CLI or local script
		// Try the global CLI first
		let result = spawnSync('task-master', fullArgs, spawnOptions);

		// If global CLI is not available, try fallback to the local script
		if (result.error && result.error.code === 'ENOENT') {
			log.info('Global task-master not found, falling back to local script');
			// Pass the same spawnOptions (including env) to the fallback
			result = spawnSync('node', ['scripts/dev.js', ...fullArgs], spawnOptions);
		}

		if (result.error) {
			throw new Error(`Command execution error: ${result.error.message}`);
		}

		if (result.status !== 0) {
			// Improve error handling by combining stderr and stdout if stderr is empty
			const errorOutput = result.stderr
				? result.stderr.trim()
				: result.stdout
					? result.stdout.trim()
					: 'Unknown error';
			throw new Error(
				`Command failed with exit code ${result.status}: ${errorOutput}`
			);
		}

		return {
			success: true,
			stdout: result.stdout,
			stderr: result.stderr
		};
	} catch (error) {
		log.error(`Error executing task-master command: ${error.message}`);
		return {
			success: false,
			error: error.message
		};
	}
}

/**
 * Checks cache for a result using the provided key. If not found, executes the action function,
 * caches the result upon success, and returns the result.
 *
 * @param {Object} options - Configuration options.
 * @param {string} options.cacheKey - The unique key for caching this operation's result.
 * @param {Function} options.actionFn - The async function to execute if the cache misses.
 *                                      Should return an object like { success: boolean, data?: any, error?: { code: string, message: string } }.
 * @param {Object} options.log - The logger instance.
 * @returns {Promise<Object>} - An object containing the result.
 *                              Format: { success: boolean, data?: any, error?: { code: string, message: string } }
 */
async function getCachedOrExecute({ cacheKey, actionFn, log }) {
	// Check cache first
	const cachedResult = contextManager.getCachedData(cacheKey);

	if (cachedResult !== undefined) {
		log.info(`Cache hit for key: ${cacheKey}`);
		return cachedResult;
	}

	log.info(`Cache miss for key: ${cacheKey}. Executing action function.`);

	// Execute the action function if cache missed
	const result = await actionFn();

	// If the action was successful, cache the result
	if (result.success && result.data !== undefined) {
		log.info(`Action successful. Caching result for key: ${cacheKey}`);
		contextManager.setCachedData(cacheKey, result);
	} else if (!result.success) {
		log.warn(
			`Action failed for cache key ${cacheKey}. Result not cached. Error: ${result.error?.message}`
		);
	} else {
		log.warn(
			`Action for cache key ${cacheKey} succeeded but returned no data. Result not cached.`
		);
	}

	return result;
}

/**
 * Recursively removes specified fields from task objects, whether single or in an array.
 * Handles common data structures returned by task commands.
 * @param {Object|Array} taskOrData - A single task object or a data object containing a 'tasks' array.
 * @param {string[]} fieldsToRemove - An array of field names to remove.
 * @returns {Object|Array} - The processed data with specified fields removed.
 */
function processMCPResponseData(
	taskOrData,
	fieldsToRemove = ['details', 'testStrategy']
) {
	if (!taskOrData) {
		return taskOrData;
	}

	// Helper function to process a single task object
	const processSingleTask = (task) => {
		if (typeof task !== 'object' || task === null) {
			return task;
		}

		const processedTask = { ...task };

		// Remove specified fields from the task
		fieldsToRemove.forEach((field) => {
			delete processedTask[field];
		});

		// Recursively process subtasks if they exist and are an array
		if (processedTask.subtasks && Array.isArray(processedTask.subtasks)) {
			// Use processArrayOfTasks to handle the subtasks array
			processedTask.subtasks = processArrayOfTasks(processedTask.subtasks);
		}

		return processedTask;
	};

	// Helper function to process an array of tasks
	const processArrayOfTasks = (tasks) => {
		return tasks.map(processSingleTask);
	};

	// Check if the input is a data structure containing a 'tasks' array (like from listTasks)
	if (
		typeof taskOrData === 'object' &&
		taskOrData !== null &&
		Array.isArray(taskOrData.tasks)
	) {
		return {
			...taskOrData, // Keep other potential fields like 'stats', 'filter'
			tasks: processArrayOfTasks(taskOrData.tasks)
		};
	}
	// Check if the input is likely a single task object (add more checks if needed)
	else if (
		typeof taskOrData === 'object' &&
		taskOrData !== null &&
		'id' in taskOrData &&
		'title' in taskOrData
	) {
		return processSingleTask(taskOrData);
	}
	// Check if the input is an array of tasks directly (less common but possible)
	else if (Array.isArray(taskOrData)) {
		return processArrayOfTasks(taskOrData);
	}

	// If it doesn't match known task structures, return it as is
	return taskOrData;
}

/**
 * Creates standard content response for tools
 * @param {string|Object} content - Content to include in response
 * @returns {Object} - Content response object in FastMCP format
 */
function createContentResponse(content) {
	// FastMCP requires text type, so we format objects as JSON strings
	return {
		content: [
			{
				type: 'text',
				text:
					typeof content === 'object'
						? // Format JSON nicely with indentation
							JSON.stringify(content, null, 2)
						: // Keep other content types as-is
							String(content)
			}
		]
	};
}

/**
 * Creates error response for tools
 * @param {string} errorMessage - Error message to include in response
 * @param {Object} [versionInfo] - Optional version information object
 * @param {Object} [tagInfo] - Optional tag information object
 * @returns {Object} - Error content response object in FastMCP format
 */
function createErrorResponse(errorMessage, versionInfo, tagInfo) {
	// Provide fallback version info if not provided
	if (!versionInfo) {
		versionInfo = getVersionInfo();
	}

	let responseText = `Error: ${errorMessage}
Version: ${versionInfo.version}
Name: ${versionInfo.name}`;

	// Add tag information if available
	if (tagInfo) {
		responseText += `
Current Tag: ${tagInfo.currentTag}`;
	}

	return {
		content: [
			{
				type: 'text',
				text: responseText
			}
		],
		isError: true
	};
}

/**
 * Creates a logger wrapper object compatible with core function expectations.
 * Adapts the MCP logger to the { info, warn, error, debug, success } structure.
 * @param {Object} log - The MCP logger instance.
 * @returns {Object} - The logger wrapper object.
 */
function createLogWrapper(log) {
	return {
		info: (message, ...args) => log.info(message, ...args),
		warn: (message, ...args) => log.warn(message, ...args),
		error: (message, ...args) => log.error(message, ...args),
		// Handle optional debug method
		debug: (message, ...args) =>
			log.debug ? log.debug(message, ...args) : null,
		// Map success to info as a common fallback
		success: (message, ...args) => log.info(message, ...args)
	};
}

/**
 * Resolves and normalizes a project root path from various formats.
 * Handles URI encoding, Windows paths, and file protocols.
 * @param {string | undefined | null} rawPath - The raw project root path.
 * @param {object} [log] - Optional logger object.
 * @returns {string | null} Normalized absolute path or null if input is invalid/empty.
 */
function normalizeProjectRoot(rawPath, log) {
	if (!rawPath) return null;
	try {
		let pathString = Array.isArray(rawPath) ? rawPath[0] : String(rawPath);
		if (!pathString) return null;

		// 1. Decode URI Encoding
		// Use try-catch for decoding as malformed URIs can throw
		try {
			pathString = decodeURIComponent(pathString);
		} catch (decodeError) {
			if (log)
				log.warn(
					`Could not decode URI component for path "${rawPath}": ${decodeError.message}. Proceeding with raw string.`
				);
			// Proceed with the original string if decoding fails
			pathString = Array.isArray(rawPath) ? rawPath[0] : String(rawPath);
		}

		// 2. Strip file:// prefix (handle 2 or 3 slashes)
		if (pathString.startsWith('file:///')) {
			pathString = pathString.slice(7); // Slice 7 for file:///, may leave leading / on Windows
		} else if (pathString.startsWith('file://')) {
			pathString = pathString.slice(7); // Slice 7 for file://
		}

		// 3. Handle potential Windows leading slash after stripping prefix (e.g., /C:/...)
		// This checks if it starts with / followed by a drive letter C: D: etc.
		if (
			pathString.startsWith('/') &&
			/[A-Za-z]:/.test(pathString.substring(1, 3))
		) {
			pathString = pathString.substring(1); // Remove the leading slash
		}

		// 4. Normalize backslashes to forward slashes
		pathString = pathString.replace(/\\/g, '/');

		// 5. Resolve to absolute path using server's OS convention
		const resolvedPath = path.resolve(pathString);
		return resolvedPath;
	} catch (error) {
		if (log) {
			log.error(
				`Error normalizing project root path "${rawPath}": ${error.message}`
			);
		}
		return null; // Return null on error
	}
}

/**
 * Extracts the raw project root path from the session (without normalization).
 * Used as a fallback within the HOF.
 * @param {Object} session - The MCP session object.
 * @param {Object} log - The MCP logger object.
 * @returns {string|null} The raw path string or null.
 */
function getRawProjectRootFromSession(session, log) {
	try {
		// Check primary location
		if (session?.roots?.[0]?.uri) {
			return session.roots[0].uri;
		}
		// Check alternate location
		else if (session?.roots?.roots?.[0]?.uri) {
			return session.roots.roots[0].uri;
		}
		return null; // Not found in expected session locations
	} catch (e) {
		log.error(`Error accessing session roots: ${e.message}`);
		return null;
	}
}

/**
 * Higher-order function to wrap MCP tool execute methods.
 * Ensures args.projectRoot is present and normalized before execution.
 * Uses TASK_MASTER_PROJECT_ROOT environment variable with proper precedence.
 * @param {Function} executeFn - The original async execute(args, context) function.
 * @returns {Function} The wrapped async execute function.
 */
function withNormalizedProjectRoot(executeFn) {
	return async (args, context) => {
		const { log, session } = context;
		let normalizedRoot = null;
		let rootSource = 'unknown';

		try {
			// PRECEDENCE ORDER:
			// 1. TASK_MASTER_PROJECT_ROOT environment variable (from process.env or session)
			// 2. args.projectRoot (explicitly provided)
			// 3. Session-based project root resolution
			// 4. Current directory fallback

			// 1. Check for TASK_MASTER_PROJECT_ROOT environment variable first
			if (process.env.TASK_MASTER_PROJECT_ROOT) {
				const envRoot = process.env.TASK_MASTER_PROJECT_ROOT;
				normalizedRoot = path.isAbsolute(envRoot)
					? envRoot
					: path.resolve(process.cwd(), envRoot);
				rootSource = 'TASK_MASTER_PROJECT_ROOT environment variable';
				log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
			}
			// Also check session environment variables for TASK_MASTER_PROJECT_ROOT
			else if (session?.env?.TASK_MASTER_PROJECT_ROOT) {
				const envRoot = session.env.TASK_MASTER_PROJECT_ROOT;
				normalizedRoot = path.isAbsolute(envRoot)
					? envRoot
					: path.resolve(process.cwd(), envRoot);
				rootSource = 'TASK_MASTER_PROJECT_ROOT session environment variable';
				log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
			}
			// 2. If no environment variable, try args.projectRoot
			else if (args.projectRoot) {
				normalizedRoot = normalizeProjectRoot(args.projectRoot, log);
				rootSource = 'args.projectRoot';
				log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
			}
			// 3. If no args.projectRoot, try session-based resolution
			else {
				const sessionRoot = getProjectRootFromSession(session, log);
				if (sessionRoot) {
					normalizedRoot = sessionRoot; // getProjectRootFromSession already normalizes
					rootSource = 'session';
					log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
				}
			}

			if (!normalizedRoot) {
				log.error(
					'Could not determine project root from environment, args, or session.'
				);
				return createErrorResponse(
					'Could not determine project root. Please provide projectRoot argument or ensure TASK_MASTER_PROJECT_ROOT environment variable is set.'
				);
			}

			// Inject the normalized root back into args
			const updatedArgs = { ...args, projectRoot: normalizedRoot };

			// Execute the original function with normalized root in args
			return await executeFn(updatedArgs, context);
		} catch (error) {
			log.error(
				`Error within withNormalizedProjectRoot HOF (Normalized Root: ${normalizedRoot}): ${error.message}`
			);
			// Add stack trace if available and debug enabled
			if (error.stack && log.debug) {
				log.debug(error.stack);
			}
			// Return a generic error or re-throw depending on desired behavior
			return createErrorResponse(`Operation failed: ${error.message}`);
		}
	};
}

// Ensure all functions are exported
export {
	getProjectRoot,
	getProjectRootFromSession,
	getTagInfo,
	handleApiResult,
	executeTaskMasterCommand,
	getCachedOrExecute,
	processMCPResponseData,
	createContentResponse,
	createErrorResponse,
	createLogWrapper,
	normalizeProjectRoot,
	getRawProjectRootFromSession,
	withNormalizedProjectRoot
};

/**
 * @fileoverview Claude Code Language Model implementation
 */

import { NoSuchModelError } from '@ai-sdk/provider';
import { generateId } from '@ai-sdk/provider-utils';
import { convertToClaudeCodeMessages } from './message-converter.js';
import { extractJson } from './json-extractor.js';
import { createAPICallError, createAuthenticationError } from './errors.js';

let query;
let AbortError;

async function loadClaudeCodeModule() {
	if (!query || !AbortError) {
		try {
			const mod = await import('@anthropic-ai/claude-code');
			query = mod.query;
			AbortError = mod.AbortError;
		} catch (err) {
			throw new Error(
				"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
			);
		}
	}
}

/**
 * @typedef {import('./types.js').ClaudeCodeSettings} ClaudeCodeSettings
 * @typedef {import('./types.js').ClaudeCodeModelId} ClaudeCodeModelId
 * @typedef {import('./types.js').ClaudeCodeLanguageModelOptions} ClaudeCodeLanguageModelOptions
 */

const modelMap = {
	opus: 'opus',
	sonnet: 'sonnet'
};

export class ClaudeCodeLanguageModel {
	specificationVersion = 'v1';
	defaultObjectGenerationMode = 'json';
	supportsImageUrls = false;
	supportsStructuredOutputs = false;

	/** @type {ClaudeCodeModelId} */
	modelId;

	/** @type {ClaudeCodeSettings} */
	settings;

	/** @type {string|undefined} */
	sessionId;

	/**
	 * @param {ClaudeCodeLanguageModelOptions} options
	 */
	constructor(options) {
		this.modelId = options.id;
		this.settings = options.settings ?? {};

		// Validate model ID format
		if (
			!this.modelId ||
			typeof this.modelId !== 'string' ||
			this.modelId.trim() === ''
		) {
			throw new NoSuchModelError({
				modelId: this.modelId,
				modelType: 'languageModel'
			});
		}
	}

	get provider() {
		return 'claude-code';
	}

	/**
	 * Get the model name for Claude Code CLI
	 * @returns {string}
	 */
	getModel() {
		const mapped = modelMap[this.modelId];
		return mapped ?? this.modelId;
	}

	/**
	 * Generate unsupported parameter warnings
	 * @param {Object} options - Generation options
	 * @returns {Array} Warnings array
	 */
	generateUnsupportedWarnings(options) {
		const warnings = [];
		const unsupportedParams = [];

		// Check for unsupported parameters
		if (options.temperature !== undefined)
			unsupportedParams.push('temperature');
		if (options.maxTokens !== undefined) unsupportedParams.push('maxTokens');
		if (options.topP !== undefined) unsupportedParams.push('topP');
		if (options.topK !== undefined) unsupportedParams.push('topK');
		if (options.presencePenalty !== undefined)
			unsupportedParams.push('presencePenalty');
		if (options.frequencyPenalty !== undefined)
			unsupportedParams.push('frequencyPenalty');
		if (options.stopSequences !== undefined && options.stopSequences.length > 0)
			unsupportedParams.push('stopSequences');
		if (options.seed !== undefined) unsupportedParams.push('seed');

		if (unsupportedParams.length > 0) {
			// Add a warning for each unsupported parameter
			for (const param of unsupportedParams) {
				warnings.push({
					type: 'unsupported-setting',
					setting: param,
					details: `Claude Code CLI does not support the ${param} parameter. It will be ignored.`
				});
			}
		}

		return warnings;
	}

	/**
	 * Generate text using Claude Code
	 * @param {Object} options - Generation options
	 * @returns {Promise<Object>}
	 */
	async doGenerate(options) {
		await loadClaudeCodeModule();
		const { messagesPrompt } = convertToClaudeCodeMessages(
			options.prompt,
			options.mode
		);

		const abortController = new AbortController();
		if (options.abortSignal) {
			options.abortSignal.addEventListener('abort', () =>
				abortController.abort()
			);
		}

		const queryOptions = {
			model: this.getModel(),
			abortController,
			resume: this.sessionId,
			pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable,
			customSystemPrompt: this.settings.customSystemPrompt,
			appendSystemPrompt: this.settings.appendSystemPrompt,
			maxTurns: this.settings.maxTurns,
			maxThinkingTokens: this.settings.maxThinkingTokens,
			cwd: this.settings.cwd,
			executable: this.settings.executable,
			executableArgs: this.settings.executableArgs,
			permissionMode: this.settings.permissionMode,
			permissionPromptToolName: this.settings.permissionPromptToolName,
			continue: this.settings.continue,
			allowedTools: this.settings.allowedTools,
			disallowedTools: this.settings.disallowedTools,
			mcpServers: this.settings.mcpServers
		};

		let text = '';
		let usage = { promptTokens: 0, completionTokens: 0 };
		let finishReason = 'stop';
		let costUsd;
		let durationMs;
		let rawUsage;
		const warnings = this.generateUnsupportedWarnings(options);

		try {
			const response = query({
				prompt: messagesPrompt,
				options: queryOptions
			});

			for await (const message of response) {
				if (message.type === 'assistant') {
					text += message.message.content
						.map((c) => (c.type === 'text' ? c.text : ''))
						.join('');
				} else if (message.type === 'result') {
					this.sessionId = message.session_id;
					costUsd = message.total_cost_usd;
					durationMs = message.duration_ms;

					if ('usage' in message) {
						rawUsage = message.usage;
						usage = {
							promptTokens:
								(message.usage.cache_creation_input_tokens ?? 0) +
								(message.usage.cache_read_input_tokens ?? 0) +
								(message.usage.input_tokens ?? 0),
							completionTokens: message.usage.output_tokens ?? 0
						};
					}

					if (message.subtype === 'error_max_turns') {
						finishReason = 'length';
					} else if (message.subtype === 'error_during_execution') {
						finishReason = 'error';
					}
				} else if (message.type === 'system' && message.subtype === 'init') {
					this.sessionId = message.session_id;
				}
			}
		} catch (error) {
			// -------------------------------------------------------------
			// Work-around for Claude-Code CLI/SDK JSON truncation bug (#913)
			// -------------------------------------------------------------
			// If the SDK throws a JSON SyntaxError *but* we already hold some
			// buffered text, assume the response was truncated by the CLI.
			// We keep the accumulated text, mark the finish reason, push a
			// provider-warning and *skip* the normal error handling so Task
			// Master can continue processing.
			const isJsonTruncation =
				error instanceof SyntaxError &&
				/JSON/i.test(error.message || '') &&
				(error.message.includes('position') ||
					error.message.includes('Unexpected end'));
			if (isJsonTruncation && text && text.length > 0) {
				warnings.push({
					type: 'provider-warning',
					details:
						'Claude Code SDK emitted a JSON parse error but Task Master recovered buffered text (possible CLI truncation).'
				});
				finishReason = 'truncated';
				// Skip re-throwing: fall through so the caller receives usable data
			} else {
				if (error instanceof AbortError) {
					throw options.abortSignal?.aborted
						? options.abortSignal.reason
						: error;
				}

				// Check for authentication errors
				if (
					error.message?.includes('not logged in') ||
					error.message?.includes('authentication') ||
					error.exitCode === 401
				) {
					throw createAuthenticationError({
						message:
							error.message ||
							'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
					});
				}

				// Wrap other errors with API call error
				throw createAPICallError({
					message: error.message || 'Claude Code CLI error',
					code: error.code,
					exitCode: error.exitCode,
					stderr: error.stderr,
					promptExcerpt: messagesPrompt.substring(0, 200),
					isRetryable: error.code === 'ENOENT' || error.code === 'ECONNREFUSED'
				});
			}
		}

		// Extract JSON if in object-json mode
		if (options.mode?.type === 'object-json' && text) {
			text = extractJson(text);
		}

		return {
			text: text || undefined,
			usage,
			finishReason,
			rawCall: {
				rawPrompt: messagesPrompt,
				rawSettings: queryOptions
			},
			warnings: warnings.length > 0 ? warnings : undefined,
			response: {
				id: generateId(),
				timestamp: new Date(),
				modelId: this.modelId
			},
			request: {
				body: messagesPrompt
			},
			providerMetadata: {
				'claude-code': {
					...(this.sessionId !== undefined && { sessionId: this.sessionId }),
					...(costUsd !== undefined && { costUsd }),
					...(durationMs !== undefined && { durationMs }),
					...(rawUsage !== undefined && { rawUsage })
				}
			}
		};
	}

	/**
	 * Stream text using Claude Code
	 * @param {Object} options - Stream options
	 * @returns {Promise<Object>}
	 */
	async doStream(options) {
		await loadClaudeCodeModule();
		const { messagesPrompt } = convertToClaudeCodeMessages(
			options.prompt,
			options.mode
		);

		const abortController = new AbortController();
		if (options.abortSignal) {
			options.abortSignal.addEventListener('abort', () =>
				abortController.abort()
			);
		}

		const queryOptions = {
			model: this.getModel(),
			abortController,
			resume: this.sessionId,
			pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable,
			customSystemPrompt: this.settings.customSystemPrompt,
			appendSystemPrompt: this.settings.appendSystemPrompt,
			maxTurns: this.settings.maxTurns,
			maxThinkingTokens: this.settings.maxThinkingTokens,
			cwd: this.settings.cwd,
			executable: this.settings.executable,
			executableArgs: this.settings.executableArgs,
			permissionMode: this.settings.permissionMode,
			permissionPromptToolName: this.settings.permissionPromptToolName,
			continue: this.settings.continue,
			allowedTools: this.settings.allowedTools,
			disallowedTools: this.settings.disallowedTools,
			mcpServers: this.settings.mcpServers
		};

		const warnings = this.generateUnsupportedWarnings(options);

		const stream = new ReadableStream({
			start: async (controller) => {
				try {
					const response = query({
						prompt: messagesPrompt,
						options: queryOptions
					});

					let usage = { promptTokens: 0, completionTokens: 0 };
					let accumulatedText = '';

					for await (const message of response) {
						if (message.type === 'assistant') {
							const text = message.message.content
								.map((c) => (c.type === 'text' ? c.text : ''))
								.join('');

							if (text) {
								accumulatedText += text;

								// In object-json mode, we need to accumulate the full text
								// and extract JSON at the end, so don't stream individual deltas
								if (options.mode?.type !== 'object-json') {
									controller.enqueue({
										type: 'text-delta',
										textDelta: text
									});
								}
							}
						} else if (message.type === 'result') {
							let rawUsage;
							if ('usage' in message) {
								rawUsage = message.usage;
								usage = {
									promptTokens:
										(message.usage.cache_creation_input_tokens ?? 0) +
										(message.usage.cache_read_input_tokens ?? 0) +
										(message.usage.input_tokens ?? 0),
									completionTokens: message.usage.output_tokens ?? 0
								};
							}

							let finishReason = 'stop';
							if (message.subtype === 'error_max_turns') {
								finishReason = 'length';
							} else if (message.subtype === 'error_during_execution') {
								finishReason = 'error';
							}

							// Store session ID in the model instance
							this.sessionId = message.session_id;

							// In object-json mode, extract JSON and send the full text at once
							if (options.mode?.type === 'object-json' && accumulatedText) {
								const extractedJson = extractJson(accumulatedText);
								controller.enqueue({
									type: 'text-delta',
									textDelta: extractedJson
								});
							}

							controller.enqueue({
								type: 'finish',
								finishReason,
								usage,
								providerMetadata: {
									'claude-code': {
										sessionId: message.session_id,
										...(message.total_cost_usd !== undefined && {
											costUsd: message.total_cost_usd
										}),
										...(message.duration_ms !== undefined && {
											durationMs: message.duration_ms
										}),
										...(rawUsage !== undefined && { rawUsage })
									}
								}
							});
						} else if (
							message.type === 'system' &&
							message.subtype === 'init'
						) {
							// Store session ID for future use
							this.sessionId = message.session_id;

							// Emit response metadata when session is initialized
							controller.enqueue({
								type: 'response-metadata',
								id: message.session_id,
								timestamp: new Date(),
								modelId: this.modelId
							});
						}
					}

					// -------------------------------------------------------------
					// Work-around for Claude-Code CLI/SDK JSON truncation bug (#913)
					// -------------------------------------------------------------
					// If we hit the SDK JSON SyntaxError but have buffered text, finalize
					// the stream gracefully instead of emitting an error.
					const isJsonTruncation =
						error instanceof SyntaxError &&
						/JSON/i.test(error.message || '') &&
						(error.message.includes('position') ||
							error.message.includes('Unexpected end'));

					if (
						isJsonTruncation &&
						accumulatedText &&
						accumulatedText.length > 0
					) {
						// Prepare final text payload
						const finalText =
							options.mode?.type === 'object-json'
								? extractJson(accumulatedText)
								: accumulatedText;

						// Emit any remaining text
						controller.enqueue({
							type: 'text-delta',
							textDelta: finalText
						});

						// Emit finish with truncated reason and warning
						controller.enqueue({
							type: 'finish',
							finishReason: 'truncated',
							usage,
							providerMetadata: { 'claude-code': { truncated: true } },
							warnings: [
								{
									type: 'provider-warning',
									details:
										'Claude Code SDK JSON truncation detected; stream recovered.'
								}
							]
						});

						controller.close();
						return; // Skip normal error path
					}

					controller.close();
				} catch (error) {
					let errorToEmit;

					if (error instanceof AbortError) {
						errorToEmit = options.abortSignal?.aborted
							? options.abortSignal.reason
							: error;
					} else if (
						error.message?.includes('not logged in') ||
						error.message?.includes('authentication') ||
						error.exitCode === 401
					) {
						errorToEmit = createAuthenticationError({
							message:
								error.message ||
								'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
						});
					} else {
						errorToEmit = createAPICallError({
							message: error.message || 'Claude Code CLI error',
							code: error.code,
							exitCode: error.exitCode,
							stderr: error.stderr,
							promptExcerpt: messagesPrompt.substring(0, 200),
							isRetryable:
								error.code === 'ENOENT' || error.code === 'ECONNREFUSED'
						});
					}

					// Emit error as a stream part
					controller.enqueue({
						type: 'error',
						error: errorToEmit
					});

					controller.close();
				}
			}
		});

		return {
			stream,
			rawCall: {
				rawPrompt: messagesPrompt,
				rawSettings: queryOptions
			},
			warnings: warnings.length > 0 ? warnings : undefined,
			request: {
				body: messagesPrompt
			}
		};
	}
}

/**
 * Profiles Utility
 * Consolidated utilities for profile detection, setup, and summary generation
 */
import fs from 'fs';
import path from 'path';
import inquirer from 'inquirer';
import chalk from 'chalk';
import boxen from 'boxen';
import { log } from '../../scripts/modules/utils.js';
import { getRulesProfile } from './rule-transformer.js';
import { RULE_PROFILES } from '../constants/profiles.js';

// =============================================================================
// PROFILE DETECTION
// =============================================================================

/**
 * Get the display name for a profile
 * @param {string} profileName - The profile name
 * @returns {string} - The display name
 */
export function getProfileDisplayName(profileName) {
	try {
		const profile = getRulesProfile(profileName);
		return profile.displayName || profileName;
	} catch (error) {
		return profileName;
	}
}

/**
 * Get installed profiles in the project directory
 * @param {string} projectRoot - Project directory path
 * @returns {string[]} - Array of installed profile names
 */
export function getInstalledProfiles(projectRoot) {
	const installedProfiles = [];

	for (const profileName of RULE_PROFILES) {
		try {
			const profile = getRulesProfile(profileName);
			const profileDir = path.join(projectRoot, profile.profileDir);

			// Check if profile directory exists (skip root directory check)
			if (profile.profileDir === '.' || fs.existsSync(profileDir)) {
				// Check if any files from the profile's fileMap exist
				const rulesDir = path.join(projectRoot, profile.rulesDir);
				if (fs.existsSync(rulesDir)) {
					const ruleFiles = Object.values(profile.fileMap);
					const hasRuleFiles = ruleFiles.some((ruleFile) =>
						fs.existsSync(path.join(rulesDir, ruleFile))
					);
					if (hasRuleFiles) {
						installedProfiles.push(profileName);
					}
				}
			}
		} catch (error) {
			// Skip profiles that can't be loaded
		}
	}

	return installedProfiles;
}

/**
 * Check if removing specified profiles would leave no profiles installed
 * @param {string} projectRoot - Project root directory
 * @param {string[]} profilesToRemove - Array of profile names to remove
 * @returns {boolean} - True if removal would leave no profiles
 */
export function wouldRemovalLeaveNoProfiles(projectRoot, profilesToRemove) {
	const installedProfiles = getInstalledProfiles(projectRoot);

	// If no profiles are currently installed, removal cannot leave no profiles
	if (installedProfiles.length === 0) {
		return false;
	}

	const remainingProfiles = installedProfiles.filter(
		(profile) => !profilesToRemove.includes(profile)
	);
	return remainingProfiles.length === 0;
}

// =============================================================================
// PROFILE SETUP
// =============================================================================

// Note: Profile choices are now generated dynamically within runInteractiveProfilesSetup()
// to ensure proper alphabetical sorting and pagination configuration

/**
 * Launches an interactive prompt for selecting which rule profiles to include in your project.
 *
 * This function dynamically lists all available profiles (from RULE_PROFILES) and presents them as checkboxes.
 * The user must select at least one profile (no defaults are pre-selected). The result is an array of selected profile names.
 *
 * Used by both project initialization (init) and the CLI 'task-master rules setup' command.
 *
 * @returns {Promise<string[]>} Array of selected profile names (e.g., ['cursor', 'windsurf'])
 */
export async function runInteractiveProfilesSetup() {
	// Generate the profile list dynamically with proper display names, alphabetized
	const profileDescriptions = RULE_PROFILES.map((profileName) => {
		const displayName = getProfileDisplayName(profileName);
		const profile = getRulesProfile(profileName);

		// Determine description based on profile capabilities
		let description;
		const hasRules = Object.keys(profile.fileMap).length > 0;
		const hasMcpConfig = profile.mcpConfig === true;

		if (!profile.includeDefaultRules) {
			// Integration guide profiles (claude, codex, gemini, opencode, zed, amp) - don't include standard coding rules
			if (profileName === 'claude') {
				description = 'Integration guide with Task Master slash commands';
			} else if (profileName === 'codex') {
				description = 'Comprehensive Task Master integration guide';
			} else if (hasMcpConfig) {
				description = 'Integration guide and MCP config';
			} else {
				description = 'Integration guide';
			}
		} else if (hasRules && hasMcpConfig) {
			// Full rule profiles with MCP config
			if (profileName === 'roo') {
				description = 'Rule profile, MCP config, and agent modes';
			} else {
				description = 'Rule profile and MCP config';
			}
		} else if (hasRules) {
			// Rule profiles without MCP config
			description = 'Rule profile';
		}

		return {
			profileName,
			displayName,
			description
		};
	}).sort((a, b) => a.displayName.localeCompare(b.displayName));

	const profileListText = profileDescriptions
		.map(
			({ displayName, description }) =>
				`${chalk.white('• ')}${chalk.yellow(displayName)}${chalk.white(` - ${description}`)}`
		)
		.join('\n');

	console.log(
		boxen(
			`${chalk.white.bold('Rule Profiles Setup')}\n\n${chalk.white(
				'Rule profiles help enforce best practices and conventions for Task Master.\n' +
					'Each profile provides coding guidelines tailored for specific AI coding environments.\n\n'
			)}${chalk.cyan('Available Profiles:')}\n${profileListText}`,
			{
				padding: 1,
				borderColor: 'blue',
				borderStyle: 'round',
				margin: { top: 1, bottom: 1 }
			}
		)
	);

	// Generate choices in the same order as the display text above
	const sortedChoices = profileDescriptions.map(
		({ profileName, displayName }) => ({
			name: displayName,
			value: profileName
		})
	);

	const ruleProfilesQuestion = {
		type: 'checkbox',
		name: 'ruleProfiles',
		message: 'Which rule profiles would you like to add to your project?',
		choices: sortedChoices,
		pageSize: sortedChoices.length, // Show all options without pagination
		loop: false, // Disable loop scrolling
		validate: (input) => input.length > 0 || 'You must select at least one.'
	};
	const { ruleProfiles } = await inquirer.prompt([ruleProfilesQuestion]);
	return ruleProfiles;
}

// =============================================================================
// PROFILE SUMMARY
// =============================================================================

/**
 * Generate appropriate summary message for a profile based on its type
 * @param {string} profileName - Name of the profile
 * @param {Object} addResult - Result object with success/failed counts
 * @returns {string} Formatted summary message
 */
export function generateProfileSummary(profileName, addResult) {
	const profileConfig = getRulesProfile(profileName);

	if (!profileConfig.includeDefaultRules) {
		// Integration guide profiles (claude, codex, gemini, amp)
		return `Summary for ${profileName}: Integration guide installed.`;
	} else {
		// Rule profiles with coding guidelines
		return `Summary for ${profileName}: ${addResult.success} files processed, ${addResult.failed} failed.`;
	}
}

/**
 * Generate appropriate summary message for profile removal
 * @param {string} profileName - Name of the profile
 * @param {Object} removeResult - Result object from removal operation
 * @returns {string} Formatted summary message
 */
export function generateProfileRemovalSummary(profileName, removeResult) {
	if (removeResult.skipped) {
		return `Summary for ${profileName}: Skipped (default or protected files)`;
	}

	if (removeResult.error && !removeResult.success) {
		return `Summary for ${profileName}: Failed to remove - ${removeResult.error}`;
	}

	const profileConfig = getRulesProfile(profileName);

	if (!profileConfig.includeDefaultRules) {
		// Integration guide profiles (claude, codex, gemini, amp)
		const baseMessage = `Summary for ${profileName}: Integration guide removed`;
		if (removeResult.notice) {
			return `${baseMessage} (${removeResult.notice})`;
		}
		return baseMessage;
	} else {
		// Rule profiles with coding guidelines
		const baseMessage = `Summary for ${profileName}: Rule profile removed`;
		if (removeResult.notice) {
			return `${baseMessage} (${removeResult.notice})`;
		}
		return baseMessage;
	}
}

/**
 * Categorize profiles and generate final summary statistics
 * @param {Array} addResults - Array of add result objects
 * @returns {Object} Object with categorized profiles and totals
 */
export function categorizeProfileResults(addResults) {
	const successfulProfiles = [];
	let totalSuccess = 0;
	let totalFailed = 0;

	addResults.forEach((r) => {
		totalSuccess += r.success;
		totalFailed += r.failed;

		// All profiles are considered successful if they completed without major errors
		if (r.success > 0 || r.failed === 0) {
			successfulProfiles.push(r.profileName);
		}
	});

	return {
		successfulProfiles,
		allSuccessfulProfiles: successfulProfiles,
		totalSuccess,
		totalFailed
	};
}

/**
 * Categorize removal results and generate final summary statistics
 * @param {Array} removalResults - Array of removal result objects
 * @returns {Object} Object with categorized removal results
 */
export function categorizeRemovalResults(removalResults) {
	const successfulRemovals = [];
	const skippedRemovals = [];
	const failedRemovals = [];
	const removalsWithNotices = [];

	removalResults.forEach((result) => {
		if (result.success) {
			successfulRemovals.push(result.profileName);
		} else if (result.skipped) {
			skippedRemovals.push(result.profileName);
		} else if (result.error) {
			failedRemovals.push(result);
		}

		if (result.notice) {
			removalsWithNotices.push(result);
		}
	});

	return {
		successfulRemovals,
		skippedRemovals,
		failedRemovals,
		removalsWithNotices
	};
}

import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';

import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js';
import { displayBanner } from '../ui.js';

/**
 * Clear subtasks from specified tasks
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} taskIds - Task IDs to clear subtasks from
 * @param {Object} context - Context object containing projectRoot and tag
 * @param {string} [context.projectRoot] - Project root path
 * @param {string} [context.tag] - Tag for the task
 */
function clearSubtasks(tasksPath, taskIds, context = {}) {
	const { projectRoot, tag } = context;
	log('info', `Reading tasks from ${tasksPath}...`);
	const data = readJSON(tasksPath, projectRoot, tag);
	if (!data || !data.tasks) {
		log('error', 'No valid tasks found.');
		process.exit(1);
	}

	if (!isSilentMode()) {
		console.log(
			boxen(chalk.white.bold('Clearing Subtasks'), {
				padding: 1,
				borderColor: 'blue',
				borderStyle: 'round',
				margin: { top: 1, bottom: 1 }
			})
		);
	}

	// Handle multiple task IDs (comma-separated)
	const taskIdArray = taskIds.split(',').map((id) => id.trim());
	let clearedCount = 0;

	// Create a summary table for the cleared subtasks
	const summaryTable = new Table({
		head: [
			chalk.cyan.bold('Task ID'),
			chalk.cyan.bold('Task Title'),
			chalk.cyan.bold('Subtasks Cleared')
		],
		colWidths: [10, 50, 20],
		style: { head: [], border: [] }
	});

	taskIdArray.forEach((taskId) => {
		const id = parseInt(taskId, 10);
		if (Number.isNaN(id)) {
			log('error', `Invalid task ID: ${taskId}`);
			return;
		}

		const task = data.tasks.find((t) => t.id === id);
		if (!task) {
			log('error', `Task ${id} not found`);
			return;
		}

		if (!task.subtasks || task.subtasks.length === 0) {
			log('info', `Task ${id} has no subtasks to clear`);
			summaryTable.push([
				id.toString(),
				truncate(task.title, 47),
				chalk.yellow('No subtasks')
			]);
			return;
		}

		const subtaskCount = task.subtasks.length;
		task.subtasks = [];
		clearedCount++;
		log('info', `Cleared ${subtaskCount} subtasks from task ${id}`);

		summaryTable.push([
			id.toString(),
			truncate(task.title, 47),
			chalk.green(`${subtaskCount} subtasks cleared`)
		]);
	});

	if (clearedCount > 0) {
		writeJSON(tasksPath, data, projectRoot, tag);

		// Show summary table
		if (!isSilentMode()) {
			console.log(
				boxen(chalk.white.bold('Subtask Clearing Summary:'), {
					padding: { left: 2, right: 2, top: 0, bottom: 0 },
					margin: { top: 1, bottom: 0 },
					borderColor: 'blue',
					borderStyle: 'round'
				})
			);
			console.log(summaryTable.toString());
		}

		// Success message
		if (!isSilentMode()) {
			console.log(
				boxen(
					chalk.green(
						`Successfully cleared subtasks from ${chalk.bold(clearedCount)} task(s)`
					),
					{
						padding: 1,
						borderColor: 'green',
						borderStyle: 'round',
						margin: { top: 1 }
					}
				)
			);

			// Next steps suggestion
			console.log(
				boxen(
					chalk.white.bold('Next Steps:') +
						'\n\n' +
						`${chalk.cyan('1.')} Run ${chalk.yellow('task-master expand --id=<id>')} to generate new subtasks\n` +
						`${chalk.cyan('2.')} Run ${chalk.yellow('task-master list --with-subtasks')} to verify changes`,
					{
						padding: 1,
						borderColor: 'cyan',
						borderStyle: 'round',
						margin: { top: 1 }
					}
				)
			);
		}
	} else {
		if (!isSilentMode()) {
			console.log(
				boxen(chalk.yellow('No subtasks were cleared'), {
					padding: 1,
					borderColor: 'yellow',
					borderStyle: 'round',
					margin: { top: 1 }
				})
			);
		}
	}
}

export default clearSubtasks;

/**
 * rules.js
 * Direct function implementation for adding or removing rules
 */

import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import {
	convertAllRulesToProfileRules,
	removeProfileRules,
	getRulesProfile,
	isValidProfile
} from '../../../../src/utils/rule-transformer.js';
import { RULE_PROFILES } from '../../../../src/constants/profiles.js';
import { RULES_ACTIONS } from '../../../../src/constants/rules-actions.js';
import {
	wouldRemovalLeaveNoProfiles,
	getInstalledProfiles
} from '../../../../src/utils/profiles.js';
import path from 'path';
import fs from 'fs';

/**
 * Direct function wrapper for adding or removing rules.
 * @param {Object} args - Command arguments
 * @param {"add"|"remove"} args.action - Action to perform: add or remove rules
 * @param {string[]} args.profiles - List of profiles to add or remove
 * @param {string} args.projectRoot - Absolute path to the project root
 * @param {boolean} [args.yes=true] - Run non-interactively
 * @param {Object} log - Logger object
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function rulesDirect(args, log, context = {}) {
	enableSilentMode();
	try {
		const { action, profiles, projectRoot, yes, force } = args;
		if (
			!action ||
			!Array.isArray(profiles) ||
			profiles.length === 0 ||
			!projectRoot
		) {
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'action, profiles, and projectRoot are required.'
				}
			};
		}

		const removalResults = [];
		const addResults = [];

		if (action === RULES_ACTIONS.REMOVE) {
			// Safety check: Ensure this won't remove all rule profiles (unless forced)
			if (!force && wouldRemovalLeaveNoProfiles(projectRoot, profiles)) {
				const installedProfiles = getInstalledProfiles(projectRoot);
				const remainingProfiles = installedProfiles.filter(
					(profile) => !profiles.includes(profile)
				);
				return {
					success: false,
					error: {
						code: 'CRITICAL_REMOVAL_BLOCKED',
						message: `CRITICAL: This operation would remove ALL remaining rule profiles (${profiles.join(', ')}), leaving your project with no rules configurations. This could significantly impact functionality. Currently installed profiles: ${installedProfiles.join(', ')}. If you're certain you want to proceed, set force: true or use the CLI with --force flag.`
					}
				};
			}

			for (const profile of profiles) {
				if (!isValidProfile(profile)) {
					removalResults.push({
						profileName: profile,
						success: false,
						error: `The requested rule profile for '${profile}' is unavailable. Supported profiles are: ${RULE_PROFILES.join(', ')}.`
					});
					continue;
				}
				const profileConfig = getRulesProfile(profile);
				const result = removeProfileRules(projectRoot, profileConfig);
				removalResults.push(result);
			}
			const successes = removalResults
				.filter((r) => r.success)
				.map((r) => r.profileName);
			const skipped = removalResults
				.filter((r) => r.skipped)
				.map((r) => r.profileName);
			const errors = removalResults.filter(
				(r) => r.error && !r.success && !r.skipped
			);
			const withNotices = removalResults.filter((r) => r.notice);

			let summary = '';
			if (successes.length > 0) {
				summary += `Successfully removed Task Master rules: ${successes.join(', ')}.`;
			}
			if (skipped.length > 0) {
				summary += `Skipped (default or protected): ${skipped.join(', ')}.`;
			}
			if (errors.length > 0) {
				summary += errors
					.map((r) => `Error removing ${r.profileName}: ${r.error}`)
					.join(' ');
			}
			if (withNotices.length > 0) {
				summary += ` Notices: ${withNotices.map((r) => `${r.profileName} - ${r.notice}`).join('; ')}.`;
			}
			disableSilentMode();
			return {
				success: errors.length === 0,
				data: { summary, results: removalResults }
			};
		} else if (action === RULES_ACTIONS.ADD) {
			for (const profile of profiles) {
				if (!isValidProfile(profile)) {
					addResults.push({
						profileName: profile,
						success: false,
						error: `Profile not found: static import missing for '${profile}'. Valid profiles: ${RULE_PROFILES.join(', ')}`
					});
					continue;
				}
				const profileConfig = getRulesProfile(profile);
				const { success, failed } = convertAllRulesToProfileRules(
					projectRoot,
					profileConfig
				);

				// Determine paths
				const rulesDir = profileConfig.rulesDir;
				const profileRulesDir = path.join(projectRoot, rulesDir);
				const profileDir = profileConfig.profileDir;
				const mcpConfig = profileConfig.mcpConfig !== false;
				const mcpPath =
					mcpConfig && profileConfig.mcpConfigPath
						? path.join(projectRoot, profileConfig.mcpConfigPath)
						: null;

				// Check what was created
				const mcpConfigCreated =
					mcpConfig && mcpPath ? fs.existsSync(mcpPath) : undefined;
				const rulesDirCreated = fs.existsSync(profileRulesDir);
				const profileFolderCreated = fs.existsSync(
					path.join(projectRoot, profileDir)
				);

				const error =
					failed > 0 ? `${failed} rule files failed to convert.` : null;
				const resultObj = {
					profileName: profile,
					mcpConfigCreated,
					rulesDirCreated,
					profileFolderCreated,
					skipped: false,
					error,
					success:
						(mcpConfig ? mcpConfigCreated : true) &&
						rulesDirCreated &&
						success > 0 &&
						!error
				};
				addResults.push(resultObj);
			}

			const successes = addResults
				.filter((r) => r.success)
				.map((r) => r.profileName);
			const errors = addResults.filter((r) => r.error && !r.success);

			let summary = '';
			if (successes.length > 0) {
				summary += `Successfully added rules: ${successes.join(', ')}.`;
			}
			if (errors.length > 0) {
				summary += errors
					.map((r) => ` Error adding ${r.profileName}: ${r.error}`)
					.join(' ');
			}
			disableSilentMode();
			return {
				success: errors.length === 0,
				data: { summary, results: addResults }
			};
		} else {
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'INVALID_ACTION',
					message: `Unknown action. Use "${RULES_ACTIONS.ADD}" or "${RULES_ACTIONS.REMOVE}".`
				}
			};
		}
	} catch (error) {
		disableSilentMode();
		log.error(`[rulesDirect] Error: ${error.message}`);
		return {
			success: false,
			error: {
				code: error.code || 'RULES_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * research.js
 * Direct function implementation for AI-powered research queries
 */

import path from 'path';
import { performResearch } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for performing AI-powered research with project context.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.query - Research query/prompt (required)
 * @param {string} [args.taskIds] - Comma-separated list of task/subtask IDs for context
 * @param {string} [args.filePaths] - Comma-separated list of file paths for context
 * @param {string} [args.customContext] - Additional custom context text
 * @param {boolean} [args.includeProjectTree=false] - Include project file tree in context
 * @param {string} [args.detailLevel='medium'] - Detail level: 'low', 'medium', 'high'
 * @param {string} [args.saveTo] - Automatically save to task/subtask ID (e.g., "15" or "15.2")
 * @param {boolean} [args.saveToFile=false] - Save research results to .taskmaster/docs/research/ directory
 * @param {string} [args.projectRoot] - Project root path
 * @param {string} [args.tag] - Tag for the task (optional)
 * @param {Object} log - Logger object
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function researchDirect(args, log, context = {}) {
	// Destructure expected args
	const {
		query,
		taskIds,
		filePaths,
		customContext,
		includeProjectTree = false,
		detailLevel = 'medium',
		saveTo,
		saveToFile = false,
		projectRoot,
		tag
	} = args;
	const { session } = context; // Destructure session from context

	// Enable silent mode to prevent console logs from interfering with JSON response
	enableSilentMode();

	// Create logger wrapper using the utility
	const mcpLog = createLogWrapper(log);

	try {
		// Check required parameters
		if (!query || typeof query !== 'string' || query.trim().length === 0) {
			log.error('Missing or invalid required parameter: query');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_PARAMETER',
					message:
						'The query parameter is required and must be a non-empty string'
				}
			};
		}

		// Parse comma-separated task IDs if provided
		const parsedTaskIds = taskIds
			? taskIds
					.split(',')
					.map((id) => id.trim())
					.filter((id) => id.length > 0)
			: [];

		// Parse comma-separated file paths if provided
		const parsedFilePaths = filePaths
			? filePaths
					.split(',')
					.map((path) => path.trim())
					.filter((path) => path.length > 0)
			: [];

		// Validate detail level
		const validDetailLevels = ['low', 'medium', 'high'];
		if (!validDetailLevels.includes(detailLevel)) {
			log.error(`Invalid detail level: ${detailLevel}`);
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'INVALID_PARAMETER',
					message: `Detail level must be one of: ${validDetailLevels.join(', ')}`
				}
			};
		}

		log.info(
			`Performing research query: "${query.substring(0, 100)}${query.length > 100 ? '...' : ''}", ` +
				`taskIds: [${parsedTaskIds.join(', ')}], ` +
				`filePaths: [${parsedFilePaths.join(', ')}], ` +
				`detailLevel: ${detailLevel}, ` +
				`includeProjectTree: ${includeProjectTree}, ` +
				`projectRoot: ${projectRoot}`
		);

		// Prepare options for the research function
		const researchOptions = {
			taskIds: parsedTaskIds,
			filePaths: parsedFilePaths,
			customContext: customContext || '',
			includeProjectTree,
			detailLevel,
			projectRoot,
			tag,
			saveToFile
		};

		// Prepare context for the research function
		const researchContext = {
			session,
			mcpLog,
			commandName: 'research',
			outputType: 'mcp'
		};

		// Call the performResearch function
		const result = await performResearch(
			query.trim(),
			researchOptions,
			researchContext,
			'json', // outputFormat - use 'json' to suppress CLI UI
			false // allowFollowUp - disable for MCP calls
		);

		// Auto-save to task/subtask if requested
		if (saveTo) {
			try {
				const isSubtask = saveTo.includes('.');

				// Format research content for saving
				const researchContent = `## Research Query: ${query.trim()}

**Detail Level:** ${result.detailLevel}
**Context Size:** ${result.contextSize} characters
**Timestamp:** ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}

### Results

${result.result}`;

				if (isSubtask) {
					// Save to subtask
					const { updateSubtaskById } = await import(
						'../../../../scripts/modules/task-manager/update-subtask-by-id.js'
					);

					const tasksPath = path.join(
						projectRoot,
						'.taskmaster',
						'tasks',
						'tasks.json'
					);
					await updateSubtaskById(
						tasksPath,
						saveTo,
						researchContent,
						false, // useResearch = false for simple append
						{
							session,
							mcpLog,
							commandName: 'research-save',
							outputType: 'mcp',
							projectRoot,
							tag
						},
						'json'
					);

					log.info(`Research saved to subtask ${saveTo}`);
				} else {
					// Save to task
					const updateTaskById = (
						await import(
							'../../../../scripts/modules/task-manager/update-task-by-id.js'
						)
					).default;

					const taskIdNum = parseInt(saveTo, 10);
					const tasksPath = path.join(
						projectRoot,
						'.taskmaster',
						'tasks',
						'tasks.json'
					);
					await updateTaskById(
						tasksPath,
						taskIdNum,
						researchContent,
						false, // useResearch = false for simple append
						{
							session,
							mcpLog,
							commandName: 'research-save',
							outputType: 'mcp',
							projectRoot,
							tag
						},
						'json',
						true // appendMode = true
					);

					log.info(`Research saved to task ${saveTo}`);
				}
			} catch (saveError) {
				log.warn(`Error saving research to task/subtask: ${saveError.message}`);
			}
		}

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				query: result.query,
				result: result.result,
				contextSize: result.contextSize,
				contextTokens: result.contextTokens,
				tokenBreakdown: result.tokenBreakdown,
				systemPromptTokens: result.systemPromptTokens,
				userPromptTokens: result.userPromptTokens,
				totalInputTokens: result.totalInputTokens,
				detailLevel: result.detailLevel,
				telemetryData: result.telemetryData,
				tagInfo: result.tagInfo,
				savedFilePath: result.savedFilePath
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in researchDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: error.code || 'RESEARCH_ERROR',
				message: error.message
			}
		};
	}
}

import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';

import {
	log,
	readJSON,
	writeJSON,
	findTaskById,
	ensureTagMetadata
} from '../utils.js';
import { displayBanner } from '../ui.js';
import { validateTaskDependencies } from '../dependency-manager.js';
import { getDebugFlag } from '../config-manager.js';
import updateSingleTaskStatus from './update-single-task-status.js';
import generateTaskFiles from './generate-task-files.js';
import {
	isValidTaskStatus,
	TASK_STATUS_OPTIONS
} from '../../../src/constants/task-status.js';

/**
 * Set the status of a task
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} taskIdInput - Task ID(s) to update
 * @param {string} newStatus - New status
 * @param {Object} options - Additional options (mcpLog for MCP mode, projectRoot for tag resolution)
 * @param {string} [options.projectRoot] - Project root path
 * @param {string} [options.tag] - Optional tag to override current tag resolution
 * @param {string} [options.mcpLog] - MCP logger object
 * @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
 */
async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
	const { projectRoot, tag } = options;
	try {
		if (!isValidTaskStatus(newStatus)) {
			throw new Error(
				`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}`
			);
		}
		// Determine if we're in MCP mode by checking for mcpLog
		const isMcpMode = !!options?.mcpLog;

		// Only display UI elements if not in MCP mode
		if (!isMcpMode) {
			console.log(
				boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), {
					padding: 1,
					borderColor: 'blue',
					borderStyle: 'round'
				})
			);
		}

		log('info', `Reading tasks from ${tasksPath}...`);

		// Read the raw data without tag resolution to preserve tagged structure
		let rawData = readJSON(tasksPath, projectRoot, tag); // No tag parameter

		// Handle the case where readJSON returns resolved data with _rawTaggedData
		if (rawData && rawData._rawTaggedData) {
			// Use the raw tagged data and discard the resolved view
			rawData = rawData._rawTaggedData;
		}

		// Ensure the tag exists in the raw data
		if (!rawData || !rawData[tag] || !Array.isArray(rawData[tag].tasks)) {
			throw new Error(
				`Invalid tasks file or tag "${tag}" not found at ${tasksPath}`
			);
		}

		// Get the tasks for the current tag
		const data = {
			tasks: rawData[tag].tasks,
			tag,
			_rawTaggedData: rawData
		};

		if (!data || !data.tasks) {
			throw new Error(`No valid tasks found in ${tasksPath}`);
		}

		// Handle multiple task IDs (comma-separated)
		const taskIds = taskIdInput.split(',').map((id) => id.trim());
		const updatedTasks = [];

		// Update each task and capture old status for display
		for (const id of taskIds) {
			// Capture old status before updating
			let oldStatus = 'unknown';

			if (id.includes('.')) {
				// Handle subtask
				const [parentId, subtaskId] = id
					.split('.')
					.map((id) => parseInt(id, 10));
				const parentTask = data.tasks.find((t) => t.id === parentId);
				if (parentTask?.subtasks) {
					const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
					oldStatus = subtask?.status || 'pending';
				}
			} else {
				// Handle regular task
				const taskId = parseInt(id, 10);
				const task = data.tasks.find((t) => t.id === taskId);
				oldStatus = task?.status || 'pending';
			}

			await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode);
			updatedTasks.push({ id, oldStatus, newStatus });
		}

		// Update the raw data structure with the modified tasks
		rawData[tag].tasks = data.tasks;

		// Ensure the tag has proper metadata
		ensureTagMetadata(rawData[tag], {
			description: `Tasks for ${tag} context`
		});

		// Write the updated raw data back to the file
		// The writeJSON function will automatically filter out _rawTaggedData
		writeJSON(tasksPath, rawData, projectRoot, tag);

		// Validate dependencies after status update
		log('info', 'Validating dependencies after status update...');
		validateTaskDependencies(data.tasks);

		// Generate individual task files
		// log('info', 'Regenerating task files...');
		// await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
		// 	mcpLog: options.mcpLog
		// });

		// Display success message - only in CLI mode
		if (!isMcpMode) {
			for (const updateInfo of updatedTasks) {
				const { id, oldStatus, newStatus: updatedStatus } = updateInfo;

				console.log(
					boxen(
						chalk.white.bold(`Successfully updated task ${id} status:`) +
							'\n' +
							`From: ${chalk.yellow(oldStatus)}\n` +
							`To:   ${chalk.green(updatedStatus)}`,
						{ padding: 1, borderColor: 'green', borderStyle: 'round' }
					)
				);
			}
		}

		// Return success value for programmatic use
		return {
			success: true,
			updatedTasks: updatedTasks.map(({ id, oldStatus, newStatus }) => ({
				id,
				oldStatus,
				newStatus
			}))
		};
	} catch (error) {
		log('error', `Error setting task status: ${error.message}`);

		// Only show error UI in CLI mode
		if (!options?.mcpLog) {
			console.error(chalk.red(`Error: ${error.message}`));

			// Pass session to getDebugFlag
			if (getDebugFlag(options?.session)) {
				// Use getter
				console.error(error);
			}

			process.exit(1);
		} else {
			// In MCP mode, throw the error for the caller to handle
			throw error;
		}
	}
}

export default setTaskStatus;

import path from 'path';
import fs from 'fs';
import inquirer from 'inquirer';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';

import {
	log,
	readJSON,
	writeJSON,
	getCurrentTag,
	resolveTag,
	getTasksForTag,
	setTasksForTag,
	findProjectRoot,
	truncate
} from '../utils.js';
import { displayBanner, getStatusWithColor } from '../ui.js';
import findNextTask from './find-next-task.js';

/**
 * Create a new tag context
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} tagName - Name of the new tag to create
 * @param {Object} options - Options object
 * @param {boolean} [options.copyFromCurrent=false] - Whether to copy tasks from current tag
 * @param {string} [options.copyFromTag] - Specific tag to copy tasks from
 * @param {string} [options.description] - Optional description for the tag
 * @param {Object} context - Context object containing session and projectRoot
 * @param {string} [context.projectRoot] - Project root path
 * @param {Object} [context.mcpLog] - MCP logger object (optional)
 * @param {string} outputFormat - Output format (text or json)
 * @returns {Promise<Object>} Result object with tag creation details
 */
async function createTag(
	tasksPath,
	tagName,
	options = {},
	context = {},
	outputFormat = 'text'
) {
	const { mcpLog, projectRoot } = context;
	const { copyFromCurrent = false, copyFromTag, description } = options;

	// Create a consistent logFn object regardless of context
	const logFn = mcpLog || {
		info: (...args) => log('info', ...args),
		warn: (...args) => log('warn', ...args),
		error: (...args) => log('error', ...args),
		debug: (...args) => log('debug', ...args),
		success: (...args) => log('success', ...args)
	};

	try {
		// Validate tag name
		if (!tagName || typeof tagName !== 'string') {
			throw new Error('Tag name is required and must be a string');
		}

		// Validate tag name format (alphanumeric, hyphens, underscores only)
		if (!/^[a-zA-Z0-9_-]+$/.test(tagName)) {
			throw new Error(
				'Tag name can only contain letters, numbers, hyphens, and underscores'
			);
		}

		// Reserved tag names
		const reservedNames = ['master', 'main', 'default'];
		if (reservedNames.includes(tagName.toLowerCase())) {
			throw new Error(`"${tagName}" is a reserved tag name`);
		}

		logFn.info(`Creating new tag: ${tagName}`);

		// Read current tasks data
		const data = readJSON(tasksPath, projectRoot);
		if (!data) {
			throw new Error(`Could not read tasks file at ${tasksPath}`);
		}

		// Use raw tagged data for tag operations - ensure we get the actual tagged structure
		let rawData;
		if (data._rawTaggedData) {
			// If we have _rawTaggedData, use it (this is the clean tagged structure)
			rawData = data._rawTaggedData;
		} else if (data.tasks && !data.master) {
			// This is legacy format - create a master tag structure
			rawData = {
				master: {
					tasks: data.tasks,
					metadata: data.metadata || {
						created: new Date().toISOString(),
						updated: new Date().toISOString(),
						description: 'Tasks live here by default'
					}
				}
			};
		} else {
			// This is already in tagged format, use it directly but exclude internal fields
			rawData = {};
			for (const [key, value] of Object.entries(data)) {
				if (key !== '_rawTaggedData' && key !== 'tag') {
					rawData[key] = value;
				}
			}
		}

		// Check if tag already exists
		if (rawData[tagName]) {
			throw new Error(`Tag "${tagName}" already exists`);
		}

		// Determine source for copying tasks (only if explicitly requested)
		let sourceTasks = [];
		if (copyFromCurrent || copyFromTag) {
			const sourceTag = copyFromTag || getCurrentTag(projectRoot);
			sourceTasks = getTasksForTag(rawData, sourceTag);

			if (copyFromTag && sourceTasks.length === 0) {
				logFn.warn(`Source tag "${copyFromTag}" not found or has no tasks`);
			}

			logFn.info(`Copying ${sourceTasks.length} tasks from tag "${sourceTag}"`);
		} else {
			logFn.info('Creating empty tag (no tasks copied)');
		}

		// Create the new tag structure in raw data
		rawData[tagName] = {
			tasks: [...sourceTasks], // Create a copy of the tasks array
			metadata: {
				created: new Date().toISOString(),
				updated: new Date().toISOString(),
				description:
					description || `Tag created on ${new Date().toLocaleDateString()}`
			}
		};

		// Create clean data for writing (exclude _rawTaggedData to prevent corruption)
		const cleanData = {};
		for (const [key, value] of Object.entries(rawData)) {
			if (key !== '_rawTaggedData') {
				cleanData[key] = value;
			}
		}

		// Write the clean data back to file with proper context to avoid tag corruption
		writeJSON(tasksPath, cleanData, projectRoot);

		logFn.success(`Successfully created tag "${tagName}"`);

		// For JSON output, return structured data
		if (outputFormat === 'json') {
			return {
				tagName,
				created: true,
				tasksCopied: sourceTasks.length,
				sourceTag:
					copyFromCurrent || copyFromTag
						? copyFromTag || getCurrentTag(projectRoot)
						: null,
				description:
					description || `Tag created on ${new Date().toLocaleDateString()}`
			};
		}

		// For text output, display success message
		if (outputFormat === 'text') {
			console.log(
				boxen(
					chalk.green.bold('✓ Tag Created Successfully') +
						`\n\nTag Name: ${chalk.cyan(tagName)}` +
						`\nTasks Copied: ${chalk.yellow(sourceTasks.length)}` +
						(copyFromCurrent || copyFromTag
							? `\nSource Tag: ${chalk.cyan(copyFromTag || getCurrentTag(projectRoot))}`
							: '') +
						(description ? `\nDescription: ${chalk.gray(description)}` : ''),
					{
						padding: 1,
						borderColor: 'green',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 }
					}
				)
			);
		}

		return {
			tagName,
			created: true,
			tasksCopied: sourceTasks.length,
			sourceTag:
				copyFromCurrent || copyFromTag
					? copyFromTag || getCurrentTag(projectRoot)
					: null,
			description:
				description || `Tag created on ${new Date().toLocaleDateString()}`
		};
	} catch (error) {
		logFn.error(`Error creating tag: ${error.message}`);
		throw error;
	}
}

/**
 * Delete an existing tag
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} tagName - Name of the tag to delete
 * @param {Object} options - Options object
 * @param {boolean} [options.yes=false] - Skip confirmation prompts
 * @param {Object} context - Context object containing session and projectRoot
 * @param {string} [context.projectRoot] - Project root path
 * @param {Object} [context.mcpLog] - MCP logger object (optional)
 * @param {string} outputFormat - Output format (text or json)
 * @returns {Promise<Object>} Result object with deletion details
 */
async function deleteTag(
	tasksPath,
	tagName,
	options = {},
	context = {},
	outputFormat = 'text'
) {
	const { mcpLog, projectRoot } = context;
	const { yes = false } = options;

	// Create a consistent logFn object regardless of context
	const logFn = mcpLog || {
		info: (...args) => log('info', ...args),
		warn: (...args) => log('warn', ...args),
		error: (...args) => log('error', ...args),
		debug: (...args) => log('debug', ...args),
		success: (...args) => log('success', ...args)
	};

	try {
		// Validate tag name
		if (!tagName || typeof tagName !== 'string') {
			throw new Error('Tag name is required and must be a string');
		}

		// Prevent deletion of master tag
		if (tagName === 'master') {
			throw new Error('Cannot delete the "master" tag');
		}

		logFn.info(`Deleting tag: ${tagName}`);

		// Read current tasks data
		const data = readJSON(tasksPath, projectRoot);
		if (!data) {
			throw new Error(`Could not read tasks file at ${tasksPath}`);
		}

		// Use raw tagged data for tag operations - ensure we get the actual tagged structure
		let rawData;
		if (data._rawTaggedData) {
			// If we have _rawTaggedData, use it (this is the clean tagged structure)
			rawData = data._rawTaggedData;
		} else if (data.tasks && !data.master) {
			// This is legacy format - create a master tag structure
			rawData = {
				master: {
					tasks: data.tasks,
					metadata: data.metadata || {
						created: new Date().toISOString(),
						updated: new Date().toISOString(),
						description: 'Tasks live here by default'
					}
				}
			};
		} else {
			// This is already in tagged format, use it directly but exclude internal fields
			rawData = {};
			for (const [key, value] of Object.entries(data)) {
				if (key !== '_rawTaggedData' && key !== 'tag') {
					rawData[key] = value;
				}
			}
		}

		// Check if tag exists
		if (!rawData[tagName]) {
			throw new Error(`Tag "${tagName}" does not exist`);
		}

		// Get current tag to check if we're deleting the active tag
		const currentTag = getCurrentTag(projectRoot);
		const isCurrentTag = currentTag === tagName;

		// Get task count for confirmation
		const tasks = getTasksForTag(rawData, tagName);
		const taskCount = tasks.length;

		// If not forced and has tasks, require confirmation (for CLI)
		if (!yes && taskCount > 0 && outputFormat === 'text') {
			console.log(
				boxen(
					chalk.yellow.bold('⚠ WARNING: Tag Deletion') +
						`\n\nYou are about to delete tag "${chalk.cyan(tagName)}"` +
						`\nThis will permanently delete ${chalk.red.bold(taskCount)} tasks` +
						'\n\nThis action cannot be undone!',
					{
						padding: 1,
						borderColor: 'yellow',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 }
					}
				)
			);

			// First confirmation
			const firstConfirm = await inquirer.prompt([
				{
					type: 'confirm',
					name: 'proceed',
					message: `Are you sure you want to delete tag "${tagName}" and its ${taskCount} tasks?`,
					default: false
				}
			]);

			if (!firstConfirm.proceed) {
				logFn.info('Tag deletion cancelled by user');
				throw new Error('Tag deletion cancelled');
			}

			// Second confirmation (double-check)
			const secondConfirm = await inquirer.prompt([
				{
					type: 'input',
					name: 'tagNameConfirm',
					message: `To confirm deletion, please type the tag name "${tagName}":`,
					validate: (input) => {
						if (input === tagName) {
							return true;
						}
						return `Please type exactly "${tagName}" to confirm deletion`;
					}
				}
			]);

			if (secondConfirm.tagNameConfirm !== tagName) {
				logFn.info('Tag deletion cancelled - incorrect tag name confirmation');
				throw new Error('Tag deletion cancelled');
			}

			logFn.info('Double confirmation received, proceeding with deletion...');
		}

		// Delete the tag
		delete rawData[tagName];

		// If we're deleting the current tag, switch to master
		if (isCurrentTag) {
			await switchCurrentTag(projectRoot, 'master');
			logFn.info('Switched current tag to "master"');
		}

		// Create clean data for writing (exclude _rawTaggedData to prevent corruption)
		const cleanData = {};
		for (const [key, value] of Object.entries(rawData)) {
			if (key !== '_rawTaggedData') {
				cleanData[key] = value;
			}
		}

		// Write the clean data back to file with proper context to avoid tag corruption
		writeJSON(tasksPath, cleanData, projectRoot);

		logFn.success(`Successfully deleted tag "${tagName}"`);

		// For JSON output, return structured data
		if (outputFormat === 'json') {
			return {
				tagName,
				deleted: true,
				tasksDeleted: taskCount,
				wasCurrentTag: isCurrentTag,
				switchedToMaster: isCurrentTag
			};
		}

		// For text output, display success message
		if (outputFormat === 'text') {
			console.log(
				boxen(
					chalk.red.bold('✓ Tag Deleted Successfully') +
						`\n\nTag Name: ${chalk.cyan(tagName)}` +
						`\nTasks Deleted: ${chalk.yellow(taskCount)}` +
						(isCurrentTag
							? `\n${chalk.yellow('⚠ Switched current tag to "master"')}`
							: ''),
					{
						padding: 1,
						borderColor: 'red',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 }
					}
				)
			);
		}

		return {
			tagName,
			deleted: true,
			tasksDeleted: taskCount,
			wasCurrentTag: isCurrentTag,
			switchedToMaster: isCurrentTag
		};
	} catch (error) {
		logFn.error(`Error deleting tag: ${error.message}`);
		throw error;
	}
}

/**
 * Enhance existing tags with metadata if they don't have it
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {Object} rawData - The raw tagged data
 * @param {Object} context - Context object
 * @returns {Promise<boolean>} True if any tags were enhanced
 */
async function enhanceTagsWithMetadata(tasksPath, rawData, context = {}) {
	let enhanced = false;

	try {
		// Get file stats for creation date fallback
		let fileCreatedDate;
		try {
			const stats = fs.statSync(tasksPath);
			fileCreatedDate =
				stats.birthtime < stats.mtime ? stats.birthtime : stats.mtime;
		} catch (error) {
			fileCreatedDate = new Date();
		}

		for (const [tagName, tagData] of Object.entries(rawData)) {
			// Skip non-tag properties
			if (
				tagName === 'tasks' ||
				tagName === 'tag' ||
				tagName === '_rawTaggedData' ||
				!tagData ||
				typeof tagData !== 'object' ||
				!Array.isArray(tagData.tasks)
			) {
				continue;
			}

			// Check if tag needs metadata enhancement
			if (!tagData.metadata) {
				tagData.metadata = {};
				enhanced = true;
			}

			// Add missing metadata fields
			if (!tagData.metadata.created) {
				tagData.metadata.created = fileCreatedDate.toISOString();
				enhanced = true;
			}

			if (!tagData.metadata.description) {
				if (tagName === 'master') {
					tagData.metadata.description = 'Tasks live here by default';
				} else {
					tagData.metadata.description = `Tag created on ${new Date(tagData.metadata.created).toLocaleDateString()}`;
				}
				enhanced = true;
			}

			// Add updated field if missing (set to created date initially)
			if (!tagData.metadata.updated) {
				tagData.metadata.updated = tagData.metadata.created;
				enhanced = true;
			}
		}

		// If we enhanced any tags, write the data back
		if (enhanced) {
			// Create clean data for writing (exclude _rawTaggedData to prevent corruption)
			const cleanData = {};
			for (const [key, value] of Object.entries(rawData)) {
				if (key !== '_rawTaggedData') {
					cleanData[key] = value;
				}
			}
			writeJSON(tasksPath, cleanData, context.projectRoot);
		}
	} catch (error) {
		// Don't throw - just log and continue
		const logFn = context.mcpLog || {
			warn: (...args) => log('warn', ...args)
		};
		logFn.warn(`Could not enhance tag metadata: ${error.message}`);
	}

	return enhanced;
}

/**
 * List all available tags with metadata
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {Object} options - Options object
 * @param {boolean} [options.showTaskCounts=true] - Whether to show task counts
 * @param {boolean} [options.showMetadata=false] - Whether to show metadata
 * @param {Object} context - Context object containing session and projectRoot
 * @param {string} [context.projectRoot] - Project root path
 * @param {Object} [context.mcpLog] - MCP logger object (optional)
 * @param {string} outputFormat - Output format (text or json)
 * @returns {Promise<Object>} Result object with tags list
 */
async function tags(
	tasksPath,
	options = {},
	context = {},
	outputFormat = 'text'
) {
	const { mcpLog, projectRoot } = context;
	const { showTaskCounts = true, showMetadata = false } = options;

	// Create a consistent logFn object regardless of context
	const logFn = mcpLog || {
		info: (...args) => log('info', ...args),
		warn: (...args) => log('warn', ...args),
		error: (...args) => log('error', ...args),
		debug: (...args) => log('debug', ...args),
		success: (...args) => log('success', ...args)
	};

	try {
		logFn.info('Listing available tags');

		// Read current tasks data
		const data = readJSON(tasksPath, projectRoot);
		if (!data) {
			throw new Error(`Could not read tasks file at ${tasksPath}`);
		}

		// Get current tag
		const currentTag = getCurrentTag(projectRoot);

		// Use raw tagged data if available, otherwise use the data directly
		const rawData = data._rawTaggedData || data;

		// Enhance existing tags with metadata if they don't have it
		await enhanceTagsWithMetadata(tasksPath, rawData, context);

		// Extract all tags
		const tagList = [];
		for (const [tagName, tagData] of Object.entries(rawData)) {
			// Skip non-tag properties (like legacy 'tasks' array, 'tag', '_rawTaggedData')
			if (
				tagName === 'tasks' ||
				tagName === 'tag' ||
				tagName === '_rawTaggedData' ||
				!tagData ||
				typeof tagData !== 'object' ||
				!Array.isArray(tagData.tasks)
			) {
				continue;
			}

			const tasks = tagData.tasks || [];
			const metadata = tagData.metadata || {};

			tagList.push({
				name: tagName,
				isCurrent: tagName === currentTag,
				completedTasks: tasks.filter(
					(t) => t.status === 'done' || t.status === 'completed'
				).length,
				tasks: tasks || [],
				created: metadata.created || 'Unknown',
				description: metadata.description || 'No description'
			});
		}

		// Sort tags: current tag first, then alphabetically
		tagList.sort((a, b) => {
			if (a.isCurrent) return -1;
			if (b.isCurrent) return 1;
			return a.name.localeCompare(b.name);
		});

		logFn.success(`Found ${tagList.length} tags`);

		// For JSON output, return structured data
		if (outputFormat === 'json') {
			return {
				tags: tagList,
				currentTag,
				totalTags: tagList.length
			};
		}

		// For text output, display formatted table
		if (outputFormat === 'text') {
			if (tagList.length === 0) {
				console.log(
					boxen(chalk.yellow('No tags found'), {
						padding: 1,
						borderColor: 'yellow',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 }
					})
				);
				return { tags: [], currentTag, totalTags: 0 };
			}

			// Create table headers based on options
			const headers = [chalk.cyan.bold('Tag Name')];
			if (showTaskCounts) {
				headers.push(chalk.cyan.bold('Tasks'));
				headers.push(chalk.cyan.bold('Completed'));
			}
			if (showMetadata) {
				headers.push(chalk.cyan.bold('Created'));
				headers.push(chalk.cyan.bold('Description'));
			}

			const table = new Table({
				head: headers,
				colWidths: showMetadata ? [20, 10, 12, 15, 50] : [25, 10, 12]
			});

			// Add rows
			tagList.forEach((tag) => {
				const row = [];

				// Tag name with current indicator
				const tagDisplay = tag.isCurrent
					? `${chalk.green('●')} ${chalk.green.bold(tag.name)} ${chalk.gray('(current)')}`
					: `  ${tag.name}`;
				row.push(tagDisplay);

				if (showTaskCounts) {
					row.push(chalk.white(tag.tasks.length.toString()));
					row.push(chalk.green(tag.completedTasks.toString()));
				}

				if (showMetadata) {
					const createdDate =
						tag.created !== 'Unknown'
							? new Date(tag.created).toLocaleDateString()
							: 'Unknown';
					row.push(chalk.gray(createdDate));
					row.push(chalk.gray(truncate(tag.description, 50)));
				}

				table.push(row);
			});

			// console.log(
			// 	boxen(
			// 		chalk.white.bold('Available Tags') +
			// 			`\n\nCurrent Tag: ${chalk.green.bold(currentTag)}`,
			// 		{
			// 			padding: { top: 0, bottom: 1, left: 1, right: 1 },
			// 			borderColor: 'blue',
			// 			borderStyle: 'round',
			// 			margin: { top: 1, bottom: 0 }
			// 		}
			// 	)
			// );

			console.log(table.toString());
		}

		return {
			tags: tagList,
			currentTag,
			totalTags: tagList.length
		};
	} catch (error) {
		logFn.error(`Error listing tags: ${error.message}`);
		throw error;
	}
}

/**
 * Switch to a different tag context
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} tagName - Name of the tag to switch to
 * @param {Object} options - Options object
 * @param {Object} context - Context object containing session and projectRoot
 * @param {string} [context.projectRoot] - Project root path
 * @param {Object} [context.mcpLog] - MCP logger object (optional)
 * @param {string} outputFormat - Output format (text or json)
 * @returns {Promise<Object>} Result object with switch details
 */
async function useTag(
	tasksPath,
	tagName,
	options = {},
	context = {},
	outputFormat = 'text'
) {
	const { mcpLog, projectRoot } = context;

	// Create a consistent logFn object regardless of context
	const logFn = mcpLog || {
		info: (...args) => log('info', ...args),
		warn: (...args) => log('warn', ...args),
		error: (...args) => log('error', ...args),
		debug: (...args) => log('debug', ...args),
		success: (...args) => log('success', ...args)
	};

	try {
		// Validate tag name
		if (!tagName || typeof tagName !== 'string') {
			throw new Error('Tag name is required and must be a string');
		}

		logFn.info(`Switching to tag: ${tagName}`);

		// Read current tasks data to verify tag exists
		const data = readJSON(tasksPath, projectRoot);
		if (!data) {
			throw new Error(`Could not read tasks file at ${tasksPath}`);
		}

		// Use raw tagged data to check if tag exists
		const rawData = data._rawTaggedData || data;

		// Check if tag exists
		if (!rawData[tagName]) {
			throw new Error(`Tag "${tagName}" does not exist`);
		}

		// Get current tag
		const previousTag = getCurrentTag(projectRoot);

		// Switch to the new tag
		await switchCurrentTag(projectRoot, tagName);

		// Get task count for the new tag - read tasks specifically for this tag
		const tagData = readJSON(tasksPath, projectRoot, tagName);
		const tasks = tagData ? tagData.tasks || [] : [];
		const taskCount = tasks.length;

		// Find the next task to work on in this tag
		const nextTask = findNextTask(tasks);

		logFn.success(`Successfully switched to tag "${tagName}"`);

		// For JSON output, return structured data
		if (outputFormat === 'json') {
			return {
				previousTag,
				currentTag: tagName,
				switched: true,
				taskCount,
				nextTask
			};
		}

		// For text output, display success message
		if (outputFormat === 'text') {
			let nextTaskInfo = '';
			if (nextTask) {
				nextTaskInfo = `\nNext Task: ${chalk.cyan(`#${nextTask.id}`)} - ${chalk.white(nextTask.title)}`;
			} else {
				nextTaskInfo = `\nNext Task: ${chalk.gray('No eligible tasks available')}`;
			}

			console.log(
				boxen(
					chalk.green.bold('✓ Tag Switched Successfully') +
						`\n\nPrevious Tag: ${chalk.cyan(previousTag)}` +
						`\nCurrent Tag: ${chalk.green.bold(tagName)}` +
						`\nAvailable Tasks: ${chalk.yellow(taskCount)}` +
						nextTaskInfo,
					{
						padding: 1,
						borderColor: 'green',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 }
					}
				)
			);
		}

		return {
			previousTag,
			currentTag: tagName,
			switched: true,
			taskCount,
			nextTask
		};
	} catch (error) {
		logFn.error(`Error switching tag: ${error.message}`);
		throw error;
	}
}

/**
 * Rename an existing tag
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} oldName - Current name of the tag
 * @param {string} newName - New name for the tag
 * @param {Object} options - Options object
 * @param {Object} context - Context object containing session and projectRoot
 * @param {string} [context.projectRoot] - Project root path
 * @param {Object} [context.mcpLog] - MCP logger object (optional)
 * @param {string} outputFormat - Output format (text or json)
 * @returns {Promise<Object>} Result object with rename details
 */
async function renameTag(
	tasksPath,
	oldName,
	newName,
	options = {},
	context = {},
	outputFormat = 'text'
) {
	const { mcpLog, projectRoot } = context;

	// Create a consistent logFn object regardless of context
	const logFn = mcpLog || {
		info: (...args) => log('info', ...args),
		warn: (...args) => log('warn', ...args),
		error: (...args) => log('error', ...args),
		debug: (...args) => log('debug', ...args),
		success: (...args) => log('success', ...args)
	};

	try {
		// Validate parameters
		if (!oldName || typeof oldName !== 'string') {
			throw new Error('Old tag name is required and must be a string');
		}
		if (!newName || typeof newName !== 'string') {
			throw new Error('New tag name is required and must be a string');
		}

		// Validate new tag name format
		if (!/^[a-zA-Z0-9_-]+$/.test(newName)) {
			throw new Error(
				'New tag name can only contain letters, numbers, hyphens, and underscores'
			);
		}

		// Prevent renaming master tag
		if (oldName === 'master') {
			throw new Error('Cannot rename the "master" tag');
		}

		// Reserved tag names
		const reservedNames = ['master', 'main', 'default'];
		if (reservedNames.includes(newName.toLowerCase())) {
			throw new Error(`"${newName}" is a reserved tag name`);
		}

		logFn.info(`Renaming tag from "${oldName}" to "${newName}"`);

		// Read current tasks data
		const data = readJSON(tasksPath, projectRoot);
		if (!data) {
			throw new Error(`Could not read tasks file at ${tasksPath}`);
		}

		// Use raw tagged data for tag operations
		const rawData = data._rawTaggedData || data;

		// Check if old tag exists
		if (!rawData[oldName]) {
			throw new Error(`Tag "${oldName}" does not exist`);
		}

		// Check if new tag name already exists
		if (rawData[newName]) {
			throw new Error(`Tag "${newName}" already exists`);
		}

		// Get current tag to check if we're renaming the active tag
		const currentTag = getCurrentTag(projectRoot);
		const isCurrentTag = currentTag === oldName;

		// Rename the tag by copying data and deleting old
		rawData[newName] = { ...rawData[oldName] };

		// Update metadata if it exists
		if (rawData[newName].metadata) {
			rawData[newName].metadata.renamed = {
				from: oldName,
				date: new Date().toISOString()
			};
		}

		delete rawData[oldName];

		// If we're renaming the current tag, update the current tag reference
		if (isCurrentTag) {
			await switchCurrentTag(projectRoot, newName);
			logFn.info(`Updated current tag reference to "${newName}"`);
		}

		// Create clean data for writing (exclude _rawTaggedData to prevent corruption)
		const cleanData = {};
		for (const [key, value] of Object.entries(rawData)) {
			if (key !== '_rawTaggedData') {
				cleanData[key] = value;
			}
		}

		// Write the clean data back to file with proper context to avoid tag corruption
		writeJSON(tasksPath, cleanData, projectRoot);

		// Get task count
		const tasks = getTasksForTag(rawData, newName);
		const taskCount = tasks.length;

		logFn.success(`Successfully renamed tag from "${oldName}" to "${newName}"`);

		// For JSON output, return structured data
		if (outputFormat === 'json') {
			return {
				oldName,
				newName,
				renamed: true,
				taskCount,
				wasCurrentTag: isCurrentTag,
				isCurrentTag: isCurrentTag
			};
		}

		// For text output, display success message
		if (outputFormat === 'text') {
			console.log(
				boxen(
					chalk.green.bold('✓ Tag Renamed Successfully') +
						`\n\nOld Name: ${chalk.cyan(oldName)}` +
						`\nNew Name: ${chalk.green.bold(newName)}` +
						`\nTasks: ${chalk.yellow(taskCount)}` +
						(isCurrentTag ? `\n${chalk.green('✓ Current tag updated')}` : ''),
					{
						padding: 1,
						borderColor: 'green',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 }
					}
				)
			);
		}

		return {
			oldName,
			newName,
			renamed: true,
			taskCount,
			wasCurrentTag: isCurrentTag,
			isCurrentTag: isCurrentTag
		};
	} catch (error) {
		logFn.error(`Error renaming tag: ${error.message}`);
		throw error;
	}
}

/**
 * Copy an existing tag to create a new tag with the same tasks
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} sourceName - Name of the source tag to copy from
 * @param {string} targetName - Name of the new tag to create
 * @param {Object} options - Options object
 * @param {string} [options.description] - Optional description for the new tag
 * @param {Object} context - Context object containing session and projectRoot
 * @param {string} [context.projectRoot] - Project root path
 * @param {Object} [context.mcpLog] - MCP logger object (optional)
 * @param {string} outputFormat - Output format (text or json)
 * @returns {Promise<Object>} Result object with copy details
 */
async function copyTag(
	tasksPath,
	sourceName,
	targetName,
	options = {},
	context = {},
	outputFormat = 'text'
) {
	const { mcpLog, projectRoot } = context;
	const { description } = options;

	// Create a consistent logFn object regardless of context
	const logFn = mcpLog || {
		info: (...args) => log('info', ...args),
		warn: (...args) => log('warn', ...args),
		error: (...args) => log('error', ...args),
		debug: (...args) => log('debug', ...args),
		success: (...args) => log('success', ...args)
	};

	try {
		// Validate parameters
		if (!sourceName || typeof sourceName !== 'string') {
			throw new Error('Source tag name is required and must be a string');
		}
		if (!targetName || typeof targetName !== 'string') {
			throw new Error('Target tag name is required and must be a string');
		}

		// Validate target tag name format
		if (!/^[a-zA-Z0-9_-]+$/.test(targetName)) {
			throw new Error(
				'Target tag name can only contain letters, numbers, hyphens, and underscores'
			);
		}

		// Reserved tag names
		const reservedNames = ['master', 'main', 'default'];
		if (reservedNames.includes(targetName.toLowerCase())) {
			throw new Error(`"${targetName}" is a reserved tag name`);
		}

		logFn.info(`Copying tag from "${sourceName}" to "${targetName}"`);

		// Read current tasks data
		const data = readJSON(tasksPath, projectRoot);
		if (!data) {
			throw new Error(`Could not read tasks file at ${tasksPath}`);
		}

		// Use raw tagged data for tag operations
		const rawData = data._rawTaggedData || data;

		// Check if source tag exists
		if (!rawData[sourceName]) {
			throw new Error(`Source tag "${sourceName}" does not exist`);
		}

		// Check if target tag already exists
		if (rawData[targetName]) {
			throw new Error(`Target tag "${targetName}" already exists`);
		}

		// Get source tasks
		const sourceTasks = getTasksForTag(rawData, sourceName);

		// Create deep copy of the source tag data
		rawData[targetName] = {
			tasks: JSON.parse(JSON.stringify(sourceTasks)), // Deep copy tasks
			metadata: {
				created: new Date().toISOString(),
				updated: new Date().toISOString(),
				description:
					description ||
					`Copy of "${sourceName}" created on ${new Date().toLocaleDateString()}`,
				copiedFrom: {
					tag: sourceName,
					date: new Date().toISOString()
				}
			}
		};

		// Create clean data for writing (exclude _rawTaggedData to prevent corruption)
		const cleanData = {};
		for (const [key, value] of Object.entries(rawData)) {
			if (key !== '_rawTaggedData') {
				cleanData[key] = value;
			}
		}

		// Write the clean data back to file with proper context to avoid tag corruption
		writeJSON(tasksPath, cleanData, projectRoot);

		logFn.success(
			`Successfully copied tag from "${sourceName}" to "${targetName}"`
		);

		// For JSON output, return structured data
		if (outputFormat === 'json') {
			return {
				sourceName,
				targetName,
				copied: true,
				description:
					description ||
					`Copy of "${sourceName}" created on ${new Date().toLocaleDateString()}`
			};
		}

		// For text output, display success message
		if (outputFormat === 'text') {
			console.log(
				boxen(
					chalk.green.bold('✓ Tag Copied Successfully') +
						`\n\nSource Tag: ${chalk.cyan(sourceName)}` +
						`\nTarget Tag: ${chalk.green.bold(targetName)}` +
						`\nTasks Copied: ${chalk.yellow(sourceTasks.length)}` +
						(description ? `\nDescription: ${chalk.gray(description)}` : ''),
					{
						padding: 1,
						borderColor: 'green',
						borderStyle: 'round',
						margin: { top: 1, bottom: 1 }
					}
				)
			);
		}

		return {
			sourceName,
			targetName,
			copied: true,
			description:
				description ||
				`Copy of "${sourceName}" created on ${new Date().toLocaleDateString()}`
		};
	} catch (error) {
		logFn.error(`Error copying tag: ${error.message}`);
		throw error;
	}
}

/**
 * Helper function to switch the current tag in state.json
 * @param {string} projectRoot - Project root directory
 * @param {string} tagName - Name of the tag to switch to
 * @returns {Promise<void>}
 */
async function switchCurrentTag(projectRoot, tagName) {
	try {
		const statePath = path.join(projectRoot, '.taskmaster', 'state.json');

		// Read current state or create default
		let state = {};
		if (fs.existsSync(statePath)) {
			const rawState = fs.readFileSync(statePath, 'utf8');
			state = JSON.parse(rawState);
		}

		// Update current tag and timestamp
		state.currentTag = tagName;
		state.lastSwitched = new Date().toISOString();

		// Ensure other required state properties exist
		if (!state.branchTagMapping) {
			state.branchTagMapping = {};
		}
		if (state.migrationNoticeShown === undefined) {
			state.migrationNoticeShown = false;
		}

		// Write updated state
		fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
	} catch (error) {
		log('warn', `Could not update current tag in state.json: ${error.message}`);
		// Don't throw - this is not critical for tag operations
	}
}

/**
 * Update branch-tag mapping in state.json
 * @param {string} projectRoot - Project root directory
 * @param {string} branchName - Git branch name
 * @param {string} tagName - Tag name to map to
 * @returns {Promise<void>}
 */
async function updateBranchTagMapping(projectRoot, branchName, tagName) {
	try {
		const statePath = path.join(projectRoot, '.taskmaster', 'state.json');

		// Read current state or create default
		let state = {};
		if (fs.existsSync(statePath)) {
			const rawState = fs.readFileSync(statePath, 'utf8');
			state = JSON.parse(rawState);
		}

		// Ensure branchTagMapping exists
		if (!state.branchTagMapping) {
			state.branchTagMapping = {};
		}

		// Update the mapping
		state.branchTagMapping[branchName] = tagName;

		// Write updated state
		fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
	} catch (error) {
		log('warn', `Could not update branch-tag mapping: ${error.message}`);
		// Don't throw - this is not critical for tag operations
	}
}

/**
 * Get tag name for a git branch from state.json mapping
 * @param {string} projectRoot - Project root directory
 * @param {string} branchName - Git branch name
 * @returns {Promise<string|null>} Mapped tag name or null if not found
 */
async function getTagForBranch(projectRoot, branchName) {
	try {
		const statePath = path.join(projectRoot, '.taskmaster', 'state.json');

		if (!fs.existsSync(statePath)) {
			return null;
		}

		const rawState = fs.readFileSync(statePath, 'utf8');
		const state = JSON.parse(rawState);

		return state.branchTagMapping?.[branchName] || null;
	} catch (error) {
		return null;
	}
}

/**
 * Create a tag from a git branch name
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} branchName - Git branch name to create tag from
 * @param {Object} options - Options object
 * @param {boolean} [options.copyFromCurrent] - Copy tasks from current tag
 * @param {string} [options.copyFromTag] - Copy tasks from specific tag
 * @param {string} [options.description] - Custom description for the tag
 * @param {boolean} [options.autoSwitch] - Automatically switch to the new tag
 * @param {Object} context - Context object containing session and projectRoot
 * @param {string} [context.projectRoot] - Project root path
 * @param {Object} [context.mcpLog] - MCP logger object (optional)
 * @param {string} outputFormat - Output format (text or json)
 * @returns {Promise<Object>} Result object with creation details
 */
async function createTagFromBranch(
	tasksPath,
	branchName,
	options = {},
	context = {},
	outputFormat = 'text'
) {
	const { mcpLog, projectRoot } = context;
	const { copyFromCurrent, copyFromTag, description, autoSwitch } = options;

	// Import git utilities
	const { sanitizeBranchNameForTag, isValidBranchForTag } = await import(
		'../utils/git-utils.js'
	);

	// Create a consistent logFn object regardless of context
	const logFn = mcpLog || {
		info: (...args) => log('info', ...args),
		warn: (...args) => log('warn', ...args),
		error: (...args) => log('error', ...args),
		debug: (...args) => log('debug', ...args),
		success: (...args) => log('success', ...args)
	};

	try {
		// Validate branch name
		if (!branchName || typeof branchName !== 'string') {
			throw new Error('Branch name is required and must be a string');
		}

		// Check if branch name is valid for tag creation
		if (!isValidBranchForTag(branchName)) {
			throw new Error(
				`Branch "${branchName}" cannot be converted to a valid tag name`
			);
		}

		// Sanitize branch name to create tag name
		const tagName = sanitizeBranchNameForTag(branchName);

		logFn.info(`Creating tag "${tagName}" from git branch "${branchName}"`);

		// Create the tag using existing createTag function
		const createResult = await createTag(
			tasksPath,
			tagName,
			{
				copyFromCurrent,
				copyFromTag,
				description:
					description || `Tag created from git branch "${branchName}"`
			},
			context,
			outputFormat
		);

		// Update branch-tag mapping
		await updateBranchTagMapping(projectRoot, branchName, tagName);
		logFn.info(`Updated branch-tag mapping: ${branchName} -> ${tagName}`);

		// Auto-switch to the new tag if requested
		if (autoSwitch) {
			await switchCurrentTag(projectRoot, tagName);
			logFn.info(`Automatically switched to tag "${tagName}"`);
		}

		// For JSON output, return structured data
		if (outputFormat === 'json') {
			return {
				...createResult,
				branchName,
				tagName,
				mappingUpdated: true,
				autoSwitched: autoSwitch || false
			};
		}

		// For text output, the createTag function already handles display
		return {
			branchName,
			tagName,
			created: true,
			mappingUpdated: true,
			autoSwitched: autoSwitch || false
		};
	} catch (error) {
		logFn.error(`Error creating tag from branch: ${error.message}`);
		throw error;
	}
}

/**
 * Automatically switch tag based on current git branch
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {Object} options - Options object
 * @param {boolean} [options.createIfMissing] - Create tag if it doesn't exist
 * @param {boolean} [options.copyFromCurrent] - Copy tasks when creating new tag
 * @param {Object} context - Context object containing session and projectRoot
 * @param {string} [context.projectRoot] - Project root path
 * @param {Object} [context.mcpLog] - MCP logger object (optional)
 * @param {string} outputFormat - Output format (text or json)
 * @returns {Promise<Object>} Result object with switch details
 */
async function autoSwitchTagForBranch(
	tasksPath,
	options = {},
	context = {},
	outputFormat = 'text'
) {
	const { mcpLog, projectRoot } = context;
	const { createIfMissing, copyFromCurrent } = options;

	// Import git utilities
	const {
		getCurrentBranch,
		isGitRepository,
		sanitizeBranchNameForTag,
		isValidBranchForTag
	} = await import('../utils/git-utils.js');

	// Create a consistent logFn object regardless of context
	const logFn = mcpLog || {
		info: (...args) => log('info', ...args),
		warn: (...args) => log('warn', ...args),
		error: (...args) => log('error', ...args),
		debug: (...args) => log('debug', ...args),
		success: (...args) => log('success', ...args)
	};

	try {
		// Check if we're in a git repository
		if (!(await isGitRepository(projectRoot))) {
			logFn.warn('Not in a git repository, cannot auto-switch tags');
			return { switched: false, reason: 'not_git_repo' };
		}

		// Get current git branch
		const currentBranch = await getCurrentBranch(projectRoot);
		if (!currentBranch) {
			logFn.warn('Could not determine current git branch');
			return { switched: false, reason: 'no_current_branch' };
		}

		logFn.info(`Current git branch: ${currentBranch}`);

		// Check if branch is valid for tag creation
		if (!isValidBranchForTag(currentBranch)) {
			logFn.info(`Branch "${currentBranch}" is not suitable for tag creation`);
			return {
				switched: false,
				reason: 'invalid_branch_for_tag',
				branchName: currentBranch
			};
		}

		// Check if there's already a mapping for this branch
		let tagName = await getTagForBranch(projectRoot, currentBranch);

		if (!tagName) {
			// No mapping exists, create tag name from branch
			tagName = sanitizeBranchNameForTag(currentBranch);
		}

		// Check if tag exists
		const data = readJSON(tasksPath, projectRoot);
		const rawData = data._rawTaggedData || data;
		const tagExists = rawData[tagName];

		if (!tagExists && createIfMissing) {
			// Create the tag from branch
			logFn.info(`Creating new tag "${tagName}" for branch "${currentBranch}"`);

			const createResult = await createTagFromBranch(
				tasksPath,
				currentBranch,
				{
					copyFromCurrent,
					autoSwitch: true
				},
				context,
				outputFormat
			);

			return {
				switched: true,
				created: true,
				branchName: currentBranch,
				tagName,
				...createResult
			};
		} else if (tagExists) {
			// Tag exists, switch to it
			logFn.info(
				`Switching to existing tag "${tagName}" for branch "${currentBranch}"`
			);

			const switchResult = await useTag(
				tasksPath,
				tagName,
				{},
				context,
				outputFormat
			);

			// Update mapping if it didn't exist
			if (!(await getTagForBranch(projectRoot, currentBranch))) {
				await updateBranchTagMapping(projectRoot, currentBranch, tagName);
			}

			return {
				switched: true,
				created: false,
				branchName: currentBranch,
				tagName,
				...switchResult
			};
		} else {
			// Tag doesn't exist and createIfMissing is false
			logFn.warn(
				`Tag "${tagName}" for branch "${currentBranch}" does not exist`
			);
			return {
				switched: false,
				reason: 'tag_not_found',
				branchName: currentBranch,
				tagName
			};
		}
	} catch (error) {
		logFn.error(`Error in auto-switch tag for branch: ${error.message}`);
		throw error;
	}
}

/**
 * Check git workflow configuration and perform auto-switch if enabled
 * @param {string} projectRoot - Project root directory
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {Object} context - Context object
 * @returns {Promise<Object|null>} Switch result or null if not enabled
 */
async function checkAndAutoSwitchTag(projectRoot, tasksPath, context = {}) {
	try {
		// Read configuration
		const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
		if (!fs.existsSync(configPath)) {
			return null;
		}

		const rawConfig = fs.readFileSync(configPath, 'utf8');
		const config = JSON.parse(rawConfig);

		// Git workflow has been removed - return null to disable auto-switching
		return null;

		// Perform auto-switch
		return await autoSwitchTagForBranch(
			tasksPath,
			{ createIfMissing: true, copyFromCurrent: false },
			context,
			'json'
		);
	} catch (error) {
		// Silently fail - this is not critical
		return null;
	}
}

// Export all tag management functions
export {
	createTag,
	deleteTag,
	tags,
	useTag,
	renameTag,
	copyTag,
	switchCurrentTag,
	updateBranchTagMapping,
	getTagForBranch,
	createTagFromBranch,
	autoSwitchTagForBranch,
	checkAndAutoSwitchTag
};

/**
 * show-task.js
 * Direct function implementation for showing task details
 */

import {
	findTaskById,
	readComplexityReport,
	readJSON
} from '../../../../scripts/modules/utils.js';
import { findTasksPath } from '../utils/path-utils.js';

/**
 * Direct function wrapper for getting task details.
 *
 * @param {Object} args - Command arguments.
 * @param {string} args.id - Task ID to show.
 * @param {string} [args.file] - Optional path to the tasks file (passed to findTasksPath).
 * @param {string} args.reportPath - Explicit path to the complexity report file.
 * @param {string} [args.status] - Optional status to filter subtasks by.
 * @param {string} args.projectRoot - Absolute path to the project root directory (already normalized by tool).
 * @param {string} [args.tag] - Tag for the task
 * @param {Object} log - Logger object.
 * @param {Object} context - Context object containing session data.
 * @returns {Promise<Object>} - Result object with success status and data/error information.
 */
export async function showTaskDirect(args, log) {
	// This function doesn't need session context since it only reads data
	// Destructure projectRoot and other args. projectRoot is assumed normalized.
	const { id, file, reportPath, status, projectRoot, tag } = args;

	log.info(
		`Showing task direct function. ID: ${id}, File: ${file}, Status Filter: ${status}, ProjectRoot: ${projectRoot}`
	);

	// --- Path Resolution using the passed (already normalized) projectRoot ---
	let tasksJsonPath;
	try {
		// Use the projectRoot passed directly from args
		tasksJsonPath = findTasksPath(
			{ projectRoot: projectRoot, file: file },
			log
		);
		log.info(`Resolved tasks path: ${tasksJsonPath}`);
	} catch (error) {
		log.error(`Error finding tasks.json: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'TASKS_FILE_NOT_FOUND',
				message: `Failed to find tasks.json: ${error.message}`
			}
		};
	}
	// --- End Path Resolution ---

	// --- Rest of the function remains the same, using tasksJsonPath ---
	try {
		const tasksData = readJSON(tasksJsonPath, projectRoot, tag);
		if (!tasksData || !tasksData.tasks) {
			return {
				success: false,
				error: { code: 'INVALID_TASKS_DATA', message: 'Invalid tasks data' }
			};
		}

		const complexityReport = readComplexityReport(reportPath);

		// Parse comma-separated IDs
		const taskIds = id
			.split(',')
			.map((taskId) => taskId.trim())
			.filter((taskId) => taskId.length > 0);

		if (taskIds.length === 0) {
			return {
				success: false,
				error: {
					code: 'INVALID_TASK_ID',
					message: 'No valid task IDs provided'
				}
			};
		}

		// Handle single task ID (existing behavior)
		if (taskIds.length === 1) {
			const { task, originalSubtaskCount } = findTaskById(
				tasksData.tasks,
				taskIds[0],
				complexityReport,
				status
			);

			if (!task) {
				return {
					success: false,
					error: {
						code: 'TASK_NOT_FOUND',
						message: `Task or subtask with ID ${taskIds[0]} not found`
					}
				};
			}

			log.info(`Successfully retrieved task ${taskIds[0]}.`);

			const returnData = { ...task };
			if (originalSubtaskCount !== null) {
				returnData._originalSubtaskCount = originalSubtaskCount;
				returnData._subtaskFilter = status;
			}

			return { success: true, data: returnData };
		}

		// Handle multiple task IDs
		const foundTasks = [];
		const notFoundIds = [];

		taskIds.forEach((taskId) => {
			const { task, originalSubtaskCount } = findTaskById(
				tasksData.tasks,
				taskId,
				complexityReport,
				status
			);

			if (task) {
				const taskData = { ...task };
				if (originalSubtaskCount !== null) {
					taskData._originalSubtaskCount = originalSubtaskCount;
					taskData._subtaskFilter = status;
				}
				foundTasks.push(taskData);
			} else {
				notFoundIds.push(taskId);
			}
		});

		log.info(
			`Successfully retrieved ${foundTasks.length} of ${taskIds.length} requested tasks.`
		);

		// Return multiple tasks with metadata
		return {
			success: true,
			data: {
				tasks: foundTasks,
				requestedIds: taskIds,
				foundCount: foundTasks.length,
				notFoundIds: notFoundIds,
				isMultiple: true
			}
		};
	} catch (error) {
		log.error(`Error showing task ${id}: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'TASK_OPERATION_ERROR',
				message: error.message
			}
		};
	}
}

import { log, readJSON, isSilentMode, findProjectRoot } from '../utils.js';
import {
	startLoadingIndicator,
	stopLoadingIndicator,
	displayAiUsageSummary
} from '../ui.js';
import expandTask from './expand-task.js';
import { getDebugFlag } from '../config-manager.js';
import { aggregateTelemetry } from '../utils.js';
import chalk from 'chalk';
import boxen from 'boxen';

/**
 * Expand all eligible pending or in-progress tasks using the expandTask function.
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {number} [numSubtasks] - Optional: Target number of subtasks per task.
 * @param {boolean} [useResearch=false] - Whether to use the research AI role.
 * @param {string} [additionalContext=''] - Optional additional context.
 * @param {boolean} [force=false] - Force expansion even if tasks already have subtasks.
 * @param {Object} context - Context object containing session and mcpLog.
 * @param {Object} [context.session] - Session object from MCP.
 * @param {Object} [context.mcpLog] - MCP logger object.
 * @param {string} [context.projectRoot] - Project root path
 * @param {string} [context.tag] - Tag for the task
 * @param {string} [context.complexityReportPath] - Path to the complexity report file
 * @param {string} [outputFormat='text'] - Output format ('text' or 'json'). MCP calls should use 'json'.
 * @returns {Promise<{success: boolean, expandedCount: number, failedCount: number, skippedCount: number, tasksToExpand: number, telemetryData: Array<Object>}>} - Result summary.
 */
async function expandAllTasks(
	tasksPath,
	numSubtasks, // Keep this signature, expandTask handles defaults
	useResearch = false,
	additionalContext = '',
	force = false, // Keep force here for the filter logic
	context = {},
	outputFormat = 'text' // Assume text default for CLI
) {
	const {
		session,
		mcpLog,
		projectRoot: providedProjectRoot,
		tag,
		complexityReportPath
	} = context;
	const isMCPCall = !!mcpLog; // Determine if called from MCP

	const projectRoot = providedProjectRoot || findProjectRoot();
	if (!projectRoot) {
		throw new Error('Could not determine project root directory');
	}

	// Use mcpLog if available, otherwise use the default console log wrapper respecting silent mode
	const logger =
		mcpLog ||
		(outputFormat === 'json'
			? {
					// Basic logger for JSON output mode
					info: (msg) => {},
					warn: (msg) => {},
					error: (msg) => console.error(`ERROR: ${msg}`), // Still log errors
					debug: (msg) => {}
				}
			: {
					// CLI logger respecting silent mode
					info: (msg) => !isSilentMode() && log('info', msg),
					warn: (msg) => !isSilentMode() && log('warn', msg),
					error: (msg) => !isSilentMode() && log('error', msg),
					debug: (msg) =>
						!isSilentMode() && getDebugFlag(session) && log('debug', msg)
				});

	let loadingIndicator = null;
	let expandedCount = 0;
	let failedCount = 0;
	let tasksToExpandCount = 0;
	const allTelemetryData = []; // Still collect individual data first

	if (!isMCPCall && outputFormat === 'text') {
		loadingIndicator = startLoadingIndicator(
			'Analyzing tasks for expansion...'
		);
	}

	try {
		logger.info(`Reading tasks from ${tasksPath}`);
		const data = readJSON(tasksPath, projectRoot, tag);
		if (!data || !data.tasks) {
			throw new Error(`Invalid tasks data in ${tasksPath}`);
		}

		// --- Restore Original Filtering Logic ---
		const tasksToExpand = data.tasks.filter(
			(task) =>
				(task.status === 'pending' || task.status === 'in-progress') && // Include 'in-progress'
				(!task.subtasks || task.subtasks.length === 0 || force) // Check subtasks/force here
		);
		tasksToExpandCount = tasksToExpand.length; // Get the count from the filtered array
		logger.info(`Found ${tasksToExpandCount} tasks eligible for expansion.`);
		// --- End Restored Filtering Logic ---

		if (loadingIndicator) {
			stopLoadingIndicator(loadingIndicator, 'Analysis complete.');
		}

		if (tasksToExpandCount === 0) {
			logger.info('No tasks eligible for expansion.');
			// --- Fix: Restore success: true and add message ---
			return {
				success: true, // Indicate overall success despite no action
				expandedCount: 0,
				failedCount: 0,
				skippedCount: 0,
				tasksToExpand: 0,
				telemetryData: allTelemetryData,
				message: 'No tasks eligible for expansion.'
			};
			// --- End Fix ---
		}

		// Iterate over the already filtered tasks
		for (const task of tasksToExpand) {
			// Start indicator for individual task expansion in CLI mode
			let taskIndicator = null;
			if (!isMCPCall && outputFormat === 'text') {
				taskIndicator = startLoadingIndicator(`Expanding task ${task.id}...`);
			}

			try {
				// Call the refactored expandTask function AND capture result
				const result = await expandTask(
					tasksPath,
					task.id,
					numSubtasks,
					useResearch,
					additionalContext,
					{
						...context,
						projectRoot,
						tag: data.tag || tag,
						complexityReportPath
					}, // Pass the whole context object with projectRoot and resolved tag
					force
				);
				expandedCount++;

				// Collect individual telemetry data
				if (result && result.telemetryData) {
					allTelemetryData.push(result.telemetryData);
				}

				if (taskIndicator) {
					stopLoadingIndicator(taskIndicator, `Task ${task.id} expanded.`);
				}
				logger.info(`Successfully expanded task ${task.id}.`);
			} catch (error) {
				failedCount++;
				if (taskIndicator) {
					stopLoadingIndicator(
						taskIndicator,
						`Failed to expand task ${task.id}.`,
						false
					);
				}
				logger.error(`Failed to expand task ${task.id}: ${error.message}`);
				// Continue to the next task
			}
		}

		// --- AGGREGATION AND DISPLAY ---
		logger.info(
			`Expansion complete: ${expandedCount} expanded, ${failedCount} failed.`
		);

		// Aggregate the collected telemetry data
		const aggregatedTelemetryData = aggregateTelemetry(
			allTelemetryData,
			'expand-all-tasks'
		);

		if (outputFormat === 'text') {
			const summaryContent =
				`${chalk.white.bold('Expansion Summary:')}\n\n` +
				`${chalk.cyan('-')} Attempted: ${chalk.bold(tasksToExpandCount)}\n` +
				`${chalk.green('-')} Expanded:  ${chalk.bold(expandedCount)}\n` +
				// Skipped count is always 0 now due to pre-filtering
				`${chalk.gray('-')} Skipped:   ${chalk.bold(0)}\n` +
				`${chalk.red('-')} Failed:    ${chalk.bold(failedCount)}`;

			console.log(
				boxen(summaryContent, {
					padding: 1,
					margin: { top: 1 },
					borderColor: failedCount > 0 ? 'red' : 'green', // Red if failures, green otherwise
					borderStyle: 'round'
				})
			);
		}

		if (outputFormat === 'text' && aggregatedTelemetryData) {
			displayAiUsageSummary(aggregatedTelemetryData, 'cli');
		}

		// Return summary including the AGGREGATED telemetry data
		return {
			success: true,
			expandedCount,
			failedCount,
			skippedCount: 0,
			tasksToExpand: tasksToExpandCount,
			telemetryData: aggregatedTelemetryData
		};
	} catch (error) {
		if (loadingIndicator)
			stopLoadingIndicator(loadingIndicator, 'Error.', false);
		logger.error(`Error during expand all operation: ${error.message}`);
		if (!isMCPCall && getDebugFlag(session)) {
			console.error(error); // Log full stack in debug CLI mode
		}
		// Re-throw error for the caller to handle, the direct function will format it
		throw error; // Let direct function wrapper handle formatting
		/* Original re-throw:
		throw new Error(`Failed to expand all tasks: ${error.message}`);
		*/
	}
}

export default expandAllTasks;

#!/usr/bin/env node

/**
 * Task Master
 * Copyright (c) 2025 Eyal Toledano, Ralph Khreish
 *
 * This software is licensed under the MIT License with Commons Clause.
 * You may use this software for any purpose, including commercial applications,
 * and modify and redistribute it freely, subject to the following restrictions:
 *
 * 1. You may not sell this software or offer it as a service.
 * 2. The origin of this software must not be misrepresented.
 * 3. Altered source versions must be plainly marked as such.
 *
 * For the full license text, see the LICENSE file in the root directory.
 */

/**
 * Claude Task Master CLI
 * Main entry point for globally installed package
 */

import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { createRequire } from 'module';
import { spawn } from 'child_process';
import { Command } from 'commander';
import { displayHelp, displayBanner } from '../scripts/modules/ui.js';
import { registerCommands } from '../scripts/modules/commands.js';
import { detectCamelCaseFlags } from '../scripts/modules/utils.js';
import chalk from 'chalk';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);

// Get package information
const packageJson = require('../package.json');
const version = packageJson.version;

// Get paths to script files
const devScriptPath = resolve(__dirname, '../scripts/dev.js');
const initScriptPath = resolve(__dirname, '../scripts/init.js');

// Helper function to run dev.js with arguments
function runDevScript(args) {
	// Debug: Show the transformed arguments when DEBUG=1 is set
	if (process.env.DEBUG === '1') {
		console.error('\nDEBUG - CLI Wrapper Analysis:');
		console.error('- Original command: ' + process.argv.join(' '));
		console.error('- Transformed args: ' + args.join(' '));
		console.error(
			'- dev.js will receive: node ' +
				devScriptPath +
				' ' +
				args.join(' ') +
				'\n'
		);
	}

	// For testing: If TEST_MODE is set, just print args and exit
	if (process.env.TEST_MODE === '1') {
		console.log('Would execute:');
		console.log(`node ${devScriptPath} ${args.join(' ')}`);
		process.exit(0);
		return;
	}

	const child = spawn('node', [devScriptPath, ...args], {
		stdio: 'inherit',
		cwd: process.cwd()
	});

	child.on('close', (code) => {
		process.exit(code);
	});
}

// Helper function to detect camelCase and convert to kebab-case
const toKebabCase = (str) => str.replace(/([A-Z])/g, '-$1').toLowerCase();

/**
 * Create a wrapper action that passes the command to dev.js
 * @param {string} commandName - The name of the command
 * @returns {Function} Wrapper action function
 */
function createDevScriptAction(commandName) {
	return (options, cmd) => {
		// Check for camelCase flags and error out with helpful message
		const camelCaseFlags = detectCamelCaseFlags(process.argv);

		// If camelCase flags were found, show error and exit
		if (camelCaseFlags.length > 0) {
			console.error('\nError: Please use kebab-case for CLI flags:');
			camelCaseFlags.forEach((flag) => {
				console.error(`  Instead of: --${flag.original}`);
				console.error(`  Use:        --${flag.kebabCase}`);
			});
			console.error(
				'\nExample: task-master parse-prd --num-tasks=5 instead of --numTasks=5\n'
			);
			process.exit(1);
		}

		// Since we've ensured no camelCase flags, we can now just:
		// 1. Start with the command name
		const args = [commandName];

		// 3. Get positional arguments and explicit flags from the command line
		const commandArgs = [];
		const positionals = new Set(); // Track positional args we've seen

		// Find the command in raw process.argv to extract args
		const commandIndex = process.argv.indexOf(commandName);
		if (commandIndex !== -1) {
			// Process all args after the command name
			for (let i = commandIndex + 1; i < process.argv.length; i++) {
				const arg = process.argv[i];

				if (arg.startsWith('--')) {
					// It's a flag - pass through as is
					commandArgs.push(arg);
					// Skip the next arg if this is a flag with a value (not --flag=value format)
					if (
						!arg.includes('=') &&
						i + 1 < process.argv.length &&
						!process.argv[i + 1].startsWith('--')
					) {
						commandArgs.push(process.argv[++i]);
					}
				} else if (!positionals.has(arg)) {
					// It's a positional argument we haven't seen
					commandArgs.push(arg);
					positionals.add(arg);
				}
			}
		}

		// Add all command line args we collected
		args.push(...commandArgs);

		// 4. Add default options from Commander if not specified on command line
		// Track which options we've seen on the command line
		const userOptions = new Set();
		for (const arg of commandArgs) {
			if (arg.startsWith('--')) {
				// Extract option name (without -- and value)
				const name = arg.split('=')[0].slice(2);
				userOptions.add(name);

				// Add the kebab-case version too, to prevent duplicates
				const kebabName = name.replace(/([A-Z])/g, '-$1').toLowerCase();
				userOptions.add(kebabName);

				// Add the camelCase version as well
				const camelName = kebabName.replace(/-([a-z])/g, (_, letter) =>
					letter.toUpperCase()
				);
				userOptions.add(camelName);
			}
		}

		// Add Commander-provided defaults for options not specified by user
		Object.entries(options).forEach(([key, value]) => {
			// Debug output to see what keys we're getting
			if (process.env.DEBUG === '1') {
				console.error(`DEBUG - Processing option: ${key} = ${value}`);
			}

			// Special case for numTasks > num-tasks (a known problem case)
			if (key === 'numTasks') {
				if (process.env.DEBUG === '1') {
					console.error('DEBUG - Converting numTasks to num-tasks');
				}
				if (!userOptions.has('num-tasks') && !userOptions.has('numTasks')) {
					args.push(`--num-tasks=${value}`);
				}
				return;
			}

			// Skip built-in Commander properties and options the user provided
			if (
				['parent', 'commands', 'options', 'rawArgs'].includes(key) ||
				userOptions.has(key)
			) {
				return;
			}

			// Also check the kebab-case version of this key
			const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
			if (userOptions.has(kebabKey)) {
				return;
			}

			// Add default values, using kebab-case for the parameter name
			if (value !== undefined) {
				if (typeof value === 'boolean') {
					if (value === true) {
						args.push(`--${kebabKey}`);
					} else if (value === false && key === 'generate') {
						args.push('--skip-generate');
					}
				} else {
					// Always use kebab-case for option names
					args.push(`--${kebabKey}=${value}`);
				}
			}
		});

		// Special handling for parent parameter (uses -p)
		if (options.parent && !args.includes('-p') && !userOptions.has('parent')) {
			args.push('-p', options.parent);
		}

		// Debug output for troubleshooting
		if (process.env.DEBUG === '1') {
			console.error('DEBUG - Command args:', commandArgs);
			console.error('DEBUG - User options:', Array.from(userOptions));
			console.error('DEBUG - Commander options:', options);
			console.error('DEBUG - Final args:', args);
		}

		// Run the script with our processed args
		runDevScript(args);
	};
}

// // Special case for the 'init' command which uses a different script
// function registerInitCommand(program) {
// 	program
// 		.command('init')
// 		.description('Initialize a new project')
// 		.option('-y, --yes', 'Skip prompts and use default values')
// 		.option('-n, --name <name>', 'Project name')
// 		.option('-d, --description <description>', 'Project description')
// 		.option('-v, --version <version>', 'Project version')
// 		.option('-a, --author <author>', 'Author name')
// 		.option('--skip-install', 'Skip installing dependencies')
// 		.option('--dry-run', 'Show what would be done without making changes')
// 		.action((options) => {
// 			// Pass through any options to the init script
// 			const args = [
// 				'--yes',
// 				'name',
// 				'description',
// 				'version',
// 				'author',
// 				'skip-install',
// 				'dry-run'
// 			]
// 				.filter((opt) => options[opt])
// 				.map((opt) => {
// 					if (opt === 'yes' || opt === 'skip-install' || opt === 'dry-run') {
// 						return `--${opt}`;
// 					}
// 					return `--${opt}=${options[opt]}`;
// 				});

// 			const child = spawn('node', [initScriptPath, ...args], {
// 				stdio: 'inherit',
// 				cwd: process.cwd()
// 			});

// 			child.on('close', (code) => {
// 				process.exit(code);
// 			});
// 		});
// }

// Set up the command-line interface
const program = new Command();

program
	.name('task-master')
	.description('Claude Task Master CLI')
	.version(version)
	.addHelpText('afterAll', () => {
		// Use the same help display function as dev.js for consistency
		displayHelp();
		return ''; // Return empty string to prevent commander's default help
	});

// Add custom help option to directly call our help display
program.helpOption('-h, --help', 'Display help information');
program.on('--help', () => {
	displayHelp();
});

// // Add special case commands
// registerInitCommand(program);

program
	.command('dev')
	.description('Run the dev.js script')
	.action(() => {
		const args = process.argv.slice(process.argv.indexOf('dev') + 1);
		runDevScript(args);
	});

// Use a temporary Command instance to get all command definitions
const tempProgram = new Command();
registerCommands(tempProgram);

// For each command in the temp instance, add a modified version to our actual program
tempProgram.commands.forEach((cmd) => {
	if (['dev'].includes(cmd.name())) {
		// Skip commands we've already defined specially
		return;
	}

	// Create a new command with the same name and description
	const newCmd = program.command(cmd.name()).description(cmd.description());

	// Copy all options
	cmd.options.forEach((opt) => {
		newCmd.option(opt.flags, opt.description, opt.defaultValue);
	});

	// Set the action to proxy to dev.js
	newCmd.action(createDevScriptAction(cmd.name()));
});

// Parse the command line arguments
program.parse(process.argv);

// Add global error handling for unknown commands and options
process.on('uncaughtException', (err) => {
	// Check if this is a commander.js unknown option error
	if (err.code === 'commander.unknownOption') {
		const option = err.message.match(/'([^']+)'/)?.[1];
		const commandArg = process.argv.find(
			(arg) =>
				!arg.startsWith('-') &&
				arg !== 'task-master' &&
				!arg.includes('/') &&
				arg !== 'node'
		);
		const command = commandArg || 'unknown';

		console.error(chalk.red(`Error: Unknown option '${option}'`));
		console.error(
			chalk.yellow(
				`Run 'task-master ${command} --help' to see available options for this command`
			)
		);
		process.exit(1);
	}

	// Check if this is a commander.js unknown command error
	if (err.code === 'commander.unknownCommand') {
		const command = err.message.match(/'([^']+)'/)?.[1];

		console.error(chalk.red(`Error: Unknown command '${command}'`));
		console.error(
			chalk.yellow(`Run 'task-master --help' to see available commands`)
		);
		process.exit(1);
	}

	// Handle other uncaught exceptions
	console.error(chalk.red(`Error: ${err.message}`));
	if (process.env.DEBUG === '1') {
		console.error(err);
	}
	process.exit(1);
});

// Show help if no command was provided (just 'task-master' with no args)
if (process.argv.length <= 2) {
	displayBanner();
	displayHelp();
	process.exit(0);
}

// Add exports at the end of the file
export { detectCamelCaseFlags };

/**
 * Task Master
 * Copyright (c) 2025 Eyal Toledano, Ralph Khreish
 *
 * This software is licensed under the MIT License with Commons Clause.
 * You may use this software for any purpose, including commercial applications,
 * and modify and redistribute it freely, subject to the following restrictions:
 *
 * 1. You may not sell this software or offer it as a service.
 * 2. The origin of this software must not be misrepresented.
 * 3. Altered source versions must be plainly marked as such.
 *
 * For the full license text, see the LICENSE file in the root directory.
 */

import fs from 'fs';
import path from 'path';
import readline from 'readline';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import chalk from 'chalk';
import figlet from 'figlet';
import boxen from 'boxen';
import gradient from 'gradient-string';
import { isSilentMode } from './modules/utils.js';
import { insideGitWorkTree } from './modules/utils/git-utils.js';
import { manageGitignoreFile } from '../src/utils/manage-gitignore.js';
import { RULE_PROFILES } from '../src/constants/profiles.js';
import {
	convertAllRulesToProfileRules,
	getRulesProfile
} from '../src/utils/rule-transformer.js';
import { updateConfigMaxTokens } from './modules/update-config-tokens.js';

import { execSync } from 'child_process';
import {
	EXAMPLE_PRD_FILE,
	TASKMASTER_CONFIG_FILE,
	TASKMASTER_TEMPLATES_DIR,
	TASKMASTER_DIR,
	TASKMASTER_TASKS_DIR,
	TASKMASTER_DOCS_DIR,
	TASKMASTER_REPORTS_DIR,
	TASKMASTER_STATE_FILE,
	ENV_EXAMPLE_FILE,
	GITIGNORE_FILE
} from '../src/constants/paths.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Define log levels
const LOG_LEVELS = {
	debug: 0,
	info: 1,
	warn: 2,
	error: 3,
	success: 4
};

// Determine log level from environment variable or default to 'info'
const LOG_LEVEL = process.env.TASKMASTER_LOG_LEVEL
	? LOG_LEVELS[process.env.TASKMASTER_LOG_LEVEL.toLowerCase()]
	: LOG_LEVELS.info; // Default to info

// Create a color gradient for the banner
const coolGradient = gradient(['#00b4d8', '#0077b6', '#03045e']);
const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']);

// Display a fancy banner
function displayBanner() {
	if (isSilentMode()) return;

	console.clear();
	const bannerText = figlet.textSync('Task Master AI', {
		font: 'Standard',
		horizontalLayout: 'default',
		verticalLayout: 'default'
	});

	console.log(coolGradient(bannerText));

	// Add creator credit line below the banner
	console.log(
		chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano')
	);

	console.log(
		boxen(chalk.white(`${chalk.bold('Initializing')} your new project`), {
			padding: 1,
			margin: { top: 0, bottom: 1 },
			borderStyle: 'round',
			borderColor: 'cyan'
		})
	);
}

// Logging function with icons and colors
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_LEVEL) {
		const icon = icons[level] || '';

		// Only output to console if not in silent mode
		if (!isSilentMode()) {
			if (level === 'error') {
				console.error(icon, chalk.red(...args));
			} else if (level === 'warn') {
				console.warn(icon, chalk.yellow(...args));
			} else if (level === 'success') {
				console.log(icon, chalk.green(...args));
			} else if (level === 'info') {
				console.log(icon, chalk.blue(...args));
			} else {
				console.log(icon, ...args);
			}
		}
	}

	// Write to debug log if DEBUG=true
	if (process.env.DEBUG === 'true') {
		const logMessage = `[${level.toUpperCase()}] ${args.join(' ')}\n`;
		fs.appendFileSync('init-debug.log', logMessage);
	}
}

// Function to create directory if it doesn't exist
function ensureDirectoryExists(dirPath) {
	if (!fs.existsSync(dirPath)) {
		fs.mkdirSync(dirPath, { recursive: true });
		log('info', `Created directory: ${dirPath}`);
	}
}

// Function to add shell aliases to the user's shell configuration
function addShellAliases() {
	const homeDir = process.env.HOME || process.env.USERPROFILE;
	let shellConfigFile;

	// Determine which shell config file to use
	if (process.env.SHELL?.includes('zsh')) {
		shellConfigFile = path.join(homeDir, '.zshrc');
	} else if (process.env.SHELL?.includes('bash')) {
		shellConfigFile = path.join(homeDir, '.bashrc');
	} else {
		log('warn', 'Could not determine shell type. Aliases not added.');
		return false;
	}

	try {
		// Check if file exists
		if (!fs.existsSync(shellConfigFile)) {
			log(
				'warn',
				`Shell config file ${shellConfigFile} not found. Aliases not added.`
			);
			return false;
		}

		// Check if aliases already exist
		const configContent = fs.readFileSync(shellConfigFile, 'utf8');
		if (configContent.includes("alias tm='task-master'")) {
			log('info', 'Task Master aliases already exist in shell config.');
			return true;
		}

		// Add aliases to the shell config file
		const aliasBlock = `
# Task Master aliases added on ${new Date().toLocaleDateString()}
alias tm='task-master'
alias taskmaster='task-master'
`;

		fs.appendFileSync(shellConfigFile, aliasBlock);
		log('success', `Added Task Master aliases to ${shellConfigFile}`);
		log(
			'info',
			`To use the aliases in your current terminal, run: source ${shellConfigFile}`
		);

		return true;
	} catch (error) {
		log('error', `Failed to add aliases: ${error.message}`);
		return false;
	}
}

// Function to create initial state.json file for tag management
function createInitialStateFile(targetDir) {
	const stateFilePath = path.join(targetDir, TASKMASTER_STATE_FILE);

	// Check if state.json already exists
	if (fs.existsSync(stateFilePath)) {
		log('info', 'State file already exists, preserving current configuration');
		return;
	}

	// Create initial state configuration
	const initialState = {
		currentTag: 'master',
		lastSwitched: new Date().toISOString(),
		branchTagMapping: {},
		migrationNoticeShown: false
	};

	try {
		fs.writeFileSync(stateFilePath, JSON.stringify(initialState, null, 2));
		log('success', `Created initial state file: ${stateFilePath}`);
		log('info', 'Default tag set to "master" for task organization');
	} catch (error) {
		log('error', `Failed to create state file: ${error.message}`);
	}
}

// Function to copy a file from the package to the target directory
function copyTemplateFile(templateName, targetPath, replacements = {}) {
	// Get the file content from the appropriate source directory
	let sourcePath;

	// Map template names to their actual source paths
	switch (templateName) {
		// case 'scripts_README.md':
		// 	sourcePath = path.join(__dirname, '..', 'assets', 'scripts_README.md');
		// 	break;
		// case 'README-task-master.md':
		// 	sourcePath = path.join(__dirname, '..', 'README-task-master.md');
		// 	break;
		default:
			// For other files like env.example, gitignore, etc. that don't have direct equivalents
			sourcePath = path.join(__dirname, '..', 'assets', templateName);
	}

	// Check if the source file exists
	if (!fs.existsSync(sourcePath)) {
		// Fall back to templates directory for files that might not have been moved yet
		sourcePath = path.join(__dirname, '..', 'assets', templateName);
		if (!fs.existsSync(sourcePath)) {
			log('error', `Source file not found: ${sourcePath}`);
			return;
		}
	}

	let content = fs.readFileSync(sourcePath, 'utf8');

	// Replace placeholders with actual values
	Object.entries(replacements).forEach(([key, value]) => {
		const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
		content = content.replace(regex, value);
	});

	// Handle special files that should be merged instead of overwritten
	if (fs.existsSync(targetPath)) {
		const filename = path.basename(targetPath);

		// Handle .gitignore - append lines that don't exist
		if (filename === '.gitignore') {
			log('info', `${targetPath} already exists, merging content...`);
			const existingContent = fs.readFileSync(targetPath, 'utf8');
			const existingLines = new Set(
				existingContent.split('\n').map((line) => line.trim())
			);
			const newLines = content
				.split('\n')
				.filter((line) => !existingLines.has(line.trim()));

			if (newLines.length > 0) {
				// Add a comment to separate the original content from our additions
				const updatedContent = `${existingContent.trim()}\n\n# Added by Task Master AI\n${newLines.join('\n')}`;
				fs.writeFileSync(targetPath, updatedContent);
				log('success', `Updated ${targetPath} with additional entries`);
			} else {
				log('info', `No new content to add to ${targetPath}`);
			}
			return;
		}

		// Handle README.md - offer to preserve or create a different file
		if (filename === 'README-task-master.md') {
			log('info', `${targetPath} already exists`);
			// Create a separate README file specifically for this project
			const taskMasterReadmePath = path.join(
				path.dirname(targetPath),
				'README-task-master.md'
			);
			fs.writeFileSync(taskMasterReadmePath, content);
			log(
				'success',
				`Created ${taskMasterReadmePath} (preserved original README-task-master.md)`
			);
			return;
		}

		// For other files, warn and prompt before overwriting
		log('warn', `${targetPath} already exists, skipping.`);
		return;
	}

	// If the file doesn't exist, create it normally
	fs.writeFileSync(targetPath, content);
	log('info', `Created file: ${targetPath}`);
}

// Main function to initialize a new project
async function initializeProject(options = {}) {
	// Receives options as argument
	// Only display banner if not in silent mode
	if (!isSilentMode()) {
		displayBanner();
	}

	// Debug logging only if not in silent mode
	// if (!isSilentMode()) {
	// 	console.log('===== DEBUG: INITIALIZE PROJECT OPTIONS RECEIVED =====');
	// 	console.log('Full options object:', JSON.stringify(options));
	// 	console.log('options.yes:', options.yes);
	// 	console.log('==================================================');
	// }

	// Handle boolean aliases flags
	if (options.aliases === true) {
		options.addAliases = true; // --aliases flag provided
	} else if (options.aliases === false) {
		options.addAliases = false; // --no-aliases flag provided
	}
	// If options.aliases and options.noAliases are undefined, we'll prompt for it

	// Handle boolean git flags
	if (options.git === true) {
		options.initGit = true; // --git flag provided
	} else if (options.git === false) {
		options.initGit = false; // --no-git flag provided
	}
	// If options.git and options.noGit are undefined, we'll prompt for it

	// Handle boolean gitTasks flags
	if (options.gitTasks === true) {
		options.storeTasksInGit = true; // --git-tasks flag provided
	} else if (options.gitTasks === false) {
		options.storeTasksInGit = false; // --no-git-tasks flag provided
	}
	// If options.gitTasks and options.noGitTasks are undefined, we'll prompt for it

	const skipPrompts = options.yes || (options.name && options.description);

	// if (!isSilentMode()) {
	// 	console.log('Skip prompts determined:', skipPrompts);
	// }

	let selectedRuleProfiles;
	if (options.rulesExplicitlyProvided) {
		// If --rules flag was used, always respect it.
		log(
			'info',
			`Using rule profiles provided via command line: ${options.rules.join(', ')}`
		);
		selectedRuleProfiles = options.rules;
	} else if (skipPrompts) {
		// If non-interactive (e.g., --yes) and no rules specified, default to ALL.
		log(
			'info',
			`No rules specified in non-interactive mode, defaulting to all profiles.`
		);
		selectedRuleProfiles = RULE_PROFILES;
	} else {
		// If interactive and no rules specified, default to NONE.
		// The 'rules --setup' wizard will handle selection.
		log(
			'info',
			'No rules specified; interactive setup will be launched to select profiles.'
		);
		selectedRuleProfiles = [];
	}

	if (skipPrompts) {
		if (!isSilentMode()) {
			console.log('SKIPPING PROMPTS - Using defaults or provided values');
		}

		// Use provided options or defaults
		const projectName = options.name || 'task-master-project';
		const projectDescription =
			options.description || 'A project managed with Task Master AI';
		const projectVersion = options.version || '0.1.0';
		const authorName = options.author || 'Vibe coder';
		const dryRun = options.dryRun || false;
		const addAliases =
			options.addAliases !== undefined ? options.addAliases : true; // Default to true if not specified
		const initGit = options.initGit !== undefined ? options.initGit : true; // Default to true if not specified
		const storeTasksInGit =
			options.storeTasksInGit !== undefined ? options.storeTasksInGit : true; // Default to true if not specified

		if (dryRun) {
			log('info', 'DRY RUN MODE: No files will be modified');
			log('info', 'Would initialize Task Master project');
			log('info', 'Would create/update necessary project files');

			// Show flag-specific behavior
			log(
				'info',
				`${addAliases ? 'Would add shell aliases (tm, taskmaster)' : 'Would skip shell aliases'}`
			);
			log(
				'info',
				`${initGit ? 'Would initialize Git repository' : 'Would skip Git initialization'}`
			);
			log(
				'info',
				`${storeTasksInGit ? 'Would store tasks in Git' : 'Would exclude tasks from Git'}`
			);

			return {
				dryRun: true
			};
		}

		createProjectStructure(
			addAliases,
			initGit,
			storeTasksInGit,
			dryRun,
			options,
			selectedRuleProfiles
		);
	} else {
		// Interactive logic
		log('info', 'Required options not provided, proceeding with prompts.');

		try {
			const rl = readline.createInterface({
				input: process.stdin,
				output: process.stdout
			});
			// Prompt for shell aliases (skip if --aliases or --no-aliases flag was provided)
			let addAliasesPrompted = true; // Default to true
			if (options.addAliases !== undefined) {
				addAliasesPrompted = options.addAliases; // Use flag value if provided
			} else {
				const addAliasesInput = await promptQuestion(
					rl,
					chalk.cyan(
						'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
					)
				);
				addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
			}

			// Prompt for Git initialization (skip if --git or --no-git flag was provided)
			let initGitPrompted = true; // Default to true
			if (options.initGit !== undefined) {
				initGitPrompted = options.initGit; // Use flag value if provided
			} else {
				const gitInitInput = await promptQuestion(
					rl,
					chalk.cyan('Initialize a Git repository in project root? (Y/n): ')
				);
				initGitPrompted = gitInitInput.trim().toLowerCase() !== 'n';
			}

			// Prompt for Git tasks storage (skip if --git-tasks or --no-git-tasks flag was provided)
			let storeGitPrompted = true; // Default to true
			if (options.storeTasksInGit !== undefined) {
				storeGitPrompted = options.storeTasksInGit; // Use flag value if provided
			} else {
				const gitTasksInput = await promptQuestion(
					rl,
					chalk.cyan(
						'Store tasks in Git (tasks.json and tasks/ directory)? (Y/n): '
					)
				);
				storeGitPrompted = gitTasksInput.trim().toLowerCase() !== 'n';
			}

			// Confirm settings...
			console.log('\nTask Master Project settings:');
			console.log(
				chalk.blue(
					'Add shell aliases (so you can use "tm" instead of "task-master"):'
				),
				chalk.white(addAliasesPrompted ? 'Yes' : 'No')
			);
			console.log(
				chalk.blue('Initialize Git repository in project root:'),
				chalk.white(initGitPrompted ? 'Yes' : 'No')
			);
			console.log(
				chalk.blue('Store tasks in Git (tasks.json and tasks/ directory):'),
				chalk.white(storeGitPrompted ? 'Yes' : 'No')
			);

			const confirmInput = await promptQuestion(
				rl,
				chalk.yellow('\nDo you want to continue with these settings? (Y/n): ')
			);
			const shouldContinue = confirmInput.trim().toLowerCase() !== 'n';

			if (!shouldContinue) {
				rl.close();
				log('info', 'Project initialization cancelled by user');
				process.exit(0);
				return;
			}

			// Only run interactive rules if rules flag not provided via command line
			if (options.rulesExplicitlyProvided) {
				log(
					'info',
					`Using rule profiles provided via command line: ${selectedRuleProfiles.join(', ')}`
				);
			}

			const dryRun = options.dryRun || false;

			if (dryRun) {
				log('info', 'DRY RUN MODE: No files will be modified');
				log('info', 'Would initialize Task Master project');
				log('info', 'Would create/update necessary project files');

				// Show flag-specific behavior
				log(
					'info',
					`${addAliasesPrompted ? 'Would add shell aliases (tm, taskmaster)' : 'Would skip shell aliases'}`
				);
				log(
					'info',
					`${initGitPrompted ? 'Would initialize Git repository' : 'Would skip Git initialization'}`
				);
				log(
					'info',
					`${storeGitPrompted ? 'Would store tasks in Git' : 'Would exclude tasks from Git'}`
				);

				return {
					dryRun: true
				};
			}

			// Create structure using only necessary values
			createProjectStructure(
				addAliasesPrompted,
				initGitPrompted,
				storeGitPrompted,
				dryRun,
				options,
				selectedRuleProfiles
			);
			rl.close();
		} catch (error) {
			if (rl) {
				rl.close();
			}
			log('error', `Error during initialization process: ${error.message}`);
			process.exit(1);
		}
	}
}

// Helper function to promisify readline question
function promptQuestion(rl, question) {
	return new Promise((resolve) => {
		rl.question(question, (answer) => {
			resolve(answer);
		});
	});
}

// Function to create the project structure
function createProjectStructure(
	addAliases,
	initGit,
	storeTasksInGit,
	dryRun,
	options,
	selectedRuleProfiles = RULE_PROFILES
) {
	const targetDir = process.cwd();
	log('info', `Initializing project in ${targetDir}`);

	// Create NEW .taskmaster directory structure (using constants)
	ensureDirectoryExists(path.join(targetDir, TASKMASTER_DIR));
	ensureDirectoryExists(path.join(targetDir, TASKMASTER_TASKS_DIR));
	ensureDirectoryExists(path.join(targetDir, TASKMASTER_DOCS_DIR));
	ensureDirectoryExists(path.join(targetDir, TASKMASTER_REPORTS_DIR));
	ensureDirectoryExists(path.join(targetDir, TASKMASTER_TEMPLATES_DIR));

	// Create initial state.json file for tag management
	createInitialStateFile(targetDir);

	// Copy template files with replacements
	const replacements = {
		year: new Date().getFullYear()
	};

	// Helper function to create rule profiles
	function _processSingleProfile(profileName) {
		const profile = getRulesProfile(profileName);
		if (profile) {
			convertAllRulesToProfileRules(targetDir, profile);
			// Also triggers MCP config setup (if applicable)
		} else {
			log('warn', `Unknown rule profile: ${profileName}`);
		}
	}

	// Copy .env.example
	copyTemplateFile(
		'env.example',
		path.join(targetDir, ENV_EXAMPLE_FILE),
		replacements
	);

	// Copy config.json with project name to NEW location
	copyTemplateFile(
		'config.json',
		path.join(targetDir, TASKMASTER_CONFIG_FILE),
		{
			...replacements
		}
	);

	// Update config.json with correct maxTokens values from supported-models.json
	const configPath = path.join(targetDir, TASKMASTER_CONFIG_FILE);
	if (updateConfigMaxTokens(configPath)) {
		log('info', 'Updated config with correct maxTokens values');
	} else {
		log('warn', 'Could not update maxTokens in config');
	}

	// Copy .gitignore with GitTasks preference
	try {
		const gitignoreTemplatePath = path.join(
			__dirname,
			'..',
			'assets',
			'gitignore'
		);
		const templateContent = fs.readFileSync(gitignoreTemplatePath, 'utf8');
		manageGitignoreFile(
			path.join(targetDir, GITIGNORE_FILE),
			templateContent,
			storeTasksInGit,
			log
		);
	} catch (error) {
		log('error', `Failed to create .gitignore: ${error.message}`);
	}

	// Copy example_prd.txt to NEW location
	copyTemplateFile('example_prd.txt', path.join(targetDir, EXAMPLE_PRD_FILE));

	// Initialize git repository if git is available
	try {
		if (initGit === false) {
			log('info', 'Git initialization skipped due to --no-git flag.');
		} else if (initGit === true) {
			if (insideGitWorkTree()) {
				log(
					'info',
					'Existing Git repository detected – skipping git init despite --git flag.'
				);
			} else {
				log('info', 'Initializing Git repository due to --git flag...');
				execSync('git init', { cwd: targetDir, stdio: 'ignore' });
				log('success', 'Git repository initialized');
			}
		} else {
			// Default behavior when no flag is provided (from interactive prompt)
			if (insideGitWorkTree()) {
				log('info', 'Existing Git repository detected – skipping git init.');
			} else {
				log(
					'info',
					'No Git repository detected. Initializing one in project root...'
				);
				execSync('git init', { cwd: targetDir, stdio: 'ignore' });
				log('success', 'Git repository initialized');
			}
		}
	} catch (error) {
		log('warn', 'Git not available, skipping repository initialization');
	}

	// Only run the manual transformer if rules were provided via flags.
	// The interactive `rules --setup` wizard handles its own installation.
	if (options.rulesExplicitlyProvided || options.yes) {
		log('info', 'Generating profile rules from command-line flags...');
		for (const profileName of selectedRuleProfiles) {
			_processSingleProfile(profileName);
		}
	}

	// Add shell aliases if requested
	if (addAliases) {
		addShellAliases();
	}

	// Run npm install automatically
	const npmInstallOptions = {
		cwd: targetDir,
		// Default to inherit for interactive CLI, change if silent
		stdio: 'inherit'
	};

	if (isSilentMode()) {
		// If silent (MCP mode), suppress npm install output
		npmInstallOptions.stdio = 'ignore';
		log('info', 'Running npm install silently...'); // Log our own message
	} else {
		// Interactive mode, show the boxen message
		console.log(
			boxen(chalk.cyan('Installing dependencies...'), {
				padding: 0.5,
				margin: 0.5,
				borderStyle: 'round',
				borderColor: 'blue'
			})
		);
	}

	// === Add Rule Profiles Setup Step ===
	if (
		!isSilentMode() &&
		!dryRun &&
		!options?.yes &&
		!options.rulesExplicitlyProvided
	) {
		console.log(
			boxen(chalk.cyan('Configuring Rule Profiles...'), {
				padding: 0.5,
				margin: { top: 1, bottom: 0.5 },
				borderStyle: 'round',
				borderColor: 'blue'
			})
		);
		log(
			'info',
			'Running interactive rules setup. Please select which rule profiles to include.'
		);
		try {
			// Correct command confirmed by you.
			execSync('npx task-master rules --setup', {
				stdio: 'inherit',
				cwd: targetDir
			});
			log('success', 'Rule profiles configured.');
		} catch (error) {
			log('error', 'Failed to configure rule profiles:', error.message);
			log('warn', 'You may need to run "task-master rules --setup" manually.');
		}
	} else if (isSilentMode() || dryRun || options?.yes) {
		// This branch can log why setup was skipped, similar to the model setup logic.
		if (options.rulesExplicitlyProvided) {
			log(
				'info',
				'Skipping interactive rules setup because --rules flag was used.'
			);
		} else {
			log('info', 'Skipping interactive rules setup in non-interactive mode.');
		}
	}
	// =====================================

	// === Add Response Language Step ===
	if (!isSilentMode() && !dryRun && !options?.yes) {
		console.log(
			boxen(chalk.cyan('Configuring Response Language...'), {
				padding: 0.5,
				margin: { top: 1, bottom: 0.5 },
				borderStyle: 'round',
				borderColor: 'blue'
			})
		);
		log(
			'info',
			'Running interactive response language setup. Please input your preferred language.'
		);
		try {
			execSync('npx task-master lang --setup', {
				stdio: 'inherit',
				cwd: targetDir
			});
			log('success', 'Response Language configured.');
		} catch (error) {
			log('error', 'Failed to configure response language:', error.message);
			log('warn', 'You may need to run "task-master lang --setup" manually.');
		}
	} else if (isSilentMode() && !dryRun) {
		log(
			'info',
			'Skipping interactive response language setup in silent (MCP) mode.'
		);
		log(
			'warn',
			'Please configure response language using "task-master models --set-response-language" or the "models" MCP tool.'
		);
	} else if (dryRun) {
		log('info', 'DRY RUN: Skipping interactive response language setup.');
	}
	// =====================================

	// === Add Model Configuration Step ===
	if (!isSilentMode() && !dryRun && !options?.yes) {
		console.log(
			boxen(chalk.cyan('Configuring AI Models...'), {
				padding: 0.5,
				margin: { top: 1, bottom: 0.5 },
				borderStyle: 'round',
				borderColor: 'blue'
			})
		);
		log(
			'info',
			'Running interactive model setup. Please select your preferred AI models.'
		);
		try {
			execSync('npx task-master models --setup', {
				stdio: 'inherit',
				cwd: targetDir
			});
			log('success', 'AI Models configured.');
		} catch (error) {
			log('error', 'Failed to configure AI models:', error.message);
			log('warn', 'You may need to run "task-master models --setup" manually.');
		}
	} else if (isSilentMode() && !dryRun) {
		log('info', 'Skipping interactive model setup in silent (MCP) mode.');
		log(
			'warn',
			'Please configure AI models using "task-master models --set-..." or the "models" MCP tool.'
		);
	} else if (dryRun) {
		log('info', 'DRY RUN: Skipping interactive model setup.');
	} else if (options?.yes) {
		log('info', 'Skipping interactive model setup due to --yes flag.');
		log(
			'info',
			'Default AI models will be used. You can configure different models later using "task-master models --setup" or "task-master models --set-..." commands.'
		);
	}
	// ====================================

	// Add shell aliases if requested
	if (addAliases && !dryRun) {
		log('info', 'Adding shell aliases...');
		const aliasResult = addShellAliases();
		if (aliasResult) {
			log('success', 'Shell aliases added successfully');
		}
	} else if (addAliases && dryRun) {
		log('info', 'DRY RUN: Would add shell aliases (tm, taskmaster)');
	}

	// Display success message
	if (!isSilentMode()) {
		console.log(
			boxen(
				`${warmGradient.multiline(
					figlet.textSync('Success!', { font: 'Standard' })
				)}\n${chalk.green('Project initialized successfully!')}`,
				{
					padding: 1,
					margin: 1,
					borderStyle: 'double',
					borderColor: 'green'
				}
			)
		);
	}

	// Display next steps in a nice box
	if (!isSilentMode()) {
		console.log(
			boxen(
				`${chalk.cyan.bold('Things you should do next:')}\n\n${chalk.white('1. ')}${chalk.yellow(
					'Configure AI models (if needed) and add API keys to `.env`'
				)}\n${chalk.white('   ├─ ')}${chalk.dim('Models: Use `task-master models` commands')}\n${chalk.white('   └─ ')}${chalk.dim(
					'Keys: Add provider API keys to .env (or inside the MCP config file i.e. .cursor/mcp.json)'
				)}\n${chalk.white('2. ')}${chalk.yellow(
					'Discuss your idea with AI and ask for a PRD using example_prd.txt, and save it to scripts/PRD.txt'
				)}\n${chalk.white('3. ')}${chalk.yellow(
					'Ask Cursor Agent (or run CLI) to parse your PRD and generate initial tasks:'
				)}\n${chalk.white('   └─ ')}${chalk.dim('MCP Tool: ')}${chalk.cyan('parse_prd')}${chalk.dim(' | CLI: ')}${chalk.cyan('task-master parse-prd scripts/prd.txt')}\n${chalk.white('4. ')}${chalk.yellow(
					'Ask Cursor to analyze the complexity of the tasks in your PRD using research'
				)}\n${chalk.white('   └─ ')}${chalk.dim('MCP Tool: ')}${chalk.cyan('analyze_project_complexity')}${chalk.dim(' | CLI: ')}${chalk.cyan('task-master analyze-complexity')}\n${chalk.white('5. ')}${chalk.yellow(
					'Ask Cursor to expand all of your tasks using the complexity analysis'
				)}\n${chalk.white('6. ')}${chalk.yellow('Ask Cursor to begin working on the next task')}\n${chalk.white('7. ')}${chalk.yellow(
					'Add new tasks anytime using the add-task command or MCP tool'
				)}\n${chalk.white('8. ')}${chalk.yellow(
					'Ask Cursor to set the status of one or many tasks/subtasks at a time. Use the task id from the task lists.'
				)}\n${chalk.white('9. ')}${chalk.yellow(
					'Ask Cursor to update all tasks from a specific task id based on new learnings or pivots in your project.'
				)}\n${chalk.white('10. ')}${chalk.green.bold('Ship it!')}\n\n${chalk.dim(
					'* Review the README.md file to learn how to use other commands via Cursor Agent.'
				)}\n${chalk.dim(
					'* Use the task-master command without arguments to see all available commands.'
				)}`,
				{
					padding: 1,
					margin: 1,
					borderStyle: 'round',
					borderColor: 'yellow',
					title: 'Getting Started',
					titleAlignment: 'center'
				}
			)
		);
	}
}

// Ensure necessary functions are exported
export { initializeProject, log };

/**
 * expand-task.js
 * Direct function implementation for expanding a task into subtasks
 */

import expandTask from '../../../../scripts/modules/task-manager/expand-task.js';
import {
	readJSON,
	writeJSON,
	enableSilentMode,
	disableSilentMode,
	isSilentMode
} from '../../../../scripts/modules/utils.js';
import path from 'path';
import fs from 'fs';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for expanding a task into subtasks with error handling.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} args.id - The ID of the task to expand.
 * @param {number|string} [args.num] - Number of subtasks to generate.
 * @param {boolean} [args.research] - Enable research role for subtask generation.
 * @param {string} [args.prompt] - Additional context to guide subtask generation.
 * @param {boolean} [args.force] - Force expansion even if subtasks exist.
 * @param {string} [args.projectRoot] - Project root directory.
 * @param {string} [args.tag] - Tag for the task
 * @param {Object} log - Logger object
 * @param {Object} context - Context object containing session
 * @param {Object} [context.session] - MCP Session object
 * @returns {Promise<Object>} - Task expansion result { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function expandTaskDirect(args, log, context = {}) {
	const { session } = context; // Extract session
	// Destructure expected args, including projectRoot
	const {
		tasksJsonPath,
		id,
		num,
		research,
		prompt,
		force,
		projectRoot,
		tag,
		complexityReportPath
	} = args;

	// Log session root data for debugging
	log.info(
		`Session data in expandTaskDirect: ${JSON.stringify({
			hasSession: !!session,
			sessionKeys: session ? Object.keys(session) : [],
			roots: session?.roots,
			rootsStr: JSON.stringify(session?.roots)
		})}`
	);

	// Check if tasksJsonPath was provided
	if (!tasksJsonPath) {
		log.error('expandTaskDirect called without tasksJsonPath');
		return {
			success: false,
			error: {
				code: 'MISSING_ARGUMENT',
				message: 'tasksJsonPath is required'
			}
		};
	}

	// Use provided path
	const tasksPath = tasksJsonPath;

	log.info(`[expandTaskDirect] Using tasksPath: ${tasksPath}`);

	// Validate task ID
	const taskId = id ? parseInt(id, 10) : null;
	if (!taskId) {
		log.error('Task ID is required');
		return {
			success: false,
			error: {
				code: 'INPUT_VALIDATION_ERROR',
				message: 'Task ID is required'
			}
		};
	}

	// Process other parameters
	const numSubtasks = num ? parseInt(num, 10) : undefined;
	const useResearch = research === true;
	const additionalContext = prompt || '';
	const forceFlag = force === true;

	try {
		log.info(
			`[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}, Force: ${forceFlag}`
		);

		// Read tasks data
		log.info(`[expandTaskDirect] Attempting to read JSON from: ${tasksPath}`);
		const data = readJSON(tasksPath, projectRoot);
		log.info(
			`[expandTaskDirect] Result of readJSON: ${data ? 'Data read successfully' : 'readJSON returned null or undefined'}`
		);

		if (!data || !data.tasks) {
			log.error(
				`[expandTaskDirect] readJSON failed or returned invalid data for path: ${tasksPath}`
			);
			return {
				success: false,
				error: {
					code: 'INVALID_TASKS_FILE',
					message: `No valid tasks found in ${tasksPath}. readJSON returned: ${JSON.stringify(data)}`
				}
			};
		}

		// Find the specific task
		log.info(`[expandTaskDirect] Searching for task ID ${taskId} in data`);
		const task = data.tasks.find((t) => t.id === taskId);
		log.info(`[expandTaskDirect] Task found: ${task ? 'Yes' : 'No'}`);

		if (!task) {
			return {
				success: false,
				error: {
					code: 'TASK_NOT_FOUND',
					message: `Task with ID ${taskId} not found`
				}
			};
		}

		// Check if task is completed
		if (task.status === 'done' || task.status === 'completed') {
			return {
				success: false,
				error: {
					code: 'TASK_COMPLETED',
					message: `Task ${taskId} is already marked as ${task.status} and cannot be expanded`
				}
			};
		}

		// Check for existing subtasks and force flag
		const hasExistingSubtasks = task.subtasks && task.subtasks.length > 0;
		if (hasExistingSubtasks && !forceFlag) {
			log.info(
				`Task ${taskId} already has ${task.subtasks.length} subtasks. Use --force to overwrite.`
			);
			return {
				success: true,
				data: {
					message: `Task ${taskId} already has subtasks. Expansion skipped.`,
					task,
					subtasksAdded: 0,
					hasExistingSubtasks
				}
			};
		}

		// If force flag is set, clear existing subtasks
		if (hasExistingSubtasks && forceFlag) {
			log.info(
				`Force flag set. Clearing existing subtasks for task ${taskId}.`
			);
			task.subtasks = [];
		}

		// Keep a copy of the task before modification
		const originalTask = JSON.parse(JSON.stringify(task));

		// Tracking subtasks count before expansion
		const subtasksCountBefore = task.subtasks ? task.subtasks.length : 0;

		// Directly modify the data instead of calling the CLI function
		if (!task.subtasks) {
			task.subtasks = [];
		}

		// Save tasks.json with potentially empty subtasks array and proper context
		writeJSON(tasksPath, data, projectRoot, tag);

		// Create logger wrapper using the utility
		const mcpLog = createLogWrapper(log);

		let wasSilent; // Declare wasSilent outside the try block
		// Process the request
		try {
			// Enable silent mode to prevent console logs from interfering with JSON response
			wasSilent = isSilentMode(); // Assign inside the try block
			if (!wasSilent) enableSilentMode();

			// Call the core expandTask function with the wrapped logger and projectRoot
			const coreResult = await expandTask(
				tasksPath,
				taskId,
				numSubtasks,
				useResearch,
				additionalContext,
				{
					complexityReportPath,
					mcpLog,
					session,
					projectRoot,
					commandName: 'expand-task',
					outputType: 'mcp',
					tag
				},
				forceFlag
			);

			// Restore normal logging
			if (!wasSilent && isSilentMode()) disableSilentMode();

			// Read the updated data
			const updatedData = readJSON(tasksPath, projectRoot);
			const updatedTask = updatedData.tasks.find((t) => t.id === taskId);

			// Calculate how many subtasks were added
			const subtasksAdded = updatedTask.subtasks
				? updatedTask.subtasks.length - subtasksCountBefore
				: 0;

			// Return the result, including telemetryData
			log.info(
				`Successfully expanded task ${taskId} with ${subtasksAdded} new subtasks`
			);
			return {
				success: true,
				data: {
					task: coreResult.task,
					subtasksAdded,
					hasExistingSubtasks,
					telemetryData: coreResult.telemetryData,
					tagInfo: coreResult.tagInfo
				}
			};
		} catch (error) {
			// Make sure to restore normal logging even if there's an error
			if (!wasSilent && isSilentMode()) disableSilentMode();

			log.error(`Error expanding task: ${error.message}`);
			return {
				success: false,
				error: {
					code: 'CORE_FUNCTION_ERROR',
					message: error.message || 'Failed to expand task'
				}
			};
		}
	} catch (error) {
		log.error(`Error expanding task: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'CORE_FUNCTION_ERROR',
				message: error.message || 'Failed to expand task'
			}
		};
	}
}

/**
 * Direct function wrapper for analyzeTaskComplexity
 */

import analyzeTaskComplexity from '../../../../scripts/modules/task-manager/analyze-task-complexity.js';
import {
	enableSilentMode,
	disableSilentMode,
	isSilentMode
} from '../../../../scripts/modules/utils.js';
import fs from 'fs';
import { createLogWrapper } from '../../tools/utils.js'; // Import the new utility

/**
 * Analyze task complexity and generate recommendations
 * @param {Object} args - Function arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} args.outputPath - Explicit absolute path to save the report.
 * @param {string|number} [args.threshold] - Minimum complexity score to recommend expansion (1-10)
 * @param {boolean} [args.research] - Use Perplexity AI for research-backed complexity analysis
 * @param {string} [args.ids] - Comma-separated list of task IDs to analyze
 * @param {number} [args.from] - Starting task ID in a range to analyze
 * @param {number} [args.to] - Ending task ID in a range to analyze
 * @param {string} [args.projectRoot] - Project root path.
 * @param {string} [args.tag] - Tag for the task (optional)
 * @param {Object} log - Logger object
 * @param {Object} [context={}] - Context object containing session data
 * @param {Object} [context.session] - MCP session object
 * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
 */
export async function analyzeTaskComplexityDirect(args, log, context = {}) {
	const { session } = context;
	const {
		tasksJsonPath,
		outputPath,
		threshold,
		research,
		projectRoot,
		ids,
		from,
		to,
		tag
	} = args;

	const logWrapper = createLogWrapper(log);

	// --- Initial Checks (remain the same) ---
	try {
		log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);

		if (!tasksJsonPath) {
			log.error('analyzeTaskComplexityDirect called without tasksJsonPath');
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}
		if (!outputPath) {
			log.error('analyzeTaskComplexityDirect called without outputPath');
			return {
				success: false,
				error: { code: 'MISSING_ARGUMENT', message: 'outputPath is required' }
			};
		}

		const tasksPath = tasksJsonPath;
		const resolvedOutputPath = outputPath;

		log.info(`Analyzing task complexity from: ${tasksPath}`);
		log.info(`Output report will be saved to: ${resolvedOutputPath}`);

		if (ids) {
			log.info(`Analyzing specific task IDs: ${ids}`);
		} else if (from || to) {
			const fromStr = from !== undefined ? from : 'first';
			const toStr = to !== undefined ? to : 'last';
			log.info(`Analyzing tasks in range: ${fromStr} to ${toStr}`);
		}

		if (research) {
			log.info('Using research role for complexity analysis');
		}

		// Prepare options for the core function - REMOVED mcpLog and session here
		const coreOptions = {
			file: tasksJsonPath,
			output: outputPath,
			threshold: threshold,
			research: research === true, // Ensure boolean
			projectRoot: projectRoot, // Pass projectRoot here
			id: ids, // Pass the ids parameter to the core function as 'id'
			from: from, // Pass from parameter
			to: to, // Pass to parameter
			tag // forward tag
		};
		// --- End Initial Checks ---

		// --- Silent Mode and Logger Wrapper ---
		const wasSilent = isSilentMode();
		if (!wasSilent) {
			enableSilentMode(); // Still enable silent mode as a backup
		}

		let report;
		let coreResult;

		try {
			// --- Call Core Function (Pass context separately) ---
			// Pass coreOptions as the first argument
			// Pass context object { session, mcpLog } as the second argument
			coreResult = await analyzeTaskComplexity(coreOptions, {
				session,
				mcpLog: logWrapper,
				commandName: 'analyze-complexity',
				outputType: 'mcp',
				projectRoot,
				tag
			});
			report = coreResult.report;
		} catch (error) {
			log.error(
				`Error in analyzeTaskComplexity core function: ${error.message}`
			);
			// Restore logging if we changed it
			if (!wasSilent && isSilentMode()) {
				disableSilentMode();
			}
			return {
				success: false,
				error: {
					code: 'ANALYZE_CORE_ERROR',
					message: `Error running core complexity analysis: ${error.message}`
				}
			};
		} finally {
			// Always restore normal logging in finally block if we enabled silent mode
			if (!wasSilent && isSilentMode()) {
				disableSilentMode();
			}
		}

		// --- Result Handling (remains largely the same) ---
		// Verify the report file was created (core function writes it)
		if (!fs.existsSync(resolvedOutputPath)) {
			return {
				success: false,
				error: {
					code: 'ANALYZE_REPORT_MISSING', // Specific code
					message:
						'Analysis completed but no report file was created at the expected path.'
				}
			};
		}

		if (
			!coreResult ||
			!coreResult.report ||
			typeof coreResult.report !== 'object'
		) {
			log.error(
				'Core analysis function returned an invalid or undefined response.'
			);
			return {
				success: false,
				error: {
					code: 'INVALID_CORE_RESPONSE',
					message: 'Core analysis function returned an invalid response.'
				}
			};
		}

		try {
			// Ensure complexityAnalysis exists and is an array
			const analysisArray = Array.isArray(coreResult.report.complexityAnalysis)
				? coreResult.report.complexityAnalysis
				: [];

			// Count tasks by complexity (remains the same)
			const highComplexityTasks = analysisArray.filter(
				(t) => t.complexityScore >= 8
			).length;
			const mediumComplexityTasks = analysisArray.filter(
				(t) => t.complexityScore >= 5 && t.complexityScore < 8
			).length;
			const lowComplexityTasks = analysisArray.filter(
				(t) => t.complexityScore < 5
			).length;

			return {
				success: true,
				data: {
					message: `Task complexity analysis complete. Report saved to ${outputPath}`,
					reportPath: outputPath,
					reportSummary: {
						taskCount: analysisArray.length,
						highComplexityTasks,
						mediumComplexityTasks,
						lowComplexityTasks
					},
					fullReport: coreResult.report,
					telemetryData: coreResult.telemetryData,
					tagInfo: coreResult.tagInfo
				}
			};
		} catch (parseError) {
			// Should not happen if core function returns object, but good safety check
			log.error(`Internal error processing report data: ${parseError.message}`);
			return {
				success: false,
				error: {
					code: 'REPORT_PROCESS_ERROR',
					message: `Internal error processing complexity report: ${parseError.message}`
				}
			};
		}
		// --- End Result Handling ---
	} catch (error) {
		// Catch errors from initial checks or path resolution
		// Make sure to restore normal logging if silent mode was enabled
		if (isSilentMode()) {
			disableSilentMode();
		}
		log.error(`Error in analyzeTaskComplexityDirect setup: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'DIRECT_FUNCTION_SETUP_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * tools/move-task.js
 * Tool for moving tasks or subtasks to a new position
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { moveTaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the moveTask tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerMoveTaskTool(server) {
	server.addTool({
		name: 'move_task',
		description: 'Move a task or subtask to a new position',
		parameters: z.object({
			from: z
				.string()
				.describe(
					'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")'
				),
			to: z
				.string()
				.describe(
					'ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated'
				),
			file: z.string().optional().describe('Custom path to tasks.json file'),
			projectRoot: z
				.string()
				.describe(
					'Root directory of the project (typically derived from session)'
				),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				// Find tasks.json path if not provided
				let tasksJsonPath = args.file;

				if (!tasksJsonPath) {
					tasksJsonPath = findTasksPath(args, log);
				}

				// Parse comma-separated IDs
				const fromIds = args.from.split(',').map((id) => id.trim());
				const toIds = args.to.split(',').map((id) => id.trim());

				// Validate matching IDs count
				if (fromIds.length !== toIds.length) {
					return createErrorResponse(
						'The number of source and destination IDs must match',
						'MISMATCHED_ID_COUNT'
					);
				}

				// If moving multiple tasks
				if (fromIds.length > 1) {
					const results = [];
					// Move tasks one by one, only generate files on the last move
					for (let i = 0; i < fromIds.length; i++) {
						const fromId = fromIds[i];
						const toId = toIds[i];

						// Skip if source and destination are the same
						if (fromId === toId) {
							log.info(`Skipping ${fromId} -> ${toId} (same ID)`);
							continue;
						}

						const shouldGenerateFiles = i === fromIds.length - 1;
						const result = await moveTaskDirect(
							{
								sourceId: fromId,
								destinationId: toId,
								tasksJsonPath,
								projectRoot: args.projectRoot,
								tag: resolvedTag
							},
							log,
							{ session }
						);

						if (!result.success) {
							log.error(
								`Failed to move ${fromId} to ${toId}: ${result.error.message}`
							);
						} else {
							results.push(result.data);
						}
					}

					return handleApiResult(
						{
							success: true,
							data: {
								moves: results,
								message: `Successfully moved ${results.length} tasks`
							}
						},
						log,
						'Error moving multiple tasks',
						undefined,
						args.projectRoot
					);
				} else {
					// Moving a single task
					return handleApiResult(
						await moveTaskDirect(
							{
								sourceId: args.from,
								destinationId: args.to,
								tasksJsonPath,
								projectRoot: args.projectRoot,
								tag: resolvedTag
							},
							log,
							{ session }
						),
						log,
						'Error moving task',
						undefined,
						args.projectRoot
					);
				}
			} catch (error) {
				return createErrorResponse(
					`Failed to move task: ${error.message}`,
					'MOVE_TASK_ERROR'
				);
			}
		})
	});
}

/**
 * ai-services-unified.js
 * Centralized AI service layer using provider modules and config-manager.
 */

// Vercel AI SDK functions are NOT called directly anymore.
// import { generateText, streamText, generateObject } from 'ai';

// --- Core Dependencies ---
import {
	MODEL_MAP,
	getAzureBaseURL,
	getBaseUrlForRole,
	getBedrockBaseURL,
	getDebugFlag,
	getFallbackModelId,
	getFallbackProvider,
	getMainModelId,
	getMainProvider,
	getOllamaBaseURL,
	getParametersForRole,
	getResearchModelId,
	getResearchProvider,
	getResponseLanguage,
	getUserId,
	getVertexLocation,
	getVertexProjectId,
	isApiKeySet,
	providersWithoutApiKeys
} from './config-manager.js';
import {
	findProjectRoot,
	getCurrentTag,
	log,
	resolveEnvVariable
} from './utils.js';

// Import provider classes
import {
	AnthropicAIProvider,
	AzureProvider,
	BedrockAIProvider,
	ClaudeCodeProvider,
	GeminiCliProvider,
	GoogleAIProvider,
	GroqProvider,
	OllamaAIProvider,
	OpenAIProvider,
	OpenRouterAIProvider,
	PerplexityAIProvider,
	VertexAIProvider,
	XAIProvider
} from '../../src/ai-providers/index.js';

// Import the provider registry
import ProviderRegistry from '../../src/provider-registry/index.js';

// Create provider instances
const PROVIDERS = {
	anthropic: new AnthropicAIProvider(),
	perplexity: new PerplexityAIProvider(),
	google: new GoogleAIProvider(),
	openai: new OpenAIProvider(),
	xai: new XAIProvider(),
	groq: new GroqProvider(),
	openrouter: new OpenRouterAIProvider(),
	ollama: new OllamaAIProvider(),
	bedrock: new BedrockAIProvider(),
	azure: new AzureProvider(),
	vertex: new VertexAIProvider(),
	'claude-code': new ClaudeCodeProvider(),
	'gemini-cli': new GeminiCliProvider()
};

function _getProvider(providerName) {
	// First check the static PROVIDERS object
	if (PROVIDERS[providerName]) {
		return PROVIDERS[providerName];
	}

	// If not found, check the provider registry
	const providerRegistry = ProviderRegistry.getInstance();
	if (providerRegistry.hasProvider(providerName)) {
		log('debug', `Provider "${providerName}" found in dynamic registry`);
		return providerRegistry.getProvider(providerName);
	}

	// Provider not found in either location
	return null;
}

// Helper function to get cost for a specific model
function _getCostForModel(providerName, modelId) {
	if (!MODEL_MAP || !MODEL_MAP[providerName]) {
		log(
			'warn',
			`Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.`
		);
		return { inputCost: 0, outputCost: 0, currency: 'USD' }; // Default to zero cost
	}

	const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);

	if (!modelData || !modelData.cost_per_1m_tokens) {
		log(
			'debug',
			`Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.`
		);
		return { inputCost: 0, outputCost: 0, currency: 'USD' }; // Default to zero cost
	}

	// Ensure currency is part of the returned object, defaulting if not present
	const currency = modelData.cost_per_1m_tokens.currency || 'USD';

	return {
		inputCost: modelData.cost_per_1m_tokens.input || 0,
		outputCost: modelData.cost_per_1m_tokens.output || 0,
		currency: currency
	};
}

// Helper function to get tag information for responses
function _getTagInfo(projectRoot) {
	try {
		if (!projectRoot) {
			return { currentTag: 'master', availableTags: ['master'] };
		}

		const currentTag = getCurrentTag(projectRoot);

		// Read available tags from tasks.json
		let availableTags = ['master']; // Default fallback
		try {
			const path = require('path');
			const fs = require('fs');
			const tasksPath = path.join(
				projectRoot,
				'.taskmaster',
				'tasks',
				'tasks.json'
			);

			if (fs.existsSync(tasksPath)) {
				const tasksData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
				if (tasksData && typeof tasksData === 'object') {
					// Check if it's tagged format (has tag-like keys with tasks arrays)
					const potentialTags = Object.keys(tasksData).filter(
						(key) =>
							tasksData[key] &&
							typeof tasksData[key] === 'object' &&
							Array.isArray(tasksData[key].tasks)
					);

					if (potentialTags.length > 0) {
						availableTags = potentialTags;
					}
				}
			}
		} catch (readError) {
			// Silently fall back to default if we can't read tasks file
			if (getDebugFlag()) {
				log(
					'debug',
					`Could not read tasks file for available tags: ${readError.message}`
				);
			}
		}

		return {
			currentTag: currentTag || 'master',
			availableTags: availableTags
		};
	} catch (error) {
		if (getDebugFlag()) {
			log('debug', `Error getting tag information: ${error.message}`);
		}
		return { currentTag: 'master', availableTags: ['master'] };
	}
}

// --- Configuration for Retries ---
const MAX_RETRIES = 2;
const INITIAL_RETRY_DELAY_MS = 1000;

// Helper function to check if an error is retryable
function isRetryableError(error) {
	const errorMessage = error.message?.toLowerCase() || '';
	return (
		errorMessage.includes('rate limit') ||
		errorMessage.includes('overloaded') ||
		errorMessage.includes('service temporarily unavailable') ||
		errorMessage.includes('timeout') ||
		errorMessage.includes('network error') ||
		error.status === 429 ||
		error.status >= 500
	);
}

/**
 * Extracts a user-friendly error message from a potentially complex AI error object.
 * Prioritizes nested messages and falls back to the top-level message.
 * @param {Error | object | any} error - The error object.
 * @returns {string} A concise error message.
 */
function _extractErrorMessage(error) {
	try {
		// Attempt 1: Look for Vercel SDK specific nested structure (common)
		if (error?.data?.error?.message) {
			return error.data.error.message;
		}

		// Attempt 2: Look for nested error message directly in the error object
		if (error?.error?.message) {
			return error.error.message;
		}

		// Attempt 3: Look for nested error message in response body if it's JSON string
		if (typeof error?.responseBody === 'string') {
			try {
				const body = JSON.parse(error.responseBody);
				if (body?.error?.message) {
					return body.error.message;
				}
			} catch (parseError) {
				// Ignore if responseBody is not valid JSON
			}
		}

		// Attempt 4: Use the top-level message if it exists
		if (typeof error?.message === 'string' && error.message) {
			return error.message;
		}

		// Attempt 5: Handle simple string errors
		if (typeof error === 'string') {
			return error;
		}

		// Fallback
		return 'An unknown AI service error occurred.';
	} catch (e) {
		// Safety net
		return 'Failed to extract error message.';
	}
}

/**
 * Internal helper to resolve the API key for a given provider.
 * @param {string} providerName - The name of the provider (lowercase).
 * @param {object|null} session - Optional MCP session object.
 * @param {string|null} projectRoot - Optional project root path for .env fallback.
 * @returns {string|null} The API key or null if not found/needed.
 * @throws {Error} If a required API key is missing.
 */
function _resolveApiKey(providerName, session, projectRoot = null) {
	// Get provider instance
	const provider = _getProvider(providerName);
	if (!provider) {
		throw new Error(
			`Unknown provider '${providerName}' for API key resolution.`
		);
	}

	// All providers must implement getRequiredApiKeyName()
	const envVarName = provider.getRequiredApiKeyName();

	// If envVarName is null (like for MCP), return null directly
	if (envVarName === null) {
		return null;
	}

	const apiKey = resolveEnvVariable(envVarName, session, projectRoot);

	// Special handling for providers that can use alternative auth or no API key
	if (!provider.isRequiredApiKey()) {
		return apiKey || null;
	}

	if (!apiKey) {
		throw new Error(
			`Required API key ${envVarName} for provider '${providerName}' is not set in environment, session, or .env file.`
		);
	}
	return apiKey;
}

/**
 * Internal helper to attempt a provider-specific AI API call with retries.
 *
 * @param {function} providerApiFn - The specific provider function to call (e.g., generateAnthropicText).
 * @param {object} callParams - Parameters object for the provider function.
 * @param {string} providerName - Name of the provider (for logging).
 * @param {string} modelId - Specific model ID (for logging).
 * @param {string} attemptRole - The role being attempted (for logging).
 * @returns {Promise<object>} The result from the successful API call.
 * @throws {Error} If the call fails after all retries.
 */
async function _attemptProviderCallWithRetries(
	provider,
	serviceType,
	callParams,
	providerName,
	modelId,
	attemptRole
) {
	let retries = 0;
	const fnName = serviceType;

	while (retries <= MAX_RETRIES) {
		try {
			if (getDebugFlag()) {
				log(
					'info',
					`Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${fnName} (Provider: ${providerName}, Model: ${modelId}, Role: ${attemptRole})`
				);
			}

			// Call the appropriate method on the provider instance
			const result = await provider[serviceType](callParams);

			if (getDebugFlag()) {
				log(
					'info',
					`${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}`
				);
			}
			return result;
		} catch (error) {
			log(
				'warn',
				`Attempt ${retries + 1} failed for role ${attemptRole} (${fnName} / ${providerName}): ${error.message}`
			);

			if (isRetryableError(error) && retries < MAX_RETRIES) {
				retries++;
				const delay = INITIAL_RETRY_DELAY_MS * 2 ** (retries - 1);
				log(
					'info',
					`Something went wrong on the provider side. Retrying in ${delay / 1000}s...`
				);
				await new Promise((resolve) => setTimeout(resolve, delay));
			} else {
				log(
					'error',
					`Something went wrong on the provider side. Max retries reached for role ${attemptRole} (${fnName} / ${providerName}).`
				);
				throw error;
			}
		}
	}
	// Should not be reached due to throw in the else block
	throw new Error(
		`Exhausted all retries for role ${attemptRole} (${fnName} / ${providerName})`
	);
}

/**
 * Base logic for unified service functions.
 * @param {string} serviceType - Type of service ('generateText', 'streamText', 'generateObject').
 * @param {object} params - Original parameters passed to the service function.
 * @param {string} params.role - The initial client role.
 * @param {object} [params.session=null] - Optional MCP session object.
 * @param {string} [params.projectRoot] - Optional project root path.
 * @param {string} params.commandName - Name of the command invoking the service.
 * @param {string} params.outputType - 'cli' or 'mcp'.
 * @param {string} [params.systemPrompt] - Optional system prompt.
 * @param {string} [params.prompt] - The prompt for the AI.
 * @param {string} [params.schema] - The Zod schema for the expected object.
 * @param {string} [params.objectName] - Name for object/tool.
 * @returns {Promise<any>} Result from the underlying provider call.
 */
async function _unifiedServiceRunner(serviceType, params) {
	const {
		role: initialRole,
		session,
		projectRoot,
		systemPrompt,
		prompt,
		schema,
		objectName,
		commandName,
		outputType,
		...restApiParams
	} = params;
	if (getDebugFlag()) {
		log('info', `${serviceType}Service called`, {
			role: initialRole,
			commandName,
			outputType,
			projectRoot
		});
	}

	const effectiveProjectRoot = projectRoot || findProjectRoot();
	const userId = getUserId(effectiveProjectRoot);

	let sequence;
	if (initialRole === 'main') {
		sequence = ['main', 'fallback', 'research'];
	} else if (initialRole === 'research') {
		sequence = ['research', 'fallback', 'main'];
	} else if (initialRole === 'fallback') {
		sequence = ['fallback', 'main', 'research'];
	} else {
		log(
			'warn',
			`Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.`
		);
		sequence = ['main', 'fallback', 'research'];
	}

	let lastError = null;
	let lastCleanErrorMessage =
		'AI service call failed for all configured roles.';

	for (const currentRole of sequence) {
		let providerName;
		let modelId;
		let apiKey;
		let roleParams;
		let provider;
		let baseURL;
		let providerResponse;
		let telemetryData = null;

		try {
			log('info', `New AI service call with role: ${currentRole}`);

			if (currentRole === 'main') {
				providerName = getMainProvider(effectiveProjectRoot);
				modelId = getMainModelId(effectiveProjectRoot);
			} else if (currentRole === 'research') {
				providerName = getResearchProvider(effectiveProjectRoot);
				modelId = getResearchModelId(effectiveProjectRoot);
			} else if (currentRole === 'fallback') {
				providerName = getFallbackProvider(effectiveProjectRoot);
				modelId = getFallbackModelId(effectiveProjectRoot);
			} else {
				log(
					'error',
					`Unknown role encountered in _unifiedServiceRunner: ${currentRole}`
				);
				lastError =
					lastError || new Error(`Unknown AI role specified: ${currentRole}`);
				continue;
			}

			if (!providerName || !modelId) {
				log(
					'warn',
					`Skipping role '${currentRole}': Provider or Model ID not configured.`
				);
				lastError =
					lastError ||
					new Error(
						`Configuration missing for role '${currentRole}'. Provider: ${providerName}, Model: ${modelId}`
					);
				continue;
			}

			// Get provider instance
			provider = _getProvider(providerName?.toLowerCase());
			if (!provider) {
				log(
					'warn',
					`Skipping role '${currentRole}': Provider '${providerName}' not supported.`
				);
				lastError =
					lastError ||
					new Error(`Unsupported provider configured: ${providerName}`);
				continue;
			}

			// Check API key if needed
			if (!providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
				if (!isApiKeySet(providerName, session, effectiveProjectRoot)) {
					log(
						'warn',
						`Skipping role '${currentRole}' (Provider: ${providerName}): API key not set or invalid.`
					);
					lastError =
						lastError ||
						new Error(
							`API key for provider '${providerName}' (role: ${currentRole}) is not set.`
						);
					continue; // Skip to the next role in the sequence
				}
			}

			// Get base URL if configured (optional for most providers)
			baseURL = getBaseUrlForRole(currentRole, effectiveProjectRoot);

			// For Azure, use the global Azure base URL if role-specific URL is not configured
			if (providerName?.toLowerCase() === 'azure' && !baseURL) {
				baseURL = getAzureBaseURL(effectiveProjectRoot);
				log('debug', `Using global Azure base URL: ${baseURL}`);
			} else if (providerName?.toLowerCase() === 'ollama' && !baseURL) {
				// For Ollama, use the global Ollama base URL if role-specific URL is not configured
				baseURL = getOllamaBaseURL(effectiveProjectRoot);
				log('debug', `Using global Ollama base URL: ${baseURL}`);
			} else if (providerName?.toLowerCase() === 'bedrock' && !baseURL) {
				// For Bedrock, use the global Bedrock base URL if role-specific URL is not configured
				baseURL = getBedrockBaseURL(effectiveProjectRoot);
				log('debug', `Using global Bedrock base URL: ${baseURL}`);
			}

			// Get AI parameters for the current role
			roleParams = getParametersForRole(currentRole, effectiveProjectRoot);
			apiKey = _resolveApiKey(
				providerName?.toLowerCase(),
				session,
				effectiveProjectRoot
			);

			// Prepare provider-specific configuration
			let providerSpecificParams = {};

			// Handle Vertex AI specific configuration
			if (providerName?.toLowerCase() === 'vertex') {
				// Get Vertex project ID and location
				const projectId =
					getVertexProjectId(effectiveProjectRoot) ||
					resolveEnvVariable(
						'VERTEX_PROJECT_ID',
						session,
						effectiveProjectRoot
					);

				const location =
					getVertexLocation(effectiveProjectRoot) ||
					resolveEnvVariable(
						'VERTEX_LOCATION',
						session,
						effectiveProjectRoot
					) ||
					'us-central1';

				// Get credentials path if available
				const credentialsPath = resolveEnvVariable(
					'GOOGLE_APPLICATION_CREDENTIALS',
					session,
					effectiveProjectRoot
				);

				// Add Vertex-specific parameters
				providerSpecificParams = {
					projectId,
					location,
					...(credentialsPath && { credentials: { credentialsFromEnv: true } })
				};

				log(
					'debug',
					`Using Vertex AI configuration: Project ID=${projectId}, Location=${location}`
				);
			}

			const messages = [];
			const responseLanguage = getResponseLanguage(effectiveProjectRoot);
			const systemPromptWithLanguage = `${systemPrompt} \n\n Always respond in ${responseLanguage}.`;
			messages.push({
				role: 'system',
				content: systemPromptWithLanguage.trim()
			});

			// IN THE FUTURE WHEN DOING CONTEXT IMPROVEMENTS
			// {
			//     type: 'text',
			//     text: 'Large cached context here like a tasks json',
			//     providerOptions: {
			//       anthropic: { cacheControl: { type: 'ephemeral' } }
			//     }
			//   }

			// Example
			// if (params.context) { // context is a json string of a tasks object or some other stu
			//     messages.push({
			//         type: 'text',
			//         text: params.context,
			//         providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } }
			//     });
			// }

			if (prompt) {
				messages.push({ role: 'user', content: prompt });
			} else {
				throw new Error('User prompt content is missing.');
			}

			const callParams = {
				apiKey,
				modelId,
				maxTokens: roleParams.maxTokens,
				temperature: roleParams.temperature,
				messages,
				...(baseURL && { baseURL }),
				...(serviceType === 'generateObject' && { schema, objectName }),
				...providerSpecificParams,
				...restApiParams
			};

			providerResponse = await _attemptProviderCallWithRetries(
				provider,
				serviceType,
				callParams,
				providerName,
				modelId,
				currentRole
			);

			if (userId && providerResponse && providerResponse.usage) {
				try {
					telemetryData = await logAiUsage({
						userId,
						commandName,
						providerName,
						modelId,
						inputTokens: providerResponse.usage.inputTokens,
						outputTokens: providerResponse.usage.outputTokens,
						outputType
					});
				} catch (telemetryError) {
					// logAiUsage already logs its own errors and returns null on failure
					// No need to log again here, telemetryData will remain null
				}
			} else if (userId && providerResponse && !providerResponse.usage) {
				log(
					'warn',
					`Cannot log telemetry for ${commandName} (${providerName}/${modelId}): AI result missing 'usage' data. (May be expected for streams)`
				);
			}

			let finalMainResult;
			if (serviceType === 'generateText') {
				finalMainResult = providerResponse.text;
			} else if (serviceType === 'generateObject') {
				finalMainResult = providerResponse.object;
			} else if (serviceType === 'streamText') {
				finalMainResult = providerResponse;
			} else {
				log(
					'error',
					`Unknown serviceType in _unifiedServiceRunner: ${serviceType}`
				);
				finalMainResult = providerResponse;
			}

			// Get tag information for the response
			const tagInfo = _getTagInfo(effectiveProjectRoot);

			return {
				mainResult: finalMainResult,
				telemetryData: telemetryData,
				tagInfo: tagInfo
			};
		} catch (error) {
			const cleanMessage = _extractErrorMessage(error);
			log(
				'error',
				`Service call failed for role ${currentRole} (Provider: ${providerName || 'unknown'}, Model: ${modelId || 'unknown'}): ${cleanMessage}`
			);
			lastError = error;
			lastCleanErrorMessage = cleanMessage;

			if (serviceType === 'generateObject') {
				const lowerCaseMessage = cleanMessage.toLowerCase();
				if (
					lowerCaseMessage.includes(
						'no endpoints found that support tool use'
					) ||
					lowerCaseMessage.includes('does not support tool_use') ||
					lowerCaseMessage.includes('tool use is not supported') ||
					lowerCaseMessage.includes('tools are not supported') ||
					lowerCaseMessage.includes('function calling is not supported') ||
					lowerCaseMessage.includes('tool use is not supported')
				) {
					const specificErrorMsg = `Model '${modelId || 'unknown'}' via provider '${providerName || 'unknown'}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`;
					log('error', `[Tool Support Error] ${specificErrorMsg}`);
					throw new Error(specificErrorMsg);
				}
			}
		}
	}

	log('error', `All roles in the sequence [${sequence.join(', ')}] failed.`);
	throw new Error(lastCleanErrorMessage);
}

/**
 * Unified service function for generating text.
 * Handles client retrieval, retries, and fallback sequence.
 *
 * @param {object} params - Parameters for the service call.
 * @param {string} params.role - The initial client role ('main', 'research', 'fallback').
 * @param {object} [params.session=null] - Optional MCP session object.
 * @param {string} [params.projectRoot=null] - Optional project root path for .env fallback.
 * @param {string} params.prompt - The prompt for the AI.
 * @param {string} [params.systemPrompt] - Optional system prompt.
 * @param {string} params.commandName - Name of the command invoking the service.
 * @param {string} [params.outputType='cli'] - 'cli' or 'mcp'.
 * @returns {Promise<object>} Result object containing generated text and usage data.
 */
async function generateTextService(params) {
	// Ensure default outputType if not provided
	const defaults = { outputType: 'cli' };
	const combinedParams = { ...defaults, ...params };
	// TODO: Validate commandName exists?
	return _unifiedServiceRunner('generateText', combinedParams);
}

/**
 * Unified service function for streaming text.
 * Handles client retrieval, retries, and fallback sequence.
 *
 * @param {object} params - Parameters for the service call.
 * @param {string} params.role - The initial client role ('main', 'research', 'fallback').
 * @param {object} [params.session=null] - Optional MCP session object.
 * @param {string} [params.projectRoot=null] - Optional project root path for .env fallback.
 * @param {string} params.prompt - The prompt for the AI.
 * @param {string} [params.systemPrompt] - Optional system prompt.
 * @param {string} params.commandName - Name of the command invoking the service.
 * @param {string} [params.outputType='cli'] - 'cli' or 'mcp'.
 * @returns {Promise<object>} Result object containing the stream and usage data.
 */
async function streamTextService(params) {
	const defaults = { outputType: 'cli' };
	const combinedParams = { ...defaults, ...params };
	// TODO: Validate commandName exists?
	// NOTE: Telemetry for streaming might be tricky as usage data often comes at the end.
	// The current implementation logs *after* the stream is returned.
	// We might need to adjust how usage is captured/logged for streams.
	return _unifiedServiceRunner('streamText', combinedParams);
}

/**
 * Unified service function for generating structured objects.
 * Handles client retrieval, retries, and fallback sequence.
 *
 * @param {object} params - Parameters for the service call.
 * @param {string} params.role - The initial client role ('main', 'research', 'fallback').
 * @param {object} [params.session=null] - Optional MCP session object.
 * @param {string} [params.projectRoot=null] - Optional project root path for .env fallback.
 * @param {import('zod').ZodSchema} params.schema - The Zod schema for the expected object.
 * @param {string} params.prompt - The prompt for the AI.
 * @param {string} [params.systemPrompt] - Optional system prompt.
 * @param {string} [params.objectName='generated_object'] - Name for object/tool.
 * @param {number} [params.maxRetries=3] - Max retries for object generation.
 * @param {string} params.commandName - Name of the command invoking the service.
 * @param {string} [params.outputType='cli'] - 'cli' or 'mcp'.
 * @returns {Promise<object>} Result object containing the generated object and usage data.
 */
async function generateObjectService(params) {
	const defaults = {
		objectName: 'generated_object',
		maxRetries: 3,
		outputType: 'cli'
	};
	const combinedParams = { ...defaults, ...params };
	// TODO: Validate commandName exists?
	return _unifiedServiceRunner('generateObject', combinedParams);
}

// --- Telemetry Function ---
/**
 * Logs AI usage telemetry data.
 * For now, it just logs to the console. Sending will be implemented later.
 * @param {object} params - Telemetry parameters.
 * @param {string} params.userId - Unique user identifier.
 * @param {string} params.commandName - The command that triggered the AI call.
 * @param {string} params.providerName - The AI provider used (e.g., 'openai').
 * @param {string} params.modelId - The specific AI model ID used.
 * @param {number} params.inputTokens - Number of input tokens.
 * @param {number} params.outputTokens - Number of output tokens.
 */
async function logAiUsage({
	userId,
	commandName,
	providerName,
	modelId,
	inputTokens,
	outputTokens,
	outputType
}) {
	try {
		const isMCP = outputType === 'mcp';
		const timestamp = new Date().toISOString();
		const totalTokens = (inputTokens || 0) + (outputTokens || 0);

		// Destructure currency along with costs
		const { inputCost, outputCost, currency } = _getCostForModel(
			providerName,
			modelId
		);

		const totalCost =
			((inputTokens || 0) / 1_000_000) * inputCost +
			((outputTokens || 0) / 1_000_000) * outputCost;

		const telemetryData = {
			timestamp,
			userId,
			commandName,
			modelUsed: modelId, // Consistent field name from requirements
			providerName, // Keep provider name for context
			inputTokens: inputTokens || 0,
			outputTokens: outputTokens || 0,
			totalTokens,
			totalCost: parseFloat(totalCost.toFixed(6)),
			currency // Add currency to the telemetry data
		};

		if (getDebugFlag()) {
			log('info', 'AI Usage Telemetry:', telemetryData);
		}

		// TODO (Subtask 77.2): Send telemetryData securely to the external endpoint.

		return telemetryData;
	} catch (error) {
		log('error', `Failed to log AI usage telemetry: ${error.message}`, {
			error
		});
		// Don't re-throw; telemetry failure shouldn't block core functionality.
		return null;
	}
}

export {
	generateTextService,
	streamTextService,
	generateObjectService,
	logAiUsage
};

/**
 * list-tags.js
 * Direct function implementation for listing all tags
 */

import { tags } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for listing all tags with error handling.
 *
 * @param {Object} args - Command arguments
 * @param {boolean} [args.showMetadata=false] - Whether to include metadata in the output
 * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
 * @param {string} [args.projectRoot] - Project root path
 * @param {Object} log - Logger object
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function listTagsDirect(args, log, context = {}) {
	// Destructure expected args
	const { tasksJsonPath, showMetadata = false, projectRoot } = args;
	const { session } = context;

	// Enable silent mode to prevent console logs from interfering with JSON response
	enableSilentMode();

	// Create logger wrapper using the utility
	const mcpLog = createLogWrapper(log);

	try {
		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('listTagsDirect called without tasksJsonPath');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		log.info('Listing all tags');

		// Prepare options
		const options = {
			showMetadata
		};

		// Call the tags function
		const result = await tags(
			tasksJsonPath,
			options,
			{
				session,
				mcpLog,
				projectRoot
			},
			'json' // outputFormat - use 'json' to suppress CLI UI
		);

		// Transform the result to remove full task data and provide summary info
		const tagsSummary = result.tags.map((tag) => {
			const tasks = tag.tasks || [];

			// Calculate status breakdown
			const statusBreakdown = tasks.reduce((acc, task) => {
				const status = task.status || 'pending';
				acc[status] = (acc[status] || 0) + 1;
				return acc;
			}, {});

			// Calculate subtask counts
			const subtaskCounts = tasks.reduce(
				(acc, task) => {
					if (task.subtasks && task.subtasks.length > 0) {
						acc.totalSubtasks += task.subtasks.length;
						task.subtasks.forEach((subtask) => {
							const subStatus = subtask.status || 'pending';
							acc.subtasksByStatus[subStatus] =
								(acc.subtasksByStatus[subStatus] || 0) + 1;
						});
					}
					return acc;
				},
				{ totalSubtasks: 0, subtasksByStatus: {} }
			);

			return {
				name: tag.name,
				isCurrent: tag.isCurrent,
				taskCount: tasks.length,
				completedTasks: tag.completedTasks,
				statusBreakdown,
				subtaskCounts,
				created: tag.created,
				description: tag.description
			};
		});

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				tags: tagsSummary,
				currentTag: result.currentTag,
				totalTags: result.totalTags,
				message: `Found ${result.totalTags} tag(s)`
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in listTagsDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: error.code || 'LIST_TAGS_ERROR',
				message: error.message
			}
		};
	}
}

// VS Code conversion profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { log } from '../../scripts/modules/utils.js';
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';

/**
 * Transform standard MCP config format to VS Code format
 * @param {Object} mcpConfig - Standard MCP configuration object
 * @returns {Object} - Transformed VS Code configuration object
 */
function transformToVSCodeFormat(mcpConfig) {
	const vscodeConfig = {};

	// Transform mcpServers to servers
	if (mcpConfig.mcpServers) {
		vscodeConfig.servers = {};

		for (const [serverName, serverConfig] of Object.entries(
			mcpConfig.mcpServers
		)) {
			// Transform server configuration
			const transformedServer = {
				...serverConfig
			};

			// Add type: "stdio" after the env block
			if (transformedServer.env) {
				// Reorder properties: keep command, args, env, then add type
				const reorderedServer = {};
				if (transformedServer.command)
					reorderedServer.command = transformedServer.command;
				if (transformedServer.args)
					reorderedServer.args = transformedServer.args;
				if (transformedServer.env) reorderedServer.env = transformedServer.env;
				reorderedServer.type = 'stdio';

				// Add any other properties that might exist
				Object.keys(transformedServer).forEach((key) => {
					if (!['command', 'args', 'env', 'type'].includes(key)) {
						reorderedServer[key] = transformedServer[key];
					}
				});

				vscodeConfig.servers[serverName] = reorderedServer;
			} else {
				// If no env block, just add type at the end
				transformedServer.type = 'stdio';
				vscodeConfig.servers[serverName] = transformedServer;
			}
		}
	}

	return vscodeConfig;
}

/**
 * Lifecycle function called after MCP config generation to transform to VS Code format
 * @param {string} targetDir - Target project directory
 * @param {string} assetsDir - Assets directory (unused for VS Code)
 */
function onPostConvertRulesProfile(targetDir, assetsDir) {
	const vscodeConfigPath = path.join(targetDir, '.vscode', 'mcp.json');

	if (!fs.existsSync(vscodeConfigPath)) {
		log('debug', '[VS Code] No .vscode/mcp.json found to transform');
		return;
	}

	try {
		// Read the generated standard MCP config
		const mcpConfigContent = fs.readFileSync(vscodeConfigPath, 'utf8');
		const mcpConfig = JSON.parse(mcpConfigContent);

		// Check if it's already in VS Code format (has servers instead of mcpServers)
		if (mcpConfig.servers) {
			log(
				'info',
				'[VS Code] mcp.json already in VS Code format, skipping transformation'
			);
			return;
		}

		// Transform to VS Code format
		const vscodeConfig = transformToVSCodeFormat(mcpConfig);

		// Write back the transformed config with proper formatting
		fs.writeFileSync(
			vscodeConfigPath,
			JSON.stringify(vscodeConfig, null, 2) + '\n'
		);

		log('info', '[VS Code] Transformed mcp.json to VS Code format');
		log('debug', `[VS Code] Renamed mcpServers->servers, added type: "stdio"`);
	} catch (error) {
		log('error', `[VS Code] Failed to transform mcp.json: ${error.message}`);
	}
}

/**
 * Lifecycle function called when removing VS Code profile
 * @param {string} targetDir - Target project directory
 */
function onRemoveRulesProfile(targetDir) {
	const vscodeConfigPath = path.join(targetDir, '.vscode', 'mcp.json');

	if (!fs.existsSync(vscodeConfigPath)) {
		log('debug', '[VS Code] No .vscode/mcp.json found to clean up');
		return;
	}

	try {
		// Read the current config
		const configContent = fs.readFileSync(vscodeConfigPath, 'utf8');
		const config = JSON.parse(configContent);

		// Check if it has the servers section and task-master-ai server
		if (config.servers && config.servers['task-master-ai']) {
			// Remove task-master-ai server
			delete config.servers['task-master-ai'];

			// Check if there are other MCP servers
			const remainingServers = Object.keys(config.servers);

			if (remainingServers.length === 0) {
				// No other servers, remove entire file
				fs.rmSync(vscodeConfigPath, { force: true });
				log('info', '[VS Code] Removed empty mcp.json file');

				// Also remove .vscode directory if it's empty
				const vscodeDir = path.dirname(vscodeConfigPath);
				try {
					const dirContents = fs.readdirSync(vscodeDir);
					if (dirContents.length === 0) {
						fs.rmSync(vscodeDir, { recursive: true, force: true });
						log('debug', '[VS Code] Removed empty .vscode directory');
					}
				} catch (err) {
					// Directory might not be empty or might not exist, that's fine
				}
			} else {
				// Write back the modified config
				fs.writeFileSync(
					vscodeConfigPath,
					JSON.stringify(config, null, 2) + '\n'
				);
				log(
					'info',
					'[VS Code] Removed TaskMaster from mcp.json, preserved other configurations'
				);
			}
		} else {
			log('debug', '[VS Code] TaskMaster not found in mcp.json');
		}
	} catch (error) {
		log('error', `[VS Code] Failed to clean up mcp.json: ${error.message}`);
	}
}

// Create and export vscode profile using the base factory
export const vscodeProfile = createProfile({
	name: 'vscode',
	displayName: 'VS Code',
	url: 'code.visualstudio.com',
	docsUrl: 'code.visualstudio.com/docs',
	rulesDir: '.github/instructions', // VS Code instructions location
	profileDir: '.vscode', // VS Code configuration directory
	mcpConfigName: 'mcp.json', // VS Code uses mcp.json in .vscode directory
	targetExtension: '.instructions.md',
	customReplacements: [
		// Core VS Code directory structure changes
		{ from: /\.cursor\/rules/g, to: '.github/instructions' },
		{ from: /\.cursor\/mcp\.json/g, to: '.vscode/mcp.json' },

		// Fix any remaining vscode/rules references that might be created during transformation
		{ from: /\.vscode\/rules/g, to: '.github/instructions' },

		// VS Code custom instructions format - use applyTo with quoted patterns instead of globs
		{ from: /^globs:\s*(.+)$/gm, to: 'applyTo: "$1"' },

		// Remove unsupported property - alwaysApply
		{ from: /^alwaysApply:\s*(true|false)\s*\n?/gm, to: '' },

		// Essential markdown link transformations for VS Code structure
		{
			from: /\[(.+?)\]\(mdc:\.cursor\/rules\/(.+?)\.mdc\)/g,
			to: '[$1](.github/instructions/$2.instructions.md)'
		},

		// VS Code specific terminology
		{ from: /rules directory/g, to: 'instructions directory' },
		{ from: /cursor rules/gi, to: 'VS Code instructions' }
	],
	onPostConvert: onPostConvertRulesProfile,
	onRemove: onRemoveRulesProfile
});

// Export lifecycle functions separately to avoid naming conflicts
export { onPostConvertRulesProfile, onRemoveRulesProfile };

import path from 'path';

import { log, readJSON, writeJSON, getCurrentTag } from '../utils.js';
import { isTaskDependentOn } from '../task-manager.js';
import generateTaskFiles from './generate-task-files.js';

/**
 * Add a subtask to a parent task
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {number|string} parentId - ID of the parent task
 * @param {number|string|null} existingTaskId - ID of an existing task to convert to subtask (optional)
 * @param {Object} newSubtaskData - Data for creating a new subtask (used if existingTaskId is null)
 * @param {boolean} generateFiles - Whether to regenerate task files after adding the subtask
 * @param {Object} context - Context object containing projectRoot and tag information
 * @param {string} context.projectRoot - Project root path
 * @param {string} context.tag - Tag for the task
 * @returns {Object} The newly created or converted subtask
 */
async function addSubtask(
	tasksPath,
	parentId,
	existingTaskId = null,
	newSubtaskData = null,
	generateFiles = false,
	context = {}
) {
	const { projectRoot, tag } = context;
	try {
		log('info', `Adding subtask to parent task ${parentId}...`);

		// Read the existing tasks with proper context
		const data = readJSON(tasksPath, projectRoot, tag);
		if (!data || !data.tasks) {
			throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
		}

		// Convert parent ID to number
		const parentIdNum = parseInt(parentId, 10);

		// Find the parent task
		const parentTask = data.tasks.find((t) => t.id === parentIdNum);
		if (!parentTask) {
			throw new Error(`Parent task with ID ${parentIdNum} not found`);
		}

		// Initialize subtasks array if it doesn't exist
		if (!parentTask.subtasks) {
			parentTask.subtasks = [];
		}

		let newSubtask;

		// Case 1: Convert an existing task to a subtask
		if (existingTaskId !== null) {
			const existingTaskIdNum = parseInt(existingTaskId, 10);

			// Find the existing task
			const existingTaskIndex = data.tasks.findIndex(
				(t) => t.id === existingTaskIdNum
			);
			if (existingTaskIndex === -1) {
				throw new Error(`Task with ID ${existingTaskIdNum} not found`);
			}

			const existingTask = data.tasks[existingTaskIndex];

			// Check if task is already a subtask
			if (existingTask.parentTaskId) {
				throw new Error(
					`Task ${existingTaskIdNum} is already a subtask of task ${existingTask.parentTaskId}`
				);
			}

			// Check for circular dependency
			if (existingTaskIdNum === parentIdNum) {
				throw new Error(`Cannot make a task a subtask of itself`);
			}

			// Check if parent task is a subtask of the task we're converting
			// This would create a circular dependency
			if (isTaskDependentOn(data.tasks, parentTask, existingTaskIdNum)) {
				throw new Error(
					`Cannot create circular dependency: task ${parentIdNum} is already a subtask or dependent of task ${existingTaskIdNum}`
				);
			}

			// Find the highest subtask ID to determine the next ID
			const highestSubtaskId =
				parentTask.subtasks.length > 0
					? Math.max(...parentTask.subtasks.map((st) => st.id))
					: 0;
			const newSubtaskId = highestSubtaskId + 1;

			// Clone the existing task to be converted to a subtask
			newSubtask = {
				...existingTask,
				id: newSubtaskId,
				parentTaskId: parentIdNum
			};

			// Add to parent's subtasks
			parentTask.subtasks.push(newSubtask);

			// Remove the task from the main tasks array
			data.tasks.splice(existingTaskIndex, 1);

			log(
				'info',
				`Converted task ${existingTaskIdNum} to subtask ${parentIdNum}.${newSubtaskId}`
			);
		}
		// Case 2: Create a new subtask
		else if (newSubtaskData) {
			// Find the highest subtask ID to determine the next ID
			const highestSubtaskId =
				parentTask.subtasks.length > 0
					? Math.max(...parentTask.subtasks.map((st) => st.id))
					: 0;
			const newSubtaskId = highestSubtaskId + 1;

			// Create the new subtask object
			newSubtask = {
				id: newSubtaskId,
				title: newSubtaskData.title,
				description: newSubtaskData.description || '',
				details: newSubtaskData.details || '',
				status: newSubtaskData.status || 'pending',
				dependencies: newSubtaskData.dependencies || [],
				parentTaskId: parentIdNum
			};

			// Add to parent's subtasks
			parentTask.subtasks.push(newSubtask);

			log('info', `Created new subtask ${parentIdNum}.${newSubtaskId}`);
		} else {
			throw new Error(
				'Either existingTaskId or newSubtaskData must be provided'
			);
		}

		// Write the updated tasks back to the file with proper context
		writeJSON(tasksPath, data, projectRoot, tag);

		// Generate task files if requested
		if (generateFiles) {
			log('info', 'Regenerating task files...');
			await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
		}

		return newSubtask;
	} catch (error) {
		log('error', `Error adding subtask: ${error.message}`);
		throw error;
	}
}

export default addSubtask;

/**
 * @fileoverview Converts AI SDK prompt format to Claude Code message format
 */

/**
 * Convert AI SDK prompt to Claude Code messages format
 * @param {Array} prompt - AI SDK prompt array
 * @param {Object} [mode] - Generation mode
 * @param {string} mode.type - Mode type ('regular', 'object-json', 'object-tool')
 * @returns {{messagesPrompt: string, systemPrompt?: string}}
 */
export function convertToClaudeCodeMessages(prompt, mode) {
	const messages = [];
	let systemPrompt;

	for (const message of prompt) {
		switch (message.role) {
			case 'system':
				systemPrompt = message.content;
				break;

			case 'user':
				if (typeof message.content === 'string') {
					messages.push(message.content);
				} else {
					// Handle multi-part content
					const textParts = message.content
						.filter((part) => part.type === 'text')
						.map((part) => part.text)
						.join('\n');

					if (textParts) {
						messages.push(textParts);
					}

					// Note: Image parts are not supported by Claude Code CLI
					const imageParts = message.content.filter(
						(part) => part.type === 'image'
					);
					if (imageParts.length > 0) {
						console.warn(
							'Claude Code CLI does not support image inputs. Images will be ignored.'
						);
					}
				}
				break;

			case 'assistant':
				if (typeof message.content === 'string') {
					messages.push(`Assistant: ${message.content}`);
				} else {
					const textParts = message.content
						.filter((part) => part.type === 'text')
						.map((part) => part.text)
						.join('\n');

					if (textParts) {
						messages.push(`Assistant: ${textParts}`);
					}

					// Handle tool calls if present
					const toolCalls = message.content.filter(
						(part) => part.type === 'tool-call'
					);
					if (toolCalls.length > 0) {
						// For now, we'll just note that tool calls were made
						messages.push(`Assistant: [Tool calls made]`);
					}
				}
				break;

			case 'tool':
				// Tool results could be included in the conversation
				messages.push(
					`Tool Result (${message.content[0].toolName}): ${JSON.stringify(
						message.content[0].result
					)}`
				);
				break;
		}
	}

	// For the SDK, we need to provide a single prompt string
	// Format the conversation history properly

	// Combine system prompt with messages
	let finalPrompt = '';

	// Add system prompt at the beginning if present
	if (systemPrompt) {
		finalPrompt = systemPrompt;
	}

	if (messages.length === 0) {
		return { messagesPrompt: finalPrompt, systemPrompt };
	}

	// Format messages
	const formattedMessages = [];
	for (let i = 0; i < messages.length; i++) {
		const msg = messages[i];
		// Check if this is a user or assistant message based on content
		if (msg.startsWith('Assistant:') || msg.startsWith('Tool Result')) {
			formattedMessages.push(msg);
		} else {
			// User messages
			formattedMessages.push(`Human: ${msg}`);
		}
	}

	// Combine system prompt with messages
	if (finalPrompt) {
		finalPrompt = finalPrompt + '\n\n' + formattedMessages.join('\n\n');
	} else {
		finalPrompt = formattedMessages.join('\n\n');
	}

	// For JSON mode, add explicit instruction to ensure JSON output
	if (mode?.type === 'object-json') {
		// Make the JSON instruction even more explicit
		finalPrompt = `${finalPrompt}

CRITICAL INSTRUCTION: You MUST respond with ONLY valid JSON. Follow these rules EXACTLY:
1. Start your response with an opening brace {
2. End your response with a closing brace }
3. Do NOT include any text before the opening brace
4. Do NOT include any text after the closing brace
5. Do NOT use markdown code blocks or backticks
6. Do NOT include explanations or commentary
7. The ENTIRE response must be valid JSON that can be parsed with JSON.parse()

Begin your response with { and end with }`;
	}

	return {
		messagesPrompt: finalPrompt,
		systemPrompt
	};
}

import chalk from 'chalk';

import { log } from '../utils.js';
import { isValidTaskStatus } from '../../../src/constants/task-status.js';

/**
 * Update the status of a single task
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} taskIdInput - Task ID to update
 * @param {string} newStatus - New status
 * @param {Object} data - Tasks data
 * @param {boolean} showUi - Whether to show UI elements
 */
async function updateSingleTaskStatus(
	tasksPath,
	taskIdInput,
	newStatus,
	data,
	showUi = true
) {
	if (!isValidTaskStatus(newStatus)) {
		throw new Error(
			`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}`
		);
	}

	// Check if it's a subtask (e.g., "1.2")
	if (taskIdInput.includes('.')) {
		const [parentId, subtaskId] = taskIdInput
			.split('.')
			.map((id) => parseInt(id, 10));

		// Find the parent task
		const parentTask = data.tasks.find((t) => t.id === parentId);
		if (!parentTask) {
			throw new Error(`Parent task ${parentId} not found`);
		}

		// Find the subtask
		if (!parentTask.subtasks) {
			throw new Error(`Parent task ${parentId} has no subtasks`);
		}

		const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
		if (!subtask) {
			throw new Error(
				`Subtask ${subtaskId} not found in parent task ${parentId}`
			);
		}

		// Update the subtask status
		const oldStatus = subtask.status || 'pending';
		subtask.status = newStatus;

		log(
			'info',
			`Updated subtask ${parentId}.${subtaskId} status from '${oldStatus}' to '${newStatus}'`
		);

		// Check if all subtasks are done (if setting to 'done')
		if (
			newStatus.toLowerCase() === 'done' ||
			newStatus.toLowerCase() === 'completed'
		) {
			const allSubtasksDone = parentTask.subtasks.every(
				(st) => st.status === 'done' || st.status === 'completed'
			);

			// Suggest updating parent task if all subtasks are done
			if (
				allSubtasksDone &&
				parentTask.status !== 'done' &&
				parentTask.status !== 'completed'
			) {
				// Only show suggestion in CLI mode
				if (showUi) {
					console.log(
						chalk.yellow(
							`All subtasks of parent task ${parentId} are now marked as done.`
						)
					);
					console.log(
						chalk.yellow(
							`Consider updating the parent task status with: task-master set-status --id=${parentId} --status=done`
						)
					);
				}
			}
		}
	} else {
		// Handle regular task
		const taskId = parseInt(taskIdInput, 10);
		const task = data.tasks.find((t) => t.id === taskId);

		if (!task) {
			throw new Error(`Task ${taskId} not found`);
		}

		// Update the task status
		const oldStatus = task.status || 'pending';
		task.status = newStatus;

		log(
			'info',
			`Updated task ${taskId} status from '${oldStatus}' to '${newStatus}'`
		);

		// If marking as done, also mark all subtasks as done
		if (
			(newStatus.toLowerCase() === 'done' ||
				newStatus.toLowerCase() === 'completed') &&
			task.subtasks &&
			task.subtasks.length > 0
		) {
			const pendingSubtasks = task.subtasks.filter(
				(st) => st.status !== 'done' && st.status !== 'completed'
			);

			if (pendingSubtasks.length > 0) {
				log(
					'info',
					`Also marking ${pendingSubtasks.length} subtasks as '${newStatus}'`
				);

				pendingSubtasks.forEach((subtask) => {
					subtask.status = newStatus;
				});
			}
		}
	}
}

export default updateSingleTaskStatus;

/**
 * Direct function wrapper for clearSubtasks
 */

import { clearSubtasks } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode,
	readJSON
} from '../../../../scripts/modules/utils.js';
import fs from 'fs';
import path from 'path';

/**
 * Clear subtasks from specified tasks
 * @param {Object} args - Function arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} [args.id] - Task IDs (comma-separated) to clear subtasks from
 * @param {boolean} [args.all] - Clear subtasks from all tasks
 * @param {string} [args.tag] - Tag context to operate on (defaults to current active tag)
 * @param {string} [args.projectRoot] - Project root path (for MCP/env fallback)
 * @param {Object} log - Logger object
 * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
 */
export async function clearSubtasksDirect(args, log) {
	// Destructure expected args
	const { tasksJsonPath, id, all, tag, projectRoot } = args;
	try {
		log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`);

		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('clearSubtasksDirect called without tasksJsonPath');
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Either id or all must be provided
		if (!id && !all) {
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message:
						'Either task IDs with id parameter or all parameter must be provided'
				}
			};
		}

		// Use provided path
		const tasksPath = tasksJsonPath;

		// Check if tasks.json exists
		if (!fs.existsSync(tasksPath)) {
			return {
				success: false,
				error: {
					code: 'FILE_NOT_FOUND_ERROR',
					message: `Tasks file not found at ${tasksPath}`
				}
			};
		}

		let taskIds;

		// Use readJSON which handles silent migration and tag resolution
		const data = readJSON(tasksPath, projectRoot, tag);

		if (!data || !data.tasks) {
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message: `No tasks found in tasks file: ${tasksPath}`
				}
			};
		}

		const currentTag = data.tag || tag;
		const tasks = data.tasks;

		// If all is specified, get all task IDs
		if (all) {
			log.info(`Clearing subtasks from all tasks in tag '${currentTag}'`);
			if (tasks.length === 0) {
				return {
					success: false,
					error: {
						code: 'INPUT_VALIDATION_ERROR',
						message: `No tasks found in tag context '${currentTag}'`
					}
				};
			}
			taskIds = tasks.map((t) => t.id).join(',');
		} else {
			// Use the provided task IDs
			taskIds = id;
		}

		log.info(`Clearing subtasks from tasks: ${taskIds} in tag '${currentTag}'`);

		// Enable silent mode to prevent console logs from interfering with JSON response
		enableSilentMode();

		// Call the core function
		clearSubtasks(tasksPath, taskIds, { projectRoot, tag: currentTag });

		// Restore normal logging
		disableSilentMode();

		// Read the updated data to provide a summary
		const updatedData = readJSON(tasksPath, projectRoot, currentTag);
		const taskIdArray = taskIds.split(',').map((id) => parseInt(id.trim(), 10));

		// Build a summary of what was done
		const clearedTasksCount = taskIdArray.length;
		const updatedTasks = updatedData.tasks || [];

		const taskSummary = taskIdArray.map((id) => {
			const task = updatedTasks.find((t) => t.id === id);
			return task ? { id, title: task.title } : { id, title: 'Task not found' };
		});

		return {
			success: true,
			data: {
				message: `Successfully cleared subtasks from ${clearedTasksCount} task(s) in tag '${currentTag}'`,
				tasksCleared: taskSummary,
				tag: currentTag
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in clearSubtasksDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'CORE_FUNCTION_ERROR',
				message: error.message
			}
		};
	}
}

import path from 'path';
import { log, readJSON, writeJSON } from '../utils.js';
import generateTaskFiles from './generate-task-files.js';

/**
 * Remove a subtask from its parent task
 * @param {string} tasksPath - Path to the tasks.json file
 * @param {string} subtaskId - ID of the subtask to remove in format "parentId.subtaskId"
 * @param {boolean} convertToTask - Whether to convert the subtask to a standalone task
 * @param {boolean} generateFiles - Whether to regenerate task files after removing the subtask
 * @param {Object} context - Context object containing projectRoot and tag information
 * @param {string} [context.projectRoot] - Project root path
 * @param {string} [context.tag] - Tag for the task
 * @returns {Object|null} The removed subtask if convertToTask is true, otherwise null
 */
async function removeSubtask(
	tasksPath,
	subtaskId,
	convertToTask = false,
	generateFiles = false,
	context = {}
) {
	const { projectRoot, tag } = context;
	try {
		log('info', `Removing subtask ${subtaskId}...`);

		// Read the existing tasks with proper context
		const data = readJSON(tasksPath, projectRoot, tag);
		if (!data || !data.tasks) {
			throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
		}

		// Parse the subtask ID (format: "parentId.subtaskId")
		if (!subtaskId.includes('.')) {
			throw new Error(
				`Invalid subtask ID format: ${subtaskId}. Expected format: "parentId.subtaskId"`
			);
		}

		const [parentIdStr, subtaskIdStr] = subtaskId.split('.');
		const parentId = parseInt(parentIdStr, 10);
		const subtaskIdNum = parseInt(subtaskIdStr, 10);

		// Find the parent task
		const parentTask = data.tasks.find((t) => t.id === parentId);
		if (!parentTask) {
			throw new Error(`Parent task with ID ${parentId} not found`);
		}

		// Check if parent has subtasks
		if (!parentTask.subtasks || parentTask.subtasks.length === 0) {
			throw new Error(`Parent task ${parentId} has no subtasks`);
		}

		// Find the subtask to remove
		const subtaskIndex = parentTask.subtasks.findIndex(
			(st) => st.id === subtaskIdNum
		);
		if (subtaskIndex === -1) {
			throw new Error(`Subtask ${subtaskId} not found`);
		}

		// Get a copy of the subtask before removing it
		const removedSubtask = { ...parentTask.subtasks[subtaskIndex] };

		// Remove the subtask from the parent
		parentTask.subtasks.splice(subtaskIndex, 1);

		// If parent has no more subtasks, remove the subtasks array
		if (parentTask.subtasks.length === 0) {
			parentTask.subtasks = undefined;
		}

		let convertedTask = null;

		// Convert the subtask to a standalone task if requested
		if (convertToTask) {
			log('info', `Converting subtask ${subtaskId} to a standalone task...`);

			// Find the highest task ID to determine the next ID
			const highestId = Math.max(...data.tasks.map((t) => t.id));
			const newTaskId = highestId + 1;

			// Create the new task from the subtask
			convertedTask = {
				id: newTaskId,
				title: removedSubtask.title,
				description: removedSubtask.description || '',
				details: removedSubtask.details || '',
				status: removedSubtask.status || 'pending',
				dependencies: removedSubtask.dependencies || [],
				priority: parentTask.priority || 'medium' // Inherit priority from parent
			};

			// Add the parent task as a dependency if not already present
			if (!convertedTask.dependencies.includes(parentId)) {
				convertedTask.dependencies.push(parentId);
			}

			// Add the converted task to the tasks array
			data.tasks.push(convertedTask);

			log('info', `Created new task ${newTaskId} from subtask ${subtaskId}`);
		} else {
			log('info', `Subtask ${subtaskId} deleted`);
		}

		// Write the updated tasks back to the file with proper context
		writeJSON(tasksPath, data, projectRoot, tag);

		// Generate task files if requested
		if (generateFiles) {
			log('info', 'Regenerating task files...');
			await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
		}

		return convertedTask;
	} catch (error) {
		log('error', `Error removing subtask: ${error.message}`);
		throw error;
	}
}

export default removeSubtask;

// Utility to manage .gitignore files with task file preferences and template merging
import fs from 'fs';
import path from 'path';

// Constants
const TASK_FILES_COMMENT = '# Task files';
const TASK_JSON_PATTERN = 'tasks.json';
const TASK_DIR_PATTERN = 'tasks/';

/**
 * Normalizes a line by removing comments and trimming whitespace
 * @param {string} line - Line to normalize
 * @returns {string} Normalized line
 */
function normalizeLine(line) {
	return line.trim().replace(/^#/, '').trim();
}

/**
 * Checks if a line is task-related (tasks.json or tasks/)
 * @param {string} line - Line to check
 * @returns {boolean} True if line is task-related
 */
function isTaskLine(line) {
	const normalized = normalizeLine(line);
	return normalized === TASK_JSON_PATTERN || normalized === TASK_DIR_PATTERN;
}

/**
 * Adjusts task-related lines in template based on storage preference
 * @param {string[]} templateLines - Array of template lines
 * @param {boolean} storeTasksInGit - Whether to comment out task lines
 * @returns {string[]} Adjusted template lines
 */
function adjustTaskLinesInTemplate(templateLines, storeTasksInGit) {
	return templateLines.map((line) => {
		if (isTaskLine(line)) {
			const normalized = normalizeLine(line);
			// Preserve original trailing whitespace from the line
			const originalTrailingSpace = line.match(/\s*$/)[0];
			return storeTasksInGit
				? `# ${normalized}${originalTrailingSpace}`
				: `${normalized}${originalTrailingSpace}`;
		}
		return line;
	});
}

/**
 * Removes existing task files section from content
 * @param {string[]} existingLines - Existing file lines
 * @returns {string[]} Lines with task section removed
 */
function removeExistingTaskSection(existingLines) {
	const cleanedLines = [];
	let inTaskSection = false;

	for (const line of existingLines) {
		// Start of task files section
		if (line.trim() === TASK_FILES_COMMENT) {
			inTaskSection = true;
			continue;
		}

		// Task lines (commented or not)
		if (isTaskLine(line)) {
			continue;
		}

		// Empty lines within task section
		if (inTaskSection && !line.trim()) {
			continue;
		}

		// End of task section (any non-empty, non-task line)
		if (inTaskSection && line.trim() && !isTaskLine(line)) {
			inTaskSection = false;
		}

		// Keep all other lines
		if (!inTaskSection) {
			cleanedLines.push(line);
		}
	}

	return cleanedLines;
}

/**
 * Filters template lines to only include new content not already present
 * @param {string[]} templateLines - Template lines
 * @param {Set<string>} existingLinesSet - Set of existing trimmed lines
 * @returns {string[]} New lines to add
 */
function filterNewTemplateLines(templateLines, existingLinesSet) {
	return templateLines.filter((line) => {
		const trimmed = line.trim();
		if (!trimmed) return false;

		// Skip task-related lines (handled separately)
		if (isTaskLine(line) || trimmed === TASK_FILES_COMMENT) {
			return false;
		}

		// Include only if not already present
		return !existingLinesSet.has(trimmed);
	});
}

/**
 * Builds the task files section based on storage preference
 * @param {boolean} storeTasksInGit - Whether to comment out task lines
 * @returns {string[]} Task files section lines
 */
function buildTaskFilesSection(storeTasksInGit) {
	const section = [TASK_FILES_COMMENT];

	if (storeTasksInGit) {
		section.push(`# ${TASK_JSON_PATTERN}`, `# ${TASK_DIR_PATTERN} `);
	} else {
		section.push(TASK_JSON_PATTERN, `${TASK_DIR_PATTERN} `);
	}

	return section;
}

/**
 * Adds a separator line if needed (avoids double spacing)
 * @param {string[]} lines - Current lines array
 */
function addSeparatorIfNeeded(lines) {
	if (lines.some((line) => line.trim())) {
		const lastLine = lines[lines.length - 1];
		if (lastLine && lastLine.trim()) {
			lines.push('');
		}
	}
}

/**
 * Validates input parameters
 * @param {string} targetPath - Path to .gitignore file
 * @param {string} content - Template content
 * @param {boolean} storeTasksInGit - Storage preference
 * @throws {Error} If validation fails
 */
function validateInputs(targetPath, content, storeTasksInGit) {
	if (!targetPath || typeof targetPath !== 'string') {
		throw new Error('targetPath must be a non-empty string');
	}

	if (!targetPath.endsWith('.gitignore')) {
		throw new Error('targetPath must end with .gitignore');
	}

	if (!content || typeof content !== 'string') {
		throw new Error('content must be a non-empty string');
	}

	if (typeof storeTasksInGit !== 'boolean') {
		throw new Error('storeTasksInGit must be a boolean');
	}
}

/**
 * Creates a new .gitignore file from template
 * @param {string} targetPath - Path to create file at
 * @param {string[]} templateLines - Adjusted template lines
 * @param {function} log - Logging function
 */
function createNewGitignoreFile(targetPath, templateLines, log) {
	try {
		fs.writeFileSync(targetPath, templateLines.join('\n') + '\n');
		if (typeof log === 'function') {
			log('success', `Created ${targetPath} with full template`);
		}
	} catch (error) {
		if (typeof log === 'function') {
			log('error', `Failed to create ${targetPath}: ${error.message}`);
		}
		throw error;
	}
}

/**
 * Merges template content with existing .gitignore file
 * @param {string} targetPath - Path to existing file
 * @param {string[]} templateLines - Adjusted template lines
 * @param {boolean} storeTasksInGit - Storage preference
 * @param {function} log - Logging function
 */
function mergeWithExistingFile(
	targetPath,
	templateLines,
	storeTasksInGit,
	log
) {
	try {
		// Read and process existing file
		const existingContent = fs.readFileSync(targetPath, 'utf8');
		const existingLines = existingContent.split('\n');

		// Remove existing task section
		const cleanedExistingLines = removeExistingTaskSection(existingLines);

		// Find new template lines to add
		const existingLinesSet = new Set(
			cleanedExistingLines.map((line) => line.trim()).filter((line) => line)
		);
		const newLines = filterNewTemplateLines(templateLines, existingLinesSet);

		// Build final content
		const finalLines = [...cleanedExistingLines];

		// Add new template content
		if (newLines.length > 0) {
			addSeparatorIfNeeded(finalLines);
			finalLines.push(...newLines);
		}

		// Add task files section
		addSeparatorIfNeeded(finalLines);
		finalLines.push(...buildTaskFilesSection(storeTasksInGit));

		// Write result
		fs.writeFileSync(targetPath, finalLines.join('\n') + '\n');

		if (typeof log === 'function') {
			const hasNewContent =
				newLines.length > 0 ? ' and merged new content' : '';
			log(
				'success',
				`Updated ${targetPath} according to user preference${hasNewContent}`
			);
		}
	} catch (error) {
		if (typeof log === 'function') {
			log(
				'error',
				`Failed to merge content with ${targetPath}: ${error.message}`
			);
		}
		throw error;
	}
}

/**
 * Manages .gitignore file creation and updates with task file preferences
 * @param {string} targetPath - Path to the .gitignore file
 * @param {string} content - Template content for .gitignore
 * @param {boolean} storeTasksInGit - Whether to store tasks in git or not
 * @param {function} log - Logging function (level, message)
 * @throws {Error} If validation or file operations fail
 */
function manageGitignoreFile(
	targetPath,
	content,
	storeTasksInGit = true,
	log = null
) {
	// Validate inputs
	validateInputs(targetPath, content, storeTasksInGit);

	// Process template with task preference
	const templateLines = content.split('\n');
	const adjustedTemplateLines = adjustTaskLinesInTemplate(
		templateLines,
		storeTasksInGit
	);

	// Handle file creation or merging
	if (!fs.existsSync(targetPath)) {
		createNewGitignoreFile(targetPath, adjustedTemplateLines, log);
	} else {
		mergeWithExistingFile(
			targetPath,
			adjustedTemplateLines,
			storeTasksInGit,
			log
		);
	}
}

export default manageGitignoreFile;
export {
	manageGitignoreFile,
	normalizeLine,
	isTaskLine,
	buildTaskFilesSection,
	TASK_FILES_COMMENT,
	TASK_JSON_PATTERN,
	TASK_DIR_PATTERN
};

// Kiro profile for rule-transformer
import { createProfile } from './base-profile.js';
import fs from 'fs';
import path from 'path';
import { log } from '../../scripts/modules/utils.js';

// Create and export kiro profile using the base factory
export const kiroProfile = createProfile({
	name: 'kiro',
	displayName: 'Kiro',
	url: 'kiro.dev',
	docsUrl: 'kiro.dev/docs',
	profileDir: '.kiro',
	rulesDir: '.kiro/steering', // Kiro rules location (full path)
	mcpConfig: true,
	mcpConfigName: 'settings/mcp.json', // Create directly in settings subdirectory
	includeDefaultRules: true, // Include default rules to get all the standard files
	targetExtension: '.md',
	fileMap: {
		// Override specific mappings - the base profile will create:
		// 'rules/cursor_rules.mdc': 'kiro_rules.md'
		// 'rules/dev_workflow.mdc': 'dev_workflow.md'
		// 'rules/self_improve.mdc': 'self_improve.md'
		// 'rules/taskmaster.mdc': 'taskmaster.md'
		// We can add additional custom mappings here if needed
		'rules/taskmaster_hooks_workflow.mdc': 'taskmaster_hooks_workflow.md'
	},
	customReplacements: [
		// Core Kiro directory structure changes
		{ from: /\.cursor\/rules/g, to: '.kiro/steering' },
		{ from: /\.cursor\/mcp\.json/g, to: '.kiro/settings/mcp.json' },

		// Fix any remaining kiro/rules references that might be created during transformation
		{ from: /\.kiro\/rules/g, to: '.kiro/steering' },

		// Essential markdown link transformations for Kiro structure
		{
			from: /\[(.+?)\]\(mdc:\.cursor\/rules\/(.+?)\.mdc\)/g,
			to: '[$1](.kiro/steering/$2.md)'
		},

		// Kiro specific terminology
		{ from: /rules directory/g, to: 'steering directory' },
		{ from: /cursor rules/gi, to: 'Kiro steering files' },

		// Transform frontmatter to Kiro format
		// This regex matches the entire frontmatter block and replaces it
		{
			from: /^---\n(?:description:\s*[^\n]*\n)?(?:globs:\s*[^\n]*\n)?(?:alwaysApply:\s*true\n)?---/m,
			to: '---\ninclusion: always\n---'
		}
	],

	// Add lifecycle hook to copy Kiro hooks
	onPostConvert: (projectRoot, assetsDir) => {
		const hooksSourceDir = path.join(assetsDir, 'kiro-hooks');
		const hooksTargetDir = path.join(projectRoot, '.kiro', 'hooks');

		// Create hooks directory if it doesn't exist
		if (!fs.existsSync(hooksTargetDir)) {
			fs.mkdirSync(hooksTargetDir, { recursive: true });
		}

		// Copy all .kiro.hook files
		if (fs.existsSync(hooksSourceDir)) {
			const hookFiles = fs
				.readdirSync(hooksSourceDir)
				.filter((f) => f.endsWith('.kiro.hook'));

			hookFiles.forEach((file) => {
				const sourcePath = path.join(hooksSourceDir, file);
				const targetPath = path.join(hooksTargetDir, file);

				fs.copyFileSync(sourcePath, targetPath);
			});

			if (hookFiles.length > 0) {
				log(
					'info',
					`[Kiro] Installed ${hookFiles.length} Taskmaster hooks in .kiro/hooks/`
				);
			}
		}
	}
});

/**
 * models.js
 * Core functionality for managing AI model configurations
 */

import https from 'https';
import http from 'http';
import {
	getMainModelId,
	getResearchModelId,
	getFallbackModelId,
	getAvailableModels,
	getMainProvider,
	getResearchProvider,
	getFallbackProvider,
	isApiKeySet,
	getMcpApiKeyStatus,
	getConfig,
	writeConfig,
	isConfigFilePresent,
	getAllProviders,
	getBaseUrlForRole
} from '../config-manager.js';
import { findConfigPath } from '../../../src/utils/path-utils.js';
import { log } from '../utils.js';
import { CUSTOM_PROVIDERS } from '../../../src/constants/providers.js';

// Constants
const CONFIG_MISSING_ERROR =
	'The configuration file is missing. Run "task-master init" to create it.';

/**
 * Fetches the list of models from OpenRouter API.
 * @returns {Promise<Array|null>} A promise that resolves with the list of model IDs or null if fetch fails.
 */
function fetchOpenRouterModels() {
	return new Promise((resolve) => {
		const options = {
			hostname: 'openrouter.ai',
			path: '/api/v1/models',
			method: 'GET',
			headers: {
				Accept: 'application/json'
			}
		};

		const req = https.request(options, (res) => {
			let data = '';
			res.on('data', (chunk) => {
				data += chunk;
			});
			res.on('end', () => {
				if (res.statusCode === 200) {
					try {
						const parsedData = JSON.parse(data);
						resolve(parsedData.data || []); // Return the array of models
					} catch (e) {
						console.error('Error parsing OpenRouter response:', e);
						resolve(null); // Indicate failure
					}
				} else {
					console.error(
						`OpenRouter API request failed with status code: ${res.statusCode}`
					);
					resolve(null); // Indicate failure
				}
			});
		});

		req.on('error', (e) => {
			console.error('Error fetching OpenRouter models:', e);
			resolve(null); // Indicate failure
		});
		req.end();
	});
}

/**
 * Fetches the list of models from Ollama instance.
 * @param {string} baseURL - The base URL for the Ollama API (e.g., "http://localhost:11434/api")
 * @returns {Promise<Array|null>} A promise that resolves with the list of model objects or null if fetch fails.
 */
function fetchOllamaModels(baseURL = 'http://localhost:11434/api') {
	return new Promise((resolve) => {
		try {
			// Parse the base URL to extract hostname, port, and base path
			const url = new URL(baseURL);
			const isHttps = url.protocol === 'https:';
			const port = url.port || (isHttps ? 443 : 80);
			const basePath = url.pathname.endsWith('/')
				? url.pathname.slice(0, -1)
				: url.pathname;

			const options = {
				hostname: url.hostname,
				port: parseInt(port, 10),
				path: `${basePath}/tags`,
				method: 'GET',
				headers: {
					Accept: 'application/json'
				}
			};

			const requestLib = isHttps ? https : http;
			const req = requestLib.request(options, (res) => {
				let data = '';
				res.on('data', (chunk) => {
					data += chunk;
				});
				res.on('end', () => {
					if (res.statusCode === 200) {
						try {
							const parsedData = JSON.parse(data);
							resolve(parsedData.models || []); // Return the array of models
						} catch (e) {
							console.error('Error parsing Ollama response:', e);
							resolve(null); // Indicate failure
						}
					} else {
						console.error(
							`Ollama API request failed with status code: ${res.statusCode}`
						);
						resolve(null); // Indicate failure
					}
				});
			});

			req.on('error', (e) => {
				console.error('Error fetching Ollama models:', e);
				resolve(null); // Indicate failure
			});
			req.end();
		} catch (e) {
			console.error('Error parsing Ollama base URL:', e);
			resolve(null); // Indicate failure
		}
	});
}

/**
 * Get the current model configuration
 * @param {Object} [options] - Options for the operation
 * @param {Object} [options.session] - Session object containing environment variables (for MCP)
 * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
 * @param {string} [options.projectRoot] - Project root directory
 * @returns {Object} RESTful response with current model configuration
 */
async function getModelConfiguration(options = {}) {
	const { mcpLog, projectRoot, session } = options;

	const report = (level, ...args) => {
		if (mcpLog && typeof mcpLog[level] === 'function') {
			mcpLog[level](...args);
		}
	};

	if (!projectRoot) {
		throw new Error('Project root is required but not found.');
	}

	// Use centralized config path finding instead of hardcoded path
	const configPath = findConfigPath(null, { projectRoot });
	const configExists = isConfigFilePresent(projectRoot);

	log(
		'debug',
		`Checking for config file using findConfigPath, found: ${configPath}`
	);
	log(
		'debug',
		`Checking config file using isConfigFilePresent(), exists: ${configExists}`
	);

	if (!configExists) {
		throw new Error(CONFIG_MISSING_ERROR);
	}

	try {
		// Get current settings - these should use the config from the found path automatically
		const mainProvider = getMainProvider(projectRoot);
		const mainModelId = getMainModelId(projectRoot);
		const researchProvider = getResearchProvider(projectRoot);
		const researchModelId = getResearchModelId(projectRoot);
		const fallbackProvider = getFallbackProvider(projectRoot);
		const fallbackModelId = getFallbackModelId(projectRoot);

		// Check API keys
		const mainCliKeyOk = isApiKeySet(mainProvider, session, projectRoot);
		const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider, projectRoot);
		const researchCliKeyOk = isApiKeySet(
			researchProvider,
			session,
			projectRoot
		);
		const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider, projectRoot);
		const fallbackCliKeyOk = fallbackProvider
			? isApiKeySet(fallbackProvider, session, projectRoot)
			: true;
		const fallbackMcpKeyOk = fallbackProvider
			? getMcpApiKeyStatus(fallbackProvider, projectRoot)
			: true;

		// Get available models to find detailed info
		const availableModels = getAvailableModels(projectRoot);

		// Find model details
		const mainModelData = availableModels.find((m) => m.id === mainModelId);
		const researchModelData = availableModels.find(
			(m) => m.id === researchModelId
		);
		const fallbackModelData = fallbackModelId
			? availableModels.find((m) => m.id === fallbackModelId)
			: null;

		// Return structured configuration data
		return {
			success: true,
			data: {
				activeModels: {
					main: {
						provider: mainProvider,
						modelId: mainModelId,
						sweScore: mainModelData?.swe_score || null,
						cost: mainModelData?.cost_per_1m_tokens || null,
						keyStatus: {
							cli: mainCliKeyOk,
							mcp: mainMcpKeyOk
						}
					},
					research: {
						provider: researchProvider,
						modelId: researchModelId,
						sweScore: researchModelData?.swe_score || null,
						cost: researchModelData?.cost_per_1m_tokens || null,
						keyStatus: {
							cli: researchCliKeyOk,
							mcp: researchMcpKeyOk
						}
					},
					fallback: fallbackProvider
						? {
								provider: fallbackProvider,
								modelId: fallbackModelId,
								sweScore: fallbackModelData?.swe_score || null,
								cost: fallbackModelData?.cost_per_1m_tokens || null,
								keyStatus: {
									cli: fallbackCliKeyOk,
									mcp: fallbackMcpKeyOk
								}
							}
						: null
				},
				message: 'Successfully retrieved current model configuration'
			}
		};
	} catch (error) {
		report('error', `Error getting model configuration: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'CONFIG_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * Get all available models not currently in use
 * @param {Object} [options] - Options for the operation
 * @param {Object} [options.session] - Session object containing environment variables (for MCP)
 * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
 * @param {string} [options.projectRoot] - Project root directory
 * @returns {Object} RESTful response with available models
 */
async function getAvailableModelsList(options = {}) {
	const { mcpLog, projectRoot } = options;

	const report = (level, ...args) => {
		if (mcpLog && typeof mcpLog[level] === 'function') {
			mcpLog[level](...args);
		}
	};

	if (!projectRoot) {
		throw new Error('Project root is required but not found.');
	}

	// Use centralized config path finding instead of hardcoded path
	const configPath = findConfigPath(null, { projectRoot });
	const configExists = isConfigFilePresent(projectRoot);

	log(
		'debug',
		`Checking for config file using findConfigPath, found: ${configPath}`
	);
	log(
		'debug',
		`Checking config file using isConfigFilePresent(), exists: ${configExists}`
	);

	if (!configExists) {
		throw new Error(CONFIG_MISSING_ERROR);
	}

	try {
		// Get all available models
		const allAvailableModels = getAvailableModels(projectRoot);

		if (!allAvailableModels || allAvailableModels.length === 0) {
			return {
				success: true,
				data: {
					models: [],
					message: 'No available models found'
				}
			};
		}

		// Get currently used model IDs
		const mainModelId = getMainModelId(projectRoot);
		const researchModelId = getResearchModelId(projectRoot);
		const fallbackModelId = getFallbackModelId(projectRoot);

		// Filter out placeholder models and active models
		const activeIds = [mainModelId, researchModelId, fallbackModelId].filter(
			Boolean
		);
		const otherAvailableModels = allAvailableModels.map((model) => ({
			provider: model.provider || 'N/A',
			modelId: model.id,
			sweScore: model.swe_score || null,
			cost: model.cost_per_1m_tokens || null,
			allowedRoles: model.allowed_roles || []
		}));

		return {
			success: true,
			data: {
				models: otherAvailableModels,
				message: `Successfully retrieved ${otherAvailableModels.length} available models`
			}
		};
	} catch (error) {
		report('error', `Error getting available models: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'MODELS_LIST_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * Update a specific model in the configuration
 * @param {string} role - The model role to update ('main', 'research', 'fallback')
 * @param {string} modelId - The model ID to set for the role
 * @param {Object} [options] - Options for the operation
 * @param {string} [options.providerHint] - Provider hint if already determined ('openrouter' or 'ollama')
 * @param {Object} [options.session] - Session object containing environment variables (for MCP)
 * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
 * @param {string} [options.projectRoot] - Project root directory
 * @returns {Object} RESTful response with result of update operation
 */
async function setModel(role, modelId, options = {}) {
	const { mcpLog, projectRoot, providerHint } = options;

	const report = (level, ...args) => {
		if (mcpLog && typeof mcpLog[level] === 'function') {
			mcpLog[level](...args);
		}
	};

	if (!projectRoot) {
		throw new Error('Project root is required but not found.');
	}

	// Use centralized config path finding instead of hardcoded path
	const configPath = findConfigPath(null, { projectRoot });
	const configExists = isConfigFilePresent(projectRoot);

	log(
		'debug',
		`Checking for config file using findConfigPath, found: ${configPath}`
	);
	log(
		'debug',
		`Checking config file using isConfigFilePresent(), exists: ${configExists}`
	);

	if (!configExists) {
		throw new Error(CONFIG_MISSING_ERROR);
	}

	// Validate role
	if (!['main', 'research', 'fallback'].includes(role)) {
		return {
			success: false,
			error: {
				code: 'INVALID_ROLE',
				message: `Invalid role: ${role}. Must be one of: main, research, fallback.`
			}
		};
	}

	// Validate model ID
	if (typeof modelId !== 'string' || modelId.trim() === '') {
		return {
			success: false,
			error: {
				code: 'INVALID_MODEL_ID',
				message: `Invalid model ID: ${modelId}. Must be a non-empty string.`
			}
		};
	}

	try {
		const availableModels = getAvailableModels(projectRoot);
		const currentConfig = getConfig(projectRoot);
		let determinedProvider = null; // Initialize provider
		let warningMessage = null;

		// Find the model data in internal list initially to see if it exists at all
		let modelData = availableModels.find((m) => m.id === modelId);

		// --- Revised Logic: Prioritize providerHint --- //

		if (providerHint) {
			// Hint provided (--ollama or --openrouter flag used)
			if (modelData && modelData.provider === providerHint) {
				// Found internally AND provider matches the hint
				determinedProvider = providerHint;
				report(
					'info',
					`Model ${modelId} found internally with matching provider hint ${determinedProvider}.`
				);
			} else {
				// Either not found internally, OR found but under a DIFFERENT provider than hinted.
				// Proceed with custom logic based ONLY on the hint.
				if (providerHint === CUSTOM_PROVIDERS.OPENROUTER) {
					// Check OpenRouter ONLY because hint was openrouter
					report('info', `Checking OpenRouter for ${modelId} (as hinted)...`);
					const openRouterModels = await fetchOpenRouterModels();

					if (
						openRouterModels &&
						openRouterModels.some((m) => m.id === modelId)
					) {
						determinedProvider = CUSTOM_PROVIDERS.OPENROUTER;

						// Check if this is a free model (ends with :free)
						if (modelId.endsWith(':free')) {
							warningMessage = `Warning: OpenRouter free model '${modelId}' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version '${modelId.replace(':free', '')}' for full functionality.`;
						} else {
							warningMessage = `Warning: Custom OpenRouter model '${modelId}' set. This model is not officially validated by Taskmaster and may not function as expected.`;
						}

						report('warn', warningMessage);
					} else {
						// Hinted as OpenRouter but not found in live check
						throw new Error(
							`Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.`
						);
					}
				} else if (providerHint === CUSTOM_PROVIDERS.OLLAMA) {
					// Check Ollama ONLY because hint was ollama
					report('info', `Checking Ollama for ${modelId} (as hinted)...`);

					// Get the Ollama base URL from config
					const ollamaBaseURL = getBaseUrlForRole(role, projectRoot);
					const ollamaModels = await fetchOllamaModels(ollamaBaseURL);

					if (ollamaModels === null) {
						// Connection failed - server probably not running
						throw new Error(
							`Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.`
						);
					} else if (ollamaModels.some((m) => m.model === modelId)) {
						determinedProvider = CUSTOM_PROVIDERS.OLLAMA;
						warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`;
						report('warn', warningMessage);
					} else {
						// Server is running but model not found
						const tagsUrl = `${ollamaBaseURL}/tags`;
						throw new Error(
							`Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}`
						);
					}
				} else if (providerHint === CUSTOM_PROVIDERS.BEDROCK) {
					// Set provider without model validation since Bedrock models are managed by AWS
					determinedProvider = CUSTOM_PROVIDERS.BEDROCK;
					warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`;
					report('warn', warningMessage);
				} else if (providerHint === CUSTOM_PROVIDERS.CLAUDE_CODE) {
					// Claude Code provider - check if model exists in our list
					determinedProvider = CUSTOM_PROVIDERS.CLAUDE_CODE;
					// Re-find modelData specifically for claude-code provider
					const claudeCodeModels = availableModels.filter(
						(m) => m.provider === 'claude-code'
					);
					const claudeCodeModelData = claudeCodeModels.find(
						(m) => m.id === modelId
					);
					if (claudeCodeModelData) {
						// Update modelData to the found claude-code model
						modelData = claudeCodeModelData;
						report('info', `Setting Claude Code model '${modelId}'.`);
					} else {
						warningMessage = `Warning: Claude Code model '${modelId}' not found in supported models. Setting without validation.`;
						report('warn', warningMessage);
					}
				} else if (providerHint === CUSTOM_PROVIDERS.AZURE) {
					// Set provider without model validation since Azure models are managed by Azure
					determinedProvider = CUSTOM_PROVIDERS.AZURE;
					warningMessage = `Warning: Custom Azure model '${modelId}' set. Please ensure the model deployment is valid and accessible in your Azure account.`;
					report('warn', warningMessage);
				} else if (providerHint === CUSTOM_PROVIDERS.VERTEX) {
					// Set provider without model validation since Vertex models are managed by Google Cloud
					determinedProvider = CUSTOM_PROVIDERS.VERTEX;
					warningMessage = `Warning: Custom Vertex AI model '${modelId}' set. Please ensure the model is valid and accessible in your Google Cloud project.`;
					report('warn', warningMessage);
				} else if (providerHint === CUSTOM_PROVIDERS.GEMINI_CLI) {
					// Gemini CLI provider - check if model exists in our list
					determinedProvider = CUSTOM_PROVIDERS.GEMINI_CLI;
					// Re-find modelData specifically for gemini-cli provider
					const geminiCliModels = availableModels.filter(
						(m) => m.provider === 'gemini-cli'
					);
					const geminiCliModelData = geminiCliModels.find(
						(m) => m.id === modelId
					);
					if (geminiCliModelData) {
						// Update modelData to the found gemini-cli model
						modelData = geminiCliModelData;
						report('info', `Setting Gemini CLI model '${modelId}'.`);
					} else {
						warningMessage = `Warning: Gemini CLI model '${modelId}' not found in supported models. Setting without validation.`;
						report('warn', warningMessage);
					}
				} else {
					// Invalid provider hint - should not happen with our constants
					throw new Error(`Invalid provider hint received: ${providerHint}`);
				}
			}
		} else {
			// No hint provided (flags not used)
			if (modelData) {
				// Found internally, use the provider from the internal list
				determinedProvider = modelData.provider;
				report(
					'info',
					`Model ${modelId} found internally with provider ${determinedProvider}.`
				);
			} else {
				// Model not found and no provider hint was given
				return {
					success: false,
					error: {
						code: 'MODEL_NOT_FOUND_NO_HINT',
						message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, or --vertex.`
					}
				};
			}
		}

		// --- End of Revised Logic --- //

		// At this point, we should have a determinedProvider if the model is valid (internally or custom)
		if (!determinedProvider) {
			// This case acts as a safeguard
			return {
				success: false,
				error: {
					code: 'PROVIDER_UNDETERMINED',
					message: `Could not determine the provider for model ID "${modelId}".`
				}
			};
		}

		// Update configuration
		currentConfig.models[role] = {
			...currentConfig.models[role], // Keep existing params like temperature
			provider: determinedProvider,
			modelId: modelId
		};

		// If model data is available, update maxTokens from supported-models.json
		if (modelData && modelData.max_tokens) {
			currentConfig.models[role].maxTokens = modelData.max_tokens;
		}

		// Write updated configuration
		const writeResult = writeConfig(currentConfig, projectRoot);
		if (!writeResult) {
			return {
				success: false,
				error: {
					code: 'CONFIG_WRITE_ERROR',
					message: 'Error writing updated configuration to configuration file'
				}
			};
		}

		const successMessage = `Successfully set ${role} model to ${modelId} (Provider: ${determinedProvider})`;
		report('info', successMessage);

		return {
			success: true,
			data: {
				role,
				provider: determinedProvider,
				modelId,
				message: successMessage,
				warning: warningMessage // Include warning in the response data
			}
		};
	} catch (error) {
		report('error', `Error setting ${role} model: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'SET_MODEL_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * Get API key status for all known providers.
 * @param {Object} [options] - Options for the operation
 * @param {Object} [options.session] - Session object containing environment variables (for MCP)
 * @param {Function} [options.mcpLog] - MCP logger object (for MCP)
 * @param {string} [options.projectRoot] - Project root directory
 * @returns {Object} RESTful response with API key status report
 */
async function getApiKeyStatusReport(options = {}) {
	const { mcpLog, projectRoot, session } = options;
	const report = (level, ...args) => {
		if (mcpLog && typeof mcpLog[level] === 'function') {
			mcpLog[level](...args);
		}
	};

	try {
		const providers = getAllProviders();
		const providersToCheck = providers.filter(
			(p) => p.toLowerCase() !== 'ollama'
		); // Ollama is not a provider, it's a service, doesn't need an api key usually
		const statusReport = providersToCheck.map((provider) => {
			// Use provided projectRoot for MCP status check
			const cliOk = isApiKeySet(provider, session, projectRoot); // Pass session and projectRoot for CLI check
			const mcpOk = getMcpApiKeyStatus(provider, projectRoot);
			return {
				provider,
				cli: cliOk,
				mcp: mcpOk
			};
		});

		report('info', 'Successfully generated API key status report.');
		return {
			success: true,
			data: {
				report: statusReport,
				message: 'API key status report generated.'
			}
		};
	} catch (error) {
		report('error', `Error generating API key status report: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'API_KEY_STATUS_ERROR',
				message: error.message
			}
		};
	}
}

export {
	getModelConfiguration,
	getAvailableModelsList,
	setModel,
	getApiKeyStatusReport
};

/**
 * task-master.js
 * This module provides a centralized path management system for the Task Master application.
 * It exports the TaskMaster class and the initTaskMaster factory function to create a single,
 * authoritative source for all critical file and directory paths, resolving circular dependencies.
 */

import path from 'path';
import fs from 'fs';
import {
	TASKMASTER_DIR,
	TASKMASTER_TASKS_FILE,
	LEGACY_TASKS_FILE,
	TASKMASTER_DOCS_DIR,
	TASKMASTER_REPORTS_DIR,
	TASKMASTER_CONFIG_FILE,
	LEGACY_CONFIG_FILE,
	COMPLEXITY_REPORT_FILE
} from './constants/paths.js';
import { findProjectRoot } from './utils/path-utils.js';

/**
 * TaskMaster class manages all the paths for the application.
 * An instance of this class is created by the initTaskMaster function.
 */
export class TaskMaster {
	#paths;
	#tag;

	/**
	 * The constructor is intended to be used only by the initTaskMaster factory function.
	 * @param {object} paths - A pre-resolved object of all application paths.
	 * @param {string|undefined} tag - The current tag.
	 */
	constructor(paths, tag) {
		this.#paths = Object.freeze({ ...paths });
		this.#tag = tag;
	}

	/**
	 * @returns {string|null} The absolute path to the project root.
	 */
	getProjectRoot() {
		return this.#paths.projectRoot;
	}

	/**
	 * @returns {string|null} The absolute path to the .taskmaster directory.
	 */
	getTaskMasterDir() {
		return this.#paths.taskMasterDir;
	}

	/**
	 * @returns {string|null} The absolute path to the tasks.json file.
	 */
	getTasksPath() {
		return this.#paths.tasksPath;
	}

	/**
	 * @returns {string|null} The absolute path to the PRD file.
	 */
	getPrdPath() {
		return this.#paths.prdPath;
	}

	/**
	 * @returns {string|null} The absolute path to the complexity report.
	 */
	getComplexityReportPath() {
		if (this.#paths.complexityReportPath) {
			return this.#paths.complexityReportPath;
		}

		const complexityReportFile =
			this.getCurrentTag() !== 'master'
				? COMPLEXITY_REPORT_FILE.replace(
						'.json',
						`_${this.getCurrentTag()}.json`
					)
				: COMPLEXITY_REPORT_FILE;

		return path.join(this.#paths.projectRoot, complexityReportFile);
	}

	/**
	 * @returns {string|null} The absolute path to the config.json file.
	 */
	getConfigPath() {
		return this.#paths.configPath;
	}

	/**
	 * @returns {string|null} The absolute path to the state.json file.
	 */
	getStatePath() {
		return this.#paths.statePath;
	}

	/**
	 * @returns {object} A frozen object containing all resolved paths.
	 */
	getAllPaths() {
		return this.#paths;
	}

	/**
	 * Gets the current tag from state.json or falls back to defaultTag from config
	 * @returns {string} The current tag name
	 */
	getCurrentTag() {
		if (this.#tag) {
			return this.#tag;
		}

		try {
			// Try to read current tag from state.json using fs directly
			if (fs.existsSync(this.#paths.statePath)) {
				const rawState = fs.readFileSync(this.#paths.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 {
			if (fs.existsSync(this.#paths.configPath)) {
				const rawConfig = fs.readFileSync(this.#paths.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';
	}
}

/**
 * Initializes a TaskMaster instance with resolved paths.
 * This function centralizes path resolution logic.
 *
 * @param {object} [overrides={}] - An object with possible path overrides.
 * @param {string} [overrides.projectRoot]
 * @param {string} [overrides.tasksPath]
 * @param {string} [overrides.prdPath]
 * @param {string} [overrides.complexityReportPath]
 * @param {string} [overrides.configPath]
 * @param {string} [overrides.statePath]
 * @param {string} [overrides.tag]
 * @returns {TaskMaster} An initialized TaskMaster instance.
 */
export function initTaskMaster(overrides = {}) {
	const resolvePath = (
		pathType,
		override,
		defaultPaths = [],
		basePath = null,
		createParentDirs = false
	) => {
		if (typeof override === 'string') {
			const resolvedPath = path.isAbsolute(override)
				? override
				: path.resolve(basePath || process.cwd(), override);

			if (createParentDirs) {
				// For output paths, create parent directory if it doesn't exist
				const parentDir = path.dirname(resolvedPath);
				if (!fs.existsSync(parentDir)) {
					try {
						fs.mkdirSync(parentDir, { recursive: true });
					} catch (error) {
						throw new Error(
							`Could not create directory for ${pathType}: ${parentDir}. Error: ${error.message}`
						);
					}
				}
			} else {
				// Original validation logic
				if (!fs.existsSync(resolvedPath)) {
					throw new Error(
						`${pathType} override path does not exist: ${resolvedPath}`
					);
				}
			}
			return resolvedPath;
		}

		if (override === true) {
			// Required path - search defaults and fail if not found
			for (const defaultPath of defaultPaths) {
				const fullPath = path.isAbsolute(defaultPath)
					? defaultPath
					: path.join(basePath || process.cwd(), defaultPath);
				if (fs.existsSync(fullPath)) {
					return fullPath;
				}
			}
			throw new Error(
				`Required ${pathType} not found. Searched: ${defaultPaths.join(', ')}`
			);
		}

		// Optional path (override === false/undefined) - search defaults, return null if not found
		for (const defaultPath of defaultPaths) {
			const fullPath = path.isAbsolute(defaultPath)
				? defaultPath
				: path.join(basePath || process.cwd(), defaultPath);
			if (fs.existsSync(fullPath)) {
				return fullPath;
			}
		}

		return null;
	};

	const paths = {};

	// Project Root
	if (overrides.projectRoot) {
		const resolvedOverride = path.resolve(overrides.projectRoot);
		if (!fs.existsSync(resolvedOverride)) {
			throw new Error(
				`Project root override path does not exist: ${resolvedOverride}`
			);
		}

		const hasTaskmasterDir = fs.existsSync(
			path.join(resolvedOverride, TASKMASTER_DIR)
		);
		const hasLegacyConfig = fs.existsSync(
			path.join(resolvedOverride, LEGACY_CONFIG_FILE)
		);

		if (!hasTaskmasterDir && !hasLegacyConfig) {
			throw new Error(
				`Project root override is not a valid taskmaster project: ${resolvedOverride}`
			);
		}

		paths.projectRoot = resolvedOverride;
	} else {
		// findProjectRoot now always returns a value (fallback to cwd)
		paths.projectRoot = findProjectRoot();
	}

	// TaskMaster Directory
	if ('taskMasterDir' in overrides) {
		paths.taskMasterDir = resolvePath(
			'taskmaster directory',
			overrides.taskMasterDir,
			[TASKMASTER_DIR],
			paths.projectRoot
		);
	} else {
		paths.taskMasterDir = resolvePath(
			'taskmaster directory',
			false,
			[TASKMASTER_DIR],
			paths.projectRoot
		);
	}

	// Always set default paths first
	// These can be overridden below if needed
	paths.configPath = path.join(paths.projectRoot, TASKMASTER_CONFIG_FILE);
	paths.statePath = path.join(
		paths.taskMasterDir || path.join(paths.projectRoot, TASKMASTER_DIR),
		'state.json'
	);
	paths.tasksPath = path.join(paths.projectRoot, TASKMASTER_TASKS_FILE);

	// Handle overrides - only validate/resolve if explicitly provided
	if ('configPath' in overrides) {
		paths.configPath = resolvePath(
			'config file',
			overrides.configPath,
			[TASKMASTER_CONFIG_FILE, LEGACY_CONFIG_FILE],
			paths.projectRoot
		);
	}

	if ('statePath' in overrides) {
		paths.statePath = resolvePath(
			'state file',
			overrides.statePath,
			['state.json'],
			paths.taskMasterDir
		);
	}

	if ('tasksPath' in overrides) {
		paths.tasksPath = resolvePath(
			'tasks file',
			overrides.tasksPath,
			[TASKMASTER_TASKS_FILE, LEGACY_TASKS_FILE],
			paths.projectRoot
		);
	}

	if ('prdPath' in overrides) {
		paths.prdPath = resolvePath(
			'PRD file',
			overrides.prdPath,
			[
				path.join(TASKMASTER_DOCS_DIR, 'PRD.md'),
				path.join(TASKMASTER_DOCS_DIR, 'prd.md'),
				path.join(TASKMASTER_DOCS_DIR, 'PRD.txt'),
				path.join(TASKMASTER_DOCS_DIR, 'prd.txt'),
				path.join('scripts', 'PRD.md'),
				path.join('scripts', 'prd.md'),
				path.join('scripts', 'PRD.txt'),
				path.join('scripts', 'prd.txt'),
				'PRD.md',
				'prd.md',
				'PRD.txt',
				'prd.txt'
			],
			paths.projectRoot
		);
	}

	if ('complexityReportPath' in overrides) {
		paths.complexityReportPath = resolvePath(
			'complexity report',
			overrides.complexityReportPath,
			[
				path.join(TASKMASTER_REPORTS_DIR, 'task-complexity-report.json'),
				path.join(TASKMASTER_REPORTS_DIR, 'complexity-report.json'),
				path.join('scripts', 'task-complexity-report.json'),
				path.join('scripts', 'complexity-report.json'),
				'task-complexity-report.json',
				'complexity-report.json'
			],
			paths.projectRoot,
			true // Enable parent directory creation for output paths
		);
	}

	return new TaskMaster(paths, overrides.tag);
}

/**
 * add-task.js
 * Direct function implementation for adding a new task
 */

import { addTask } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for adding a new task with error handling.
 *
 * @param {Object} args - Command arguments
 * @param {string} [args.prompt] - Description of the task to add (required if not using manual fields)
 * @param {string} [args.title] - Task title (for manual task creation)
 * @param {string} [args.description] - Task description (for manual task creation)
 * @param {string} [args.details] - Implementation details (for manual task creation)
 * @param {string} [args.testStrategy] - Test strategy (for manual task creation)
 * @param {string} [args.dependencies] - Comma-separated list of task IDs this task depends on
 * @param {string} [args.priority='medium'] - Task priority (high, medium, low)
 * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
 * @param {boolean} [args.research=false] - Whether to use research capabilities for task creation
 * @param {string} [args.projectRoot] - Project root path
 * @param {string} [args.tag] - Tag for the task (optional)
 * @param {Object} log - Logger object
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function addTaskDirect(args, log, context = {}) {
	// Destructure expected args (including research and projectRoot)
	const {
		tasksJsonPath,
		prompt,
		dependencies,
		priority,
		research,
		projectRoot,
		tag
	} = args;
	const { session } = context; // Destructure session from context

	// Enable silent mode to prevent console logs from interfering with JSON response
	enableSilentMode();

	// Create logger wrapper using the utility
	const mcpLog = createLogWrapper(log);

	try {
		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('addTaskDirect called without tasksJsonPath');
			disableSilentMode(); // Disable before returning
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Use provided path
		const tasksPath = tasksJsonPath;

		// Check if this is manual task creation or AI-driven task creation
		const isManualCreation = args.title && args.description;

		// Check required parameters
		if (!args.prompt && !isManualCreation) {
			log.error(
				'Missing required parameters: either prompt or title+description must be provided'
			);
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_PARAMETER',
					message:
						'Either the prompt parameter or both title and description parameters are required for adding a task'
				}
			};
		}

		// Extract and prepare parameters
		const taskDependencies = Array.isArray(dependencies)
			? dependencies // Already an array if passed directly
			: dependencies // Check if dependencies exist and are a string
				? String(dependencies)
						.split(',')
						.map((id) => parseInt(id.trim(), 10)) // Split, trim, and parse
				: []; // Default to empty array if null/undefined
		const taskPriority = priority || 'medium'; // Default priority

		let manualTaskData = null;
		let newTaskId;
		let telemetryData;
		let tagInfo;

		if (isManualCreation) {
			// Create manual task data object
			manualTaskData = {
				title: args.title,
				description: args.description,
				details: args.details || '',
				testStrategy: args.testStrategy || ''
			};

			log.info(
				`Adding new task manually with title: "${args.title}", dependencies: [${taskDependencies.join(', ')}], priority: ${priority}`
			);

			// Call the addTask function with manual task data
			const result = await addTask(
				tasksPath,
				null, // prompt is null for manual creation
				taskDependencies,
				taskPriority,
				{
					session,
					mcpLog,
					projectRoot,
					commandName: 'add-task',
					outputType: 'mcp',
					tag
				},
				'json', // outputFormat
				manualTaskData, // Pass the manual task data
				false // research flag is false for manual creation
			);
			newTaskId = result.newTaskId;
			telemetryData = result.telemetryData;
			tagInfo = result.tagInfo;
		} else {
			// AI-driven task creation
			log.info(
				`Adding new task with prompt: "${prompt}", dependencies: [${taskDependencies.join(', ')}], priority: ${taskPriority}, research: ${research}`
			);

			// Call the addTask function, passing the research flag
			const result = await addTask(
				tasksPath,
				prompt, // Use the prompt for AI creation
				taskDependencies,
				taskPriority,
				{
					session,
					mcpLog,
					projectRoot,
					commandName: 'add-task',
					outputType: 'mcp',
					tag
				},
				'json', // outputFormat
				null, // manualTaskData is null for AI creation
				research // Pass the research flag
			);
			newTaskId = result.newTaskId;
			telemetryData = result.telemetryData;
			tagInfo = result.tagInfo;
		}

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				taskId: newTaskId,
				message: `Successfully added new task #${newTaskId}`,
				telemetryData: telemetryData,
				tagInfo: tagInfo
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in addTaskDirect: ${error.message}`);
		// Add specific error code checks if needed
		return {
			success: false,
			error: {
				code: error.code || 'ADD_TASK_ERROR', // Use error code if available
				message: error.message
			}
		};
	}
}

/**
 * Checks if a task with the given ID exists
 * @param {Array} tasks - Array of tasks to search
 * @param {string|number} taskId - ID of task or subtask to check
 * @returns {boolean} Whether the task exists
 */
function taskExists(tasks, taskId) {
	// Handle subtask IDs (e.g., "1.2")
	if (typeof taskId === 'string' && taskId.includes('.')) {
		const [parentIdStr, subtaskIdStr] = taskId.split('.');
		const parentId = parseInt(parentIdStr, 10);
		const subtaskId = parseInt(subtaskIdStr, 10);

		// Find the parent task
		const parentTask = tasks.find((t) => t.id === parentId);

		// If parent exists, check if subtask exists
		return (
			parentTask &&
			parentTask.subtasks &&
			parentTask.subtasks.some((st) => st.id === subtaskId)
		);
	}

	// Handle regular task IDs
	const id = parseInt(taskId, 10);
	return tasks.some((t) => t.id === id);
}

export default taskExists;

/**
 * git-utils.js
 * Git integration utilities for Task Master
 * Uses raw git commands and gh CLI for operations
 * MCP-friendly: All functions require projectRoot parameter
 */

import { exec, execSync } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import fs from 'fs';

const execAsync = promisify(exec);

/**
 * Check if the specified directory is inside a git repository
 * @param {string} projectRoot - Directory to check (required)
 * @returns {Promise<boolean>} True if inside a git repository
 */
async function isGitRepository(projectRoot) {
	if (!projectRoot) {
		throw new Error('projectRoot is required for isGitRepository');
	}

	try {
		await execAsync('git rev-parse --git-dir', { cwd: projectRoot });
		return true;
	} catch (error) {
		return false;
	}
}

/**
 * Get the current git branch name
 * @param {string} projectRoot - Directory to check (required)
 * @returns {Promise<string|null>} Current branch name or null if not in git repo
 */
async function getCurrentBranch(projectRoot) {
	if (!projectRoot) {
		throw new Error('projectRoot is required for getCurrentBranch');
	}

	try {
		const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
			cwd: projectRoot
		});
		return stdout.trim();
	} catch (error) {
		return null;
	}
}

/**
 * Get list of all local git branches
 * @param {string} projectRoot - Directory to check (required)
 * @returns {Promise<string[]>} Array of branch names
 */
async function getLocalBranches(projectRoot) {
	if (!projectRoot) {
		throw new Error('projectRoot is required for getLocalBranches');
	}

	try {
		const { stdout } = await execAsync(
			'git branch --format="%(refname:short)"',
			{ cwd: projectRoot }
		);
		return stdout
			.trim()
			.split('\n')
			.filter((branch) => branch.length > 0)
			.map((branch) => branch.trim());
	} catch (error) {
		return [];
	}
}

/**
 * Get list of all remote branches
 * @param {string} projectRoot - Directory to check (required)
 * @returns {Promise<string[]>} Array of remote branch names (without remote prefix)
 */
async function getRemoteBranches(projectRoot) {
	if (!projectRoot) {
		throw new Error('projectRoot is required for getRemoteBranches');
	}

	try {
		const { stdout } = await execAsync(
			'git branch -r --format="%(refname:short)"',
			{ cwd: projectRoot }
		);
		return stdout
			.trim()
			.split('\n')
			.filter((branch) => branch.length > 0 && !branch.includes('HEAD'))
			.map((branch) => branch.replace(/^origin\//, '').trim());
	} catch (error) {
		return [];
	}
}

/**
 * Check if gh CLI is available and authenticated
 * @param {string} [projectRoot] - Directory context (optional for this check)
 * @returns {Promise<boolean>} True if gh CLI is available and authenticated
 */
async function isGhCliAvailable(projectRoot = null) {
	try {
		const options = projectRoot ? { cwd: projectRoot } : {};
		await execAsync('gh auth status', options);
		return true;
	} catch (error) {
		return false;
	}
}

/**
 * Get GitHub repository information using gh CLI
 * @param {string} projectRoot - Directory to check (required)
 * @returns {Promise<Object|null>} Repository info or null if not available
 */
async function getGitHubRepoInfo(projectRoot) {
	if (!projectRoot) {
		throw new Error('projectRoot is required for getGitHubRepoInfo');
	}

	try {
		const { stdout } = await execAsync(
			'gh repo view --json name,owner,defaultBranchRef',
			{ cwd: projectRoot }
		);
		return JSON.parse(stdout);
	} catch (error) {
		return null;
	}
}

/**
 * Sanitize branch name to be a valid tag name
 * @param {string} branchName - Git branch name
 * @returns {string} Sanitized tag name
 */
function sanitizeBranchNameForTag(branchName) {
	if (!branchName || typeof branchName !== 'string') {
		return 'unknown-branch';
	}

	// Replace invalid characters with hyphens and clean up
	return branchName
		.replace(/[^a-zA-Z0-9_-]/g, '-') // Replace invalid chars with hyphens
		.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
		.replace(/-+/g, '-') // Collapse multiple hyphens
		.toLowerCase() // Convert to lowercase
		.substring(0, 50); // Limit length
}

/**
 * Check if a branch name would create a valid tag name
 * @param {string} branchName - Git branch name
 * @returns {boolean} True if branch name can be converted to valid tag
 */
function isValidBranchForTag(branchName) {
	if (!branchName || typeof branchName !== 'string') {
		return false;
	}

	// Check if it's a reserved branch name that shouldn't become tags
	const reservedBranches = ['main', 'master', 'develop', 'dev', 'HEAD'];
	if (reservedBranches.includes(branchName.toLowerCase())) {
		return false;
	}

	// Check if sanitized name would be meaningful
	const sanitized = sanitizeBranchNameForTag(branchName);
	return sanitized.length > 0 && sanitized !== 'unknown-branch';
}

/**
 * Get git repository root directory
 * @param {string} projectRoot - Directory to start search from (required)
 * @returns {Promise<string|null>} Git repository root path or null
 */
async function getGitRepositoryRoot(projectRoot) {
	if (!projectRoot) {
		throw new Error('projectRoot is required for getGitRepositoryRoot');
	}

	try {
		const { stdout } = await execAsync('git rev-parse --show-toplevel', {
			cwd: projectRoot
		});
		return stdout.trim();
	} catch (error) {
		return null;
	}
}

/**
 * Check if specified directory is the git repository root
 * @param {string} projectRoot - Directory to check (required)
 * @returns {Promise<boolean>} True if directory is git root
 */
async function isGitRepositoryRoot(projectRoot) {
	if (!projectRoot) {
		throw new Error('projectRoot is required for isGitRepositoryRoot');
	}

	try {
		const gitRoot = await getGitRepositoryRoot(projectRoot);
		return gitRoot && path.resolve(gitRoot) === path.resolve(projectRoot);
	} catch (error) {
		return false;
	}
}

/**
 * Get the default branch name for the repository
 * @param {string} projectRoot - Directory to check (required)
 * @returns {Promise<string|null>} Default branch name or null
 */
async function getDefaultBranch(projectRoot) {
	if (!projectRoot) {
		throw new Error('projectRoot is required for getDefaultBranch');
	}

	try {
		// Try to get from GitHub first (if gh CLI is available)
		if (await isGhCliAvailable(projectRoot)) {
			const repoInfo = await getGitHubRepoInfo(projectRoot);
			if (repoInfo && repoInfo.defaultBranchRef) {
				return repoInfo.defaultBranchRef.name;
			}
		}

		// Fallback to git remote info
		const { stdout } = await execAsync(
			'git symbolic-ref refs/remotes/origin/HEAD',
			{ cwd: projectRoot }
		);
		return stdout.replace('refs/remotes/origin/', '').trim();
	} catch (error) {
		// Final fallback - common default branch names
		const commonDefaults = ['main', 'master'];
		const branches = await getLocalBranches(projectRoot);

		for (const defaultName of commonDefaults) {
			if (branches.includes(defaultName)) {
				return defaultName;
			}
		}

		return null;
	}
}

/**
 * Check if we're currently on the default branch
 * @param {string} projectRoot - Directory to check (required)
 * @returns {Promise<boolean>} True if on default branch
 */
async function isOnDefaultBranch(projectRoot) {
	if (!projectRoot) {
		throw new Error('projectRoot is required for isOnDefaultBranch');
	}

	try {
		const currentBranch = await getCurrentBranch(projectRoot);
		const defaultBranch = await getDefaultBranch(projectRoot);
		return currentBranch && defaultBranch && currentBranch === defaultBranch;
	} catch (error) {
		return false;
	}
}

/**
 * Check and automatically switch tags based on git branch if enabled
 * This runs automatically during task operations, similar to migration
 * @param {string} projectRoot - Project root directory (required)
 * @param {string} tasksPath - Path to tasks.json file
 * @returns {Promise<void>}
 */
async function checkAndAutoSwitchGitTag(projectRoot, tasksPath) {
	if (!projectRoot) {
		throw new Error('projectRoot is required for checkAndAutoSwitchGitTag');
	}

	// DISABLED: Automatic git workflow is too rigid and opinionated
	// Users should explicitly use git-tag commands if they want integration
	return;
}

/**
 * Synchronous version of git tag checking and switching
 * This runs during readJSON to ensure git integration happens BEFORE tag resolution
 * @param {string} projectRoot - Project root directory (required)
 * @param {string} tasksPath - Path to tasks.json file
 * @returns {void}
 */
function checkAndAutoSwitchGitTagSync(projectRoot, tasksPath) {
	if (!projectRoot) {
		return; // Can't proceed without project root
	}

	// DISABLED: Automatic git workflow is too rigid and opinionated
	// Users should explicitly use git-tag commands if they want integration
	return;
}

/**
 * Synchronous check if directory is in a git repository
 * @param {string} projectRoot - Directory to check (required)
 * @returns {boolean} True if inside a git repository
 */
function isGitRepositorySync(projectRoot) {
	if (!projectRoot) {
		return false;
	}

	try {
		execSync('git rev-parse --git-dir', {
			cwd: projectRoot,
			stdio: 'ignore' // Suppress output
		});
		return true;
	} catch (error) {
		return false;
	}
}

/**
 * Synchronous get current git branch name
 * @param {string} projectRoot - Directory to check (required)
 * @returns {string|null} Current branch name or null if not in git repo
 */
function getCurrentBranchSync(projectRoot) {
	if (!projectRoot) {
		return null;
	}

	try {
		const stdout = execSync('git rev-parse --abbrev-ref HEAD', {
			cwd: projectRoot,
			encoding: 'utf8'
		});
		return stdout.trim();
	} catch (error) {
		return null;
	}
}

/**
 * Check if the current working directory is inside a Git work-tree.
 * Uses `git rev-parse --is-inside-work-tree` which is more specific than --git-dir
 * for detecting work-trees (excludes bare repos and .git directories).
 * This is ideal for preventing accidental git init in existing work-trees.
 * @returns {boolean} True if inside a Git work-tree, false otherwise.
 */
function insideGitWorkTree() {
	try {
		execSync('git rev-parse --is-inside-work-tree', {
			stdio: 'ignore',
			cwd: process.cwd()
		});
		return true;
	} catch {
		return false;
	}
}

// Export all functions
export {
	isGitRepository,
	getCurrentBranch,
	getLocalBranches,
	getRemoteBranches,
	isGhCliAvailable,
	getGitHubRepoInfo,
	sanitizeBranchNameForTag,
	isValidBranchForTag,
	getGitRepositoryRoot,
	isGitRepositoryRoot,
	getDefaultBranch,
	isOnDefaultBranch,
	checkAndAutoSwitchGitTag,
	checkAndAutoSwitchGitTagSync,
	isGitRepositorySync,
	getCurrentBranchSync,
	insideGitWorkTree
};

import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { log, findProjectRoot } from './utils.js';
import { getProjectName } from './config-manager.js';
import listTasks from './task-manager/list-tasks.js';

/**
 * Creates a basic README structure if one doesn't exist
 * @param {string} projectName - Name of the project
 * @returns {string} - Basic README content
 */
function createBasicReadme(projectName) {
	return `# ${projectName}

This project is managed using Task Master.

`;
}

/**
 * Create UTM tracking URL for task-master.dev
 * @param {string} projectRoot - The project root path
 * @returns {string} - UTM tracked URL
 */
function createTaskMasterUrl(projectRoot) {
	// Get the actual folder name from the project root path
	const folderName = path.basename(projectRoot);

	// Clean folder name for UTM (replace spaces/special chars with hyphens)
	const cleanFolderName = folderName
		.toLowerCase()
		.replace(/[^a-z0-9]/g, '-')
		.replace(/-+/g, '-')
		.replace(/^-|-$/g, '');

	const utmParams = new URLSearchParams({
		utm_source: 'github-readme',
		utm_medium: 'readme-export',
		utm_campaign: cleanFolderName || 'task-sync',
		utm_content: 'task-export-link'
	});

	return `https://task-master.dev?${utmParams.toString()}`;
}

/**
 * Create the start marker with metadata
 * @param {Object} options - Export options
 * @returns {string} - Formatted start marker
 */
function createStartMarker(options) {
	const { timestamp, withSubtasks, status, projectRoot } = options;

	// Format status filter text
	const statusText = status
		? `Status filter: ${status}`
		: 'Status filter: none';
	const subtasksText = withSubtasks ? 'with subtasks' : 'without subtasks';

	// Create the export info content
	const exportInfo =
		`🎯 **Taskmaster Export** - ${timestamp}\n` +
		`📋 Export: ${subtasksText} • ${statusText}\n` +
		`🔗 Powered by [Task Master](${createTaskMasterUrl(projectRoot)})`;

	// Create a markdown box using code blocks and emojis to mimic our UI style
	const boxContent =
		`<!-- TASKMASTER_EXPORT_START -->\n` +
		`> ${exportInfo.split('\n').join('\n> ')}\n\n`;

	return boxContent;
}

/**
 * Create the end marker
 * @returns {string} - Formatted end marker
 */
function createEndMarker() {
	return (
		`\n> 📋 **End of Taskmaster Export** - Tasks are synced from your project using the \`sync-readme\` command.\n` +
		`<!-- TASKMASTER_EXPORT_END -->\n`
	);
}

/**
 * Syncs the current task list to README.md at the project root
 * @param {string} projectRoot - Path to the project root directory
 * @param {Object} options - Options for syncing
 * @param {boolean} options.withSubtasks - Include subtasks in the output (default: false)
 * @param {string} options.status - Filter by status (e.g., 'pending', 'done')
 * @param {string} options.tasksPath - Custom path to tasks.json
 * @returns {boolean} - True if sync was successful, false otherwise
 * TODO: Add tag support - this is not currently supported how we want to handle this  - Parthy
 */
export async function syncTasksToReadme(projectRoot = null, options = {}) {
	try {
		const actualProjectRoot = projectRoot || findProjectRoot() || '.';
		const { withSubtasks = false, status, tasksPath, tag } = options;

		// Get current tasks using the list-tasks functionality with markdown-readme format
		const tasksOutput = await listTasks(
			tasksPath ||
				path.join(actualProjectRoot, '.taskmaster', 'tasks', 'tasks.json'),
			status,
			null,
			withSubtasks,
			'markdown-readme',
			{ projectRoot, tag }
		);

		if (!tasksOutput) {
			console.log(chalk.red('❌ Failed to generate task output'));
			return false;
		}

		// Generate timestamp and metadata
		const timestamp =
			new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
		const projectName = getProjectName(actualProjectRoot);

		// Create the export markers with metadata
		const startMarker = createStartMarker({
			timestamp,
			withSubtasks,
			status,
			projectRoot: actualProjectRoot
		});

		const endMarker = createEndMarker();

		// Create the complete task section
		const taskSection = startMarker + tasksOutput + endMarker;

		// Read current README content
		const readmePath = path.join(actualProjectRoot, 'README.md');
		let readmeContent = '';
		try {
			readmeContent = fs.readFileSync(readmePath, 'utf8');
		} catch (err) {
			if (err.code === 'ENOENT') {
				// Create basic README if it doesn't exist
				readmeContent = createBasicReadme(projectName);
			} else {
				throw err;
			}
		}

		// Check if export markers exist and replace content between them
		const startComment = '<!-- TASKMASTER_EXPORT_START -->';
		const endComment = '<!-- TASKMASTER_EXPORT_END -->';

		let updatedContent;
		const startIndex = readmeContent.indexOf(startComment);
		const endIndex = readmeContent.indexOf(endComment);

		if (startIndex !== -1 && endIndex !== -1) {
			// Replace existing task section
			const beforeTasks = readmeContent.substring(0, startIndex);
			const afterTasks = readmeContent.substring(endIndex + endComment.length);
			updatedContent = beforeTasks + taskSection + afterTasks;
		} else {
			// Append to end of README
			updatedContent = readmeContent + '\n' + taskSection;
		}

		// Write updated content to README
		fs.writeFileSync(readmePath, updatedContent, 'utf8');

		console.log(chalk.green('✅ Successfully synced tasks to README.md'));
		console.log(
			chalk.cyan(
				`📋 Export details: ${withSubtasks ? 'with' : 'without'} subtasks${status ? `, status: ${status}` : ''}`
			)
		);
		console.log(chalk.gray(`📍 Location: ${readmePath}`));

		return true;
	} catch (error) {
		console.log(chalk.red('❌ Failed to sync tasks to README:'), error.message);
		log('error', `README sync error: ${error.message}`);
		return false;
	}
}

export default syncTasksToReadme;

/**
 * src/ai-providers/custom-sdk/mcp/language-model.js
 *
 * MCP Language Model implementation following AI SDK LanguageModelV1 interface.
 * Uses MCP session.requestSampling() for AI operations.
 */

import {
	convertToMCPFormat,
	convertFromMCPFormat
} from './message-converter.js';
import { MCPError, mapMCPError } from './errors.js';
import { extractJson } from './json-extractor.js';
import {
	convertSchemaToInstructions,
	enhancePromptForJSON
} from './schema-converter.js';

/**
 * MCP Language Model implementing AI SDK LanguageModelV1 interface
 */
export class MCPLanguageModel {
	specificationVersion = 'v1';
	defaultObjectGenerationMode = 'json';
	supportsImageUrls = false;
	supportsStructuredOutputs = true;

	constructor(options) {
		this.session = options.session; // MCP session object
		this.modelId = options.modelId;
		this.settings = options.settings || {};
		this.provider = 'mcp-ai-sdk';
		this.maxTokens = this.settings.maxTokens;
		this.temperature = this.settings.temperature;

		this.validateSession();
	}

	/**
	 * Validate that the MCP session has required capabilities
	 */
	validateSession() {
		if (!this.session?.clientCapabilities?.sampling) {
			throw new MCPError('MCP session must have client sampling capabilities');
		}
	}

	/**
	 * Generate text using MCP session sampling
	 * @param {object} options - Generation options
	 * @param {Array} options.prompt - AI SDK prompt format
	 * @param {AbortSignal} options.abortSignal - Abort signal
	 * @returns {Promise<object>} Generation result in AI SDK format
	 */
	async doGenerate(options) {
		try {
			// Convert AI SDK prompt to MCP format
			const { messages, systemPrompt } = convertToMCPFormat(options.prompt);

			// Use MCP session.requestSampling (same as MCPRemoteProvider)
			const response = await this.session.requestSampling(
				{
					messages,
					systemPrompt,
					temperature: this.settings.temperature,
					maxTokens: this.settings.maxTokens,
					includeContext: 'thisServer'
				},
				{
					// signal: options.abortSignal,
					timeout: 240000 // 4 minutes timeout
				}
			);

			// Convert MCP response back to AI SDK format
			const result = convertFromMCPFormat(response);

			return {
				text: result.text,
				finishReason: result.finishReason || 'stop',
				usage: {
					promptTokens: result.usage?.inputTokens || 0,
					completionTokens: result.usage?.outputTokens || 0,
					totalTokens:
						(result.usage?.inputTokens || 0) + (result.usage?.outputTokens || 0)
				},
				rawResponse: response,
				warnings: result.warnings
			};
		} catch (error) {
			throw mapMCPError(error);
		}
	}

	/**
	 * Generate structured object using MCP session sampling
	 * @param {object} options - Generation options
	 * @param {Array} options.prompt - AI SDK prompt format
	 * @param {import('zod').ZodSchema} options.schema - Zod schema for validation
	 * @param {string} [options.mode='json'] - Generation mode ('json' or 'tool')
	 * @param {AbortSignal} options.abortSignal - Abort signal
	 * @returns {Promise<object>} Generation result with structured object
	 */
	async doGenerateObject(options) {
		try {
			const { schema, mode = 'json', ...restOptions } = options;

			if (!schema) {
				throw new MCPError('Schema is required for object generation');
			}

			// Convert schema to JSON instructions
			const objectName = restOptions.objectName || 'generated_object';
			const jsonInstructions = convertSchemaToInstructions(schema, objectName);

			// Enhance prompt with JSON generation instructions
			const enhancedPrompt = enhancePromptForJSON(
				options.prompt,
				jsonInstructions
			);

			// Convert enhanced prompt to MCP format
			const { messages, systemPrompt } = convertToMCPFormat(enhancedPrompt);

			// Use MCP session.requestSampling with enhanced prompt
			const response = await this.session.requestSampling(
				{
					messages,
					systemPrompt,
					temperature: this.settings.temperature,
					maxTokens: this.settings.maxTokens,
					includeContext: 'thisServer'
				},
				{
					timeout: 240000 // 4 minutes timeout
				}
			);

			// Convert MCP response back to AI SDK format
			const result = convertFromMCPFormat(response);

			// Extract JSON from the response text
			const jsonText = extractJson(result.text);

			// Parse and validate JSON
			let parsedObject;
			try {
				parsedObject = JSON.parse(jsonText);
			} catch (parseError) {
				throw new MCPError(
					`Failed to parse JSON response: ${parseError.message}. Response: ${result.text.substring(0, 200)}...`
				);
			}

			// Validate against schema
			try {
				const validatedObject = schema.parse(parsedObject);

				return {
					object: validatedObject,
					finishReason: result.finishReason || 'stop',
					usage: {
						promptTokens: result.usage?.inputTokens || 0,
						completionTokens: result.usage?.outputTokens || 0,
						totalTokens:
							(result.usage?.inputTokens || 0) +
							(result.usage?.outputTokens || 0)
					},
					rawResponse: response,
					warnings: result.warnings
				};
			} catch (validationError) {
				throw new MCPError(
					`Generated object does not match schema: ${validationError.message}. Generated: ${JSON.stringify(parsedObject, null, 2)}`
				);
			}
		} catch (error) {
			throw mapMCPError(error);
		}
	}

	/**
	 * Stream text generation using MCP session sampling
	 * Note: MCP may not support native streaming, so this may simulate streaming
	 * @param {object} options - Generation options
	 * @returns {AsyncIterable} Stream of generation chunks
	 */
	async doStream(options) {
		try {
			// For now, simulate streaming by chunking the complete response
			// TODO: Implement native streaming if MCP supports it
			const result = await this.doGenerate(options);

			// Create async generator that yields chunks
			return this.simulateStreaming(result);
		} catch (error) {
			throw mapMCPError(error);
		}
	}

	/**
	 * Simulate streaming by chunking a complete response
	 * @param {object} result - Complete generation result
	 * @returns {AsyncIterable} Simulated stream chunks
	 */
	async *simulateStreaming(result) {
		const text = result.text;
		const chunkSize = Math.max(1, Math.floor(text.length / 10)); // 10 chunks

		for (let i = 0; i < text.length; i += chunkSize) {
			const chunk = text.slice(i, i + chunkSize);
			const isLast = i + chunkSize >= text.length;

			yield {
				type: 'text-delta',
				textDelta: chunk
			};

			// Small delay to simulate streaming
			await new Promise((resolve) => setTimeout(resolve, 50));
		}

		// Final chunk with finish reason and usage
		yield {
			type: 'finish',
			finishReason: result.finishReason,
			usage: result.usage
		};
	}
}

/**
 * tools/update-task.js
 * Tool to update a single task by ID with new information
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { updateTaskByIdDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the update-task tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerUpdateTaskTool(server) {
	server.addTool({
		name: 'update_task',
		description:
			'Updates a single task by ID with new information or context provided in the prompt.',
		parameters: z.object({
			id: z
				.string() // ID can be number or string like "1.2"
				.describe(
					"ID of the task (e.g., '15') to update. Subtasks are supported using the update-subtask tool."
				),
			prompt: z
				.string()
				.describe('New information or context to incorporate into the task'),
			research: z
				.boolean()
				.optional()
				.describe('Use Perplexity AI for research-backed updates'),
			append: z
				.boolean()
				.optional()
				.describe(
					'Append timestamped information to task details instead of full update'
				),
			file: z.string().optional().describe('Absolute path to the tasks file'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			const toolName = 'update_task';
			try {
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				log.info(
					`Executing ${toolName} tool with args: ${JSON.stringify(args)}`
				);

				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
					log.info(`${toolName}: Resolved tasks path: ${tasksJsonPath}`);
				} catch (error) {
					log.error(`${toolName}: Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// 3. Call Direct Function - Include projectRoot
				const result = await updateTaskByIdDirect(
					{
						tasksJsonPath: tasksJsonPath,
						id: args.id,
						prompt: args.prompt,
						research: args.research,
						append: args.append,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				// 4. Handle Result
				log.info(
					`${toolName}: Direct function result: success=${result.success}`
				);
				return handleApiResult(
					result,
					log,
					'Error updating task',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(
					`Critical error in ${toolName} tool execute: ${error.message}`
				);
				return createErrorResponse(
					`Internal tool error (${toolName}): ${error.message}`
				);
			}
		})
	});
}

/**
 * src/ai-providers/custom-sdk/mcp/message-converter.js
 *
 * Message conversion utilities for converting between AI SDK prompt format
 * and MCP sampling format.
 */

/**
 * Convert AI SDK prompt format to MCP sampling format
 * @param {Array} prompt - AI SDK prompt array
 * @returns {object} MCP format with messages and systemPrompt
 */
export function convertToMCPFormat(prompt) {
	const messages = [];
	let systemPrompt = '';

	for (const message of prompt) {
		if (message.role === 'system') {
			// Extract system prompt
			systemPrompt = extractTextContent(message.content);
		} else if (message.role === 'user' || message.role === 'assistant') {
			// Convert user/assistant messages
			messages.push({
				role: message.role,
				content: {
					type: 'text',
					text: extractTextContent(message.content)
				}
			});
		}
	}

	return {
		messages,
		systemPrompt
	};
}

/**
 * Convert MCP response format to AI SDK format
 * @param {object} response - MCP sampling response
 * @returns {object} AI SDK compatible result
 */
export function convertFromMCPFormat(response) {
	// Handle different possible response formats
	let text = '';
	let usage = null;
	let finishReason = 'stop';
	let warnings = [];

	if (typeof response === 'string') {
		text = response;
	} else if (response.content) {
		text = extractTextContent(response.content);
		usage = response.usage;
		finishReason = response.finishReason || 'stop';
	} else if (response.text) {
		text = response.text;
		usage = response.usage;
		finishReason = response.finishReason || 'stop';
	} else {
		// Fallback: try to extract text from response
		text = JSON.stringify(response);
		warnings.push('Unexpected MCP response format, used JSON fallback');
	}

	return {
		text,
		usage,
		finishReason,
		warnings
	};
}

/**
 * Extract text content from various content formats
 * @param {string|Array|object} content - Content in various formats
 * @returns {string} Extracted text
 */
function extractTextContent(content) {
	if (typeof content === 'string') {
		return content;
	}

	if (Array.isArray(content)) {
		// Handle array of content parts
		return content
			.map((part) => {
				if (typeof part === 'string') {
					return part;
				}
				if (part.type === 'text' && part.text) {
					return part.text;
				}
				if (part.text) {
					return part.text;
				}
				// Skip non-text content (images, etc.)
				return '';
			})
			.filter((text) => text.length > 0)
			.join(' ');
	}

	if (content && typeof content === 'object') {
		if (content.type === 'text' && content.text) {
			return content.text;
		}
		if (content.text) {
			return content.text;
		}
	}

	// Fallback
	return String(content || '');
}

/**
 * tools/analyze.js
 * Tool for analyzing task complexity and generating recommendations
 */

import { z } from 'zod';
import path from 'path';
import fs from 'fs'; // Import fs for directory check/creation
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { analyzeTaskComplexityDirect } from '../core/task-master-core.js'; // Assuming core functions are exported via task-master-core.js
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
import { resolveComplexityReportOutputPath } from '../../../src/utils/path-utils.js';

/**
 * Register the analyze_project_complexity tool
 * @param {Object} server - FastMCP server instance
 */
export function registerAnalyzeProjectComplexityTool(server) {
	server.addTool({
		name: 'analyze_project_complexity',
		description:
			'Analyze task complexity and generate expansion recommendations.',
		parameters: z.object({
			threshold: z.coerce // Use coerce for number conversion from string if needed
				.number()
				.int()
				.min(1)
				.max(10)
				.optional()
				.default(5) // Default threshold
				.describe('Complexity score threshold (1-10) to recommend expansion.'),
			research: z
				.boolean()
				.optional()
				.default(false)
				.describe('Use Perplexity AI for research-backed analysis.'),
			output: z
				.string()
				.optional()
				.describe(
					`Output file path relative to project root (default: ${COMPLEXITY_REPORT_FILE}).`
				),
			file: z
				.string()
				.optional()
				.describe(
					'Path to the tasks file relative to project root (default: tasks/tasks.json).'
				),
			ids: z
				.string()
				.optional()
				.describe(
					'Comma-separated list of task IDs to analyze specifically (e.g., "1,3,5").'
				),
			from: z.coerce
				.number()
				.int()
				.positive()
				.optional()
				.describe('Starting task ID in a range to analyze.'),
			to: z.coerce
				.number()
				.int()
				.positive()
				.optional()
				.describe('Ending task ID in a range to analyze.'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			const toolName = 'analyze_project_complexity'; // Define tool name for logging

			try {
				log.info(
					`Executing ${toolName} tool with args: ${JSON.stringify(args)}`
				);

				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});

				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
					log.info(`${toolName}: Resolved tasks path: ${tasksJsonPath}`);
				} catch (error) {
					log.error(`${toolName}: Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json within project root '${args.projectRoot}': ${error.message}`
					);
				}

				const outputPath = resolveComplexityReportOutputPath(
					args.output,
					{
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log
				);

				log.info(`${toolName}: Report output path: ${outputPath}`);

				// Ensure output directory exists
				const outputDir = path.dirname(outputPath);
				try {
					if (!fs.existsSync(outputDir)) {
						fs.mkdirSync(outputDir, { recursive: true });
						log.info(`${toolName}: Created output directory: ${outputDir}`);
					}
				} catch (dirError) {
					log.error(
						`${toolName}: Failed to create output directory ${outputDir}: ${dirError.message}`
					);
					return createErrorResponse(
						`Failed to create output directory: ${dirError.message}`
					);
				}

				// 3. Call Direct Function - Pass projectRoot in first arg object
				const result = await analyzeTaskComplexityDirect(
					{
						tasksJsonPath: tasksJsonPath,
						outputPath: outputPath,
						threshold: args.threshold,
						research: args.research,
						projectRoot: args.projectRoot,
						tag: resolvedTag,
						ids: args.ids,
						from: args.from,
						to: args.to
					},
					log,
					{ session }
				);

				// 4. Handle Result
				log.info(
					`${toolName}: Direct function result: success=${result.success}`
				);
				return handleApiResult(
					result,
					log,
					'Error analyzing task complexity',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(
					`Critical error in ${toolName} tool execute: ${error.message}`
				);
				return createErrorResponse(
					`Internal tool error (${toolName}): ${error.message}`
				);
			}
		})
	});
}

#!/usr/bin/env node

/**
 * test-claude.js
 *
 * A simple test script to verify the improvements to the callClaude function.
 * This script tests different scenarios:
 * 1. Normal operation with a small PRD
 * 2. Testing with a large number of tasks (to potentially trigger task reduction)
 * 3. Simulating a failure to test retry logic
 */

import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Load environment variables from .env file
dotenv.config();

// Create a simple PRD for testing
const createTestPRD = (size = 'small', taskComplexity = 'simple') => {
	let content = `# Test PRD - ${size.toUpperCase()} SIZE, ${taskComplexity.toUpperCase()} COMPLEXITY\n\n`;

	// Add more content based on size
	if (size === 'small') {
		content += `
## Overview
This is a small test PRD to verify the callClaude function improvements.

## Requirements
1. Create a simple web application
2. Implement user authentication
3. Add a dashboard for users
4. Create an admin panel
5. Implement data visualization

## Technical Stack
- Frontend: React
- Backend: Node.js
- Database: MongoDB
`;
	} else if (size === 'medium') {
		// Medium-sized PRD with more requirements
		content += `
## Overview
This is a medium-sized test PRD to verify the callClaude function improvements.

## Requirements
1. Create a web application with multiple pages
2. Implement user authentication with OAuth
3. Add a dashboard for users with customizable widgets
4. Create an admin panel with user management
5. Implement data visualization with charts and graphs
6. Add real-time notifications
7. Implement a search feature
8. Add user profile management
9. Implement role-based access control
10. Add a reporting system
11. Implement file uploads and management
12. Add a commenting system
13. Implement a rating system
14. Add a recommendation engine
15. Implement a payment system

## Technical Stack
- Frontend: React with TypeScript
- Backend: Node.js with Express
- Database: MongoDB with Mongoose
- Authentication: JWT and OAuth
- Deployment: Docker and Kubernetes
- CI/CD: GitHub Actions
- Monitoring: Prometheus and Grafana
`;
	} else if (size === 'large') {
		// Large PRD with many requirements
		content += `
## Overview
This is a large test PRD to verify the callClaude function improvements.

## Requirements
`;
		// Generate 30 requirements
		for (let i = 1; i <= 30; i++) {
			content += `${i}. Requirement ${i} - This is a detailed description of requirement ${i}.\n`;
		}

		content += `
## Technical Stack
- Frontend: React with TypeScript
- Backend: Node.js with Express
- Database: MongoDB with Mongoose
- Authentication: JWT and OAuth
- Deployment: Docker and Kubernetes
- CI/CD: GitHub Actions
- Monitoring: Prometheus and Grafana

## User Stories
`;
		// Generate 20 user stories
		for (let i = 1; i <= 20; i++) {
			content += `- As a user, I want to be able to ${i} so that I can achieve benefit ${i}.\n`;
		}

		content += `
## Non-Functional Requirements
- Performance: The system should respond within 200ms
- Scalability: The system should handle 10,000 concurrent users
- Availability: The system should have 99.9% uptime
- Security: The system should comply with OWASP top 10
- Accessibility: The system should comply with WCAG 2.1 AA
`;
	}

	// Add complexity if needed
	if (taskComplexity === 'complex') {
		content += `
## Complex Requirements
- Implement a real-time collaboration system
- Add a machine learning-based recommendation engine
- Implement a distributed caching system
- Add a microservices architecture
- Implement a custom analytics engine
- Add support for multiple languages and locales
- Implement a custom search engine with advanced filtering
- Add a custom workflow engine
- Implement a custom reporting system
- Add a custom dashboard builder
`;
	}

	return content;
};

// Function to run the tests
async function runTests() {
	console.log('Starting tests for callClaude function improvements...');

	try {
		// Instead of importing the callClaude function directly, we'll use the dev.js script
		// with our test PRDs by running it as a child process

		// Test 1: Small PRD, 5 tasks
		console.log('\n=== Test 1: Small PRD, 5 tasks ===');
		const smallPRD = createTestPRD('small', 'simple');
		const smallPRDPath = path.join(__dirname, 'test-small-prd.txt');
		fs.writeFileSync(smallPRDPath, smallPRD, 'utf8');

		console.log(`Created test PRD at ${smallPRDPath}`);
		console.log('Running dev.js with small PRD...');

		// Use the child_process module to run the dev.js script
		const { execSync } = await import('child_process');

		try {
			const smallResult = execSync(
				`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${smallPRDPath} --num-tasks=5`,
				{
					stdio: 'inherit'
				}
			);
			console.log('Small PRD test completed successfully');
		} catch (error) {
			console.error('Small PRD test failed:', error.message);
		}

		// Test 2: Medium PRD, 15 tasks
		console.log('\n=== Test 2: Medium PRD, 15 tasks ===');
		const mediumPRD = createTestPRD('medium', 'simple');
		const mediumPRDPath = path.join(__dirname, 'test-medium-prd.txt');
		fs.writeFileSync(mediumPRDPath, mediumPRD, 'utf8');

		console.log(`Created test PRD at ${mediumPRDPath}`);
		console.log('Running dev.js with medium PRD...');

		try {
			const mediumResult = execSync(
				`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${mediumPRDPath} --num-tasks=15`,
				{
					stdio: 'inherit'
				}
			);
			console.log('Medium PRD test completed successfully');
		} catch (error) {
			console.error('Medium PRD test failed:', error.message);
		}

		// Test 3: Large PRD, 25 tasks
		console.log('\n=== Test 3: Large PRD, 25 tasks ===');
		const largePRD = createTestPRD('large', 'complex');
		const largePRDPath = path.join(__dirname, 'test-large-prd.txt');
		fs.writeFileSync(largePRDPath, largePRD, 'utf8');

		console.log(`Created test PRD at ${largePRDPath}`);
		console.log('Running dev.js with large PRD...');

		try {
			const largeResult = execSync(
				`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${largePRDPath} --num-tasks=25`,
				{
					stdio: 'inherit'
				}
			);
			console.log('Large PRD test completed successfully');
		} catch (error) {
			console.error('Large PRD test failed:', error.message);
		}

		console.log('\nAll tests completed!');
	} catch (error) {
		console.error('Test failed:', error);
	} finally {
		// Clean up test files
		console.log('\nCleaning up test files...');
		const testFiles = [
			path.join(__dirname, 'test-small-prd.txt'),
			path.join(__dirname, 'test-medium-prd.txt'),
			path.join(__dirname, 'test-large-prd.txt')
		];

		testFiles.forEach((file) => {
			if (fs.existsSync(file)) {
				fs.unlinkSync(file);
				console.log(`Deleted ${file}`);
			}
		});

		console.log('Cleanup complete.');
	}
}

// Run the tests
runTests().catch((error) => {
	console.error('Error running tests:', error);
	process.exit(1);
});

/**
 * update-task-by-id.js
 * Direct function implementation for updating a single task by ID with new information
 */

import { updateTaskById } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode,
	isSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for updateTaskById with error handling.
 *
 * @param {Object} args - Command arguments containing id, prompt, useResearch, tasksJsonPath, and projectRoot.
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} args.id - Task ID (or subtask ID like "1.2").
 * @param {string} args.prompt - New information/context prompt.
 * @param {boolean} [args.research] - Whether to use research role.
 * @param {boolean} [args.append] - Whether to append timestamped information instead of full update.
 * @param {string} [args.projectRoot] - Project root path.
 * @param {string} [args.tag] - Tag for the task (optional)
 * @param {Object} log - Logger object.
 * @param {Object} context - Context object containing session data.
 * @returns {Promise<Object>} - Result object with success status and data/error information.
 */
export async function updateTaskByIdDirect(args, log, context = {}) {
	const { session } = context;
	// Destructure expected args, including projectRoot
	const { tasksJsonPath, id, prompt, research, append, projectRoot, tag } =
		args;

	const logWrapper = createLogWrapper(log);

	try {
		logWrapper.info(
			`Updating task by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}`
		);

		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			const errorMessage = 'tasksJsonPath is required but was not provided.';
			logWrapper.error(errorMessage);
			return {
				success: false,
				error: { code: 'MISSING_ARGUMENT', message: errorMessage }
			};
		}

		// Check required parameters (id and prompt)
		if (!id) {
			const errorMessage =
				'No task ID specified. Please provide a task ID to update.';
			logWrapper.error(errorMessage);
			return {
				success: false,
				error: { code: 'MISSING_TASK_ID', message: errorMessage }
			};
		}

		if (!prompt) {
			const errorMessage =
				'No prompt specified. Please provide a prompt with new information for the task update.';
			logWrapper.error(errorMessage);
			return {
				success: false,
				error: { code: 'MISSING_PROMPT', message: errorMessage }
			};
		}

		// Parse taskId - handle both string and number values
		let taskId;
		if (typeof id === 'string') {
			// Handle subtask IDs (e.g., "5.2")
			if (id.includes('.')) {
				taskId = id; // Keep as string for subtask IDs
			} else {
				// Parse as integer for main task IDs
				taskId = parseInt(id, 10);
				if (Number.isNaN(taskId)) {
					const errorMessage = `Invalid task ID: ${id}. Task ID must be a positive integer or subtask ID (e.g., "5.2").`;
					logWrapper.error(errorMessage);
					return {
						success: false,
						error: { code: 'INVALID_TASK_ID', message: errorMessage }
					};
				}
			}
		} else {
			taskId = id;
		}

		// Use the provided path
		const tasksPath = tasksJsonPath;

		// Get research flag
		const useResearch = research === true;

		logWrapper.info(
			`Updating task with ID ${taskId} with prompt "${prompt}" and research: ${useResearch}`
		);

		const wasSilent = isSilentMode();
		if (!wasSilent) {
			enableSilentMode();
		}

		try {
			// Execute core updateTaskById function with proper parameters
			const coreResult = await updateTaskById(
				tasksPath,
				taskId,
				prompt,
				useResearch,
				{
					mcpLog: logWrapper,
					session,
					projectRoot,
					tag,
					commandName: 'update-task',
					outputType: 'mcp'
				},
				'json',
				append || false
			);

			// Check if the core function returned null or an object without success
			if (!coreResult || coreResult.updatedTask === null) {
				// Core function logs the reason, just return success with info
				const message = `Task ${taskId} was not updated (likely already completed).`;
				logWrapper.info(message);
				return {
					success: true,
					data: {
						message: message,
						taskId: taskId,
						updated: false,
						telemetryData: coreResult?.telemetryData,
						tagInfo: coreResult?.tagInfo
					}
				};
			}

			// Task was updated successfully
			const successMessage = `Successfully updated task with ID ${taskId} based on the prompt`;
			logWrapper.success(successMessage);
			return {
				success: true,
				data: {
					message: successMessage,
					taskId: taskId,
					tasksPath: tasksPath,
					useResearch: useResearch,
					updated: true,
					updatedTask: coreResult.updatedTask,
					telemetryData: coreResult.telemetryData,
					tagInfo: coreResult.tagInfo
				}
			};
		} catch (error) {
			logWrapper.error(`Error updating task by ID: ${error.message}`);
			return {
				success: false,
				error: {
					code: 'UPDATE_TASK_CORE_ERROR',
					message: error.message || 'Unknown error updating task'
				}
			};
		} finally {
			if (!wasSilent && isSilentMode()) {
				disableSilentMode();
			}
		}
	} catch (error) {
		logWrapper.error(`Setup error in updateTaskByIdDirect: ${error.message}`);
		if (isSilentMode()) disableSilentMode();
		return {
			success: false,
			error: {
				code: 'DIRECT_FUNCTION_SETUP_ERROR',
				message: error.message || 'Unknown setup error'
			}
		};
	}
}

import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import { createLogWrapper } from '../../../mcp-server/src/tools/utils.js';
import { findProjectRoot } from '../utils.js';
import {
	LEGACY_CONFIG_FILE,
	TASKMASTER_CONFIG_FILE
} from '../../../src/constants/paths.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Create a simple log wrapper for CLI use
const log = createLogWrapper({
	info: (msg) => console.log(chalk.blue('ℹ'), msg),
	warn: (msg) => console.log(chalk.yellow('⚠'), msg),
	error: (msg) => console.error(chalk.red('✗'), msg),
	success: (msg) => console.log(chalk.green('✓'), msg)
});

/**
 * Main migration function
 * @param {Object} options - Migration options
 */
export async function migrateProject(options = {}) {
	const projectRoot = findProjectRoot() || process.cwd();

	log.info(`Starting migration in: ${projectRoot}`);

	// Check if .taskmaster directory already exists
	const taskmasterDir = path.join(projectRoot, '.taskmaster');
	if (fs.existsSync(taskmasterDir) && !options.force) {
		log.warn(
			'.taskmaster directory already exists. Use --force to overwrite or skip migration.'
		);
		return;
	}

	// Analyze what needs to be migrated
	const migrationPlan = analyzeMigrationNeeds(projectRoot);

	if (migrationPlan.length === 0) {
		log.info(
			'No files to migrate. Project may already be using the new structure.'
		);
		return;
	}

	// Show migration plan
	log.info('Migration plan:');
	for (const item of migrationPlan) {
		const action = options.dryRun ? 'Would move' : 'Will move';
		log.info(`  ${action}: ${item.from} → ${item.to}`);
	}

	if (options.dryRun) {
		log.info(
			'Dry run complete. Use --dry-run=false to perform actual migration.'
		);
		return;
	}

	// Confirm migration
	if (!options.yes) {
		const readline = await import('readline');
		const rl = readline.createInterface({
			input: process.stdin,
			output: process.stdout
		});

		const answer = await new Promise((resolve) => {
			rl.question('Proceed with migration? (y/N): ', resolve);
		});
		rl.close();

		if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
			log.info('Migration cancelled.');
			return;
		}
	}

	// Perform migration
	try {
		await performMigration(projectRoot, migrationPlan, options);
		log.success('Migration completed successfully!');
		log.info('You can now use the new .taskmaster directory structure.');
		if (!options.cleanup) {
			log.info(
				'Old files were preserved. Use --cleanup to remove them after verification.'
			);
		}
	} catch (error) {
		log.error(`Migration failed: ${error.message}`);
		throw error;
	}
}

/**
 * Analyze what files need to be migrated
 * @param {string} projectRoot - Project root directory
 * @returns {Array} Migration plan items
 */
function analyzeMigrationNeeds(projectRoot) {
	const migrationPlan = [];

	// Check for tasks directory
	const tasksDir = path.join(projectRoot, 'tasks');
	if (fs.existsSync(tasksDir)) {
		const tasksFiles = fs.readdirSync(tasksDir);
		for (const file of tasksFiles) {
			migrationPlan.push({
				from: path.join('tasks', file),
				to: path.join('.taskmaster', 'tasks', file),
				type: 'task'
			});
		}
	}

	// Check for scripts directory files
	const scriptsDir = path.join(projectRoot, 'scripts');
	if (fs.existsSync(scriptsDir)) {
		const scriptsFiles = fs.readdirSync(scriptsDir);
		for (const file of scriptsFiles) {
			const filePath = path.join(scriptsDir, file);
			if (fs.statSync(filePath).isFile()) {
				// Categorize files more intelligently
				let destination;
				const lowerFile = file.toLowerCase();

				if (
					lowerFile.includes('example') ||
					lowerFile.includes('template') ||
					lowerFile.includes('boilerplate') ||
					lowerFile.includes('sample')
				) {
					// Template/example files go to templates (including example_prd.txt)
					destination = path.join('.taskmaster', 'templates', file);
				} else if (
					lowerFile.includes('complexity') &&
					lowerFile.includes('report') &&
					lowerFile.endsWith('.json')
				) {
					// Only actual complexity reports go to reports
					destination = path.join('.taskmaster', 'reports', file);
				} else if (
					lowerFile.includes('prd') ||
					lowerFile.endsWith('.md') ||
					lowerFile.endsWith('.txt')
				) {
					// Documentation files go to docs (but not examples or reports)
					destination = path.join('.taskmaster', 'docs', file);
				} else {
					// Other files stay in scripts or get skipped - don't force everything into templates
					log.warn(
						`Skipping migration of '${file}' - uncertain categorization. You may need to move this manually.`
					);
					continue;
				}

				migrationPlan.push({
					from: path.join('scripts', file),
					to: destination,
					type: 'script'
				});
			}
		}
	}

	// Check for .taskmasterconfig
	const oldConfig = path.join(projectRoot, LEGACY_CONFIG_FILE);
	if (fs.existsSync(oldConfig)) {
		migrationPlan.push({
			from: LEGACY_CONFIG_FILE,
			to: TASKMASTER_CONFIG_FILE,
			type: 'config'
		});
	}

	return migrationPlan;
}

/**
 * Perform the actual migration
 * @param {string} projectRoot - Project root directory
 * @param {Array} migrationPlan - List of files to migrate
 * @param {Object} options - Migration options
 */
async function performMigration(projectRoot, migrationPlan, options) {
	// Create .taskmaster directory
	const taskmasterDir = path.join(projectRoot, '.taskmaster');
	if (!fs.existsSync(taskmasterDir)) {
		fs.mkdirSync(taskmasterDir, { recursive: true });
	}

	// Group migration items by destination directory to create only needed subdirs
	const neededDirs = new Set();
	for (const item of migrationPlan) {
		const destDir = path.dirname(item.to);
		neededDirs.add(destDir);
	}

	// Create only the directories we actually need
	for (const dir of neededDirs) {
		const fullDirPath = path.join(projectRoot, dir);
		if (!fs.existsSync(fullDirPath)) {
			fs.mkdirSync(fullDirPath, { recursive: true });
			log.info(`Created directory: ${dir}`);
		}
	}

	// Create backup if requested
	if (options.backup) {
		const backupDir = path.join(projectRoot, '.taskmaster-migration-backup');
		log.info(`Creating backup in: ${backupDir}`);
		if (fs.existsSync(backupDir)) {
			fs.rmSync(backupDir, { recursive: true, force: true });
		}
		fs.mkdirSync(backupDir, { recursive: true });
	}

	// Migrate files
	for (const item of migrationPlan) {
		const fromPath = path.join(projectRoot, item.from);
		const toPath = path.join(projectRoot, item.to);

		if (!fs.existsSync(fromPath)) {
			log.warn(`Source file not found: ${item.from}`);
			continue;
		}

		// Create backup if requested
		if (options.backup) {
			const backupPath = path.join(
				projectRoot,
				'.taskmaster-migration-backup',
				item.from
			);
			const backupDir = path.dirname(backupPath);
			if (!fs.existsSync(backupDir)) {
				fs.mkdirSync(backupDir, { recursive: true });
			}
			fs.copyFileSync(fromPath, backupPath);
		}

		// Ensure destination directory exists
		const toDir = path.dirname(toPath);
		if (!fs.existsSync(toDir)) {
			fs.mkdirSync(toDir, { recursive: true });
		}

		// Copy file
		fs.copyFileSync(fromPath, toPath);
		log.info(`Migrated: ${item.from} → ${item.to}`);

		// Remove original if cleanup is requested
		if (options.cleanup) {
			fs.unlinkSync(fromPath);
		}
	}

	// Clean up empty directories if cleanup is requested
	if (options.cleanup) {
		const dirsToCheck = ['tasks', 'scripts'];
		for (const dir of dirsToCheck) {
			const dirPath = path.join(projectRoot, dir);
			if (fs.existsSync(dirPath)) {
				try {
					const files = fs.readdirSync(dirPath);
					if (files.length === 0) {
						fs.rmdirSync(dirPath);
						log.info(`Removed empty directory: ${dir}`);
					}
				} catch (error) {
					// Directory not empty or other error, skip
				}
			}
		}
	}
}

export default { migrateProject };

/**
 * @fileoverview Schema conversion utilities for MCP AI SDK provider
 */

/**
 * Convert Zod schema to human-readable JSON instructions
 * @param {import('zod').ZodSchema} schema - Zod schema object
 * @param {string} [objectName='result'] - Name of the object being generated
 * @returns {string} Instructions for JSON generation
 */
export function convertSchemaToInstructions(schema, objectName = 'result') {
	try {
		// Generate example structure from schema
		const exampleStructure = generateExampleFromSchema(schema);

		return `
CRITICAL JSON GENERATION INSTRUCTIONS:

You must respond with ONLY valid JSON that matches this exact structure for "${objectName}":

${JSON.stringify(exampleStructure, null, 2)}

STRICT REQUIREMENTS:
1. Response must start with { and end with }
2. Use double quotes for all strings and property names
3. Do not include any text before or after the JSON
4. Do not wrap in markdown code blocks
5. Do not include explanations or comments
6. Follow the exact property names and types shown above
7. All required fields must be present

Begin your response immediately with the opening brace {`;
	} catch (error) {
		// Fallback to basic JSON instructions if schema parsing fails
		return `
CRITICAL JSON GENERATION INSTRUCTIONS:

You must respond with ONLY valid JSON for "${objectName}".

STRICT REQUIREMENTS:
1. Response must start with { and end with }
2. Use double quotes for all strings and property names
3. Do not include any text before or after the JSON
4. Do not wrap in markdown code blocks
5. Do not include explanations or comments

Begin your response immediately with the opening brace {`;
	}
}

/**
 * Generate example structure from Zod schema
 * @param {import('zod').ZodSchema} schema - Zod schema
 * @returns {any} Example object matching the schema
 */
function generateExampleFromSchema(schema) {
	// This is a simplified schema-to-example converter
	// For production, you might want to use a more sophisticated library

	if (!schema || typeof schema._def === 'undefined') {
		return {};
	}

	const def = schema._def;

	switch (def.typeName) {
		case 'ZodObject':
			const result = {};
			const shape = def.shape();

			for (const [key, fieldSchema] of Object.entries(shape)) {
				result[key] = generateExampleFromSchema(fieldSchema);
			}

			return result;

		case 'ZodString':
			return 'string';

		case 'ZodNumber':
			return 0;

		case 'ZodBoolean':
			return false;

		case 'ZodArray':
			const elementExample = generateExampleFromSchema(def.type);
			return [elementExample];

		case 'ZodOptional':
			return generateExampleFromSchema(def.innerType);

		case 'ZodNullable':
			return generateExampleFromSchema(def.innerType);

		case 'ZodEnum':
			return def.values[0] || 'enum_value';

		case 'ZodLiteral':
			return def.value;

		case 'ZodUnion':
			// Use the first option from the union
			if (def.options && def.options.length > 0) {
				return generateExampleFromSchema(def.options[0]);
			}
			return 'union_value';

		case 'ZodRecord':
			return {
				key: generateExampleFromSchema(def.valueType)
			};

		default:
			// For unknown types, return a placeholder
			return `<${def.typeName || 'unknown'}>`;
	}
}

/**
 * Enhance prompt with JSON generation instructions
 * @param {Array} prompt - AI SDK prompt array
 * @param {string} jsonInstructions - JSON generation instructions
 * @returns {Array} Enhanced prompt array
 */
export function enhancePromptForJSON(prompt, jsonInstructions) {
	const enhancedPrompt = [...prompt];

	// Find system message or create one
	let systemMessageIndex = enhancedPrompt.findIndex(
		(msg) => msg.role === 'system'
	);

	if (systemMessageIndex >= 0) {
		// Append to existing system message
		const currentContent = enhancedPrompt[systemMessageIndex].content;
		enhancedPrompt[systemMessageIndex] = {
			...enhancedPrompt[systemMessageIndex],
			content: currentContent + '\n\n' + jsonInstructions
		};
	} else {
		// Add new system message at the beginning
		enhancedPrompt.unshift({
			role: 'system',
			content: jsonInstructions
		});
	}

	return enhancedPrompt;
}

/**
 * Path utility functions for Task Master
 * Provides centralized path resolution logic for both CLI and MCP use cases
 */

import path from 'path';
import fs from 'fs';
import {
	TASKMASTER_TASKS_FILE,
	LEGACY_TASKS_FILE,
	TASKMASTER_DOCS_DIR,
	TASKMASTER_REPORTS_DIR,
	COMPLEXITY_REPORT_FILE,
	TASKMASTER_CONFIG_FILE,
	LEGACY_CONFIG_FILE
} from '../constants/paths.js';
import { getLoggerOrDefault } from './logger-utils.js';

/**
 * Normalize project root to ensure it doesn't end with .taskmaster
 * This prevents double .taskmaster paths when using constants that include .taskmaster
 * @param {string} projectRoot - The project root path to normalize
 * @returns {string} - Normalized project root path
 */
export function normalizeProjectRoot(projectRoot) {
	if (!projectRoot) return projectRoot;

	// Ensure it's a string
	projectRoot = String(projectRoot);

	// Split the path into segments
	const segments = projectRoot.split(path.sep);

	// Find the index of .taskmaster segment
	const taskmasterIndex = segments.findIndex(
		(segment) => segment === '.taskmaster'
	);

	if (taskmasterIndex !== -1) {
		// If .taskmaster is found, return everything up to but not including .taskmaster
		const normalizedSegments = segments.slice(0, taskmasterIndex);
		return normalizedSegments.join(path.sep) || path.sep;
	}

	return projectRoot;
}

/**
 * Find the project root directory by looking for project markers
 * @param {string} startDir - Directory to start searching from
 * @returns {string|null} - Project root path or null if not found
 */
export function findProjectRoot(startDir = process.cwd()) {
	const projectMarkers = [
		'.taskmaster',
		TASKMASTER_TASKS_FILE,
		'tasks.json',
		LEGACY_TASKS_FILE,
		'.git',
		'.svn',
		'package.json',
		'yarn.lock',
		'package-lock.json',
		'pnpm-lock.yaml'
	];

	let currentDir = path.resolve(startDir);
	const rootDir = path.parse(currentDir).root;
	const maxDepth = 50; // Reasonable limit to prevent infinite loops
	let depth = 0;

	while (currentDir !== rootDir && depth < maxDepth) {
		// Check if current directory contains any project markers
		for (const marker of projectMarkers) {
			const markerPath = path.join(currentDir, marker);
			if (fs.existsSync(markerPath)) {
				return currentDir;
			}
		}
		currentDir = path.dirname(currentDir);
		depth++;
	}

	// Fallback to current working directory if no project root found
	return process.cwd();
}

/**
 * Find the tasks.json file path with fallback logic
 * @param {string|null} explicitPath - Explicit path provided by user (highest priority)
 * @param {Object|null} args - Args object from MCP args (optional)
 * @param {Object|null} log - Logger object (optional)
 * @returns {string|null} - Resolved tasks.json path or null if not found
 */
export function findTasksPath(explicitPath = null, args = null, log = null) {
	// Use the passed logger if available, otherwise use the default logger
	const logger = getLoggerOrDefault(log);

	// 1. First determine project root to use as base for all path resolution
	const rawProjectRoot = args?.projectRoot || findProjectRoot();

	if (!rawProjectRoot) {
		logger.warn?.('Could not determine project root directory');
		return null;
	}

	// 2. Normalize project root to prevent double .taskmaster paths
	const projectRoot = normalizeProjectRoot(rawProjectRoot);

	// 3. If explicit path is provided, resolve it relative to project root (highest priority)
	if (explicitPath) {
		const resolvedPath = path.isAbsolute(explicitPath)
			? explicitPath
			: path.resolve(projectRoot, explicitPath);

		if (fs.existsSync(resolvedPath)) {
			logger.info?.(`Using explicit tasks path: ${resolvedPath}`);
			return resolvedPath;
		} else {
			logger.warn?.(
				`Explicit tasks path not found: ${resolvedPath}, trying fallbacks`
			);
		}
	}

	// 4. Check possible locations in order of preference
	const possiblePaths = [
		path.join(projectRoot, TASKMASTER_TASKS_FILE), // .taskmaster/tasks/tasks.json (NEW)
		path.join(projectRoot, LEGACY_TASKS_FILE) // tasks/tasks.json (LEGACY)
	];

	for (const tasksPath of possiblePaths) {
		if (fs.existsSync(tasksPath)) {
			logger.info?.(`Found tasks file at: ${tasksPath}`);

			// Issue deprecation warning for legacy paths
			if (
				tasksPath.includes('tasks/tasks.json') &&
				!tasksPath.includes('.taskmaster')
			) {
				logger.warn?.(
					`⚠  DEPRECATION WARNING: Found tasks.json in legacy location '${tasksPath}'. Please migrate to the new .taskmaster directory structure. Run 'task-master migrate' to automatically migrate your project.`
				);
			} else if (
				tasksPath.endsWith('tasks.json') &&
				!tasksPath.includes('.taskmaster') &&
				!tasksPath.includes('tasks/')
			) {
				logger.warn?.(
					`⚠  DEPRECATION WARNING: Found tasks.json in legacy root location '${tasksPath}'. Please migrate to the new .taskmaster directory structure. Run 'task-master migrate' to automatically migrate your project.`
				);
			}

			return tasksPath;
		}
	}

	logger.warn?.(`No tasks.json found in project: ${projectRoot}`);
	return null;
}

/**
 * Find the PRD document file path with fallback logic
 * @param {string|null} explicitPath - Explicit path provided by user (highest priority)
 * @param {Object|null} args - Args object for MCP context (optional)
 * @param {Object|null} log - Logger object (optional)
 * @returns {string|null} - Resolved PRD document path or null if not found
 */
export function findPRDPath(explicitPath = null, args = null, log = null) {
	const logger = getLoggerOrDefault(log);

	// 1. If explicit path is provided, use it (highest priority)
	if (explicitPath) {
		const resolvedPath = path.isAbsolute(explicitPath)
			? explicitPath
			: path.resolve(process.cwd(), explicitPath);

		if (fs.existsSync(resolvedPath)) {
			logger.info?.(`Using explicit PRD path: ${resolvedPath}`);
			return resolvedPath;
		} else {
			logger.warn?.(
				`Explicit PRD path not found: ${resolvedPath}, trying fallbacks`
			);
		}
	}

	// 2. Try to get project root from args (MCP) or find it
	const rawProjectRoot = args?.projectRoot || findProjectRoot();

	if (!rawProjectRoot) {
		logger.warn?.('Could not determine project root directory');
		return null;
	}

	// 3. Normalize project root to prevent double .taskmaster paths
	const projectRoot = normalizeProjectRoot(rawProjectRoot);

	// 4. Check possible locations in order of preference
	const locations = [
		TASKMASTER_DOCS_DIR, // .taskmaster/docs/ (NEW)
		'scripts/', // Legacy location
		'' // Project root
	];

	const fileNames = ['PRD.md', 'prd.md', 'PRD.txt', 'prd.txt'];

	for (const location of locations) {
		for (const fileName of fileNames) {
			const prdPath = path.join(projectRoot, location, fileName);
			if (fs.existsSync(prdPath)) {
				logger.info?.(`Found PRD document at: ${prdPath}`);

				// Issue deprecation warning for legacy paths
				if (location === 'scripts/' || location === '') {
					logger.warn?.(
						`⚠  DEPRECATION WARNING: Found PRD file in legacy location '${prdPath}'. Please migrate to .taskmaster/docs/ directory. Run 'task-master migrate' to automatically migrate your project.`
					);
				}

				return prdPath;
			}
		}
	}

	logger.warn?.(`No PRD document found in project: ${projectRoot}`);
	return null;
}

/**
 * Find the complexity report file path with fallback logic
 * @param {string|null} explicitPath - Explicit path provided by user (highest priority)
 * @param {Object|null} args - Args object for MCP context (optional)
 * @param {Object|null} log - Logger object (optional)
 * @returns {string|null} - Resolved complexity report path or null if not found
 */
export function findComplexityReportPath(
	explicitPath = null,
	args = null,
	log = null
) {
	const logger = getLoggerOrDefault(log);

	// 1. If explicit path is provided, use it (highest priority)
	if (explicitPath) {
		const resolvedPath = path.isAbsolute(explicitPath)
			? explicitPath
			: path.resolve(process.cwd(), explicitPath);

		if (fs.existsSync(resolvedPath)) {
			logger.info?.(`Using explicit complexity report path: ${resolvedPath}`);
			return resolvedPath;
		} else {
			logger.warn?.(
				`Explicit complexity report path not found: ${resolvedPath}, trying fallbacks`
			);
		}
	}

	// 2. Try to get project root from args (MCP) or find it
	const rawProjectRoot = args?.projectRoot || findProjectRoot();

	if (!rawProjectRoot) {
		logger.warn?.('Could not determine project root directory');
		return null;
	}

	// 3. Normalize project root to prevent double .taskmaster paths
	const projectRoot = normalizeProjectRoot(rawProjectRoot);

	// 4. Check possible locations in order of preference
	const locations = [
		TASKMASTER_REPORTS_DIR, // .taskmaster/reports/ (NEW)
		'scripts/', // Legacy location
		'' // Project root
	];

	const fileNames = ['task-complexity', 'complexity-report'].map((fileName) => {
		if (args?.tag && args?.tag !== 'master') {
			return `${fileName}_${args.tag}.json`;
		}
		return `${fileName}.json`;
	});

	for (const location of locations) {
		for (const fileName of fileNames) {
			const reportPath = path.join(projectRoot, location, fileName);
			if (fs.existsSync(reportPath)) {
				logger.info?.(`Found complexity report at: ${reportPath}`);

				// Issue deprecation warning for legacy paths
				if (location === 'scripts/' || location === '') {
					logger.warn?.(
						`⚠  DEPRECATION WARNING: Found complexity report in legacy location '${reportPath}'. Please migrate to .taskmaster/reports/ directory. Run 'task-master migrate' to automatically migrate your project.`
					);
				}

				return reportPath;
			}
		}
	}

	logger.warn?.(`No complexity report found in project: ${projectRoot}`);
	return null;
}

/**
 * Resolve output path for tasks.json (create if needed)
 * @param {string|null} explicitPath - Explicit output path provided by user
 * @param {Object|null} args - Args object for MCP context (optional)
 * @param {Object|null} log - Logger object (optional)
 * @returns {string} - Resolved output path for tasks.json
 */
export function resolveTasksOutputPath(
	explicitPath = null,
	args = null,
	log = null
) {
	const logger = getLoggerOrDefault(log);

	// 1. If explicit path is provided, use it
	if (explicitPath) {
		const resolvedPath = path.isAbsolute(explicitPath)
			? explicitPath
			: path.resolve(process.cwd(), explicitPath);

		logger.info?.(`Using explicit output path: ${resolvedPath}`);
		return resolvedPath;
	}

	// 2. Try to get project root from args (MCP) or find it
	const rawProjectRoot =
		args?.projectRoot || findProjectRoot() || process.cwd();

	// 3. Normalize project root to prevent double .taskmaster paths
	const projectRoot = normalizeProjectRoot(rawProjectRoot);

	// 4. Use new .taskmaster structure by default
	const defaultPath = path.join(projectRoot, TASKMASTER_TASKS_FILE);
	logger.info?.(`Using default output path: ${defaultPath}`);

	// Ensure the directory exists
	const outputDir = path.dirname(defaultPath);
	if (!fs.existsSync(outputDir)) {
		logger.info?.(`Creating tasks directory: ${outputDir}`);
		fs.mkdirSync(outputDir, { recursive: true });
	}

	return defaultPath;
}

/**
 * Resolve output path for complexity report (create if needed)
 * @param {string|null} explicitPath - Explicit output path provided by user
 * @param {Object|null} args - Args object for MCP context (optional)
 * @param {Object|null} log - Logger object (optional)
 * @returns {string} - Resolved output path for complexity report
 */
export function resolveComplexityReportOutputPath(
	explicitPath = null,
	args = null,
	log = null
) {
	const logger = getLoggerOrDefault(log);
	const tag = args?.tag;

	// 1. If explicit path is provided, use it
	if (explicitPath) {
		const resolvedPath = path.isAbsolute(explicitPath)
			? explicitPath
			: path.resolve(process.cwd(), explicitPath);

		logger.info?.(
			`Using explicit complexity report output path: ${resolvedPath}`
		);
		return resolvedPath;
	}

	// 2. Try to get project root from args (MCP) or find it
	const rawProjectRoot =
		args?.projectRoot || findProjectRoot() || process.cwd();
	const projectRoot = normalizeProjectRoot(rawProjectRoot);

	// 3. Use tag-aware filename
	let filename = 'task-complexity-report.json';
	if (tag && tag !== 'master') {
		filename = `task-complexity-report_${tag}.json`;
	}

	// 4. Use new .taskmaster structure by default
	const defaultPath = path.join(projectRoot, '.taskmaster/reports', filename);
	logger.info?.(
		`Using tag-aware complexity report output path: ${defaultPath}`
	);

	// Ensure the directory exists
	const outputDir = path.dirname(defaultPath);
	if (!fs.existsSync(outputDir)) {
		logger.info?.(`Creating reports directory: ${outputDir}`);
		fs.mkdirSync(outputDir, { recursive: true });
	}

	return defaultPath;
}

/**
 * Find the configuration file path with fallback logic
 * @param {string|null} explicitPath - Explicit path provided by user (highest priority)
 * @param {Object|null} args - Args object for MCP context (optional)
 * @param {Object|null} log - Logger object (optional)
 * @returns {string|null} - Resolved config file path or null if not found
 */
export function findConfigPath(explicitPath = null, args = null, log = null) {
	const logger = getLoggerOrDefault(log);

	// 1. If explicit path is provided, use it (highest priority)
	if (explicitPath) {
		const resolvedPath = path.isAbsolute(explicitPath)
			? explicitPath
			: path.resolve(process.cwd(), explicitPath);

		if (fs.existsSync(resolvedPath)) {
			logger.info?.(`Using explicit config path: ${resolvedPath}`);
			return resolvedPath;
		} else {
			logger.warn?.(
				`Explicit config path not found: ${resolvedPath}, trying fallbacks`
			);
		}
	}

	// 2. Try to get project root from args (MCP) or find it
	const rawProjectRoot = args?.projectRoot || findProjectRoot();

	if (!rawProjectRoot) {
		logger.warn?.('Could not determine project root directory');
		return null;
	}

	// 3. Normalize project root to prevent double .taskmaster paths
	const projectRoot = normalizeProjectRoot(rawProjectRoot);

	// 4. Check possible locations in order of preference
	const possiblePaths = [
		path.join(projectRoot, TASKMASTER_CONFIG_FILE), // NEW location
		path.join(projectRoot, LEGACY_CONFIG_FILE) // LEGACY location
	];

	for (const configPath of possiblePaths) {
		if (fs.existsSync(configPath)) {
			// Issue deprecation warning for legacy paths
			if (configPath?.endsWith(LEGACY_CONFIG_FILE)) {
				logger.warn?.(
					`⚠  DEPRECATION WARNING: Found configuration in legacy location '${configPath}'. Please migrate to .taskmaster/config.json. Run 'task-master migrate' to automatically migrate your project.`
				);
			}

			return configPath;
		}
	}

	logger.warn?.(`No configuration file found in project: ${projectRoot}`);
	return null;
}

/**
 * remove-task.js
 * Direct function implementation for removing a task
 */

import {
	removeTask,
	taskExists
} from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode,
	readJSON
} from '../../../../scripts/modules/utils.js';

/**
 * Direct function wrapper for removeTask with error handling.
 * Supports removing multiple tasks at once with comma-separated IDs.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} args.id - The ID(s) of the task(s) or subtask(s) to remove (comma-separated for multiple).
 * @param {string} args.projectRoot - Project root path (for MCP/env fallback)
 * @param {string} args.tag - Tag for the task (optional)
 * @param {Object} log - Logger object
 * @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function removeTaskDirect(args, log, context = {}) {
	// Destructure expected args
	const { tasksJsonPath, id, projectRoot, tag } = args;
	const { session } = context;
	try {
		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('removeTaskDirect called without tasksJsonPath');
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Validate task ID parameter
		if (!id) {
			log.error('Task ID is required');
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message: 'Task ID is required'
				}
			};
		}

		// Split task IDs if comma-separated
		const taskIdArray = id.split(',').map((taskId) => taskId.trim());

		log.info(
			`Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}${tag ? ` in tag '${tag}'` : ''}`
		);

		// Validate all task IDs exist before proceeding
		const data = readJSON(tasksJsonPath, projectRoot, tag);
		if (!data || !data.tasks) {
			return {
				success: false,
				error: {
					code: 'INVALID_TASKS_FILE',
					message: `No valid tasks found in ${tasksJsonPath}${tag ? ` for tag '${tag}'` : ''}`
				}
			};
		}

		const invalidTasks = taskIdArray.filter(
			(taskId) => !taskExists(data.tasks, taskId)
		);

		if (invalidTasks.length > 0) {
			return {
				success: false,
				error: {
					code: 'INVALID_TASK_ID',
					message: `The following tasks were not found${tag ? ` in tag '${tag}'` : ''}: ${invalidTasks.join(', ')}`
				}
			};
		}

		// Enable silent mode to prevent console logs from interfering with JSON response
		enableSilentMode();

		try {
			// Call removeTask with proper context including tag
			const result = await removeTask(tasksJsonPath, id, {
				projectRoot,
				tag
			});

			if (!result.success) {
				return {
					success: false,
					error: {
						code: 'REMOVE_TASK_ERROR',
						message: result.error || 'Failed to remove tasks'
					}
				};
			}

			log.info(`Successfully removed ${result.removedTasks.length} task(s)`);

			return {
				success: true,
				data: {
					totalTasks: taskIdArray.length,
					successful: result.removedTasks.length,
					failed: taskIdArray.length - result.removedTasks.length,
					removedTasks: result.removedTasks,
					message: result.message,
					tasksPath: tasksJsonPath,
					tag
				}
			};
		} finally {
			// Restore normal logging
			disableSilentMode();
		}
	} catch (error) {
		// Ensure silent mode is disabled even if an outer error occurs
		disableSilentMode();

		// Catch any unexpected errors
		log.error(`Unexpected error in removeTaskDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'UNEXPECTED_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * Direct function wrapper for expandAllTasks
 */

import { expandAllTasks } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Expand all pending tasks with subtasks (Direct Function Wrapper)
 * @param {Object} args - Function arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {number|string} [args.num] - Number of subtasks to generate
 * @param {boolean} [args.research] - Enable research-backed subtask generation
 * @param {string} [args.prompt] - Additional context to guide subtask generation
 * @param {boolean} [args.force] - Force regeneration of subtasks for tasks that already have them
 * @param {string} [args.projectRoot] - Project root path.
 * @param {string} [args.tag] - Tag for the task (optional)
 * @param {Object} log - Logger object from FastMCP
 * @param {Object} context - Context object containing session
 * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
 */
export async function expandAllTasksDirect(args, log, context = {}) {
	const { session } = context; // Extract session
	// Destructure expected args, including projectRoot
	const { tasksJsonPath, num, research, prompt, force, projectRoot, tag } =
		args;

	// Create logger wrapper using the utility
	const mcpLog = createLogWrapper(log);

	if (!tasksJsonPath) {
		log.error('expandAllTasksDirect called without tasksJsonPath');
		return {
			success: false,
			error: {
				code: 'MISSING_ARGUMENT',
				message: 'tasksJsonPath is required'
			}
		};
	}

	enableSilentMode(); // Enable silent mode for the core function call
	try {
		log.info(
			`Calling core expandAllTasks with args: ${JSON.stringify({ num, research, prompt, force, projectRoot, tag })}`
		);

		// Parse parameters (ensure correct types)
		const numSubtasks = num ? parseInt(num, 10) : undefined;
		const useResearch = research === true;
		const additionalContext = prompt || '';
		const forceFlag = force === true;

		// Call the core function, passing options and the context object { session, mcpLog, projectRoot }
		const result = await expandAllTasks(
			tasksJsonPath,
			numSubtasks,
			useResearch,
			additionalContext,
			forceFlag,
			{ session, mcpLog, projectRoot, tag },
			'json'
		);

		// Core function now returns a summary object including the *aggregated* telemetryData
		return {
			success: true,
			data: {
				message: `Expand all operation completed. Expanded: ${result.expandedCount}, Failed: ${result.failedCount}, Skipped: ${result.skippedCount}`,
				details: {
					expandedCount: result.expandedCount,
					failedCount: result.failedCount,
					skippedCount: result.skippedCount,
					tasksToExpand: result.tasksToExpand
				},
				telemetryData: result.telemetryData // Pass the aggregated object
			}
		};
	} catch (error) {
		// Log the error using the MCP logger
		log.error(`Error during core expandAllTasks execution: ${error.message}`);
		// Optionally log stack trace if available and debug enabled
		// if (error.stack && log.debug) { log.debug(error.stack); }

		return {
			success: false,
			error: {
				code: 'CORE_FUNCTION_ERROR', // Or a more specific code if possible
				message: error.message
			}
		};
	} finally {
		disableSilentMode(); // IMPORTANT: Ensure silent mode is always disabled
	}
}

#!/usr/bin/env node

/**
 * test-claude-errors.js
 *
 * A test script to verify the error handling and retry logic in the callClaude function.
 * This script creates a modified version of dev.js that simulates different error scenarios.
 */

import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { execSync, spawn } from 'child_process';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Load environment variables from .env file
dotenv.config();

// Create a simple PRD for testing
const createTestPRD = () => {
	return `# Test PRD for Error Handling

## Overview
This is a simple test PRD to verify the error handling in the callClaude function.

## Requirements
1. Create a simple web application
2. Implement user authentication
3. Add a dashboard for users
`;
};

// Create a modified version of dev.js that simulates errors
function createErrorSimulationScript(errorType, failureCount = 2) {
	// Read the original dev.js file
	const devJsPath = path.join(__dirname, 'dev.js');
	const devJsContent = fs.readFileSync(devJsPath, 'utf8');

	// Create a modified version that simulates errors
	let modifiedContent = devJsContent;

	// Find the anthropic.messages.create call and replace it with our mock
	const anthropicCallRegex =
		/const response = await anthropic\.messages\.create\(/;

	let mockCode = '';

	switch (errorType) {
		case 'network':
			mockCode = `
      // Mock for network error simulation
      let currentAttempt = 0;
      const failureCount = ${failureCount};

      // Simulate network error for the first few attempts
      currentAttempt++;
      console.log(\`[Mock] API call attempt \${currentAttempt}\`);

      if (currentAttempt <= failureCount) {
        console.log(\`[Mock] Simulating network error (attempt \${currentAttempt}/\${failureCount})\`);
        throw new Error('Network error: Connection refused');
      }

      const response = await anthropic.messages.create(`;
			break;

		case 'timeout':
			mockCode = `
      // Mock for timeout error simulation
      let currentAttempt = 0;
      const failureCount = ${failureCount};

      // Simulate timeout error for the first few attempts
      currentAttempt++;
      console.log(\`[Mock] API call attempt \${currentAttempt}\`);

      if (currentAttempt <= failureCount) {
        console.log(\`[Mock] Simulating timeout error (attempt \${currentAttempt}/\${failureCount})\`);
        throw new Error('Request timed out after 60000ms');
      }

      const response = await anthropic.messages.create(`;
			break;

		case 'invalid-json':
			mockCode = `
      // Mock for invalid JSON response
      let currentAttempt = 0;
      const failureCount = ${failureCount};

      // Simulate invalid JSON for the first few attempts
      currentAttempt++;
      console.log(\`[Mock] API call attempt \${currentAttempt}\`);

      if (currentAttempt <= failureCount) {
        console.log(\`[Mock] Simulating invalid JSON response (attempt \${currentAttempt}/\${failureCount})\`);
        return {
          content: [
            {
              text: \`\`\`json\\n{"meta": {"projectName": "Test Project"}, "tasks": [{"id": 1, "title": "Task 1"\`
            }
          ]
        };
      }

      const response = await anthropic.messages.create(`;
			break;

		case 'empty-tasks':
			mockCode = `
      // Mock for empty tasks array
      let currentAttempt = 0;
      const failureCount = ${failureCount};

      // Simulate empty tasks array for the first few attempts
      currentAttempt++;
      console.log(\`[Mock] API call attempt \${currentAttempt}\`);

      if (currentAttempt <= failureCount) {
        console.log(\`[Mock] Simulating empty tasks array (attempt \${currentAttempt}/\${failureCount})\`);
        return {
          content: [
            {
              text: \`\`\`json\\n{"meta": {"projectName": "Test Project"}, "tasks": []}\\n\`\`\`
            }
          ]
        };
      }

      const response = await anthropic.messages.create(`;
			break;

		default:
			// No modification
			mockCode = `const response = await anthropic.messages.create(`;
	}

	// Replace the anthropic call with our mock
	modifiedContent = modifiedContent.replace(anthropicCallRegex, mockCode);

	// Write the modified script to a temporary file
	const tempScriptPath = path.join(__dirname, `temp-dev-${errorType}.js`);
	fs.writeFileSync(tempScriptPath, modifiedContent, 'utf8');

	return tempScriptPath;
}

// Function to run a test with a specific error type
async function runErrorTest(errorType, numTasks = 5, failureCount = 2) {
	console.log(`\n=== Test: ${errorType.toUpperCase()} Error Simulation ===`);

	// Create a test PRD
	const testPRD = createTestPRD();
	const testPRDPath = path.join(__dirname, `test-prd-${errorType}.txt`);
	fs.writeFileSync(testPRDPath, testPRD, 'utf8');

	// Create a modified dev.js that simulates the specified error
	const tempScriptPath = createErrorSimulationScript(errorType, failureCount);

	console.log(`Created test PRD at ${testPRDPath}`);
	console.log(`Created error simulation script at ${tempScriptPath}`);
	console.log(
		`Running with error type: ${errorType}, failure count: ${failureCount}, tasks: ${numTasks}`
	);

	try {
		// Run the modified script
		execSync(
			`node ${tempScriptPath} parse-prd --input=${testPRDPath} --tasks=${numTasks}`,
			{
				stdio: 'inherit'
			}
		);
		console.log(`${errorType} error test completed successfully`);
	} catch (error) {
		console.error(`${errorType} error test failed:`, error.message);
	} finally {
		// Clean up temporary files
		if (fs.existsSync(tempScriptPath)) {
			fs.unlinkSync(tempScriptPath);
		}
		if (fs.existsSync(testPRDPath)) {
			fs.unlinkSync(testPRDPath);
		}
	}
}

// Function to run all error tests
async function runAllErrorTests() {
	console.log('Starting error handling tests for callClaude function...');

	// Test 1: Network error with automatic retry
	await runErrorTest('network', 5, 2);

	// Test 2: Timeout error with automatic retry
	await runErrorTest('timeout', 5, 2);

	// Test 3: Invalid JSON response with task reduction
	await runErrorTest('invalid-json', 10, 2);

	// Test 4: Empty tasks array with task reduction
	await runErrorTest('empty-tasks', 15, 2);

	// Test 5: Exhausted retries (more failures than MAX_RETRIES)
	await runErrorTest('network', 5, 4);

	console.log('\nAll error tests completed!');
}

// Run the tests
runAllErrorTests().catch((error) => {
	console.error('Error running tests:', error);
	process.exit(1);
});

/**
 * Direct function wrapper for addSubtask
 */

import { addSubtask } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';

/**
 * Add a subtask to an existing task
 * @param {Object} args - Function arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} args.id - Parent task ID
 * @param {string} [args.taskId] - Existing task ID to convert to subtask (optional)
 * @param {string} [args.title] - Title for new subtask (when creating a new subtask)
 * @param {string} [args.description] - Description for new subtask
 * @param {string} [args.details] - Implementation details for new subtask
 * @param {string} [args.status] - Status for new subtask (default: 'pending')
 * @param {string} [args.dependencies] - Comma-separated list of dependency IDs
 * @param {boolean} [args.skipGenerate] - Skip regenerating task files
 * @param {string} [args.projectRoot] - Project root directory
 * @param {string} [args.tag] - Tag for the task
 * @param {Object} log - Logger object
 * @returns {Promise<{success: boolean, data?: Object, error?: string}>}
 */
export async function addSubtaskDirect(args, log) {
	// Destructure expected args
	const {
		tasksJsonPath,
		id,
		taskId,
		title,
		description,
		details,
		status,
		dependencies: dependenciesStr,
		skipGenerate,
		projectRoot,
		tag
	} = args;
	try {
		log.info(`Adding subtask with args: ${JSON.stringify(args)}`);

		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('addSubtaskDirect called without tasksJsonPath');
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		if (!id) {
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message: 'Parent task ID is required'
				}
			};
		}

		// Either taskId or title must be provided
		if (!taskId && !title) {
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message: 'Either taskId or title must be provided'
				}
			};
		}

		// Use provided path
		const tasksPath = tasksJsonPath;

		// Parse dependencies if provided
		let dependencies = [];
		if (dependenciesStr) {
			dependencies = dependenciesStr.split(',').map((depId) => {
				// Handle both regular IDs and dot notation
				return depId.includes('.') ? depId.trim() : parseInt(depId.trim(), 10);
			});
		}

		// Convert existingTaskId to a number if provided
		const existingTaskId = taskId ? parseInt(taskId, 10) : null;

		// Convert parent ID to a number
		const parentId = parseInt(id, 10);

		// Determine if we should generate files
		const generateFiles = !skipGenerate;

		// Enable silent mode to prevent console logs from interfering with JSON response
		enableSilentMode();

		const context = { projectRoot, tag };

		// Case 1: Convert existing task to subtask
		if (existingTaskId) {
			log.info(`Converting task ${existingTaskId} to a subtask of ${parentId}`);
			const result = await addSubtask(
				tasksPath,
				parentId,
				existingTaskId,
				null,
				generateFiles,
				context
			);

			// Restore normal logging
			disableSilentMode();

			return {
				success: true,
				data: {
					message: `Task ${existingTaskId} successfully converted to a subtask of task ${parentId}`,
					subtask: result
				}
			};
		}
		// Case 2: Create new subtask
		else {
			log.info(`Creating new subtask for parent task ${parentId}`);

			const newSubtaskData = {
				title: title,
				description: description || '',
				details: details || '',
				status: status || 'pending',
				dependencies: dependencies
			};

			const result = await addSubtask(
				tasksPath,
				parentId,
				null,
				newSubtaskData,
				generateFiles,
				context
			);

			// Restore normal logging
			disableSilentMode();

			return {
				success: true,
				data: {
					message: `New subtask ${parentId}.${result.id} successfully created`,
					subtask: result
				}
			};
		}
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in addSubtaskDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'CORE_FUNCTION_ERROR',
				message: error.message
			}
		};
	}
}

// Claude Code profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { isSilentMode, log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js';

// Helper function to recursively copy directory (adopted from Roo profile)
function copyRecursiveSync(src, dest) {
	const exists = fs.existsSync(src);
	const stats = exists && fs.statSync(src);
	const isDirectory = exists && stats.isDirectory();
	if (isDirectory) {
		if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
		fs.readdirSync(src).forEach((childItemName) => {
			copyRecursiveSync(
				path.join(src, childItemName),
				path.join(dest, childItemName)
			);
		});
	} else {
		fs.copyFileSync(src, dest);
	}
}

// Helper function to recursively remove directory
function removeDirectoryRecursive(dirPath) {
	if (fs.existsSync(dirPath)) {
		try {
			fs.rmSync(dirPath, { recursive: true, force: true });
			return true;
		} catch (err) {
			log('error', `Failed to remove directory ${dirPath}: ${err.message}`);
			return false;
		}
	}
	return true;
}

// Lifecycle functions for Claude Code profile
function onAddRulesProfile(targetDir, assetsDir) {
	// Copy .claude directory recursively
	const claudeSourceDir = path.join(assetsDir, 'claude');
	const claudeDestDir = path.join(targetDir, '.claude');

	if (!fs.existsSync(claudeSourceDir)) {
		log(
			'error',
			`[Claude] Source directory does not exist: ${claudeSourceDir}`
		);
		return;
	}

	try {
		copyRecursiveSync(claudeSourceDir, claudeDestDir);
		log('debug', `[Claude] Copied .claude directory to ${claudeDestDir}`);
	} catch (err) {
		log(
			'error',
			`[Claude] An error occurred during directory copy: ${err.message}`
		);
	}

	// Handle CLAUDE.md import for non-destructive integration
	const sourceFile = path.join(assetsDir, 'AGENTS.md');
	const userClaudeFile = path.join(targetDir, 'CLAUDE.md');
	const taskMasterClaudeFile = path.join(targetDir, '.taskmaster', 'CLAUDE.md');
	const importLine = '@./.taskmaster/CLAUDE.md';
	const importSection = `\n## Task Master AI Instructions\n**Import Task Master's development workflow commands and guidelines, treat as if import is in the main CLAUDE.md file.**\n${importLine}`;

	if (fs.existsSync(sourceFile)) {
		try {
			// Ensure .taskmaster directory exists
			const taskMasterDir = path.join(targetDir, '.taskmaster');
			if (!fs.existsSync(taskMasterDir)) {
				fs.mkdirSync(taskMasterDir, { recursive: true });
			}

			// Copy Task Master instructions to .taskmaster/CLAUDE.md
			fs.copyFileSync(sourceFile, taskMasterClaudeFile);
			log(
				'debug',
				`[Claude] Created Task Master instructions at ${taskMasterClaudeFile}`
			);

			// Handle user's CLAUDE.md
			if (fs.existsSync(userClaudeFile)) {
				// Check if import already exists
				const content = fs.readFileSync(userClaudeFile, 'utf8');
				if (!content.includes(importLine)) {
					// Append import section at the end
					const updatedContent = content.trim() + '\n' + importSection + '\n';
					fs.writeFileSync(userClaudeFile, updatedContent);
					log(
						'info',
						`[Claude] Added Task Master import to existing ${userClaudeFile}`
					);
				} else {
					log(
						'info',
						`[Claude] Task Master import already present in ${userClaudeFile}`
					);
				}
			} else {
				// Create minimal CLAUDE.md with the import section
				const minimalContent = `# Claude Code Instructions\n${importSection}\n`;
				fs.writeFileSync(userClaudeFile, minimalContent);
				log(
					'info',
					`[Claude] Created ${userClaudeFile} with Task Master import`
				);
			}
		} catch (err) {
			log(
				'error',
				`[Claude] Failed to set up Claude instructions: ${err.message}`
			);
		}
	}
}

function onRemoveRulesProfile(targetDir) {
	// Remove .claude directory recursively
	const claudeDir = path.join(targetDir, '.claude');
	if (removeDirectoryRecursive(claudeDir)) {
		log('debug', `[Claude] Removed .claude directory from ${claudeDir}`);
	}

	// Clean up CLAUDE.md import
	const userClaudeFile = path.join(targetDir, 'CLAUDE.md');
	const taskMasterClaudeFile = path.join(targetDir, '.taskmaster', 'CLAUDE.md');
	const importLine = '@./.taskmaster/CLAUDE.md';

	try {
		// Remove Task Master CLAUDE.md from .taskmaster
		if (fs.existsSync(taskMasterClaudeFile)) {
			fs.rmSync(taskMasterClaudeFile, { force: true });
			log('debug', `[Claude] Removed ${taskMasterClaudeFile}`);
		}

		// Clean up import from user's CLAUDE.md
		if (fs.existsSync(userClaudeFile)) {
			const content = fs.readFileSync(userClaudeFile, 'utf8');
			const lines = content.split('\n');
			const filteredLines = [];
			let skipNextLines = 0;

			// Remove the Task Master section
			for (let i = 0; i < lines.length; i++) {
				if (skipNextLines > 0) {
					skipNextLines--;
					continue;
				}

				// Check if this is the start of our Task Master section
				if (lines[i].includes('## Task Master AI Instructions')) {
					// Skip this line and the next two lines (bold text and import)
					skipNextLines = 2;
					continue;
				}

				// Also remove standalone import lines (for backward compatibility)
				if (lines[i].trim() === importLine) {
					continue;
				}

				filteredLines.push(lines[i]);
			}

			// Join back and clean up excessive newlines
			let updatedContent = filteredLines
				.join('\n')
				.replace(/\n{3,}/g, '\n\n')
				.trim();

			// Check if file only contained our minimal template
			if (
				updatedContent === '# Claude Code Instructions' ||
				updatedContent === ''
			) {
				// File only contained our import, remove it
				fs.rmSync(userClaudeFile, { force: true });
				log('debug', `[Claude] Removed empty ${userClaudeFile}`);
			} else {
				// Write back without the import
				fs.writeFileSync(userClaudeFile, updatedContent + '\n');
				log(
					'debug',
					`[Claude] Removed Task Master import from ${userClaudeFile}`
				);
			}
		}
	} catch (err) {
		log(
			'error',
			`[Claude] Failed to remove Claude instructions: ${err.message}`
		);
	}
}

/**
 * Transform standard MCP config format to Claude format
 * @param {Object} mcpConfig - Standard MCP configuration object
 * @returns {Object} - Transformed Claude configuration object
 */
function transformToClaudeFormat(mcpConfig) {
	const claudeConfig = {};

	// Transform mcpServers to servers (keeping the same structure but adding type)
	if (mcpConfig.mcpServers) {
		claudeConfig.mcpServers = {};

		for (const [serverName, serverConfig] of Object.entries(
			mcpConfig.mcpServers
		)) {
			// Transform server configuration with type as first key
			const reorderedServer = {};

			// Add type: "stdio" as the first key
			reorderedServer.type = 'stdio';

			// Then add the rest of the properties in order
			if (serverConfig.command) reorderedServer.command = serverConfig.command;
			if (serverConfig.args) reorderedServer.args = serverConfig.args;
			if (serverConfig.env) reorderedServer.env = serverConfig.env;

			// Add any other properties that might exist
			Object.keys(serverConfig).forEach((key) => {
				if (!['command', 'args', 'env', 'type'].includes(key)) {
					reorderedServer[key] = serverConfig[key];
				}
			});

			claudeConfig.mcpServers[serverName] = reorderedServer;
		}
	}

	return claudeConfig;
}

function onPostConvertRulesProfile(targetDir, assetsDir) {
	// For Claude, post-convert is the same as add since we don't transform rules
	onAddRulesProfile(targetDir, assetsDir);

	// Transform MCP configuration to Claude format
	const mcpConfigPath = path.join(targetDir, '.mcp.json');
	if (fs.existsSync(mcpConfigPath)) {
		try {
			const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
			const claudeConfig = transformToClaudeFormat(mcpConfig);

			// Write back the transformed configuration
			fs.writeFileSync(
				mcpConfigPath,
				JSON.stringify(claudeConfig, null, '\t') + '\n'
			);
			log(
				'debug',
				`[Claude] Transformed MCP configuration to Claude format at ${mcpConfigPath}`
			);
		} catch (err) {
			log(
				'error',
				`[Claude] Failed to transform MCP configuration: ${err.message}`
			);
		}
	}
}

// Create and export claude profile using the base factory
export const claudeProfile = createProfile({
	name: 'claude',
	displayName: 'Claude Code',
	url: 'claude.ai',
	docsUrl: 'docs.anthropic.com/en/docs/claude-code',
	profileDir: '.', // Root directory
	rulesDir: '.', // No specific rules directory needed
	mcpConfigName: '.mcp.json', // Place MCP config in project root
	includeDefaultRules: false,
	fileMap: {
		'AGENTS.md': '.taskmaster/CLAUDE.md'
	},
	onAdd: onAddRulesProfile,
	onRemove: onRemoveRulesProfile,
	onPostConvert: onPostConvertRulesProfile
});

// Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile };

/**
 * tools/research.js
 * Tool to perform AI-powered research queries with project context
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { researchDirect } from '../core/task-master-core.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the research tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerResearchTool(server) {
	server.addTool({
		name: 'research',
		description: 'Perform AI-powered research queries with project context',

		parameters: z.object({
			query: z.string().describe('Research query/prompt (required)'),
			taskIds: z
				.string()
				.optional()
				.describe(
					'Comma-separated list of task/subtask IDs for context (e.g., "15,16.2,17")'
				),
			filePaths: z
				.string()
				.optional()
				.describe(
					'Comma-separated list of file paths for context (e.g., "src/api.js,docs/readme.md")'
				),
			customContext: z
				.string()
				.optional()
				.describe('Additional custom context text to include in the research'),
			includeProjectTree: z
				.boolean()
				.optional()
				.describe(
					'Include project file tree structure in context (default: false)'
				),
			detailLevel: z
				.enum(['low', 'medium', 'high'])
				.optional()
				.describe('Detail level for the research response (default: medium)'),
			saveTo: z
				.string()
				.optional()
				.describe(
					'Automatically save research results to specified task/subtask ID (e.g., "15" or "15.2")'
				),
			saveToFile: z
				.boolean()
				.optional()
				.describe(
					'Save research results to .taskmaster/docs/research/ directory (default: false)'
				),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				log.info(
					`Starting research with query: "${args.query.substring(0, 100)}${args.query.length > 100 ? '...' : ''}"`
				);

				// Call the direct function
				const result = await researchDirect(
					{
						query: args.query,
						taskIds: args.taskIds,
						filePaths: args.filePaths,
						customContext: args.customContext,
						includeProjectTree: args.includeProjectTree || false,
						detailLevel: args.detailLevel || 'medium',
						saveTo: args.saveTo,
						saveToFile: args.saveToFile || false,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error performing research',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in research tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

// Amp profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { isSilentMode, log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js';

/**
 * Transform standard MCP config format to Amp format
 * @param {Object} mcpConfig - Standard MCP configuration object
 * @returns {Object} - Transformed Amp configuration object
 */
function transformToAmpFormat(mcpConfig) {
	const ampConfig = {};

	// Transform mcpServers to amp.mcpServers
	if (mcpConfig.mcpServers) {
		ampConfig['amp.mcpServers'] = mcpConfig.mcpServers;
	}

	// Preserve any other existing settings
	for (const [key, value] of Object.entries(mcpConfig)) {
		if (key !== 'mcpServers') {
			ampConfig[key] = value;
		}
	}

	return ampConfig;
}

// Lifecycle functions for Amp profile
function onAddRulesProfile(targetDir, assetsDir) {
	// Handle AGENT.md import for non-destructive integration (Amp uses AGENT.md, copies from AGENTS.md)
	const sourceFile = path.join(assetsDir, 'AGENTS.md');
	const userAgentFile = path.join(targetDir, 'AGENT.md');
	const taskMasterAgentFile = path.join(targetDir, '.taskmaster', 'AGENT.md');
	const importLine = '@./.taskmaster/AGENT.md';
	const importSection = `\n## Task Master AI Instructions\n**Import Task Master's development workflow commands and guidelines, treat as if import is in the main AGENT.md file.**\n${importLine}`;

	if (fs.existsSync(sourceFile)) {
		try {
			// Ensure .taskmaster directory exists
			const taskMasterDir = path.join(targetDir, '.taskmaster');
			if (!fs.existsSync(taskMasterDir)) {
				fs.mkdirSync(taskMasterDir, { recursive: true });
			}

			// Copy Task Master instructions to .taskmaster/AGENT.md
			fs.copyFileSync(sourceFile, taskMasterAgentFile);
			log(
				'debug',
				`[Amp] Created Task Master instructions at ${taskMasterAgentFile}`
			);

			// Handle user's AGENT.md
			if (fs.existsSync(userAgentFile)) {
				// Check if import already exists
				const content = fs.readFileSync(userAgentFile, 'utf8');
				if (!content.includes(importLine)) {
					// Append import section at the end
					const updatedContent = content.trim() + '\n' + importSection + '\n';
					fs.writeFileSync(userAgentFile, updatedContent);
					log(
						'info',
						`[Amp] Added Task Master import to existing ${userAgentFile}`
					);
				} else {
					log(
						'info',
						`[Amp] Task Master import already present in ${userAgentFile}`
					);
				}
			} else {
				// Create minimal AGENT.md with the import section
				const minimalContent = `# Amp Instructions\n${importSection}\n`;
				fs.writeFileSync(userAgentFile, minimalContent);
				log('info', `[Amp] Created ${userAgentFile} with Task Master import`);
			}
		} catch (err) {
			log('error', `[Amp] Failed to set up Amp instructions: ${err.message}`);
		}
	}

	// MCP transformation will be handled in onPostConvertRulesProfile
}

function onRemoveRulesProfile(targetDir) {
	// Clean up AGENT.md import (Amp uses AGENT.md, not AGENTS.md)
	const userAgentFile = path.join(targetDir, 'AGENT.md');
	const taskMasterAgentFile = path.join(targetDir, '.taskmaster', 'AGENT.md');
	const importLine = '@./.taskmaster/AGENT.md';

	try {
		// Remove Task Master AGENT.md from .taskmaster
		if (fs.existsSync(taskMasterAgentFile)) {
			fs.rmSync(taskMasterAgentFile, { force: true });
			log('debug', `[Amp] Removed ${taskMasterAgentFile}`);
		}

		// Clean up import from user's AGENT.md
		if (fs.existsSync(userAgentFile)) {
			const content = fs.readFileSync(userAgentFile, 'utf8');
			const lines = content.split('\n');
			const filteredLines = [];
			let skipNextLines = 0;

			// Remove the Task Master section
			for (let i = 0; i < lines.length; i++) {
				if (skipNextLines > 0) {
					skipNextLines--;
					continue;
				}

				// Check if this is the start of our Task Master section
				if (lines[i].includes('## Task Master AI Instructions')) {
					// Skip this line and the next two lines (bold text and import)
					skipNextLines = 2;
					continue;
				}

				// Also remove standalone import lines (for backward compatibility)
				if (lines[i].trim() === importLine) {
					continue;
				}

				filteredLines.push(lines[i]);
			}

			// Join back and clean up excessive newlines
			let updatedContent = filteredLines
				.join('\n')
				.replace(/\n{3,}/g, '\n\n')
				.trim();

			// Check if file only contained our minimal template
			if (updatedContent === '# Amp Instructions' || updatedContent === '') {
				// File only contained our import, remove it
				fs.rmSync(userAgentFile, { force: true });
				log('debug', `[Amp] Removed empty ${userAgentFile}`);
			} else {
				// Write back without the import
				fs.writeFileSync(userAgentFile, updatedContent + '\n');
				log('debug', `[Amp] Removed Task Master import from ${userAgentFile}`);
			}
		}
	} catch (err) {
		log('error', `[Amp] Failed to remove Amp instructions: ${err.message}`);
	}

	// MCP Removal: Remove amp.mcpServers section
	const mcpConfigPath = path.join(targetDir, '.vscode', 'settings.json');

	if (!fs.existsSync(mcpConfigPath)) {
		log('debug', '[Amp] No .vscode/settings.json found to clean up');
		return;
	}

	try {
		// Read the current config
		const configContent = fs.readFileSync(mcpConfigPath, 'utf8');
		const config = JSON.parse(configContent);

		// Check if it has the amp.mcpServers section and task-master-ai server
		if (
			config['amp.mcpServers'] &&
			config['amp.mcpServers']['task-master-ai']
		) {
			// Remove task-master-ai server
			delete config['amp.mcpServers']['task-master-ai'];

			// Check if there are other MCP servers in amp.mcpServers
			const remainingServers = Object.keys(config['amp.mcpServers']);

			if (remainingServers.length === 0) {
				// No other servers, remove entire amp.mcpServers section
				delete config['amp.mcpServers'];
				log('debug', '[Amp] Removed empty amp.mcpServers section');
			}

			// Check if config is now empty
			const remainingKeys = Object.keys(config);

			if (remainingKeys.length === 0) {
				// Config is empty, remove entire file
				fs.rmSync(mcpConfigPath, { force: true });
				log('info', '[Amp] Removed empty settings.json file');

				// Check if .vscode directory is empty
				const vscodeDirPath = path.join(targetDir, '.vscode');
				if (fs.existsSync(vscodeDirPath)) {
					const remainingContents = fs.readdirSync(vscodeDirPath);
					if (remainingContents.length === 0) {
						fs.rmSync(vscodeDirPath, { recursive: true, force: true });
						log('debug', '[Amp] Removed empty .vscode directory');
					}
				}
			} else {
				// Write back the modified config
				fs.writeFileSync(
					mcpConfigPath,
					JSON.stringify(config, null, '\t') + '\n'
				);
				log(
					'info',
					'[Amp] Removed TaskMaster from settings.json, preserved other configurations'
				);
			}
		} else {
			log('debug', '[Amp] TaskMaster not found in amp.mcpServers');
		}
	} catch (error) {
		log('error', `[Amp] Failed to clean up settings.json: ${error.message}`);
	}
}

function onPostConvertRulesProfile(targetDir, assetsDir) {
	// Handle AGENT.md setup (same as onAddRulesProfile)
	onAddRulesProfile(targetDir, assetsDir);

	// Transform MCP config to Amp format
	const mcpConfigPath = path.join(targetDir, '.vscode', 'settings.json');

	if (!fs.existsSync(mcpConfigPath)) {
		log('debug', '[Amp] No .vscode/settings.json found to transform');
		return;
	}

	try {
		// Read the generated standard MCP config
		const mcpConfigContent = fs.readFileSync(mcpConfigPath, 'utf8');
		const mcpConfig = JSON.parse(mcpConfigContent);

		// Check if it's already in Amp format (has amp.mcpServers)
		if (mcpConfig['amp.mcpServers']) {
			log(
				'info',
				'[Amp] settings.json already in Amp format, skipping transformation'
			);
			return;
		}

		// Transform to Amp format
		const ampConfig = transformToAmpFormat(mcpConfig);

		// Write back the transformed config with proper formatting
		fs.writeFileSync(
			mcpConfigPath,
			JSON.stringify(ampConfig, null, '\t') + '\n'
		);

		log('info', '[Amp] Transformed settings.json to Amp format');
		log('debug', '[Amp] Renamed mcpServers to amp.mcpServers');
	} catch (error) {
		log('error', `[Amp] Failed to transform settings.json: ${error.message}`);
	}
}

// Create and export amp profile using the base factory
export const ampProfile = createProfile({
	name: 'amp',
	displayName: 'Amp',
	url: 'ampcode.com',
	docsUrl: 'ampcode.com/manual',
	profileDir: '.vscode',
	rulesDir: '.',
	mcpConfig: true,
	mcpConfigName: 'settings.json',
	includeDefaultRules: false,
	fileMap: {
		'AGENTS.md': '.taskmaster/AGENT.md'
	},
	onAdd: onAddRulesProfile,
	onRemove: onRemoveRulesProfile,
	onPostConvert: onPostConvertRulesProfile
});

// Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile };

/**
 * tools/update-subtask.js
 * Tool to append additional information to a specific subtask
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { updateSubtaskByIdDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the update-subtask tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerUpdateSubtaskTool(server) {
	server.addTool({
		name: 'update_subtask',
		description:
			'Appends timestamped information to a specific subtask without replacing existing content. If you just want to update the subtask status, use set_task_status instead.',
		parameters: z.object({
			id: z
				.string()
				.describe(
					'ID of the subtask to update in format "parentId.subtaskId" (e.g., "5.2"). Parent ID is the ID of the task that contains the subtask.'
				),
			prompt: z.string().describe('Information to add to the subtask'),
			research: z
				.boolean()
				.optional()
				.describe('Use Perplexity AI for research-backed updates'),
			file: z.string().optional().describe('Absolute path to the tasks file'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			const toolName = 'update_subtask';

			try {
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				log.info(`Updating subtask with args: ${JSON.stringify(args)}`);

				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`${toolName}: Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				const result = await updateSubtaskByIdDirect(
					{
						tasksJsonPath: tasksJsonPath,
						id: args.id,
						prompt: args.prompt,
						research: args.research,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				if (result.success) {
					log.info(`Successfully updated subtask with ID ${args.id}`);
				} else {
					log.error(
						`Failed to update subtask: ${result.error?.message || 'Unknown error'}`
					);
				}

				return handleApiResult(
					result,
					log,
					'Error updating subtask',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(
					`Critical error in ${toolName} tool execute: ${error.message}`
				);
				return createErrorResponse(
					`Internal tool error (${toolName}): ${error.message}`
				);
			}
		})
	});
}

/**
 * tools/expand-task.js
 * Tool to expand a task into subtasks
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { expandTaskDirect } from '../core/task-master-core.js';
import {
	findTasksPath,
	findComplexityReportPath
} from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the expand-task tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerExpandTaskTool(server) {
	server.addTool({
		name: 'expand_task',
		description: 'Expand a task into subtasks for detailed implementation',
		parameters: z.object({
			id: z.string().describe('ID of task to expand'),
			num: z.string().optional().describe('Number of subtasks to generate'),
			research: z
				.boolean()
				.optional()
				.default(false)
				.describe('Use research role for generation'),
			prompt: z
				.string()
				.optional()
				.describe('Additional context for subtask generation'),
			file: z
				.string()
				.optional()
				.describe(
					'Path to the tasks file relative to project root (e.g., tasks/tasks.json)'
				),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			force: z
				.boolean()
				.optional()
				.default(false)
				.describe('Force expansion even if subtasks exist'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Starting expand-task with args: ${JSON.stringify(args)}`);
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				const complexityReportPath = findComplexityReportPath(
					{ ...args, tag: resolvedTag },
					log
				);

				const result = await expandTaskDirect(
					{
						tasksJsonPath: tasksJsonPath,
						id: args.id,
						num: args.num,
						research: args.research,
						prompt: args.prompt,
						force: args.force,
						complexityReportPath,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error expanding task',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in expand-task tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * tools/remove-task.js
 * Tool to remove a task by ID
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { removeTaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the remove-task tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerRemoveTaskTool(server) {
	server.addTool({
		name: 'remove_task',
		description: 'Remove a task or subtask permanently from the tasks list',
		parameters: z.object({
			id: z
				.string()
				.describe(
					"ID of the task or subtask to remove (e.g., '5' or '5.2'). Can be comma-separated to update multiple tasks/subtasks at once."
				),
			file: z.string().optional().describe('Absolute path to the tasks file'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			confirm: z
				.boolean()
				.optional()
				.describe('Whether to skip confirmation prompt (default: false)'),
			tag: z
				.string()
				.optional()
				.describe(
					'Specify which tag context to operate on. Defaults to the current active tag.'
				)
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Removing task(s) with ID(s): ${args.id}`);

				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				log.info(`Using tasks file path: ${tasksJsonPath}`);

				const result = await removeTaskDirect(
					{
						tasksJsonPath: tasksJsonPath,
						id: args.id,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				if (result.success) {
					log.info(`Successfully removed task: ${args.id}`);
				} else {
					log.error(`Failed to remove task: ${result.error.message}`);
				}

				return handleApiResult(
					result,
					log,
					'Error removing task',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in remove-task tool: ${error.message}`);
				return createErrorResponse(`Failed to remove task: ${error.message}`);
			}
		})
	});
}

/**
 * tools/setTaskStatus.js
 * Tool to set the status of a task
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import {
	setTaskStatusDirect,
	nextTaskDirect
} from '../core/task-master-core.js';
import {
	findTasksPath,
	findComplexityReportPath
} from '../core/utils/path-utils.js';
import { TASK_STATUS_OPTIONS } from '../../../src/constants/task-status.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the setTaskStatus tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerSetTaskStatusTool(server) {
	server.addTool({
		name: 'set_task_status',
		description: 'Set the status of one or more tasks or subtasks.',
		parameters: z.object({
			id: z
				.string()
				.describe(
					"Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated to update multiple tasks/subtasks at once."
				),
			status: z
				.enum(TASK_STATUS_OPTIONS)
				.describe(
					"New status to set (e.g., 'pending', 'done', 'in-progress', 'review', 'deferred', 'cancelled'."
				),
			file: z.string().optional().describe('Absolute path to the tasks file'),
			complexityReport: z
				.string()
				.optional()
				.describe(
					'Path to the complexity report file (relative to project root or absolute)'
				),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Optional tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(
					`Setting status of task(s) ${args.id} to: ${args.status} ${
						args.tag ? `in tag: ${args.tag}` : 'in current tag'
					}`
				);
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				let complexityReportPath;
				try {
					complexityReportPath = findComplexityReportPath(
						{
							projectRoot: args.projectRoot,
							complexityReport: args.complexityReport,
							tag: resolvedTag
						},
						log
					);
				} catch (error) {
					log.error(`Error finding complexity report: ${error.message}`);
				}

				const result = await setTaskStatusDirect(
					{
						tasksJsonPath: tasksJsonPath,
						id: args.id,
						status: args.status,
						complexityReportPath,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				if (result.success) {
					log.info(
						`Successfully updated status for task(s) ${args.id} to "${args.status}": ${result.data.message}`
					);
				} else {
					log.error(
						`Failed to update task status: ${result.error?.message || 'Unknown error'}`
					);
				}

				return handleApiResult(
					result,
					log,
					'Error setting task status',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in setTaskStatus tool: ${error.message}`);
				return createErrorResponse(
					`Error setting task status: ${error.message}`
				);
			}
		})
	});
}

/**
 * tools/add-task.js
 * Tool to add a new task using AI
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { addTaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the addTask tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerAddTaskTool(server) {
	server.addTool({
		name: 'add_task',
		description: 'Add a new task using AI',
		parameters: z.object({
			prompt: z
				.string()
				.optional()
				.describe(
					'Description of the task to add (required if not using manual fields)'
				),
			title: z
				.string()
				.optional()
				.describe('Task title (for manual task creation)'),
			description: z
				.string()
				.optional()
				.describe('Task description (for manual task creation)'),
			details: z
				.string()
				.optional()
				.describe('Implementation details (for manual task creation)'),
			testStrategy: z
				.string()
				.optional()
				.describe('Test strategy (for manual task creation)'),
			dependencies: z
				.string()
				.optional()
				.describe('Comma-separated list of task IDs this task depends on'),
			priority: z
				.string()
				.optional()
				.describe('Task priority (high, medium, low)'),
			file: z
				.string()
				.optional()
				.describe('Path to the tasks file (default: tasks/tasks.json)'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on'),
			research: z
				.boolean()
				.optional()
				.describe('Whether to use research capabilities for task creation')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Starting add-task with args: ${JSON.stringify(args)}`);

				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Call the direct functionP
				const result = await addTaskDirect(
					{
						tasksJsonPath: tasksJsonPath,
						prompt: args.prompt,
						title: args.title,
						description: args.description,
						details: args.details,
						testStrategy: args.testStrategy,
						dependencies: args.dependencies,
						priority: args.priority,
						research: args.research,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error adding task',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in add-task tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

import fs from 'fs';
import path from 'path';
import { log } from '../../scripts/modules/utils.js';

// Return JSON with existing mcp.json formatting style
function formatJSONWithTabs(obj) {
	let json = JSON.stringify(obj, null, '\t');

	json = json.replace(
		/(\[\n\t+)([^[\]]+?)(\n\t+\])/g,
		(match, openBracket, content, closeBracket) => {
			// Only convert to single line if content doesn't contain nested objects/arrays
			if (!content.includes('{') && !content.includes('[')) {
				const singleLineContent = content
					.replace(/\n\t+/g, ' ')
					.replace(/\s+/g, ' ')
					.trim();
				return `[${singleLineContent}]`;
			}
			return match;
		}
	);

	return json;
}

// Structure matches project conventions (see scripts/init.js)
export function setupMCPConfiguration(projectRoot, mcpConfigPath) {
	// Handle null mcpConfigPath (e.g., for Claude/Codex profiles)
	if (!mcpConfigPath) {
		log(
			'debug',
			'[MCP Config] No mcpConfigPath provided, skipping MCP configuration setup'
		);
		return;
	}

	// Build the full path to the MCP config file
	const mcpPath = path.join(projectRoot, mcpConfigPath);
	const configDir = path.dirname(mcpPath);

	log('info', `Setting up MCP configuration at ${mcpPath}...`);

	// New MCP config to be added - references the installed package
	const newMCPServer = {
		'task-master-ai': {
			command: 'npx',
			args: ['-y', '--package=task-master-ai', 'task-master-ai'],
			env: {
				ANTHROPIC_API_KEY: 'YOUR_ANTHROPIC_API_KEY_HERE',
				PERPLEXITY_API_KEY: 'YOUR_PERPLEXITY_API_KEY_HERE',
				OPENAI_API_KEY: 'YOUR_OPENAI_KEY_HERE',
				GOOGLE_API_KEY: 'YOUR_GOOGLE_KEY_HERE',
				XAI_API_KEY: 'YOUR_XAI_KEY_HERE',
				OPENROUTER_API_KEY: 'YOUR_OPENROUTER_KEY_HERE',
				MISTRAL_API_KEY: 'YOUR_MISTRAL_KEY_HERE',
				AZURE_OPENAI_API_KEY: 'YOUR_AZURE_KEY_HERE',
				OLLAMA_API_KEY: 'YOUR_OLLAMA_API_KEY_HERE'
			}
		}
	};

	// Create config directory if it doesn't exist
	if (!fs.existsSync(configDir)) {
		fs.mkdirSync(configDir, { recursive: true });
	}

	if (fs.existsSync(mcpPath)) {
		log(
			'info',
			'MCP configuration file already exists, checking for existing task-master-ai...'
		);
		try {
			// Read existing config
			const mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
			// Initialize mcpServers if it doesn't exist
			if (!mcpConfig.mcpServers) {
				mcpConfig.mcpServers = {};
			}
			// Check if any existing server configuration already has task-master-ai in its args
			const hasMCPString = Object.values(mcpConfig.mcpServers).some(
				(server) =>
					server.args &&
					Array.isArray(server.args) &&
					server.args.some(
						(arg) => typeof arg === 'string' && arg.includes('task-master-ai')
					)
			);
			if (hasMCPString) {
				log(
					'info',
					'Found existing task-master-ai MCP configuration in mcp.json, leaving untouched'
				);
				return; // Exit early, don't modify the existing configuration
			}
			// Add the task-master-ai server if it doesn't exist
			if (!mcpConfig.mcpServers['task-master-ai']) {
				mcpConfig.mcpServers['task-master-ai'] = newMCPServer['task-master-ai'];
				log(
					'info',
					'Added task-master-ai server to existing MCP configuration'
				);
			} else {
				log('info', 'task-master-ai server already configured in mcp.json');
			}
			// Write the updated configuration
			fs.writeFileSync(mcpPath, formatJSONWithTabs(mcpConfig) + '\n');
			log('success', 'Updated MCP configuration file');
		} catch (error) {
			log('error', `Failed to update MCP configuration: ${error.message}`);
			// Create a backup before potentially modifying
			const backupPath = `${mcpPath}.backup-${Date.now()}`;
			if (fs.existsSync(mcpPath)) {
				fs.copyFileSync(mcpPath, backupPath);
				log('info', `Created backup of existing mcp.json at ${backupPath}`);
			}
			// Create new configuration
			const newMCPConfig = {
				mcpServers: newMCPServer
			};
			fs.writeFileSync(mcpPath, formatJSONWithTabs(newMCPConfig) + '\n');
			log(
				'warn',
				'Created new MCP configuration file (backup of original file was created if it existed)'
			);
		}
	} else {
		// If mcp.json doesn't exist, create it
		const newMCPConfig = {
			mcpServers: newMCPServer
		};
		fs.writeFileSync(mcpPath, formatJSONWithTabs(newMCPConfig) + '\n');
		log('success', `Created MCP configuration file at ${mcpPath}`);
	}

	// Add note to console about MCP integration
	log('info', 'MCP server will use the installed task-master-ai package');
}

/**
 * Remove Task Master MCP server configuration from an existing mcp.json file
 * Only removes Task Master entries, preserving other MCP servers
 * @param {string} projectRoot - Target project directory
 * @param {string} mcpConfigPath - Relative path to MCP config file (e.g., '.cursor/mcp.json')
 * @returns {Object} Result object with success status and details
 */
export function removeTaskMasterMCPConfiguration(projectRoot, mcpConfigPath) {
	// Handle null mcpConfigPath (e.g., for Claude/Codex profiles)
	if (!mcpConfigPath) {
		return {
			success: true,
			removed: false,
			deleted: false,
			error: null,
			hasOtherServers: false
		};
	}

	const mcpPath = path.join(projectRoot, mcpConfigPath);

	let result = {
		success: false,
		removed: false,
		deleted: false,
		error: null,
		hasOtherServers: false
	};

	if (!fs.existsSync(mcpPath)) {
		result.success = true;
		result.removed = false;
		log('debug', `[MCP Config] MCP config file does not exist: ${mcpPath}`);
		return result;
	}

	try {
		// Read existing config
		const mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));

		if (!mcpConfig.mcpServers) {
			result.success = true;
			result.removed = false;
			log('debug', `[MCP Config] No mcpServers section found in: ${mcpPath}`);
			return result;
		}

		// Check if Task Master is configured
		const hasTaskMaster =
			mcpConfig.mcpServers['task-master-ai'] ||
			Object.values(mcpConfig.mcpServers).some(
				(server) =>
					server.args &&
					Array.isArray(server.args) &&
					server.args.some(
						(arg) => typeof arg === 'string' && arg.includes('task-master-ai')
					)
			);

		if (!hasTaskMaster) {
			result.success = true;
			result.removed = false;
			log(
				'debug',
				`[MCP Config] Task Master not found in MCP config: ${mcpPath}`
			);
			return result;
		}

		// Remove task-master-ai server
		delete mcpConfig.mcpServers['task-master-ai'];

		// Also remove any servers that have task-master-ai in their args
		Object.keys(mcpConfig.mcpServers).forEach((serverName) => {
			const server = mcpConfig.mcpServers[serverName];
			if (
				server.args &&
				Array.isArray(server.args) &&
				server.args.some(
					(arg) => typeof arg === 'string' && arg.includes('task-master-ai')
				)
			) {
				delete mcpConfig.mcpServers[serverName];
				log(
					'debug',
					`[MCP Config] Removed server '${serverName}' containing task-master-ai`
				);
			}
		});

		// Check if there are other MCP servers remaining
		const remainingServers = Object.keys(mcpConfig.mcpServers);
		result.hasOtherServers = remainingServers.length > 0;

		if (result.hasOtherServers) {
			// Write back the modified config with remaining servers
			fs.writeFileSync(mcpPath, formatJSONWithTabs(mcpConfig) + '\n');
			result.success = true;
			result.removed = true;
			result.deleted = false;
			log(
				'info',
				`[MCP Config] Removed Task Master from MCP config, preserving other servers: ${remainingServers.join(', ')}`
			);
		} else {
			// No other servers, delete the entire file
			fs.rmSync(mcpPath, { force: true });
			result.success = true;
			result.removed = true;
			result.deleted = true;
			log(
				'info',
				`[MCP Config] Removed MCP config file (no other servers remaining): ${mcpPath}`
			);
		}
	} catch (error) {
		result.error = error.message;
		log(
			'error',
			`[MCP Config] Failed to remove Task Master from MCP config: ${error.message}`
		);
	}

	return result;
}

/**
 * update-subtask-by-id.js
 * Direct function implementation for appending information to a specific subtask
 */

import { updateSubtaskById } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode,
	isSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for updateSubtaskById with error handling.
 *
 * @param {Object} args - Command arguments containing id, prompt, useResearch, tasksJsonPath, and projectRoot.
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} args.id - Subtask ID in format "parent.sub".
 * @param {string} args.prompt - Information to append to the subtask.
 * @param {boolean} [args.research] - Whether to use research role.
 * @param {string} [args.projectRoot] - Project root path.
 * @param {string} [args.tag] - Tag for the task (optional)
 * @param {Object} log - Logger object.
 * @param {Object} context - Context object containing session data.
 * @returns {Promise<Object>} - Result object with success status and data/error information.
 */
export async function updateSubtaskByIdDirect(args, log, context = {}) {
	const { session } = context;
	// Destructure expected args, including projectRoot
	const { tasksJsonPath, id, prompt, research, projectRoot, tag } = args;

	const logWrapper = createLogWrapper(log);

	try {
		logWrapper.info(
			`Updating subtask by ID via direct function. ID: ${id}, ProjectRoot: ${projectRoot}`
		);

		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			const errorMessage = 'tasksJsonPath is required but was not provided.';
			logWrapper.error(errorMessage);
			return {
				success: false,
				error: { code: 'MISSING_ARGUMENT', message: errorMessage }
			};
		}

		// Basic validation for ID format (e.g., '5.2')
		if (!id || typeof id !== 'string' || !id.includes('.')) {
			const errorMessage =
				'Invalid subtask ID format. Must be in format "parentId.subtaskId" (e.g., "5.2").';
			logWrapper.error(errorMessage);
			return {
				success: false,
				error: { code: 'INVALID_SUBTASK_ID', message: errorMessage }
			};
		}

		if (!prompt) {
			const errorMessage =
				'No prompt specified. Please provide the information to append.';
			logWrapper.error(errorMessage);
			return {
				success: false,
				error: { code: 'MISSING_PROMPT', message: errorMessage }
			};
		}

		// Validate subtask ID format
		const subtaskId = id;
		if (typeof subtaskId !== 'string' && typeof subtaskId !== 'number') {
			const errorMessage = `Invalid subtask ID type: ${typeof subtaskId}. Subtask ID must be a string or number.`;
			log.error(errorMessage);
			return {
				success: false,
				error: { code: 'INVALID_SUBTASK_ID_TYPE', message: errorMessage }
			};
		}

		const subtaskIdStr = String(subtaskId);
		if (!subtaskIdStr.includes('.')) {
			const errorMessage = `Invalid subtask ID format: ${subtaskIdStr}. Subtask ID must be in format "parentId.subtaskId" (e.g., "5.2").`;
			log.error(errorMessage);
			return {
				success: false,
				error: { code: 'INVALID_SUBTASK_ID_FORMAT', message: errorMessage }
			};
		}

		// Use the provided path
		const tasksPath = tasksJsonPath;
		const useResearch = research === true;

		log.info(
			`Updating subtask with ID ${subtaskIdStr} with prompt "${prompt}" and research: ${useResearch}`
		);

		const wasSilent = isSilentMode();
		if (!wasSilent) {
			enableSilentMode();
		}

		try {
			// Execute core updateSubtaskById function
			const coreResult = await updateSubtaskById(
				tasksPath,
				subtaskIdStr,
				prompt,
				useResearch,
				{
					mcpLog: logWrapper,
					session,
					projectRoot,
					tag,
					commandName: 'update-subtask',
					outputType: 'mcp'
				},
				'json'
			);

			if (!coreResult || coreResult.updatedSubtask === null) {
				const message = `Subtask ${id} or its parent task not found.`;
				logWrapper.error(message);
				return {
					success: false,
					error: { code: 'SUBTASK_NOT_FOUND', message: message }
				};
			}

			// Subtask updated successfully
			const successMessage = `Successfully updated subtask with ID ${subtaskIdStr}`;
			logWrapper.success(successMessage);
			return {
				success: true,
				data: {
					message: `Successfully updated subtask with ID ${subtaskIdStr}`,
					subtaskId: subtaskIdStr,
					parentId: subtaskIdStr.split('.')[0],
					subtask: coreResult.updatedSubtask,
					tasksPath,
					useResearch,
					telemetryData: coreResult.telemetryData,
					tagInfo: coreResult.tagInfo
				}
			};
		} catch (error) {
			logWrapper.error(`Error updating subtask by ID: ${error.message}`);
			return {
				success: false,
				error: {
					code: 'UPDATE_SUBTASK_CORE_ERROR',
					message: error.message || 'Unknown error updating subtask'
				}
			};
		} finally {
			if (!wasSilent && isSilentMode()) {
				disableSilentMode();
			}
		}
	} catch (error) {
		logWrapper.error(
			`Setup error in updateSubtaskByIdDirect: ${error.message}`
		);
		if (isSilentMode()) disableSilentMode();
		return {
			success: false,
			error: {
				code: 'DIRECT_FUNCTION_SETUP_ERROR',
				message: error.message || 'Unknown setup error'
			}
		};
	}
}

import fs from 'fs';
import {
	createTag,
	listTags
} from './scripts/modules/task-manager/tag-management.js';

console.log('=== Testing Tag Management with Clean File ===');

// Create a clean test tasks.json file
const testTasksPath = './test-tasks.json';
const cleanData = {
	master: {
		tasks: [
			{ id: 1, title: 'Test Task 1', status: 'pending' },
			{ id: 2, title: 'Test Task 2', status: 'done' }
		],
		metadata: {
			created: new Date().toISOString(),
			description: 'Master tag'
		}
	}
};

// Write clean test file
fs.writeFileSync(testTasksPath, JSON.stringify(cleanData, null, 2));
console.log('Created clean test file');

try {
	// Test creating a new tag
	console.log('\n--- Testing createTag ---');
	await createTag(
		testTasksPath,
		'test-branch',
		{ copyFromCurrent: true, description: 'Test branch' },
		{ projectRoot: process.cwd() },
		'json'
	);

	// Read the file and check for corruption
	const resultData = JSON.parse(fs.readFileSync(testTasksPath, 'utf8'));
	console.log('Keys in result file:', Object.keys(resultData));
	console.log('Has _rawTaggedData in file:', !!resultData._rawTaggedData);

	if (resultData._rawTaggedData) {
		console.log('❌ CORRUPTION DETECTED: _rawTaggedData found in file!');
	} else {
		console.log('✅ SUCCESS: No _rawTaggedData corruption in file');
	}

	// Test listing tags
	console.log('\n--- Testing listTags ---');
	const tagList = await listTags(
		testTasksPath,
		{},
		{ projectRoot: process.cwd() },
		'json'
	);
	console.log(
		'Found tags:',
		tagList.tags.map((t) => t.name)
	);
} catch (error) {
	console.error('Error during test:', error.message);
} finally {
	// Clean up test file
	if (fs.existsSync(testTasksPath)) {
		fs.unlinkSync(testTasksPath);
		console.log('\nCleaned up test file');
	}
}

/**
 * tools/clear-subtasks.js
 * Tool for clearing subtasks from parent tasks
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { clearSubtasksDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the clearSubtasks tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerClearSubtasksTool(server) {
	server.addTool({
		name: 'clear_subtasks',
		description: 'Clear subtasks from specified tasks',
		parameters: z
			.object({
				id: z
					.string()
					.optional()
					.describe('Task IDs (comma-separated) to clear subtasks from'),
				all: z.boolean().optional().describe('Clear subtasks from all tasks'),
				file: z
					.string()
					.optional()
					.describe(
						'Absolute path to the tasks file (default: tasks/tasks.json)'
					),
				projectRoot: z
					.string()
					.describe('The directory of the project. Must be an absolute path.'),
				tag: z.string().optional().describe('Tag context to operate on')
			})
			.refine((data) => data.id || data.all, {
				message: "Either 'id' or 'all' parameter must be provided",
				path: ['id', 'all']
			}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`);

				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				const result = await clearSubtasksDirect(
					{
						tasksJsonPath: tasksJsonPath,
						id: args.id,
						all: args.all,

						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				if (result.success) {
					log.info(`Subtasks cleared successfully: ${result.data.message}`);
				} else {
					log.error(`Failed to clear subtasks: ${result.error.message}`);
				}

				return handleApiResult(
					result,
					log,
					'Error clearing subtasks',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in clearSubtasks tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * next-task.js
 * Direct function implementation for finding the next task to work on
 */

import { findNextTask } from '../../../../scripts/modules/task-manager.js';
import {
	readJSON,
	readComplexityReport
} from '../../../../scripts/modules/utils.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';

/**
 * Direct function wrapper for finding the next task to work on with error handling and caching.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} args.reportPath - Path to the report file.
 * @param {string} args.projectRoot - Project root path (for MCP/env fallback)
 * @param {string} args.tag - Tag for the task (optional)
 * @param {Object} log - Logger object
 * @returns {Promise<Object>} - Next task result { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function nextTaskDirect(args, log, context = {}) {
	// Destructure expected args
	const { tasksJsonPath, reportPath, projectRoot, tag } = args;
	const { session } = context;

	if (!tasksJsonPath) {
		log.error('nextTaskDirect called without tasksJsonPath');
		return {
			success: false,
			error: {
				code: 'MISSING_ARGUMENT',
				message: 'tasksJsonPath is required'
			}
		};
	}

	// Define the action function to be executed on cache miss
	const coreNextTaskAction = async () => {
		try {
			// Enable silent mode to prevent console logs from interfering with JSON response
			enableSilentMode();

			log.info(`Finding next task from ${tasksJsonPath}`);

			// Read tasks data using the provided path
			const data = readJSON(tasksJsonPath, projectRoot, tag);
			if (!data || !data.tasks) {
				disableSilentMode(); // Disable before return
				return {
					success: false,
					error: {
						code: 'INVALID_TASKS_FILE',
						message: `No valid tasks found in ${tasksJsonPath}`
					}
				};
			}

			// Read the complexity report
			const complexityReport = readComplexityReport(reportPath);

			// Find the next task
			const nextTask = findNextTask(data.tasks, complexityReport);

			if (!nextTask) {
				log.info(
					'No eligible next task found. All tasks are either completed or have unsatisfied dependencies'
				);
				return {
					success: true,
					data: {
						message:
							'No eligible next task found. All tasks are either completed or have unsatisfied dependencies',
						nextTask: null
					}
				};
			}

			// Check if it's a subtask
			const isSubtask =
				typeof nextTask.id === 'string' && nextTask.id.includes('.');

			const taskOrSubtask = isSubtask ? 'subtask' : 'task';

			const additionalAdvice = isSubtask
				? 'Subtasks can be updated with timestamped details as you implement them. This is useful for tracking progress, marking milestones and insights (of successful or successive falures in attempting to implement the subtask). Research can be used when updating the subtask to collect up-to-date information, and can be helpful to solve a repeating problem the agent is unable to solve. It is a good idea to get-task the parent task to collect the overall context of the task, and to get-task the subtask to collect the specific details of the subtask.'
				: 'Tasks can be updated to reflect a change in the direction of the task, or to reformulate the task per your prompt. Research can be used when updating the task to collect up-to-date information. It is best to update subtasks as you work on them, and to update the task for more high-level changes that may affect pending subtasks or the general direction of the task.';

			// Restore normal logging
			disableSilentMode();

			// Return the next task data with the full tasks array for reference
			log.info(
				`Successfully found next task ${nextTask.id}: ${nextTask.title}. Is subtask: ${isSubtask}`
			);
			return {
				success: true,
				data: {
					nextTask,
					isSubtask,
					nextSteps: `When ready to work on the ${taskOrSubtask}, use set-status to set the status to "in progress" ${additionalAdvice}`
				}
			};
		} catch (error) {
			// Make sure to restore normal logging even if there's an error
			disableSilentMode();

			log.error(`Error finding next task: ${error.message}`);
			return {
				success: false,
				error: {
					code: 'CORE_FUNCTION_ERROR',
					message: error.message || 'Failed to find next task'
				}
			};
		}
	};

	// Use the caching utility
	try {
		const result = await coreNextTaskAction();
		log.info('nextTaskDirect completed.');
		return result;
	} catch (error) {
		log.error(`Unexpected error during nextTask: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'UNEXPECTED_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * parse-prd.js
 * Direct function implementation for parsing PRD documents
 */

import path from 'path';
import fs from 'fs';
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode,
	isSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
import { getDefaultNumTasks } from '../../../../scripts/modules/config-manager.js';
import { resolvePrdPath, resolveProjectPath } from '../utils/path-utils.js';
import { TASKMASTER_TASKS_FILE } from '../../../../src/constants/paths.js';

/**
 * Direct function wrapper for parsing PRD documents and generating tasks.
 *
 * @param {Object} args - Command arguments containing projectRoot, input, output, numTasks options.
 * @param {string} args.input - Path to the input PRD file.
 * @param {string} args.output - Path to the output directory.
 * @param {string} args.numTasks - Number of tasks to generate.
 * @param {boolean} args.force - Whether to force parsing.
 * @param {boolean} args.append - Whether to append to the output file.
 * @param {boolean} args.research - Whether to use research mode.
 * @param {string} args.tag - Tag context for organizing tasks into separate task lists.
 * @param {Object} log - Logger object.
 * @param {Object} context - Context object containing session data.
 * @returns {Promise<Object>} - Result object with success status and data/error information.
 */
export async function parsePRDDirect(args, log, context = {}) {
	const { session } = context;
	// Extract projectRoot from args
	const {
		input: inputArg,
		output: outputArg,
		numTasks: numTasksArg,
		force,
		append,
		research,
		projectRoot,
		tag
	} = args;

	// Create the standard logger wrapper
	const logWrapper = createLogWrapper(log);

	// --- Input Validation and Path Resolution ---
	if (!projectRoot) {
		logWrapper.error('parsePRDDirect requires a projectRoot argument.');
		return {
			success: false,
			error: {
				code: 'MISSING_ARGUMENT',
				message: 'projectRoot is required.'
			}
		};
	}

	// Resolve input path using path utilities
	let inputPath;
	if (inputArg) {
		try {
			inputPath = resolvePrdPath({ input: inputArg, projectRoot }, session);
		} catch (error) {
			logWrapper.error(`Error resolving PRD path: ${error.message}`);
			return {
				success: false,
				error: { code: 'FILE_NOT_FOUND', message: error.message }
			};
		}
	} else {
		logWrapper.error('parsePRDDirect called without input path');
		return {
			success: false,
			error: { code: 'MISSING_ARGUMENT', message: 'Input path is required' }
		};
	}

	// Resolve output path - use new path utilities for default
	const outputPath = outputArg
		? path.isAbsolute(outputArg)
			? outputArg
			: path.resolve(projectRoot, outputArg)
		: resolveProjectPath(TASKMASTER_TASKS_FILE, args) ||
			path.resolve(projectRoot, TASKMASTER_TASKS_FILE);

	// Check if input file exists
	if (!fs.existsSync(inputPath)) {
		const errorMsg = `Input PRD file not found at resolved path: ${inputPath}`;
		logWrapper.error(errorMsg);
		return {
			success: false,
			error: { code: 'FILE_NOT_FOUND', message: errorMsg }
		};
	}

	const outputDir = path.dirname(outputPath);
	try {
		if (!fs.existsSync(outputDir)) {
			logWrapper.info(`Creating output directory: ${outputDir}`);
			fs.mkdirSync(outputDir, { recursive: true });
		}
	} catch (error) {
		const errorMsg = `Failed to create output directory ${outputDir}: ${error.message}`;
		logWrapper.error(errorMsg);
		return {
			success: false,
			error: { code: 'DIRECTORY_CREATE_FAILED', message: errorMsg }
		};
	}

	let numTasks = getDefaultNumTasks(projectRoot);
	if (numTasksArg) {
		numTasks =
			typeof numTasksArg === 'string' ? parseInt(numTasksArg, 10) : numTasksArg;
		if (Number.isNaN(numTasks) || numTasks < 0) {
			// Ensure positive number
			numTasks = getDefaultNumTasks(projectRoot); // Fallback to default if parsing fails or invalid
			logWrapper.warn(
				`Invalid numTasks value: ${numTasksArg}. Using default: ${numTasks}`
			);
		}
	}

	if (append) {
		logWrapper.info('Append mode enabled.');
		if (force) {
			logWrapper.warn(
				'Both --force and --append flags were provided. --force takes precedence; append mode will be ignored.'
			);
		}
	}

	if (research) {
		logWrapper.info(
			'Research mode enabled. Using Perplexity AI for enhanced PRD analysis.'
		);
	}

	logWrapper.info(
		`Parsing PRD via direct function. Input: ${inputPath}, Output: ${outputPath}, NumTasks: ${numTasks}, Force: ${force}, Append: ${append}, Research: ${research}, ProjectRoot: ${projectRoot}`
	);

	const wasSilent = isSilentMode();
	if (!wasSilent) {
		enableSilentMode();
	}

	try {
		// Call the core parsePRD function
		const result = await parsePRD(
			inputPath,
			outputPath,
			numTasks,
			{
				session,
				mcpLog: logWrapper,
				projectRoot,
				tag,
				force,
				append,
				research,
				commandName: 'parse-prd',
				outputType: 'mcp'
			},
			'json'
		);

		// Adjust check for the new return structure
		if (result && result.success) {
			const successMsg = `Successfully parsed PRD and generated tasks in ${result.tasksPath}`;
			logWrapper.success(successMsg);
			return {
				success: true,
				data: {
					message: successMsg,
					outputPath: result.tasksPath,
					telemetryData: result.telemetryData,
					tagInfo: result.tagInfo
				}
			};
		} else {
			// Handle case where core function didn't return expected success structure
			logWrapper.error(
				'Core parsePRD function did not return a successful structure.'
			);
			return {
				success: false,
				error: {
					code: 'CORE_FUNCTION_ERROR',
					message:
						result?.message ||
						'Core function failed to parse PRD or returned unexpected result.'
				}
			};
		}
	} catch (error) {
		logWrapper.error(`Error executing core parsePRD: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'PARSE_PRD_CORE_ERROR',
				message: error.message || 'Unknown error parsing PRD'
			}
		};
	} finally {
		if (!wasSilent && isSilentMode()) {
			disableSilentMode();
		}
	}
}

/**
 * update-tasks.js
 * Direct function implementation for updating tasks based on new context
 */

import path from 'path';
import { updateTasks } from '../../../../scripts/modules/task-manager.js';
import { createLogWrapper } from '../../tools/utils.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';

/**
 * Direct function wrapper for updating tasks based on new context.
 *
 * @param {Object} args - Command arguments containing projectRoot, from, prompt, research options.
 * @param {string} args.from - The ID of the task to update.
 * @param {string} args.prompt - The prompt to update the task with.
 * @param {boolean} args.research - Whether to use research mode.
 * @param {string} args.tasksJsonPath - Path to the tasks.json file.
 * @param {string} args.projectRoot - Project root path (for MCP/env fallback)
 * @param {string} args.tag - Tag for the task (optional)
 * @param {Object} log - Logger object.
 * @param {Object} context - Context object containing session data.
 * @returns {Promise<Object>} - Result object with success status and data/error information.
 */
export async function updateTasksDirect(args, log, context = {}) {
	const { session } = context;
	const { from, prompt, research, tasksJsonPath, projectRoot, tag } = args;

	// Create the standard logger wrapper
	const logWrapper = createLogWrapper(log);

	// --- Input Validation ---
	if (!projectRoot) {
		logWrapper.error('updateTasksDirect requires a projectRoot argument.');
		return {
			success: false,
			error: {
				code: 'MISSING_ARGUMENT',
				message: 'projectRoot is required.'
			}
		};
	}

	if (!from) {
		logWrapper.error('updateTasksDirect called without from ID');
		return {
			success: false,
			error: {
				code: 'MISSING_ARGUMENT',
				message: 'Starting task ID (from) is required'
			}
		};
	}

	if (!prompt) {
		logWrapper.error('updateTasksDirect called without prompt');
		return {
			success: false,
			error: {
				code: 'MISSING_ARGUMENT',
				message: 'Update prompt is required'
			}
		};
	}

	logWrapper.info(
		`Updating tasks via direct function. From: ${from}, Research: ${research}, File: ${tasksJsonPath}, ProjectRoot: ${projectRoot}`
	);

	enableSilentMode(); // Enable silent mode
	try {
		// Call the core updateTasks function
		const result = await updateTasks(
			tasksJsonPath,
			from,
			prompt,
			research,
			{
				session,
				mcpLog: logWrapper,
				projectRoot,
				tag
			},
			'json'
		);

		if (result && result.success && Array.isArray(result.updatedTasks)) {
			logWrapper.success(
				`Successfully updated ${result.updatedTasks.length} tasks.`
			);
			return {
				success: true,
				data: {
					message: `Successfully updated ${result.updatedTasks.length} tasks.`,
					tasksPath: tasksJsonPath,
					updatedCount: result.updatedTasks.length,
					telemetryData: result.telemetryData,
					tagInfo: result.tagInfo
				}
			};
		} else {
			// Handle case where core function didn't return expected success structure
			logWrapper.error(
				'Core updateTasks function did not return a successful structure.'
			);
			return {
				success: false,
				error: {
					code: 'CORE_FUNCTION_ERROR',
					message:
						result?.message ||
						'Core function failed to update tasks or returned unexpected result.'
				}
			};
		}
	} catch (error) {
		logWrapper.error(`Error executing core updateTasks: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'UPDATE_TASKS_CORE_ERROR',
				message: error.message || 'Unknown error updating tasks'
			}
		};
	} finally {
		disableSilentMode(); // Ensure silent mode is disabled
	}
}

/**
 * tools/get-task.js
 * Tool to get task details by ID
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { showTaskDirect } from '../core/task-master-core.js';
import {
	findTasksPath,
	findComplexityReportPath
} from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Custom processor function that removes allTasks from the response
 * @param {Object} data - The data returned from showTaskDirect
 * @returns {Object} - The processed data with allTasks removed
 */
function processTaskResponse(data) {
	if (!data) return data;

	// If we have the expected structure with task and allTasks
	if (typeof data === 'object' && data !== null && data.id && data.title) {
		// If the data itself looks like the task object, return it
		return data;
	} else if (data.task) {
		return data.task;
	}

	// If structure is unexpected, return as is
	return data;
}

/**
 * Register the get-task tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerShowTaskTool(server) {
	server.addTool({
		name: 'get_task',
		description: 'Get detailed information about a specific task',
		parameters: z.object({
			id: z
				.string()
				.describe(
					'Task ID(s) to get (can be comma-separated for multiple tasks)'
				),
			status: z
				.string()
				.optional()
				.describe("Filter subtasks by status (e.g., 'pending', 'done')"),
			file: z
				.string()
				.optional()
				.describe('Path to the tasks file relative to project root'),
			complexityReport: z
				.string()
				.optional()
				.describe(
					'Path to the complexity report file (relative to project root or absolute)'
				),
			projectRoot: z
				.string()
				.describe(
					'Absolute path to the project root directory (Optional, usually from session)'
				),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			const { id, file, status, projectRoot } = args;

			try {
				log.info(
					`Getting task details for ID: ${id}${status ? ` (filtering subtasks by status: ${status})` : ''} in root: ${projectRoot}`
				);
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});

				// Resolve the path to tasks.json using the NORMALIZED projectRoot from args
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: projectRoot, file: file },
						log
					);
					log.info(`Resolved tasks path: ${tasksJsonPath}`);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Call the direct function, passing the normalized projectRoot
				// Resolve the path to complexity report
				let complexityReportPath;
				try {
					complexityReportPath = findComplexityReportPath(
						{
							projectRoot: projectRoot,
							complexityReport: args.complexityReport,
							tag: resolvedTag
						},
						log
					);
				} catch (error) {
					log.error(`Error finding complexity report: ${error.message}`);
				}
				const result = await showTaskDirect(
					{
						tasksJsonPath: tasksJsonPath,
						reportPath: complexityReportPath,
						// Pass other relevant args
						id: id,
						status: status,
						projectRoot: projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				if (result.success) {
					log.info(`Successfully retrieved task details for ID: ${args.id}`);
				} else {
					log.error(`Failed to get task: ${result.error.message}`);
				}

				// Use our custom processor function
				return handleApiResult(
					result,
					log,
					'Error retrieving task details',
					processTaskResponse,
					projectRoot
				);
			} catch (error) {
				log.error(`Error in get-task tool: ${error.message}\n${error.stack}`);
				return createErrorResponse(`Failed to get task: ${error.message}`);
			}
		})
	});
}

/**
 * add-dependency.js
 * Direct function implementation for adding a dependency to a task
 */

import { addDependency } from '../../../../scripts/modules/dependency-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';

/**
 * Direct function wrapper for addDependency with error handling.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string|number} args.id - Task ID to add dependency to
 * @param {string|number} args.dependsOn - Task ID that will become a dependency
 * @param {string} args.tag - Tag for the task (optional)
 * @param {string} args.projectRoot - Project root path (for MCP/env fallback)
 * @param {Object} log - Logger object
 * @returns {Promise<Object>} - Result object with success status and data/error information
 */
export async function addDependencyDirect(args, log) {
	// Destructure expected args
	const { tasksJsonPath, id, dependsOn, tag, projectRoot } = args;
	try {
		log.info(`Adding dependency with args: ${JSON.stringify(args)}`);

		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('addDependencyDirect called without tasksJsonPath');
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Validate required parameters
		if (!id) {
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message: 'Task ID (id) is required'
				}
			};
		}

		if (!dependsOn) {
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message: 'Dependency ID (dependsOn) is required'
				}
			};
		}

		// Use provided path
		const tasksPath = tasksJsonPath;

		// Format IDs for the core function
		const taskId =
			id && id.includes && id.includes('.') ? id : parseInt(id, 10);
		const dependencyId =
			dependsOn && dependsOn.includes && dependsOn.includes('.')
				? dependsOn
				: parseInt(dependsOn, 10);

		log.info(
			`Adding dependency: task ${taskId} will depend on ${dependencyId}`
		);

		// Enable silent mode to prevent console logs from interfering with JSON response
		enableSilentMode();

		// Create context object
		const context = { projectRoot, tag };

		// Call the core function using the provided path
		await addDependency(tasksPath, taskId, dependencyId, context);

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				message: `Successfully added dependency: Task ${taskId} now depends on ${dependencyId}`,
				taskId: taskId,
				dependencyId: dependencyId
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in addDependencyDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'CORE_FUNCTION_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * models.js
 * Direct function for managing AI model configurations via MCP
 */

import {
	getModelConfiguration,
	getAvailableModelsList,
	setModel
} from '../../../../scripts/modules/task-manager/models.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
import { CUSTOM_PROVIDERS_ARRAY } from '../../../../src/constants/providers.js';

// Define supported roles for model setting
const MODEL_ROLES = ['main', 'research', 'fallback'];

/**
 * Determine provider hint from custom provider flags
 * @param {Object} args - Arguments containing provider flags
 * @returns {string|undefined} Provider hint or undefined if no custom provider flag is set
 */
function getProviderHint(args) {
	return CUSTOM_PROVIDERS_ARRAY.find((provider) => args[provider]);
}

/**
 * Handle setting models for different roles
 * @param {Object} args - Arguments containing role-specific model IDs
 * @param {Object} context - Context object with session, mcpLog, projectRoot
 * @returns {Object|null} Result if a model was set, null if no model setting was requested
 */
async function handleModelSetting(args, context) {
	for (const role of MODEL_ROLES) {
		const roleKey = `set${role.charAt(0).toUpperCase() + role.slice(1)}`; // setMain, setResearch, setFallback

		if (args[roleKey]) {
			const providerHint = getProviderHint(args);

			return await setModel(role, args[roleKey], {
				...context,
				providerHint
			});
		}
	}
	return null; // No model setting was requested
}

/**
 * Get or update model configuration
 * @param {Object} args - Arguments passed by the MCP tool
 * @param {Object} log - MCP logger
 * @param {Object} context - MCP context (contains session)
 * @returns {Object} Result object with success, data/error fields
 */
export async function modelsDirect(args, log, context = {}) {
	const { session } = context;
	const { projectRoot } = args; // Extract projectRoot from args

	// Create a logger wrapper that the core functions can use
	const mcpLog = createLogWrapper(log);

	log.info(`Executing models_direct with args: ${JSON.stringify(args)}`);
	log.info(`Using project root: ${projectRoot}`);

	// Validate flags: only one custom provider flag can be used simultaneously
	const customProviderFlags = CUSTOM_PROVIDERS_ARRAY.filter(
		(provider) => args[provider]
	);

	if (customProviderFlags.length > 1) {
		log.error(
			'Error: Cannot use multiple custom provider flags simultaneously.'
		);
		return {
			success: false,
			error: {
				code: 'INVALID_ARGS',
				message:
					'Cannot use multiple custom provider flags simultaneously. Choose only one: openrouter, ollama, bedrock, azure, or vertex.'
			}
		};
	}

	try {
		enableSilentMode();

		try {
			// Check for the listAvailableModels flag
			if (args.listAvailableModels === true) {
				return await getAvailableModelsList({
					session,
					mcpLog,
					projectRoot
				});
			}

			// Handle setting any model role using unified function
			const modelContext = { session, mcpLog, projectRoot };
			const modelSetResult = await handleModelSetting(args, modelContext);
			if (modelSetResult) {
				return modelSetResult;
			}

			// Default action: get current configuration
			return await getModelConfiguration({
				session,
				mcpLog,
				projectRoot
			});
		} finally {
			disableSilentMode();
		}
	} catch (error) {
		log.error(`Error in models_direct: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'DIRECT_FUNCTION_ERROR',
				message: error.message,
				details: error.stack
			}
		};
	}
}

/**
 * Direct function wrapper for removeDependency
 */

import { removeDependency } from '../../../../scripts/modules/dependency-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';

/**
 * Remove a dependency from a task
 * @param {Object} args - Function arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string|number} args.id - Task ID to remove dependency from
 * @param {string|number} args.dependsOn - Task ID to remove as a dependency
 * @param {string} args.projectRoot - Project root path (for MCP/env fallback)
 * @param {string} args.tag - Tag for the task (optional)
 * @param {Object} log - Logger object
 * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
 */
export async function removeDependencyDirect(args, log) {
	// Destructure expected args
	const { tasksJsonPath, id, dependsOn, projectRoot, tag } = args;
	try {
		log.info(`Removing dependency with args: ${JSON.stringify(args)}`);

		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('removeDependencyDirect called without tasksJsonPath');
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Validate required parameters
		if (!id) {
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message: 'Task ID (id) is required'
				}
			};
		}

		if (!dependsOn) {
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message: 'Dependency ID (dependsOn) is required'
				}
			};
		}

		// Use provided path
		const tasksPath = tasksJsonPath;

		// Format IDs for the core function
		const taskId =
			id && id.includes && id.includes('.') ? id : parseInt(id, 10);
		const dependencyId =
			dependsOn && dependsOn.includes && dependsOn.includes('.')
				? dependsOn
				: parseInt(dependsOn, 10);

		log.info(
			`Removing dependency: task ${taskId} no longer depends on ${dependencyId}`
		);

		// Enable silent mode to prevent console logs from interfering with JSON response
		enableSilentMode();

		// Call the core function using the provided tasksPath
		await removeDependency(tasksPath, taskId, dependencyId, {
			projectRoot,
			tag
		});

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				message: `Successfully removed dependency: Task ${taskId} no longer depends on ${dependencyId}`,
				taskId: taskId,
				dependencyId: dependencyId
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in removeDependencyDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'CORE_FUNCTION_ERROR',
				message: error.message
			}
		};
	}
}

import { initializeProject } from '../../../../scripts/init.js'; // Import core function and its logger if needed separately
import {
	enableSilentMode,
	disableSilentMode
	// isSilentMode // Not used directly here
} from '../../../../scripts/modules/utils.js';
import os from 'os'; // Import os module for home directory check
import { RULE_PROFILES } from '../../../../src/constants/profiles.js';
import { convertAllRulesToProfileRules } from '../../../../src/utils/rule-transformer.js';

/**
 * Direct function wrapper for initializing a project.
 * Derives target directory from session, sets CWD, and calls core init logic.
 * @param {object} args - Arguments containing initialization options (addAliases, initGit, storeTasksInGit, skipInstall, yes, projectRoot, rules)
 * @param {object} log - The FastMCP logger instance.
 * @param {object} context - The context object, must contain { session }.
 * @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
 */
export async function initializeProjectDirect(args, log, context = {}) {
	const { session } = context; // Keep session if core logic needs it
	const homeDir = os.homedir();

	log.info(`Args received in direct function: ${JSON.stringify(args)}`);

	// --- Determine Target Directory ---
	// TRUST the projectRoot passed from the tool layer via args
	// The HOF in the tool layer already normalized and validated it came from a reliable source (args or session)
	const targetDirectory = args.projectRoot;

	// --- Validate the targetDirectory (basic sanity checks) ---
	if (
		!targetDirectory ||
		typeof targetDirectory !== 'string' || // Ensure it's a string
		targetDirectory === '/' ||
		targetDirectory === homeDir
	) {
		log.error(
			`Invalid target directory received from tool layer: '${targetDirectory}'`
		);
		return {
			success: false,
			error: {
				code: 'INVALID_TARGET_DIRECTORY',
				message: `Cannot initialize project: Invalid target directory '${targetDirectory}' received. Please ensure a valid workspace/folder is open or specified.`,
				details: `Received args.projectRoot: ${args.projectRoot}` // Show what was received
			}
		};
	}

	// --- Proceed with validated targetDirectory ---
	log.info(`Validated target directory for initialization: ${targetDirectory}`);

	const originalCwd = process.cwd();
	let resultData;
	let success = false;
	let errorResult = null;

	log.info(
		`Temporarily changing CWD to ${targetDirectory} for initialization.`
	);
	process.chdir(targetDirectory); // Change CWD to the HOF-provided root

	enableSilentMode();
	try {
		// Construct options ONLY from the relevant flags in args
		// The core initializeProject operates in the current CWD, which we just set
		const options = {
			addAliases: args.addAliases,
			initGit: args.initGit,
			storeTasksInGit: args.storeTasksInGit,
			skipInstall: args.skipInstall,
			yes: true // Force yes mode
		};

		// Handle rules option with MCP-specific defaults
		if (Array.isArray(args.rules) && args.rules.length > 0) {
			options.rules = args.rules;
			options.rulesExplicitlyProvided = true;
			log.info(`Including rules: ${args.rules.join(', ')}`);
		} else {
			// For MCP initialization, default to Cursor profile only
			options.rules = ['cursor'];
			options.rulesExplicitlyProvided = true;
			log.info(`No rule profiles specified, defaulting to: Cursor`);
		}

		log.info(`Initializing project with options: ${JSON.stringify(options)}`);
		const result = await initializeProject(options); // Call core logic

		resultData = {
			message: 'Project initialized successfully.',
			next_step:
				'Now that the project is initialized, the next step is to create the tasks by parsing a PRD. This will create the tasks folder and the initial task files (tasks folder will be created when parse-prd is run). The parse-prd tool will require a prd.txt file as input (typically found in .taskmaster/docs/ directory). You can create a prd.txt file by asking the user about their idea, and then using the .taskmaster/templates/example_prd.txt file as a template to generate a prd.txt file in .taskmaster/docs/. You may skip all of this if the user already has a prd.txt file. You can THEN use the parse-prd tool to create the tasks. So: step 1 after initialization is to create a prd.txt file in .taskmaster/docs/prd.txt or confirm the user already has one. Step 2 is to use the parse-prd tool to create the tasks. Do not bother looking for tasks after initialization, just use the parse-prd tool to create the tasks after creating a prd.txt from which to parse the tasks. You do NOT need to reinitialize the project to parse-prd.',
			...result
		};
		success = true;
		log.info(
			`Project initialization completed successfully in ${targetDirectory}.`
		);
	} catch (error) {
		log.error(`Core initializeProject failed: ${error.message}`);
		errorResult = {
			code: 'INITIALIZATION_FAILED',
			message: `Core project initialization failed: ${error.message}`,
			details: error.stack
		};
		success = false;
	} finally {
		disableSilentMode();
		log.info(`Restoring original CWD: ${originalCwd}`);
		process.chdir(originalCwd);
	}

	if (success) {
		return { success: true, data: resultData };
	} else {
		return { success: false, error: errorResult };
	}
}

import { generateObject, generateText, streamText } from 'ai';
import { log } from '../../scripts/modules/utils.js';

/**
 * Base class for all AI providers
 */
export class BaseAIProvider {
	constructor() {
		if (this.constructor === BaseAIProvider) {
			throw new Error('BaseAIProvider cannot be instantiated directly');
		}

		// Each provider must set their name
		this.name = this.constructor.name;
	}

	/**
	 * Validates authentication parameters - can be overridden by providers
	 * @param {object} params - Parameters to validate
	 */
	validateAuth(params) {
		// Default: require API key (most providers need this)
		if (!params.apiKey) {
			throw new Error(`${this.name} API key is required`);
		}
	}

	/**
	 * Validates common parameters across all methods
	 * @param {object} params - Parameters to validate
	 */
	validateParams(params) {
		// Validate authentication (can be overridden by providers)
		this.validateAuth(params);

		// Validate required model ID
		if (!params.modelId) {
			throw new Error(`${this.name} Model ID is required`);
		}

		// Validate optional parameters
		this.validateOptionalParams(params);
	}

	/**
	 * Validates optional parameters like temperature and maxTokens
	 * @param {object} params - Parameters to validate
	 */
	validateOptionalParams(params) {
		if (
			params.temperature !== undefined &&
			(params.temperature < 0 || params.temperature > 1)
		) {
			throw new Error('Temperature must be between 0 and 1');
		}
		if (params.maxTokens !== undefined && params.maxTokens <= 0) {
			throw new Error('maxTokens must be greater than 0');
		}
	}

	/**
	 * Validates message array structure
	 */
	validateMessages(messages) {
		if (!messages || !Array.isArray(messages) || messages.length === 0) {
			throw new Error('Invalid or empty messages array provided');
		}

		for (const msg of messages) {
			if (!msg.role || !msg.content) {
				throw new Error(
					'Invalid message format. Each message must have role and content'
				);
			}
		}
	}

	/**
	 * Common error handler
	 */
	handleError(operation, error) {
		const errorMessage = error.message || 'Unknown error occurred';
		log('error', `${this.name} ${operation} failed: ${errorMessage}`, {
			error
		});
		throw new Error(
			`${this.name} API error during ${operation}: ${errorMessage}`
		);
	}

	/**
	 * Creates and returns a client instance for the provider
	 * @abstract
	 */
	getClient(params) {
		throw new Error('getClient must be implemented by provider');
	}

	/**
	 * Returns if the API key is required
	 * @abstract
	 * @returns {boolean} if the API key is required, defaults to true
	 */
	isRequiredApiKey() {
		return true;
	}

	/**
	 * Returns the required API key environment variable name
	 * @abstract
	 * @returns {string|null} The environment variable name, or null if no API key is required
	 */
	getRequiredApiKeyName() {
		throw new Error('getRequiredApiKeyName must be implemented by provider');
	}

	/**
	 * Generates text using the provider's model
	 */
	async generateText(params) {
		try {
			this.validateParams(params);
			this.validateMessages(params.messages);

			log(
				'debug',
				`Generating ${this.name} text with model: ${params.modelId}`
			);

			const client = await this.getClient(params);
			const result = await generateText({
				model: client(params.modelId),
				messages: params.messages,
				maxTokens: params.maxTokens,
				temperature: params.temperature
			});

			log(
				'debug',
				`${this.name} generateText completed successfully for model: ${params.modelId}`
			);

			return {
				text: result.text,
				usage: {
					inputTokens: result.usage?.promptTokens,
					outputTokens: result.usage?.completionTokens,
					totalTokens: result.usage?.totalTokens
				}
			};
		} catch (error) {
			this.handleError('text generation', error);
		}
	}

	/**
	 * Streams text using the provider's model
	 */
	async streamText(params) {
		try {
			this.validateParams(params);
			this.validateMessages(params.messages);

			log('debug', `Streaming ${this.name} text with model: ${params.modelId}`);

			const client = await this.getClient(params);
			const stream = await streamText({
				model: client(params.modelId),
				messages: params.messages,
				maxTokens: params.maxTokens,
				temperature: params.temperature
			});

			log(
				'debug',
				`${this.name} streamText initiated successfully for model: ${params.modelId}`
			);

			return stream;
		} catch (error) {
			this.handleError('text streaming', error);
		}
	}

	/**
	 * Generates a structured object using the provider's model
	 */
	async generateObject(params) {
		try {
			this.validateParams(params);
			this.validateMessages(params.messages);

			if (!params.schema) {
				throw new Error('Schema is required for object generation');
			}
			if (!params.objectName) {
				throw new Error('Object name is required for object generation');
			}

			log(
				'debug',
				`Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}`
			);

			const client = await this.getClient(params);
			const result = await generateObject({
				model: client(params.modelId),
				messages: params.messages,
				schema: params.schema,
				mode: 'auto',
				maxTokens: params.maxTokens,
				temperature: params.temperature
			});

			log(
				'debug',
				`${this.name} generateObject completed successfully for model: ${params.modelId}`
			);

			return {
				object: result.object,
				usage: {
					inputTokens: result.usage?.promptTokens,
					outputTokens: result.usage?.completionTokens,
					totalTokens: result.usage?.totalTokens
				}
			};
		} catch (error) {
			this.handleError('object generation', error);
		}
	}
}

/**
 * tools/add-subtask.js
 * Tool for adding subtasks to existing tasks
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { addSubtaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the addSubtask tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerAddSubtaskTool(server) {
	server.addTool({
		name: 'add_subtask',
		description: 'Add a subtask to an existing task',
		parameters: z.object({
			id: z.string().describe('Parent task ID (required)'),
			taskId: z
				.string()
				.optional()
				.describe('Existing task ID to convert to subtask'),
			title: z
				.string()
				.optional()
				.describe('Title for the new subtask (when creating a new subtask)'),
			description: z
				.string()
				.optional()
				.describe('Description for the new subtask'),
			details: z
				.string()
				.optional()
				.describe('Implementation details for the new subtask'),
			status: z
				.string()
				.optional()
				.describe("Status for the new subtask (default: 'pending')"),
			dependencies: z
				.string()
				.optional()
				.describe('Comma-separated list of dependency IDs for the new subtask'),
			file: z
				.string()
				.optional()
				.describe(
					'Absolute path to the tasks file (default: tasks/tasks.json)'
				),
			skipGenerate: z
				.boolean()
				.optional()
				.describe('Skip regenerating task files'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				log.info(`Adding subtask with args: ${JSON.stringify(args)}`);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				const result = await addSubtaskDirect(
					{
						tasksJsonPath: tasksJsonPath,
						id: args.id,
						taskId: args.taskId,
						title: args.title,
						description: args.description,
						details: args.details,
						status: args.status,
						dependencies: args.dependencies,
						skipGenerate: args.skipGenerate,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				if (result.success) {
					log.info(`Subtask added successfully: ${result.data.message}`);
				} else {
					log.error(`Failed to add subtask: ${result.error.message}`);
				}

				return handleApiResult(
					result,
					log,
					'Error adding subtask',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in addSubtask tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * tools/update.js
 * Tool to update tasks based on new context/prompt
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { updateTasksDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the update tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerUpdateTool(server) {
	server.addTool({
		name: 'update',
		description:
			"Update multiple upcoming tasks (with ID >= 'from' ID) based on new context or changes provided in the prompt. Use 'update_task' instead for a single specific task or 'update_subtask' for subtasks.",
		parameters: z.object({
			from: z
				.string()
				.describe(
					"Task ID from which to start updating (inclusive). IMPORTANT: This tool uses 'from', not 'id'"
				),
			prompt: z
				.string()
				.describe('Explanation of changes or new context to apply'),
			research: z
				.boolean()
				.optional()
				.describe('Use Perplexity AI for research-backed updates'),
			file: z
				.string()
				.optional()
				.describe('Path to the tasks file relative to project root'),
			projectRoot: z
				.string()
				.optional()
				.describe(
					'The directory of the project. (Optional, usually from session)'
				),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			const toolName = 'update';
			const { from, prompt, research, file, projectRoot, tag } = args;

			const resolvedTag = resolveTag({
				projectRoot: args.projectRoot,
				tag: args.tag
			});

			try {
				log.info(
					`Executing ${toolName} tool with normalized root: ${projectRoot}`
				);

				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath({ projectRoot, file }, log);
					log.info(`${toolName}: Resolved tasks path: ${tasksJsonPath}`);
				} catch (error) {
					log.error(`${toolName}: Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json within project root '${projectRoot}': ${error.message}`
					);
				}

				const result = await updateTasksDirect(
					{
						tasksJsonPath: tasksJsonPath,
						from: from,
						prompt: prompt,
						research: research,
						projectRoot: projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				log.info(
					`${toolName}: Direct function result: success=${result.success}`
				);
				return handleApiResult(
					result,
					log,
					'Error updating tasks',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(
					`Critical error in ${toolName} tool execute: ${error.message}`
				);
				return createErrorResponse(
					`Internal tool error (${toolName}): ${error.message}`
				);
			}
		})
	});
}

#!/usr/bin/env node

/**
 * Task Master
 * Copyright (c) 2025 Eyal Toledano, Ralph Khreish
 *
 * This software is licensed under the MIT License with Commons Clause.
 * You may use this software for any purpose, including commercial applications,
 * and modify and redistribute it freely, subject to the following restrictions:
 *
 * 1. You may not sell this software or offer it as a service.
 * 2. The origin of this software must not be misrepresented.
 * 3. Altered source versions must be plainly marked as such.
 *
 * For the full license text, see the LICENSE file in the root directory.
 */

/**
 * Claude Task Master
 * A task management system for AI-driven development with Claude
 */

// This file serves as the main entry point for the package
// The primary functionality is provided through the CLI commands

import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { createRequire } from 'module';
import { spawn } from 'child_process';
import { Command } from 'commander';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);

// Get package information
const packageJson = require('./package.json');

// Export the path to the dev.js script for programmatic usage
export const devScriptPath = resolve(__dirname, './scripts/dev.js');

// Export a function to initialize a new project programmatically
export const initProject = async (options = {}) => {
	const init = await import('./scripts/init.js');
	return init.initializeProject(options);
};

// Export a function to run init as a CLI command
export const runInitCLI = async (options = {}) => {
	try {
		const init = await import('./scripts/init.js');
		const result = await init.initializeProject(options);
		return result;
	} catch (error) {
		console.error('Initialization failed:', error.message);
		if (process.env.DEBUG === 'true') {
			console.error('Debug stack trace:', error.stack);
		}
		throw error; // Re-throw to be handled by the command handler
	}
};

// Export version information
export const version = packageJson.version;

// CLI implementation
if (import.meta.url === `file://${process.argv[1]}`) {
	const program = new Command();

	program
		.name('task-master')
		.description('Claude Task Master CLI')
		.version(version);

	program
		.command('init')
		.description('Initialize a new project')
		.option('-y, --yes', 'Skip prompts and use default values')
		.option('-n, --name <n>', 'Project name')
		.option('-d, --description <description>', 'Project description')
		.option('-v, --version <version>', 'Project version', '0.1.0')
		.option('-a, --author <author>', 'Author name')
		.option('--skip-install', 'Skip installing dependencies')
		.option('--dry-run', 'Show what would be done without making changes')
		.option('--aliases', 'Add shell aliases (tm, taskmaster)')
		.option('--no-aliases', 'Skip shell aliases (tm, taskmaster)')
		.option('--git', 'Initialize Git repository')
		.option('--no-git', 'Skip Git repository initialization')
		.option('--git-tasks', 'Store tasks in Git')
		.option('--no-git-tasks', 'No Git storage of tasks')
		.action(async (cmdOptions) => {
			try {
				await runInitCLI(cmdOptions);
			} catch (err) {
				console.error('Init failed:', err.message);
				process.exit(1);
			}
		});

	program
		.command('dev')
		.description('Run the dev.js script')
		.allowUnknownOption(true)
		.action(() => {
			const args = process.argv.slice(process.argv.indexOf('dev') + 1);
			const child = spawn('node', [devScriptPath, ...args], {
				stdio: 'inherit',
				cwd: process.cwd()
			});

			child.on('close', (code) => {
				process.exit(code);
			});
		});

	// Add shortcuts for common dev.js commands
	program
		.command('list')
		.description('List all tasks')
		.action(() => {
			const child = spawn('node', [devScriptPath, 'list'], {
				stdio: 'inherit',
				cwd: process.cwd()
			});

			child.on('close', (code) => {
				process.exit(code);
			});
		});

	program
		.command('next')
		.description('Show the next task to work on')
		.action(() => {
			const child = spawn('node', [devScriptPath, 'next'], {
				stdio: 'inherit',
				cwd: process.cwd()
			});

			child.on('close', (code) => {
				process.exit(code);
			});
		});

	program
		.command('generate')
		.description('Generate task files')
		.action(() => {
			const child = spawn('node', [devScriptPath, 'generate'], {
				stdio: 'inherit',
				cwd: process.cwd()
			});

			child.on('close', (code) => {
				process.exit(code);
			});
		});

	program.parse(process.argv);
}

/**
 * set-task-status.js
 * Direct function implementation for setting task status
 */

import { setTaskStatus } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode,
	isSilentMode
} from '../../../../scripts/modules/utils.js';
import { nextTaskDirect } from './next-task.js';
/**
 * Direct function wrapper for setTaskStatus with error handling.
 *
 * @param {Object} args - Command arguments containing id, status, tasksJsonPath, and projectRoot.
 * @param {string} args.id - The ID of the task to update.
 * @param {string} args.status - The new status to set for the task.
 * @param {string} args.tasksJsonPath - Path to the tasks.json file.
 * @param {string} args.projectRoot - Project root path (for MCP/env fallback)
 * @param {string} args.tag - Tag for the task (optional)
 * @param {Object} log - Logger object.
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object with success status and data/error information.
 */
export async function setTaskStatusDirect(args, log, context = {}) {
	// Destructure expected args, including the resolved tasksJsonPath and projectRoot
	const { tasksJsonPath, id, status, complexityReportPath, projectRoot, tag } =
		args;
	const { session } = context;
	try {
		log.info(`Setting task status with args: ${JSON.stringify(args)}`);

		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			const errorMessage = 'tasksJsonPath is required but was not provided.';
			log.error(errorMessage);
			return {
				success: false,
				error: { code: 'MISSING_ARGUMENT', message: errorMessage }
			};
		}

		// Check required parameters (id and status)
		if (!id) {
			const errorMessage =
				'No task ID specified. Please provide a task ID to update.';
			log.error(errorMessage);
			return {
				success: false,
				error: { code: 'MISSING_TASK_ID', message: errorMessage }
			};
		}

		if (!status) {
			const errorMessage =
				'No status specified. Please provide a new status value.';
			log.error(errorMessage);
			return {
				success: false,
				error: { code: 'MISSING_STATUS', message: errorMessage }
			};
		}

		// Use the provided path
		const tasksPath = tasksJsonPath;

		// Execute core setTaskStatus function
		const taskId = id;
		const newStatus = status;

		log.info(`Setting task ${taskId} status to "${newStatus}"`);

		// Call the core function with proper silent mode handling
		enableSilentMode(); // Enable silent mode before calling core function
		try {
			// Call the core function
			await setTaskStatus(tasksPath, taskId, newStatus, {
				mcpLog: log,
				projectRoot,
				session,
				tag
			});

			log.info(`Successfully set task ${taskId} status to ${newStatus}`);

			// Return success data
			const result = {
				success: true,
				data: {
					message: `Successfully updated task ${taskId} status to "${newStatus}"`,
					taskId,
					status: newStatus,
					tasksPath: tasksPath // Return the path used
				}
			};

			// If the task was completed, attempt to fetch the next task
			if (result.data.status === 'done') {
				try {
					log.info(`Attempting to fetch next task for task ${taskId}`);
					const nextResult = await nextTaskDirect(
						{
							tasksJsonPath: tasksJsonPath,
							reportPath: complexityReportPath,
							projectRoot: projectRoot,
							tag
						},
						log,
						{ session }
					);

					if (nextResult.success) {
						log.info(
							`Successfully retrieved next task: ${nextResult.data.nextTask}`
						);
						result.data = {
							...result.data,
							nextTask: nextResult.data.nextTask,
							isNextSubtask: nextResult.data.isSubtask,
							nextSteps: nextResult.data.nextSteps
						};
					} else {
						log.warn(
							`Failed to retrieve next task: ${nextResult.error?.message || 'Unknown error'}`
						);
					}
				} catch (nextErr) {
					log.error(`Error retrieving next task: ${nextErr.message}`);
				}
			}

			return result;
		} catch (error) {
			log.error(`Error setting task status: ${error.message}`);
			return {
				success: false,
				error: {
					code: 'SET_STATUS_ERROR',
					message: error.message || 'Unknown error setting task status'
				}
			};
		} finally {
			// ALWAYS restore normal logging in finally block
			disableSilentMode();
		}
	} catch (error) {
		// Ensure silent mode is disabled if there was an uncaught error in the outer try block
		if (isSilentMode()) {
			disableSilentMode();
		}

		log.error(`Error setting task status: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'SET_STATUS_ERROR',
				message: error.message || 'Unknown error setting task status'
			}
		};
	}
}

/**
 * Check if a task is dependent on another task (directly or indirectly)
 * Used to prevent circular dependencies
 * @param {Array} allTasks - Array of all tasks
 * @param {Object} task - The task to check
 * @param {number} targetTaskId - The task ID to check dependency against
 * @returns {boolean} Whether the task depends on the target task
 */
function isTaskDependentOn(allTasks, task, targetTaskId) {
	// If the task is a subtask, check if its parent is the target
	if (task.parentTaskId === targetTaskId) {
		return true;
	}

	// Check direct dependencies
	if (task.dependencies && task.dependencies.includes(targetTaskId)) {
		return true;
	}

	// Check dependencies of dependencies (recursive)
	if (task.dependencies) {
		for (const depId of task.dependencies) {
			const depTask = allTasks.find((t) => t.id === depId);
			if (depTask && isTaskDependentOn(allTasks, depTask, targetTaskId)) {
				return true;
			}
		}
	}

	// Check subtasks for dependencies
	if (task.subtasks) {
		for (const subtask of task.subtasks) {
			if (isTaskDependentOn(allTasks, subtask, targetTaskId)) {
				return true;
			}
		}
	}

	return false;
}

export default isTaskDependentOn;

/**
 * tools/get-tasks.js
 * Tool to get all tasks from Task Master
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { listTasksDirect } from '../core/task-master-core.js';
import {
	resolveTasksPath,
	resolveComplexityReportPath
} from '../core/utils/path-utils.js';

import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the getTasks tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerListTasksTool(server) {
	server.addTool({
		name: 'get_tasks',
		description:
			'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
		parameters: z.object({
			status: z
				.string()
				.optional()
				.describe(
					"Filter tasks by status (e.g., 'pending', 'done') or multiple statuses separated by commas (e.g., 'blocked,deferred')"
				),
			withSubtasks: z
				.boolean()
				.optional()
				.describe(
					'Include subtasks nested within their parent tasks in the response'
				),
			file: z
				.string()
				.optional()
				.describe(
					'Path to the tasks file (relative to project root or absolute)'
				),
			complexityReport: z
				.string()
				.optional()
				.describe(
					'Path to the complexity report file (relative to project root or absolute)'
				),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Getting tasks with filters: ${JSON.stringify(args)}`);

				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				// Resolve the path to tasks.json using new path utilities
				let tasksJsonPath;
				try {
					tasksJsonPath = resolveTasksPath(args, log);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Resolve the path to complexity report
				let complexityReportPath;
				try {
					complexityReportPath = resolveComplexityReportPath(
						{ ...args, tag: resolvedTag },
						session
					);
				} catch (error) {
					log.error(`Error finding complexity report: ${error.message}`);
					// This is optional, so we don't fail the operation
					complexityReportPath = null;
				}

				const result = await listTasksDirect(
					{
						tasksJsonPath: tasksJsonPath,
						status: args.status,
						withSubtasks: args.withSubtasks,
						reportPath: complexityReportPath,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				log.info(
					`Retrieved ${result.success ? result.data?.tasks?.length || 0 : 0} tasks`
				);
				return handleApiResult(
					result,
					log,
					'Error getting tasks',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error getting tasks: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

// We no longer need the formatTasksResponse function as we're returning raw JSON data

/**
 * tools/parsePRD.js
 * Tool to parse PRD document and generate tasks
 */

import { z } from 'zod';
import {
	handleApiResult,
	withNormalizedProjectRoot,
	createErrorResponse
} from './utils.js';
import { parsePRDDirect } from '../core/task-master-core.js';
import {
	PRD_FILE,
	TASKMASTER_DOCS_DIR,
	TASKMASTER_TASKS_FILE
} from '../../../src/constants/paths.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the parse_prd tool
 * @param {Object} server - FastMCP server instance
 */
export function registerParsePRDTool(server) {
	server.addTool({
		name: 'parse_prd',
		description: `Parse a Product Requirements Document (PRD) text file to automatically generate initial tasks. Reinitializing the project is not necessary to run this tool. It is recommended to run parse-prd after initializing the project and creating/importing a prd.txt file in the project root's ${TASKMASTER_DOCS_DIR} directory.`,

		parameters: z.object({
			input: z
				.string()
				.optional()
				.default(PRD_FILE)
				.describe('Absolute path to the PRD document file (.txt, .md, etc.)'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on'),
			output: z
				.string()
				.optional()
				.describe(
					`Output path for tasks.json file (default: ${TASKMASTER_TASKS_FILE})`
				),
			numTasks: z
				.string()
				.optional()
				.describe(
					'Approximate number of top-level tasks to generate (default: 10). As the agent, if you have enough information, ensure to enter a number of tasks that would logically scale with project complexity. Setting to 0 will allow Taskmaster to determine the appropriate number of tasks based on the complexity of the PRD. Avoid entering numbers above 50 due to context window limitations.'
				),
			force: z
				.boolean()
				.optional()
				.default(false)
				.describe('Overwrite existing output file without prompting.'),
			research: z
				.boolean()
				.optional()
				.describe(
					'Enable Taskmaster to use the research role for potentially more informed task generation. Requires appropriate API key.'
				),
			append: z
				.boolean()
				.optional()
				.describe('Append generated tasks to existing file.')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				const result = await parsePRDDirect(
					{
						...args,
						tag: resolvedTag
					},
					log,
					{ session }
				);
				return handleApiResult(
					result,
					log,
					'Error parsing PRD',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in parse_prd: ${error.message}`);
				return createErrorResponse(`Failed to parse PRD: ${error.message}`);
			}
		})
	});
}

/**
 * tools/remove-subtask.js
 * Tool for removing subtasks from parent tasks
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { removeSubtaskDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the removeSubtask tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerRemoveSubtaskTool(server) {
	server.addTool({
		name: 'remove_subtask',
		description: 'Remove a subtask from its parent task',
		parameters: z.object({
			id: z
				.string()
				.describe(
					"Subtask ID to remove in format 'parentId.subtaskId' (required)"
				),
			convert: z
				.boolean()
				.optional()
				.describe(
					'Convert the subtask to a standalone task instead of deleting it'
				),
			file: z
				.string()
				.optional()
				.describe(
					'Absolute path to the tasks file (default: tasks/tasks.json)'
				),
			skipGenerate: z
				.boolean()
				.optional()
				.describe('Skip regenerating task files'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				log.info(`Removing subtask with args: ${JSON.stringify(args)}`);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				const result = await removeSubtaskDirect(
					{
						tasksJsonPath: tasksJsonPath,
						id: args.id,
						convert: args.convert,
						skipGenerate: args.skipGenerate,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				if (result.success) {
					log.info(`Subtask removed successfully: ${result.data.message}`);
				} else {
					log.error(`Failed to remove subtask: ${result.error.message}`);
				}

				return handleApiResult(
					result,
					log,
					'Error removing subtask',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in removeSubtask tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * @fileoverview Extract JSON from MCP response, handling markdown blocks and other formatting
 */

/**
 * Extract JSON from MCP AI response
 * @param {string} text - The text to extract JSON from
 * @returns {string} - The extracted JSON string
 */
export function extractJson(text) {
	// Remove markdown code blocks if present
	let jsonText = text.trim();

	// Remove ```json blocks
	jsonText = jsonText.replace(/^```json\s*/gm, '');
	jsonText = jsonText.replace(/^```\s*/gm, '');
	jsonText = jsonText.replace(/```\s*$/gm, '');

	// Remove common TypeScript/JavaScript patterns
	jsonText = jsonText.replace(/^const\s+\w+\s*=\s*/, ''); // Remove "const varName = "
	jsonText = jsonText.replace(/^let\s+\w+\s*=\s*/, ''); // Remove "let varName = "
	jsonText = jsonText.replace(/^var\s+\w+\s*=\s*/, ''); // Remove "var varName = "
	jsonText = jsonText.replace(/;?\s*$/, ''); // Remove trailing semicolons

	// Remove explanatory text before JSON (common with AI responses)
	jsonText = jsonText.replace(/^.*?(?=\{|\[)/s, '');

	// Remove explanatory text after JSON
	const lines = jsonText.split('\n');
	let jsonEndIndex = -1;
	let braceCount = 0;
	let inString = false;
	let escapeNext = false;

	// Find the end of the JSON by tracking braces
	for (let i = 0; i < jsonText.length; i++) {
		const char = jsonText[i];

		if (escapeNext) {
			escapeNext = false;
			continue;
		}

		if (char === '\\') {
			escapeNext = true;
			continue;
		}

		if (char === '"' && !escapeNext) {
			inString = !inString;
			continue;
		}

		if (!inString) {
			if (char === '{' || char === '[') {
				braceCount++;
			} else if (char === '}' || char === ']') {
				braceCount--;
				if (braceCount === 0) {
					jsonEndIndex = i;
					break;
				}
			}
		}
	}

	if (jsonEndIndex > -1) {
		jsonText = jsonText.substring(0, jsonEndIndex + 1);
	}

	// Try to extract JSON object or array if previous method didn't work
	if (jsonEndIndex === -1) {
		const objectMatch = jsonText.match(/{[\s\S]*}/);
		const arrayMatch = jsonText.match(/\[[\s\S]*\]/);

		if (objectMatch) {
			jsonText = objectMatch[0];
		} else if (arrayMatch) {
			jsonText = arrayMatch[0];
		}
	}

	// First try to parse as valid JSON
	try {
		JSON.parse(jsonText);
		return jsonText;
	} catch {
		// If it's not valid JSON, it might be a JavaScript object literal
		// Try to convert it to valid JSON
		try {
			// This is a simple conversion that handles basic cases
			// Replace unquoted keys with quoted keys
			const converted = jsonText
				.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
				// Replace single quotes with double quotes
				.replace(/'/g, '"')
				// Handle trailing commas
				.replace(/,\s*([}\]])/g, '$1');

			// Validate the converted JSON
			JSON.parse(converted);
			return converted;
		} catch {
			// If all else fails, return the original text
			// The calling code will handle the error appropriately
			return text;
		}
	}
}

/**
 * tools/expand-all.js
 * Tool for expanding all pending tasks with subtasks
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { expandAllTasksDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the expandAll tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerExpandAllTool(server) {
	server.addTool({
		name: 'expand_all',
		description:
			'Expand all pending tasks into subtasks based on complexity or defaults',
		parameters: z.object({
			num: z
				.string()
				.optional()
				.describe(
					'Target number of subtasks per task (uses complexity/defaults otherwise)'
				),
			research: z
				.boolean()
				.optional()
				.describe(
					'Enable research-backed subtask generation (e.g., using Perplexity)'
				),
			prompt: z
				.string()
				.optional()
				.describe(
					'Additional context to guide subtask generation for all tasks'
				),
			force: z
				.boolean()
				.optional()
				.describe(
					'Force regeneration of subtasks for tasks that already have them'
				),
			file: z
				.string()
				.optional()
				.describe(
					'Absolute path to the tasks file in the /tasks folder inside the project root (default: tasks/tasks.json)'
				),
			projectRoot: z
				.string()
				.optional()
				.describe(
					'Absolute path to the project root directory (derived from session if possible)'
				),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(
					`Tool expand_all execution started with args: ${JSON.stringify(args)}`
				);

				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
					log.info(`Resolved tasks.json path: ${tasksJsonPath}`);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				const result = await expandAllTasksDirect(
					{
						tasksJsonPath: tasksJsonPath,
						num: args.num,
						research: args.research,
						prompt: args.prompt,
						force: args.force,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error expanding all tasks',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(
					`Unexpected error in expand_all tool execute: ${error.message}`
				);
				if (error.stack) {
					log.error(error.stack);
				}
				return createErrorResponse(
					`An unexpected error occurred: ${error.message}`
				);
			}
		})
	});
}

/**
 * tools/remove-dependency.js
 * Tool for removing a dependency from a task
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { removeDependencyDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the removeDependency tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerRemoveDependencyTool(server) {
	server.addTool({
		name: 'remove_dependency',
		description: 'Remove a dependency from a task',
		parameters: z.object({
			id: z.string().describe('Task ID to remove dependency from'),
			dependsOn: z.string().describe('Task ID to remove as a dependency'),
			file: z
				.string()
				.optional()
				.describe(
					'Absolute path to the tasks file (default: tasks/tasks.json)'
				),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				log.info(
					`Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}`
				);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				const result = await removeDependencyDirect(
					{
						tasksJsonPath: tasksJsonPath,
						id: args.id,
						dependsOn: args.dependsOn,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log
				);

				if (result.success) {
					log.info(`Successfully removed dependency: ${result.data.message}`);
				} else {
					log.error(`Failed to remove dependency: ${result.error.message}`);
				}

				return handleApiResult(
					result,
					log,
					'Error removing dependency',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in removeDependency tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * tools/generate.js
 * Tool to generate individual task files from tasks.json
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { generateTaskFilesDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
import path from 'path';

/**
 * Register the generate tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerGenerateTool(server) {
	server.addTool({
		name: 'generate',
		description:
			'Generates individual task files in tasks/ directory based on tasks.json',
		parameters: z.object({
			file: z.string().optional().describe('Absolute path to the tasks file'),
			output: z
				.string()
				.optional()
				.describe('Output directory (default: same directory as tasks file)'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Generating task files with args: ${JSON.stringify(args)}`);

				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				const outputDir = args.output
					? path.resolve(args.projectRoot, args.output)
					: path.dirname(tasksJsonPath);

				const result = await generateTaskFilesDirect(
					{
						tasksJsonPath: tasksJsonPath,
						outputDir: outputDir,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				if (result.success) {
					log.info(`Successfully generated task files: ${result.data.message}`);
				} else {
					log.error(
						`Failed to generate task files: ${result.error?.message || 'Unknown error'}`
					);
				}

				return handleApiResult(
					result,
					log,
					'Error generating task files',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in generate tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * @fileoverview Error handling utilities for Claude Code provider
 */

import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';

/**
 * @typedef {import('./types.js').ClaudeCodeErrorMetadata} ClaudeCodeErrorMetadata
 */

/**
 * Create an API call error with Claude Code specific metadata
 * @param {Object} params - Error parameters
 * @param {string} params.message - Error message
 * @param {string} [params.code] - Error code
 * @param {number} [params.exitCode] - Process exit code
 * @param {string} [params.stderr] - Standard error output
 * @param {string} [params.promptExcerpt] - Excerpt of the prompt
 * @param {boolean} [params.isRetryable=false] - Whether the error is retryable
 * @returns {APICallError}
 */
export function createAPICallError({
	message,
	code,
	exitCode,
	stderr,
	promptExcerpt,
	isRetryable = false
}) {
	/** @type {ClaudeCodeErrorMetadata} */
	const metadata = {
		code,
		exitCode,
		stderr,
		promptExcerpt
	};

	return new APICallError({
		message,
		isRetryable,
		url: 'claude-code-cli://command',
		requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
		data: metadata
	});
}

/**
 * Create an authentication error
 * @param {Object} params - Error parameters
 * @param {string} params.message - Error message
 * @returns {LoadAPIKeyError}
 */
export function createAuthenticationError({ message }) {
	return new LoadAPIKeyError({
		message:
			message ||
			'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
	});
}

/**
 * Create a timeout error
 * @param {Object} params - Error parameters
 * @param {string} params.message - Error message
 * @param {string} [params.promptExcerpt] - Excerpt of the prompt
 * @param {number} params.timeoutMs - Timeout in milliseconds
 * @returns {APICallError}
 */
export function createTimeoutError({ message, promptExcerpt, timeoutMs }) {
	// Store timeoutMs in metadata for potential use by error handlers
	/** @type {ClaudeCodeErrorMetadata & { timeoutMs: number }} */
	const metadata = {
		code: 'TIMEOUT',
		promptExcerpt,
		timeoutMs
	};

	return new APICallError({
		message,
		isRetryable: true,
		url: 'claude-code-cli://command',
		requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
		data: metadata
	});
}

/**
 * Check if an error is an authentication error
 * @param {unknown} error - Error to check
 * @returns {boolean}
 */
export function isAuthenticationError(error) {
	if (error instanceof LoadAPIKeyError) return true;
	if (
		error instanceof APICallError &&
		/** @type {ClaudeCodeErrorMetadata} */ (error.data)?.exitCode === 401
	)
		return true;
	return false;
}

/**
 * Check if an error is a timeout error
 * @param {unknown} error - Error to check
 * @returns {boolean}
 */
export function isTimeoutError(error) {
	if (
		error instanceof APICallError &&
		/** @type {ClaudeCodeErrorMetadata} */ (error.data)?.code === 'TIMEOUT'
	)
		return true;
	return false;
}

/**
 * Get error metadata from an error
 * @param {unknown} error - Error to extract metadata from
 * @returns {ClaudeCodeErrorMetadata|undefined}
 */
export function getErrorMetadata(error) {
	if (error instanceof APICallError && error.data) {
		return /** @type {ClaudeCodeErrorMetadata} */ (error.data);
	}
	return undefined;
}

/**
 * list-tasks.js
 * Direct function implementation for listing tasks
 */

import { listTasks } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';

/**
 * Direct function wrapper for listTasks with error handling and caching.
 *
 * @param {Object} args - Command arguments (now expecting tasksJsonPath explicitly).
 * @param {string} args.tasksJsonPath - Path to the tasks.json file.
 * @param {string} args.reportPath - Path to the report file.
 * @param {string} args.status - Status of the task.
 * @param {boolean} args.withSubtasks - Whether to include subtasks.
 * @param {string} args.projectRoot - Project root path (for MCP/env fallback)
 * @param {string} args.tag - Tag for the task (optional)
 * @param {Object} log - Logger object.
 * @returns {Promise<Object>} - Task list result { success: boolean, data?: any, error?: { code: string, message: string } }.
 */
export async function listTasksDirect(args, log, context = {}) {
	// Destructure the explicit tasksJsonPath from args
	const { tasksJsonPath, reportPath, status, withSubtasks, projectRoot, tag } =
		args;
	const { session } = context;

	if (!tasksJsonPath) {
		log.error('listTasksDirect called without tasksJsonPath');
		return {
			success: false,
			error: {
				code: 'MISSING_ARGUMENT',
				message: 'tasksJsonPath is required'
			}
		};
	}

	// Use the explicit tasksJsonPath for cache key
	const statusFilter = status || 'all';
	const withSubtasksFilter = withSubtasks || false;

	// Define the action function to be executed on cache miss
	const coreListTasksAction = async () => {
		try {
			// Enable silent mode to prevent console logs from interfering with JSON response
			enableSilentMode();

			log.info(
				`Executing core listTasks function for path: ${tasksJsonPath}, filter: ${statusFilter}, subtasks: ${withSubtasksFilter}`
			);
			// Pass the explicit tasksJsonPath to the core function
			const resultData = listTasks(
				tasksJsonPath,
				statusFilter,
				reportPath,
				withSubtasksFilter,
				'json',
				{ projectRoot, session, tag }
			);

			if (!resultData || !resultData.tasks) {
				log.error('Invalid or empty response from listTasks core function');
				return {
					success: false,
					error: {
						code: 'INVALID_CORE_RESPONSE',
						message: 'Invalid or empty response from listTasks core function'
					}
				};
			}

			log.info(
				`Core listTasks function retrieved ${resultData.tasks.length} tasks`
			);

			// Restore normal logging
			disableSilentMode();

			return { success: true, data: resultData };
		} catch (error) {
			// Make sure to restore normal logging even if there's an error
			disableSilentMode();

			log.error(`Core listTasks function failed: ${error.message}`);
			return {
				success: false,
				error: {
					code: 'LIST_TASKS_CORE_ERROR',
					message: error.message || 'Failed to list tasks'
				}
			};
		}
	};

	try {
		const result = await coreListTasksAction();
		log.info('listTasksDirect completed');
		return result;
	} catch (error) {
		log.error(`Unexpected error during listTasks: ${error.message}`);
		console.error(error.stack);
		return {
			success: false,
			error: {
				code: 'UNEXPECTED_ERROR',
				message: error.message
			}
		};
	}
}

import chalk from 'chalk';
import { isSilentMode } from '../../scripts/modules/utils.js';
import { getLogLevel } from '../../scripts/modules/config-manager.js';

// Define log levels
const LOG_LEVELS = {
	debug: 0,
	info: 1,
	warn: 2,
	error: 3,
	success: 4
};

// Get log level from config manager or default to info
const LOG_LEVEL = LOG_LEVELS[getLogLevel().toLowerCase()] ?? LOG_LEVELS.info;

/**
 * Logs a message with the specified level
 * @param {string} level - The log level (debug, info, warn, error, success)
 * @param  {...any} args - Arguments to log
 */
function log(level, ...args) {
	// Skip logging if silent mode is enabled
	if (isSilentMode()) {
		return;
	}

	// 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]')
	};

	if (LOG_LEVELS[level] !== undefined && LOG_LEVELS[level] >= LOG_LEVEL) {
		const prefix = prefixes[level] || '';
		let coloredArgs = args;

		try {
			switch (level) {
				case 'error':
					coloredArgs = args.map((arg) =>
						typeof arg === 'string' ? chalk.red(arg) : arg
					);
					break;
				case 'warn':
					coloredArgs = args.map((arg) =>
						typeof arg === 'string' ? chalk.yellow(arg) : arg
					);
					break;
				case 'success':
					coloredArgs = args.map((arg) =>
						typeof arg === 'string' ? chalk.green(arg) : arg
					);
					break;
				case 'info':
					coloredArgs = args.map((arg) =>
						typeof arg === 'string' ? chalk.blue(arg) : arg
					);
					break;
				case 'debug':
					coloredArgs = args.map((arg) =>
						typeof arg === 'string' ? chalk.gray(arg) : arg
					);
					break;
				// default: use original args (no color)
			}
		} catch (colorError) {
			// Fallback if chalk fails on an argument
			// Use console.error here for internal logger errors, separate from normal logging
			console.error('Internal Logger Error applying chalk color:', colorError);
			coloredArgs = args;
		}

		// Revert to console.log - FastMCP's context logger (context.log)
		// is responsible for directing logs correctly (e.g., to stderr)
		// during tool execution without upsetting the client connection.
		// Logs outside of tool execution (like startup) will go to stdout.
		console.log(prefix, ...coloredArgs);
	}
}

/**
 * Create a logger object with methods for different log levels
 * @returns {Object} Logger object with info, error, debug, warn, and success methods
 */
export function createLogger() {
	const createLogMethod =
		(level) =>
		(...args) =>
			log(level, ...args);

	return {
		debug: createLogMethod('debug'),
		info: createLogMethod('info'),
		warn: createLogMethod('warn'),
		error: createLogMethod('error'),
		success: createLogMethod('success'),
		log: log // Also expose the raw log function
	};
}

// Export a default logger instance
const logger = createLogger();

export default logger;
export { log, LOG_LEVELS };

/**
 * tools/add-dependency.js
 * Tool for adding a dependency to a task
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { addDependencyDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the addDependency tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerAddDependencyTool(server) {
	server.addTool({
		name: 'add_dependency',
		description: 'Add a dependency relationship between two tasks',
		parameters: z.object({
			id: z.string().describe('ID of task that will depend on another task'),
			dependsOn: z
				.string()
				.describe('ID of task that will become a dependency'),
			file: z
				.string()
				.optional()
				.describe(
					'Absolute path to the tasks file (default: tasks/tasks.json)'
				),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(
					`Adding dependency for task ${args.id} to depend on ${args.dependsOn}`
				);
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Call the direct function with the resolved path
				const result = await addDependencyDirect(
					{
						// Pass the explicitly resolved path
						tasksJsonPath: tasksJsonPath,
						// Pass other relevant args
						id: args.id,
						dependsOn: args.dependsOn,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log
					// Remove context object
				);

				// Log result
				if (result.success) {
					log.info(`Successfully added dependency: ${result.data.message}`);
				} else {
					log.error(`Failed to add dependency: ${result.error.message}`);
				}

				// Use handleApiResult to format the response
				return handleApiResult(
					result,
					log,
					'Error adding dependency',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in addDependency tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * Direct function wrapper for moveTask
 */

import { moveTask } from '../../../../scripts/modules/task-manager.js';
import { findTasksPath } from '../utils/path-utils.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';

/**
 * Move a task or subtask to a new position
 * @param {Object} args - Function arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file
 * @param {string} args.sourceId - ID of the task/subtask to move (e.g., '5' or '5.2' or '5,6,7')
 * @param {string} args.destinationId - ID of the destination (e.g., '7' or '7.3' or '7,8,9')
 * @param {string} args.file - Alternative path to the tasks.json file
 * @param {string} args.projectRoot - Project root directory
 * @param {string} args.tag - Tag for the task (optional)
 * @param {boolean} args.generateFiles - Whether to regenerate task files after moving (default: true)
 * @param {Object} log - Logger object
 * @returns {Promise<{success: boolean, data?: Object, error?: Object}>}
 */
export async function moveTaskDirect(args, log, context = {}) {
	const { session } = context;
	const { projectRoot, tag } = args;

	// Validate required parameters
	if (!args.sourceId) {
		return {
			success: false,
			error: {
				message: 'Source ID is required',
				code: 'MISSING_SOURCE_ID'
			}
		};
	}

	if (!args.destinationId) {
		return {
			success: false,
			error: {
				message: 'Destination ID is required',
				code: 'MISSING_DESTINATION_ID'
			}
		};
	}

	try {
		// Find tasks.json path if not provided
		let tasksPath = args.tasksJsonPath || args.file;
		if (!tasksPath) {
			if (!args.projectRoot) {
				return {
					success: false,
					error: {
						message:
							'Project root is required if tasksJsonPath is not provided',
						code: 'MISSING_PROJECT_ROOT'
					}
				};
			}
			tasksPath = findTasksPath(args, log);
		}

		// Enable silent mode to prevent console output during MCP operation
		enableSilentMode();

		// Call the core moveTask function with file generation control
		const generateFiles = args.generateFiles !== false; // Default to true
		const result = await moveTask(
			tasksPath,
			args.sourceId,
			args.destinationId,
			generateFiles,
			{
				projectRoot,
				tag
			}
		);

		// Restore console output
		disableSilentMode();

		return {
			success: true,
			data: {
				...result,
				message: `Successfully moved task/subtask ${args.sourceId} to ${args.destinationId}`
			}
		};
	} catch (error) {
		// Restore console output in case of error
		disableSilentMode();

		log.error(`Failed to move task: ${error.message}`);

		return {
			success: false,
			error: {
				message: error.message,
				code: 'MOVE_TASK_ERROR'
			}
		};
	}
}

/**
 * context-manager.js
 * Context and cache management for Task Master MCP Server
 */

import { FastMCP } from 'fastmcp';
import { LRUCache } from 'lru-cache';

/**
 * Configuration options for the ContextManager
 * @typedef {Object} ContextManagerConfig
 * @property {number} maxCacheSize - Maximum number of items in the cache
 * @property {number} ttl - Time to live for cached items in milliseconds
 * @property {number} maxContextSize - Maximum size of context window in tokens
 */

export class ContextManager {
	/**
	 * Create a new ContextManager instance
	 * @param {ContextManagerConfig} config - Configuration options
	 */
	constructor(config = {}) {
		this.config = {
			maxCacheSize: config.maxCacheSize || 1000,
			ttl: config.ttl || 1000 * 60 * 5, // 5 minutes default
			maxContextSize: config.maxContextSize || 4000
		};

		// Initialize LRU cache for context data
		this.cache = new LRUCache({
			max: this.config.maxCacheSize,
			ttl: this.config.ttl,
			updateAgeOnGet: true
		});

		// Cache statistics
		this.stats = {
			hits: 0,
			misses: 0,
			invalidations: 0
		};
	}

	/**
	 * Create a new context or retrieve from cache
	 * @param {string} contextId - Unique identifier for the context
	 * @param {Object} metadata - Additional metadata for the context
	 * @returns {Object} Context object with metadata
	 */
	async getContext(contextId, metadata = {}) {
		const cacheKey = this._getCacheKey(contextId, metadata);

		// Try to get from cache first
		const cached = this.cache.get(cacheKey);
		if (cached) {
			this.stats.hits++;
			return cached;
		}

		this.stats.misses++;

		// Create new context if not in cache
		const context = {
			id: contextId,
			metadata: {
				...metadata,
				created: new Date().toISOString()
			}
		};

		// Cache the new context
		this.cache.set(cacheKey, context);

		return context;
	}

	/**
	 * Update an existing context
	 * @param {string} contextId - Context identifier
	 * @param {Object} updates - Updates to apply to the context
	 * @returns {Object} Updated context
	 */
	async updateContext(contextId, updates) {
		const context = await this.getContext(contextId);

		// Apply updates to context
		Object.assign(context.metadata, updates);

		// Update cache
		const cacheKey = this._getCacheKey(contextId, context.metadata);
		this.cache.set(cacheKey, context);

		return context;
	}

	/**
	 * Invalidate a context in the cache
	 * @param {string} contextId - Context identifier
	 * @param {Object} metadata - Metadata used in the cache key
	 */
	invalidateContext(contextId, metadata = {}) {
		const cacheKey = this._getCacheKey(contextId, metadata);
		this.cache.delete(cacheKey);
		this.stats.invalidations++;
	}

	/**
	 * Get cached data associated with a specific key.
	 * Increments cache hit stats if found.
	 * @param {string} key - The cache key.
	 * @returns {any | undefined} The cached data or undefined if not found/expired.
	 */
	getCachedData(key) {
		const cached = this.cache.get(key);
		if (cached !== undefined) {
			// Check for undefined specifically, as null/false might be valid cached values
			this.stats.hits++;
			return cached;
		}
		this.stats.misses++;
		return undefined;
	}

	/**
	 * Set data in the cache with a specific key.
	 * @param {string} key - The cache key.
	 * @param {any} data - The data to cache.
	 */
	setCachedData(key, data) {
		this.cache.set(key, data);
	}

	/**
	 * Invalidate a specific cache key.
	 * Increments invalidation stats.
	 * @param {string} key - The cache key to invalidate.
	 */
	invalidateCacheKey(key) {
		this.cache.delete(key);
		this.stats.invalidations++;
	}

	/**
	 * Get cache statistics
	 * @returns {Object} Cache statistics
	 */
	getStats() {
		return {
			hits: this.stats.hits,
			misses: this.stats.misses,
			invalidations: this.stats.invalidations,
			size: this.cache.size,
			maxSize: this.config.maxCacheSize,
			ttl: this.config.ttl
		};
	}

	/**
	 * Generate a cache key from context ID and metadata
	 * @private
	 * @deprecated No longer used for direct cache key generation outside the manager.
	 *             Prefer generating specific keys in calling functions.
	 */
	_getCacheKey(contextId, metadata) {
		// Kept for potential backward compatibility or internal use if needed later.
		return `${contextId}:${JSON.stringify(metadata)}`;
	}
}

// Export a singleton instance with default config
export const contextManager = new ContextManager();

/**
 * tools/next-task.js
 * Tool to find the next task to work on based on dependencies and status
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { nextTaskDirect } from '../core/task-master-core.js';
import {
	resolveTasksPath,
	resolveComplexityReportPath
} from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the nextTask tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerNextTaskTool(server) {
	server.addTool({
		name: 'next_task',
		description:
			'Find the next task to work on based on dependencies and status',
		parameters: z.object({
			file: z.string().optional().describe('Absolute path to the tasks file'),
			complexityReport: z
				.string()
				.optional()
				.describe(
					'Path to the complexity report file (relative to project root or absolute)'
				),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Finding next task with args: ${JSON.stringify(args)}`);
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});

				// Resolve the path to tasks.json using new path utilities
				let tasksJsonPath;
				try {
					tasksJsonPath = resolveTasksPath(args, session);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Resolve the path to complexity report (optional)
				let complexityReportPath;
				try {
					complexityReportPath = resolveComplexityReportPath(
						{ ...args, tag: resolvedTag },
						session
					);
				} catch (error) {
					log.error(`Error finding complexity report: ${error.message}`);
					// This is optional, so we don't fail the operation
					complexityReportPath = null;
				}

				const result = await nextTaskDirect(
					{
						tasksJsonPath: tasksJsonPath,
						reportPath: complexityReportPath,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log,
					{ session }
				);

				log.info(`Next task result: ${result.success ? 'found' : 'none'}`);
				return handleApiResult(
					result,
					log,
					'Error finding next task',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error finding next task: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

import chalk from 'chalk';
import boxen from 'boxen';

/**
 * Confirm removing profile rules (destructive operation)
 * @param {string[]} profiles - Array of profile names to remove
 * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise
 */
async function confirmProfilesRemove(profiles) {
	const profileList = profiles
		.map((b) => b.charAt(0).toUpperCase() + b.slice(1))
		.join(', ');
	console.log(
		boxen(
			chalk.yellow(
				`WARNING: This will selectively remove Task Master components for: ${profileList}.

What will be removed:
• Task Master specific rule files (e.g., cursor_rules.mdc, taskmaster.mdc, etc.)
• Task Master MCP server configuration (if no other MCP servers exist)

What will be preserved:
• Your existing custom rule files
• Other MCP server configurations
• The profile directory itself (unless completely empty after removal)

The .[profile] directory will only be removed if ALL of the following are true:
• All rules in the directory were Task Master rules (no custom rules)
• No other files or folders exist in the profile directory
• The MCP configuration was completely removed (no other servers)

Are you sure you want to proceed?`
			),
			{ padding: 1, borderColor: 'yellow', borderStyle: 'round' }
		)
	);
	const inquirer = await import('inquirer');
	const { confirm } = await inquirer.default.prompt([
		{
			type: 'confirm',
			name: 'confirm',
			message: 'Type y to confirm selective removal, or n to abort:',
			default: false
		}
	]);
	return confirm;
}

/**
 * Confirm removing ALL remaining profile rules (extremely critical operation)
 * @param {string[]} profiles - Array of profile names to remove
 * @param {string[]} remainingProfiles - Array of profiles that would be left after removal
 * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise
 */
async function confirmRemoveAllRemainingProfiles(profiles, remainingProfiles) {
	const profileList = profiles
		.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
		.join(', ');

	console.log(
		boxen(
			chalk.red.bold(
				`⚠  CRITICAL WARNING: REMOVING ALL TASK MASTER RULE PROFILES ⚠\n\n` +
					`You are about to remove Task Master components for: ${profileList}\n` +
					`This will leave your project with NO Task Master rule profiles remaining!\n\n` +
					`What will be removed:\n` +
					`• All Task Master specific rule files\n` +
					`• Task Master MCP server configurations\n` +
					`• Profile directories (only if completely empty after removal)\n\n` +
					`What will be preserved:\n` +
					`• Your existing custom rule files\n` +
					`• Other MCP server configurations\n` +
					`• Profile directories with custom content\n\n` +
					`This could impact Task Master functionality but will preserve your custom configurations.\n\n` +
					`Are you absolutely sure you want to proceed?`
			),
			{
				padding: 1,
				borderColor: 'red',
				borderStyle: 'double',
				title: '🚨 CRITICAL OPERATION',
				titleAlignment: 'center'
			}
		)
	);

	const inquirer = await import('inquirer');
	const { confirm } = await inquirer.default.prompt([
		{
			type: 'confirm',
			name: 'confirm',
			message:
				'Type y to confirm removing ALL Task Master rule profiles, or n to abort:',
			default: false
		}
	]);
	return confirm;
}

export { confirmProfilesRemove, confirmRemoveAllRemainingProfiles };

// Opencode profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js';

/**
 * Transform standard MCP config format to OpenCode format
 * @param {Object} mcpConfig - Standard MCP configuration object
 * @returns {Object} - Transformed OpenCode configuration object
 */
function transformToOpenCodeFormat(mcpConfig) {
	const openCodeConfig = {
		$schema: 'https://opencode.ai/config.json'
	};

	// Transform mcpServers to mcp
	if (mcpConfig.mcpServers) {
		openCodeConfig.mcp = {};

		for (const [serverName, serverConfig] of Object.entries(
			mcpConfig.mcpServers
		)) {
			// Transform server configuration
			const transformedServer = {
				type: 'local'
			};

			// Combine command and args into single command array
			if (serverConfig.command && serverConfig.args) {
				transformedServer.command = [
					serverConfig.command,
					...serverConfig.args
				];
			} else if (serverConfig.command) {
				transformedServer.command = [serverConfig.command];
			}

			// Add enabled flag
			transformedServer.enabled = true;

			// Transform env to environment
			if (serverConfig.env) {
				transformedServer.environment = serverConfig.env;
			}

			// update with transformed config
			openCodeConfig.mcp[serverName] = transformedServer;
		}
	}

	return openCodeConfig;
}

/**
 * Lifecycle function called after MCP config generation to transform to OpenCode format
 * @param {string} targetDir - Target project directory
 * @param {string} assetsDir - Assets directory (unused for OpenCode)
 */
function onPostConvertRulesProfile(targetDir, assetsDir) {
	const openCodeConfigPath = path.join(targetDir, 'opencode.json');

	if (!fs.existsSync(openCodeConfigPath)) {
		log('debug', '[OpenCode] No opencode.json found to transform');
		return;
	}

	try {
		// Read the generated standard MCP config
		const mcpConfigContent = fs.readFileSync(openCodeConfigPath, 'utf8');
		const mcpConfig = JSON.parse(mcpConfigContent);

		// Check if it's already in OpenCode format (has $schema)
		if (mcpConfig.$schema) {
			log(
				'info',
				'[OpenCode] opencode.json already in OpenCode format, skipping transformation'
			);
			return;
		}

		// Transform to OpenCode format
		const openCodeConfig = transformToOpenCodeFormat(mcpConfig);

		// Write back the transformed config with proper formatting
		fs.writeFileSync(
			openCodeConfigPath,
			JSON.stringify(openCodeConfig, null, 2) + '\n'
		);

		log('info', '[OpenCode] Transformed opencode.json to OpenCode format');
		log(
			'debug',
			`[OpenCode] Added schema, renamed mcpServers->mcp, combined command+args, added type/enabled, renamed env->environment`
		);
	} catch (error) {
		log(
			'error',
			`[OpenCode] Failed to transform opencode.json: ${error.message}`
		);
	}
}

/**
 * Lifecycle function called when removing OpenCode profile
 * @param {string} targetDir - Target project directory
 */
function onRemoveRulesProfile(targetDir) {
	const openCodeConfigPath = path.join(targetDir, 'opencode.json');

	if (!fs.existsSync(openCodeConfigPath)) {
		log('debug', '[OpenCode] No opencode.json found to clean up');
		return;
	}

	try {
		// Read the current config
		const configContent = fs.readFileSync(openCodeConfigPath, 'utf8');
		const config = JSON.parse(configContent);

		// Check if it has the mcp section and taskmaster-ai server
		if (config.mcp && config.mcp['taskmaster-ai']) {
			// Remove taskmaster-ai server
			delete config.mcp['taskmaster-ai'];

			// Check if there are other MCP servers
			const remainingServers = Object.keys(config.mcp);

			if (remainingServers.length === 0) {
				// No other servers, remove entire mcp section
				delete config.mcp;
			}

			// Check if config is now empty (only has $schema)
			const remainingKeys = Object.keys(config).filter(
				(key) => key !== '$schema'
			);

			if (remainingKeys.length === 0) {
				// Config only has schema left, remove entire file
				fs.rmSync(openCodeConfigPath, { force: true });
				log('info', '[OpenCode] Removed empty opencode.json file');
			} else {
				// Write back the modified config
				fs.writeFileSync(
					openCodeConfigPath,
					JSON.stringify(config, null, 2) + '\n'
				);
				log(
					'info',
					'[OpenCode] Removed TaskMaster from opencode.json, preserved other configurations'
				);
			}
		} else {
			log('debug', '[OpenCode] TaskMaster not found in opencode.json');
		}
	} catch (error) {
		log(
			'error',
			`[OpenCode] Failed to clean up opencode.json: ${error.message}`
		);
	}
}

// Create and export opencode profile using the base factory
export const opencodeProfile = createProfile({
	name: 'opencode',
	displayName: 'OpenCode',
	url: 'opencode.ai',
	docsUrl: 'opencode.ai/docs/',
	profileDir: '.', // Root directory
	rulesDir: '.', // Root directory for AGENTS.md
	mcpConfigName: 'opencode.json', // Override default 'mcp.json'
	includeDefaultRules: false,
	fileMap: {
		'AGENTS.md': 'AGENTS.md'
	},
	onPostConvert: onPostConvertRulesProfile,
	onRemove: onRemoveRulesProfile
});

// Export lifecycle functions separately to avoid naming conflicts
export { onPostConvertRulesProfile, onRemoveRulesProfile };

/**
 * google-vertex.js
 * AI provider implementation for Google Vertex AI models using Vercel AI SDK.
 */

import { createVertex } from '@ai-sdk/google-vertex';
import { BaseAIProvider } from './base-provider.js';
import { resolveEnvVariable } from '../../scripts/modules/utils.js';
import { log } from '../../scripts/modules/utils.js';

// Vertex-specific error classes
class VertexAuthError extends Error {
	constructor(message) {
		super(message);
		this.name = 'VertexAuthError';
		this.code = 'vertex_auth_error';
	}
}

class VertexConfigError extends Error {
	constructor(message) {
		super(message);
		this.name = 'VertexConfigError';
		this.code = 'vertex_config_error';
	}
}

class VertexApiError extends Error {
	constructor(message, statusCode) {
		super(message);
		this.name = 'VertexApiError';
		this.code = 'vertex_api_error';
		this.statusCode = statusCode;
	}
}

export class VertexAIProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'Google Vertex AI';
	}

	/**
	 * Returns the required API key environment variable name for Google Vertex AI.
	 * @returns {string} The environment variable name
	 */
	getRequiredApiKeyName() {
		return 'GOOGLE_API_KEY';
	}

	/**
	 * Validates Vertex AI-specific authentication parameters
	 * @param {object} params - Parameters to validate
	 * @throws {Error} If required parameters are missing
	 */
	validateAuth(params) {
		const { apiKey, projectId, location, credentials } = params;

		// Check for API key OR service account credentials
		if (!apiKey && !credentials) {
			throw new VertexAuthError(
				'Either Google API key (GOOGLE_API_KEY) or service account credentials (GOOGLE_APPLICATION_CREDENTIALS) is required for Vertex AI'
			);
		}

		// Project ID is required for Vertex AI
		if (!projectId) {
			throw new VertexConfigError(
				'Google Cloud project ID is required for Vertex AI. Set VERTEX_PROJECT_ID environment variable.'
			);
		}

		// Location is required for Vertex AI
		if (!location) {
			throw new VertexConfigError(
				'Google Cloud location is required for Vertex AI. Set VERTEX_LOCATION environment variable (e.g., "us-central1").'
			);
		}
	}

	/**
	 * Creates and returns a Google Vertex AI client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} [params.apiKey] - Google API key
	 * @param {string} params.projectId - Google Cloud project ID
	 * @param {string} params.location - Google Cloud location (e.g., "us-central1")
	 * @param {object} [params.credentials] - Service account credentials object
	 * @param {string} [params.baseURL] - Optional custom API endpoint
	 * @returns {Function} Google Vertex AI client function
	 * @throws {Error} If required parameters are missing or initialization fails
	 */
	getClient(params) {
		try {
			// Validate required parameters
			this.validateAuth(params);

			const { apiKey, projectId, location, credentials, baseURL } = params;

			// Configure auth options - either API key or service account
			const authOptions = {};
			if (apiKey) {
				authOptions.apiKey = apiKey;
			} else if (credentials) {
				authOptions.googleAuthOptions = credentials;
			}

			// Return Vertex AI client
			return createVertex({
				...authOptions,
				projectId,
				location,
				...(baseURL && { baseURL })
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}

	/**
	 * Handle errors from Vertex AI
	 * @param {string} operation - Description of the operation that failed
	 * @param {Error} error - The error object
	 * @throws {Error} Rethrows the error with additional context
	 */
	handleError(operation, error) {
		log('error', `Vertex AI ${operation} error:`, error);

		// Handle known error types
		if (
			error.name === 'VertexAuthError' ||
			error.name === 'VertexConfigError' ||
			error.name === 'VertexApiError'
		) {
			throw error;
		}

		// Handle network/API errors
		if (error.response) {
			const statusCode = error.response.status;
			const errorMessage = error.response.data?.error?.message || error.message;

			// Categorize by status code
			if (statusCode === 401 || statusCode === 403) {
				throw new VertexAuthError(`Authentication failed: ${errorMessage}`);
			} else if (statusCode === 400) {
				throw new VertexConfigError(`Invalid request: ${errorMessage}`);
			} else {
				throw new VertexApiError(
					`API error (${statusCode}): ${errorMessage}`,
					statusCode
				);
			}
		}

		// Generic error handling
		throw new Error(`Vertex AI ${operation} failed: ${error.message}`);
	}
}

import { FastMCP } from 'fastmcp';
import path from 'path';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import fs from 'fs';
import logger from './logger.js';
import { registerTaskMasterTools } from './tools/index.js';
import ProviderRegistry from '../../src/provider-registry/index.js';
import { MCPProvider } from './providers/mcp-provider.js';

// Load environment variables
dotenv.config();

// Constants
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

/**
 * Main MCP server class that integrates with Task Master
 */
class TaskMasterMCPServer {
	constructor() {
		// Get version from package.json using synchronous fs
		const packagePath = path.join(__dirname, '../../package.json');
		const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));

		this.options = {
			name: 'Task Master MCP Server',
			version: packageJson.version
		};

		this.server = new FastMCP(this.options);
		this.initialized = false;

		// Bind methods
		this.init = this.init.bind(this);
		this.start = this.start.bind(this);
		this.stop = this.stop.bind(this);

		// Setup logging
		this.logger = logger;
	}

	/**
	 * Initialize the MCP server with necessary tools and routes
	 */
	async init() {
		if (this.initialized) return;

		// Pass the manager instance to the tool registration function
		registerTaskMasterTools(this.server, this.asyncManager);

		this.initialized = true;

		return this;
	}

	/**
	 * Start the MCP server
	 */
	async start() {
		if (!this.initialized) {
			await this.init();
		}

		this.server.on('connect', (event) => {
			event.session.server.sendLoggingMessage({
				data: {
					context: event.session.context,
					message: `MCP Server connected: ${event.session.name}`
				},
				level: 'info'
			});
			this.registerRemoteProvider(event.session);
		});

		// Start the FastMCP server with increased timeout
		await this.server.start({
			transportType: 'stdio',
			timeout: 120000 // 2 minutes timeout (in milliseconds)
		});

		return this;
	}

	/**
	 * Register both MCP providers with the provider registry
	 */
	registerRemoteProvider(session) {
		// Check if the server has at least one session
		if (session) {
			// Make sure session has required capabilities
			if (!session.clientCapabilities || !session.clientCapabilities.sampling) {
				session.server.sendLoggingMessage({
					data: {
						context: session.context,
						message: `MCP session missing required sampling capabilities, providers not registered`
					},
					level: 'info'
				});
				return;
			}

			// Register MCP provider with the Provider Registry

			// Register the unified MCP provider
			const mcpProvider = new MCPProvider();
			mcpProvider.setSession(session);

			// Register provider with the registry
			const providerRegistry = ProviderRegistry.getInstance();
			providerRegistry.registerProvider('mcp', mcpProvider);

			session.server.sendLoggingMessage({
				data: {
					context: session.context,
					message: `MCP Server connected`
				},
				level: 'info'
			});
		} else {
			session.server.sendLoggingMessage({
				data: {
					context: session.context,
					message: `No MCP sessions available, providers not registered`
				},
				level: 'warn'
			});
		}
	}

	/**
	 * Stop the MCP server
	 */
	async stop() {
		if (this.server) {
			await this.server.stop();
		}
	}
}

export default TaskMasterMCPServer;

// Zed profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { isSilentMode, log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js';

/**
 * Transform standard MCP config format to Zed format
 * @param {Object} mcpConfig - Standard MCP configuration object
 * @returns {Object} - Transformed Zed configuration object
 */
function transformToZedFormat(mcpConfig) {
	const zedConfig = {};

	// Transform mcpServers to context_servers
	if (mcpConfig.mcpServers) {
		zedConfig['context_servers'] = mcpConfig.mcpServers;
	}

	// Preserve any other existing settings
	for (const [key, value] of Object.entries(mcpConfig)) {
		if (key !== 'mcpServers') {
			zedConfig[key] = value;
		}
	}

	return zedConfig;
}

// Lifecycle functions for Zed profile
function onAddRulesProfile(targetDir, assetsDir) {
	// MCP transformation will be handled in onPostConvertRulesProfile
	// File copying is handled by the base profile via fileMap
}

function onRemoveRulesProfile(targetDir) {
	// Clean up .rules (Zed uses .rules directly in root)
	const userRulesFile = path.join(targetDir, '.rules');

	try {
		// Remove Task Master .rules
		if (fs.existsSync(userRulesFile)) {
			fs.rmSync(userRulesFile, { force: true });
			log('debug', `[Zed] Removed ${userRulesFile}`);
		}
	} catch (err) {
		log('error', `[Zed] Failed to remove Zed instructions: ${err.message}`);
	}

	// MCP Removal: Remove context_servers section
	const mcpConfigPath = path.join(targetDir, '.zed', 'settings.json');

	if (!fs.existsSync(mcpConfigPath)) {
		log('debug', '[Zed] No .zed/settings.json found to clean up');
		return;
	}

	try {
		// Read the current config
		const configContent = fs.readFileSync(mcpConfigPath, 'utf8');
		const config = JSON.parse(configContent);

		// Check if it has the context_servers section and task-master-ai server
		if (
			config['context_servers'] &&
			config['context_servers']['task-master-ai']
		) {
			// Remove task-master-ai server
			delete config['context_servers']['task-master-ai'];

			// Check if there are other MCP servers in context_servers
			const remainingServers = Object.keys(config['context_servers']);

			if (remainingServers.length === 0) {
				// No other servers, remove entire context_servers section
				delete config['context_servers'];
				log('debug', '[Zed] Removed empty context_servers section');
			}

			// Check if config is now empty
			const remainingKeys = Object.keys(config);

			if (remainingKeys.length === 0) {
				// Config is empty, remove entire file
				fs.rmSync(mcpConfigPath, { force: true });
				log('info', '[Zed] Removed empty settings.json file');

				// Check if .zed directory is empty
				const zedDirPath = path.join(targetDir, '.zed');
				if (fs.existsSync(zedDirPath)) {
					const remainingContents = fs.readdirSync(zedDirPath);
					if (remainingContents.length === 0) {
						fs.rmSync(zedDirPath, { recursive: true, force: true });
						log('debug', '[Zed] Removed empty .zed directory');
					}
				}
			} else {
				// Write back the modified config
				fs.writeFileSync(
					mcpConfigPath,
					JSON.stringify(config, null, '\t') + '\n'
				);
				log(
					'info',
					'[Zed] Removed TaskMaster from settings.json, preserved other configurations'
				);
			}
		} else {
			log('debug', '[Zed] TaskMaster not found in context_servers');
		}
	} catch (error) {
		log('error', `[Zed] Failed to clean up settings.json: ${error.message}`);
	}
}

function onPostConvertRulesProfile(targetDir, assetsDir) {
	// Handle .rules setup (same as onAddRulesProfile)
	onAddRulesProfile(targetDir, assetsDir);

	// Transform MCP config to Zed format
	const mcpConfigPath = path.join(targetDir, '.zed', 'settings.json');

	if (!fs.existsSync(mcpConfigPath)) {
		log('debug', '[Zed] No .zed/settings.json found to transform');
		return;
	}

	try {
		// Read the generated standard MCP config
		const mcpConfigContent = fs.readFileSync(mcpConfigPath, 'utf8');
		const mcpConfig = JSON.parse(mcpConfigContent);

		// Check if it's already in Zed format (has context_servers)
		if (mcpConfig['context_servers']) {
			log(
				'info',
				'[Zed] settings.json already in Zed format, skipping transformation'
			);
			return;
		}

		// Transform to Zed format
		const zedConfig = transformToZedFormat(mcpConfig);

		// Write back the transformed config with proper formatting
		fs.writeFileSync(
			mcpConfigPath,
			JSON.stringify(zedConfig, null, '\t') + '\n'
		);

		log('info', '[Zed] Transformed settings.json to Zed format');
		log('debug', '[Zed] Renamed mcpServers to context_servers');
	} catch (error) {
		log('error', `[Zed] Failed to transform settings.json: ${error.message}`);
	}
}

// Create and export zed profile using the base factory
export const zedProfile = createProfile({
	name: 'zed',
	displayName: 'Zed',
	url: 'zed.dev',
	docsUrl: 'zed.dev/docs',
	profileDir: '.zed',
	rulesDir: '.',
	mcpConfig: true,
	mcpConfigName: 'settings.json',
	includeDefaultRules: false,
	fileMap: {
		'AGENTS.md': '.rules'
	},
	onAdd: onAddRulesProfile,
	onRemove: onRemoveRulesProfile,
	onPostConvert: onPostConvertRulesProfile
});

// Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile };

/**
 * tools/add-tag.js
 * Tool to create a new tag
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { addTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';

/**
 * Register the addTag tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerAddTagTool(server) {
	server.addTool({
		name: 'add_tag',
		description: 'Create a new tag for organizing tasks in different contexts',
		parameters: z.object({
			name: z.string().describe('Name of the new tag to create'),
			copyFromCurrent: z
				.boolean()
				.optional()
				.describe(
					'Whether to copy tasks from the current tag (default: false)'
				),
			copyFromTag: z
				.string()
				.optional()
				.describe('Specific tag to copy tasks from'),
			fromBranch: z
				.boolean()
				.optional()
				.describe(
					'Create tag name from current git branch (ignores name parameter)'
				),
			description: z
				.string()
				.optional()
				.describe('Optional description for the tag'),
			file: z
				.string()
				.optional()
				.describe('Path to the tasks file (default: tasks/tasks.json)'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Starting add-tag with args: ${JSON.stringify(args)}`);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Call the direct function
				const result = await addTagDirect(
					{
						tasksJsonPath: tasksJsonPath,
						name: args.name,
						copyFromCurrent: args.copyFromCurrent,
						copyFromTag: args.copyFromTag,
						fromBranch: args.fromBranch,
						description: args.description,
						projectRoot: args.projectRoot
					},
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error creating tag',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in add-tag tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * tools/list-tags.js
 * Tool to list all available tags
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { listTagsDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';

/**
 * Register the listTags tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerListTagsTool(server) {
	server.addTool({
		name: 'list_tags',
		description: 'List all available tags with task counts and metadata',
		parameters: z.object({
			showMetadata: z
				.boolean()
				.optional()
				.describe('Whether to include metadata in the output (default: false)'),
			file: z
				.string()
				.optional()
				.describe('Path to the tasks file (default: tasks/tasks.json)'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Starting list-tags with args: ${JSON.stringify(args)}`);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Call the direct function
				const result = await listTagsDirect(
					{
						tasksJsonPath: tasksJsonPath,
						showMetadata: args.showMetadata,
						projectRoot: args.projectRoot
					},
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error listing tags',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in list-tags tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * tools/use-tag.js
 * Tool to switch to a different tag context
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { useTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';

/**
 * Register the useTag tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerUseTagTool(server) {
	server.addTool({
		name: 'use_tag',
		description: 'Switch to a different tag context for task operations',
		parameters: z.object({
			name: z.string().describe('Name of the tag to switch to'),
			file: z
				.string()
				.optional()
				.describe('Path to the tasks file (default: tasks/tasks.json)'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Starting use-tag with args: ${JSON.stringify(args)}`);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Call the direct function
				const result = await useTagDirect(
					{
						tasksJsonPath: tasksJsonPath,
						name: args.name,
						projectRoot: args.projectRoot
					},
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error switching tag',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in use-tag tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * tools/rename-tag.js
 * Tool to rename an existing tag
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { renameTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';

/**
 * Register the renameTag tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerRenameTagTool(server) {
	server.addTool({
		name: 'rename_tag',
		description: 'Rename an existing tag',
		parameters: z.object({
			oldName: z.string().describe('Current name of the tag to rename'),
			newName: z.string().describe('New name for the tag'),
			file: z
				.string()
				.optional()
				.describe('Path to the tasks file (default: tasks/tasks.json)'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Starting rename-tag with args: ${JSON.stringify(args)}`);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Call the direct function
				const result = await renameTagDirect(
					{
						tasksJsonPath: tasksJsonPath,
						oldName: args.oldName,
						newName: args.newName,
						projectRoot: args.projectRoot
					},
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error renaming tag',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in rename-tag tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * tools/validate-dependencies.js
 * Tool for validating task dependencies
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { validateDependenciesDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';

/**
 * Register the validateDependencies tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerValidateDependenciesTool(server) {
	server.addTool({
		name: 'validate_dependencies',
		description:
			'Check tasks for dependency issues (like circular references or links to non-existent tasks) without making changes.',
		parameters: z.object({
			file: z.string().optional().describe('Absolute path to the tasks file'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});
				log.info(`Validating dependencies with args: ${JSON.stringify(args)}`);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				const result = await validateDependenciesDirect(
					{
						tasksJsonPath: tasksJsonPath,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log
				);

				if (result.success) {
					log.info(
						`Successfully validated dependencies: ${result.data.message}`
					);
				} else {
					log.error(`Failed to validate dependencies: ${result.error.message}`);
				}

				return handleApiResult(
					result,
					log,
					'Error validating dependencies',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in validateDependencies tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * Direct function wrapper for removeSubtask
 */

import { removeSubtask } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';

/**
 * Remove a subtask from its parent task
 * @param {Object} args - Function arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} args.id - Subtask ID in format "parentId.subtaskId" (required)
 * @param {boolean} [args.convert] - Whether to convert the subtask to a standalone task
 * @param {boolean} [args.skipGenerate] - Skip regenerating task files
 * @param {string} args.projectRoot - Project root path (for MCP/env fallback)
 * @param {string} args.tag - Tag for the task (optional)
 * @param {Object} log - Logger object
 * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
 */
export async function removeSubtaskDirect(args, log) {
	// Destructure expected args
	const { tasksJsonPath, id, convert, skipGenerate, projectRoot, tag } = args;
	try {
		// Enable silent mode to prevent console logs from interfering with JSON response
		enableSilentMode();

		log.info(`Removing subtask with args: ${JSON.stringify(args)}`);

		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('removeSubtaskDirect called without tasksJsonPath');
			disableSilentMode(); // Disable before returning
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		if (!id) {
			disableSilentMode(); // Disable before returning
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message:
						'Subtask ID is required and must be in format "parentId.subtaskId"'
				}
			};
		}

		// Validate subtask ID format
		if (!id.includes('.')) {
			disableSilentMode(); // Disable before returning
			return {
				success: false,
				error: {
					code: 'INPUT_VALIDATION_ERROR',
					message: `Invalid subtask ID format: ${id}. Expected format: "parentId.subtaskId"`
				}
			};
		}

		// Use provided path
		const tasksPath = tasksJsonPath;

		// Convert convertToTask to a boolean
		const convertToTask = convert === true;

		// Determine if we should generate files
		const generateFiles = !skipGenerate;

		log.info(
			`Removing subtask ${id} (convertToTask: ${convertToTask}, generateFiles: ${generateFiles})`
		);

		// Use the provided tasksPath
		const result = await removeSubtask(
			tasksPath,
			id,
			convertToTask,
			generateFiles,
			{
				projectRoot,
				tag
			}
		);

		// Restore normal logging
		disableSilentMode();

		if (convertToTask && result) {
			// Return info about the converted task
			return {
				success: true,
				data: {
					message: `Subtask ${id} successfully converted to task #${result.id}`,
					task: result
				}
			};
		} else {
			// Return simple success message for deletion
			return {
				success: true,
				data: {
					message: `Subtask ${id} successfully removed`
				}
			};
		}
	} catch (error) {
		// Ensure silent mode is disabled even if an outer error occurs
		disableSilentMode();

		log.error(`Error in removeSubtaskDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'CORE_FUNCTION_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * tools/delete-tag.js
 * Tool to delete an existing tag
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { deleteTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';

/**
 * Register the deleteTag tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerDeleteTagTool(server) {
	server.addTool({
		name: 'delete_tag',
		description: 'Delete an existing tag and all its tasks',
		parameters: z.object({
			name: z.string().describe('Name of the tag to delete'),
			yes: z
				.boolean()
				.optional()
				.describe('Skip confirmation prompts (default: true for MCP)'),
			file: z
				.string()
				.optional()
				.describe('Path to the tasks file (default: tasks/tasks.json)'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Starting delete-tag with args: ${JSON.stringify(args)}`);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Call the direct function (always skip confirmation for MCP)
				const result = await deleteTagDirect(
					{
						tasksJsonPath: tasksJsonPath,
						name: args.name,
						yes: args.yes !== undefined ? args.yes : true, // Default to true for MCP
						projectRoot: args.projectRoot
					},
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error deleting tag',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in delete-tag tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * tools/fix-dependencies.js
 * Tool for automatically fixing invalid task dependencies
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { fixDependenciesDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
 * Register the fixDependencies tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerFixDependenciesTool(server) {
	server.addTool({
		name: 'fix_dependencies',
		description: 'Fix invalid dependencies in tasks automatically',
		parameters: z.object({
			file: z.string().optional().describe('Absolute path to the tasks file'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			tag: z.string().optional().describe('Tag context to operate on')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Fixing dependencies with args: ${JSON.stringify(args)}`);

				const resolvedTag = resolveTag({
					projectRoot: args.projectRoot,
					tag: args.tag
				});

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				const result = await fixDependenciesDirect(
					{
						tasksJsonPath: tasksJsonPath,
						projectRoot: args.projectRoot,
						tag: resolvedTag
					},
					log
				);

				if (result.success) {
					log.info(`Successfully fixed dependencies: ${result.data.message}`);
				} else {
					log.error(`Failed to fix dependencies: ${result.error.message}`);
				}

				return handleApiResult(
					result,
					log,
					'Error fixing dependencies',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in fixDependencies tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * tools/copy-tag.js
 * Tool to copy an existing tag to a new tag
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { copyTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';

/**
 * Register the copyTag tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerCopyTagTool(server) {
	server.addTool({
		name: 'copy_tag',
		description:
			'Copy an existing tag to create a new tag with all tasks and metadata',
		parameters: z.object({
			sourceName: z.string().describe('Name of the source tag to copy from'),
			targetName: z.string().describe('Name of the new tag to create'),
			description: z
				.string()
				.optional()
				.describe('Optional description for the new tag'),
			file: z
				.string()
				.optional()
				.describe('Path to the tasks file (default: tasks/tasks.json)'),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Starting copy-tag with args: ${JSON.stringify(args)}`);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				let tasksJsonPath;
				try {
					tasksJsonPath = findTasksPath(
						{ projectRoot: args.projectRoot, file: args.file },
						log
					);
				} catch (error) {
					log.error(`Error finding tasks.json: ${error.message}`);
					return createErrorResponse(
						`Failed to find tasks.json: ${error.message}`
					);
				}

				// Call the direct function
				const result = await copyTagDirect(
					{
						tasksJsonPath: tasksJsonPath,
						sourceName: args.sourceName,
						targetName: args.targetName,
						description: args.description,
						projectRoot: args.projectRoot
					},
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error copying tag',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in copy-tag tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * add-tag.js
 * Direct function implementation for creating a new tag
 */

import {
	createTag,
	createTagFromBranch
} from '../../../../scripts/modules/task-manager/tag-management.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for creating a new tag with error handling.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.name - Name of the new tag to create
 * @param {boolean} [args.copyFromCurrent=false] - Whether to copy tasks from current tag
 * @param {string} [args.copyFromTag] - Specific tag to copy tasks from
 * @param {boolean} [args.fromBranch=false] - Create tag name from current git branch
 * @param {string} [args.description] - Optional description for the tag
 * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
 * @param {string} [args.projectRoot] - Project root path
 * @param {Object} log - Logger object
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function addTagDirect(args, log, context = {}) {
	// Destructure expected args
	const {
		tasksJsonPath,
		name,
		copyFromCurrent = false,
		copyFromTag,
		fromBranch = false,
		description,
		projectRoot
	} = args;
	const { session } = context;

	// Enable silent mode to prevent console logs from interfering with JSON response
	enableSilentMode();

	// Create logger wrapper using the utility
	const mcpLog = createLogWrapper(log);

	try {
		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('addTagDirect called without tasksJsonPath');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Handle --from-branch option
		if (fromBranch) {
			log.info('Creating tag from current git branch');

			// Import git utilities
			const gitUtils = await import(
				'../../../../scripts/modules/utils/git-utils.js'
			);

			// Check if we're in a git repository
			if (!(await gitUtils.isGitRepository(projectRoot))) {
				log.error('Not in a git repository');
				disableSilentMode();
				return {
					success: false,
					error: {
						code: 'NOT_GIT_REPO',
						message: 'Not in a git repository. Cannot use fromBranch option.'
					}
				};
			}

			// Get current git branch
			const currentBranch = await gitUtils.getCurrentBranch(projectRoot);
			if (!currentBranch) {
				log.error('Could not determine current git branch');
				disableSilentMode();
				return {
					success: false,
					error: {
						code: 'NO_CURRENT_BRANCH',
						message: 'Could not determine current git branch.'
					}
				};
			}

			// Prepare options for branch-based tag creation
			const branchOptions = {
				copyFromCurrent,
				copyFromTag,
				description:
					description || `Tag created from git branch "${currentBranch}"`
			};

			// Call the createTagFromBranch function
			const result = await createTagFromBranch(
				tasksJsonPath,
				currentBranch,
				branchOptions,
				{
					session,
					mcpLog,
					projectRoot
				},
				'json' // outputFormat - use 'json' to suppress CLI UI
			);

			// Restore normal logging
			disableSilentMode();

			return {
				success: true,
				data: {
					branchName: result.branchName,
					tagName: result.tagName,
					created: result.created,
					mappingUpdated: result.mappingUpdated,
					message: `Successfully created tag "${result.tagName}" from git branch "${result.branchName}"`
				}
			};
		} else {
			// Check required parameters for regular tag creation
			if (!name || typeof name !== 'string') {
				log.error('Missing required parameter: name');
				disableSilentMode();
				return {
					success: false,
					error: {
						code: 'MISSING_PARAMETER',
						message: 'Tag name is required and must be a string'
					}
				};
			}

			log.info(`Creating new tag: ${name}`);

			// Prepare options
			const options = {
				copyFromCurrent,
				copyFromTag,
				description
			};

			// Call the createTag function
			const result = await createTag(
				tasksJsonPath,
				name,
				options,
				{
					session,
					mcpLog,
					projectRoot
				},
				'json' // outputFormat - use 'json' to suppress CLI UI
			);

			// Restore normal logging
			disableSilentMode();

			return {
				success: true,
				data: {
					tagName: result.tagName,
					created: result.created,
					tasksCopied: result.tasksCopied,
					sourceTag: result.sourceTag,
					description: result.description,
					message: `Successfully created tag "${result.tagName}"`
				}
			};
		}
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in addTagDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: error.code || 'ADD_TAG_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * models.js
 * MCP tool for managing AI model configurations
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { modelsDirect } from '../core/task-master-core.js';

/**
 * Register the models tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerModelsTool(server) {
	server.addTool({
		name: 'models',
		description:
			'Get information about available AI models or set model configurations. Run without arguments to get the current model configuration and API key status for the selected model providers.',
		parameters: z.object({
			setMain: z
				.string()
				.optional()
				.describe(
					'Set the primary model for task generation/updates. Model provider API key is required in the MCP config ENV.'
				),
			setResearch: z
				.string()
				.optional()
				.describe(
					'Set the model for research-backed operations. Model provider API key is required in the MCP config ENV.'
				),
			setFallback: z
				.string()
				.optional()
				.describe(
					'Set the model to use if the primary fails. Model provider API key is required in the MCP config ENV.'
				),
			listAvailableModels: z
				.boolean()
				.optional()
				.describe(
					'List all available models not currently in use. Input/output costs values are in dollars (3 is $3.00).'
				),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.'),
			openrouter: z
				.boolean()
				.optional()
				.describe('Indicates the set model ID is a custom OpenRouter model.'),
			ollama: z
				.boolean()
				.optional()
				.describe('Indicates the set model ID is a custom Ollama model.'),
			bedrock: z
				.boolean()
				.optional()
				.describe('Indicates the set model ID is a custom AWS Bedrock model.'),
			azure: z
				.boolean()
				.optional()
				.describe('Indicates the set model ID is a custom Azure OpenAI model.'),
			vertex: z
				.boolean()
				.optional()
				.describe(
					'Indicates the set model ID is a custom Google Vertex AI model.'
				)
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(`Starting models tool with args: ${JSON.stringify(args)}`);

				// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
				const result = await modelsDirect(
					{ ...args, projectRoot: args.projectRoot },
					log,
					{ session }
				);

				return handleApiResult(
					result,
					log,
					'Error managing models',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in models tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

import path from 'path';
import {
	findTasksPath as coreFindTasksPath,
	findPRDPath as coreFindPrdPath,
	findComplexityReportPath as coreFindComplexityReportPath,
	findProjectRoot as coreFindProjectRoot,
	normalizeProjectRoot
} from '../../../../src/utils/path-utils.js';
import { PROJECT_MARKERS } from '../../../../src/constants/paths.js';

/**
 * MCP-specific path utilities that extend core path utilities with session support
 * This module handles session-specific path resolution for the MCP server
 */

/**
 * Silent logger for MCP context to prevent console output
 */
const silentLogger = {
	info: () => {},
	warn: () => {},
	error: () => {},
	debug: () => {},
	success: () => {}
};

/**
 * Cache for last found project root to improve performance
 */
export const lastFoundProjectRoot = null;

/**
 * Find PRD file with MCP support
 * @param {string} [explicitPath] - Explicit path to PRD file (highest priority)
 * @param {Object} [args] - Arguments object for context
 * @param {Object} [log] - Logger object to prevent console logging
 * @returns {string|null} - Resolved path to PRD file or null if not found
 */
export function findPrdPath(explicitPath, args = null, log = silentLogger) {
	return coreFindPrdPath(explicitPath, args, log);
}

/**
 * Resolve tasks.json path from arguments
 * Prioritizes explicit path parameter, then uses fallback logic
 * @param {Object} args - Arguments object containing projectRoot and optional file path
 * @param {Object} [log] - Logger object to prevent console logging
 * @returns {string|null} - Resolved path to tasks.json or null if not found
 */
export function resolveTasksPath(args, log = silentLogger) {
	// Get explicit path from args.file if provided
	const explicitPath = args?.file;
	const rawProjectRoot = args?.projectRoot;

	// If explicit path is provided and absolute, use it directly
	if (explicitPath && path.isAbsolute(explicitPath)) {
		return explicitPath;
	}

	// Normalize project root if provided
	const projectRoot = rawProjectRoot
		? normalizeProjectRoot(rawProjectRoot)
		: null;

	// If explicit path is relative, resolve it relative to normalized projectRoot
	if (explicitPath && projectRoot) {
		return path.resolve(projectRoot, explicitPath);
	}

	// Use core findTasksPath with explicit path and normalized projectRoot context
	if (projectRoot) {
		return coreFindTasksPath(explicitPath, { projectRoot }, log);
	}

	// Fallback to core function without projectRoot context
	return coreFindTasksPath(explicitPath, null, log);
}

/**
 * Resolve PRD path from arguments
 * @param {Object} args - Arguments object containing projectRoot and optional input path
 * @param {Object} [log] - Logger object to prevent console logging
 * @returns {string|null} - Resolved path to PRD file or null if not found
 */
export function resolvePrdPath(args, log = silentLogger) {
	// Get explicit path from args.input if provided
	const explicitPath = args?.input;
	const rawProjectRoot = args?.projectRoot;

	// If explicit path is provided and absolute, use it directly
	if (explicitPath && path.isAbsolute(explicitPath)) {
		return explicitPath;
	}

	// Normalize project root if provided
	const projectRoot = rawProjectRoot
		? normalizeProjectRoot(rawProjectRoot)
		: null;

	// If explicit path is relative, resolve it relative to normalized projectRoot
	if (explicitPath && projectRoot) {
		return path.resolve(projectRoot, explicitPath);
	}

	// Use core findPRDPath with explicit path and normalized projectRoot context
	if (projectRoot) {
		return coreFindPrdPath(explicitPath, { projectRoot }, log);
	}

	// Fallback to core function without projectRoot context
	return coreFindPrdPath(explicitPath, null, log);
}

/**
 * Resolve complexity report path from arguments
 * @param {Object} args - Arguments object containing projectRoot and optional complexityReport path
 * @param {Object} [log] - Logger object to prevent console logging
 * @returns {string|null} - Resolved path to complexity report or null if not found
 */
export function resolveComplexityReportPath(args, log = silentLogger) {
	// Get explicit path from args.complexityReport if provided
	const explicitPath = args?.complexityReport;
	const rawProjectRoot = args?.projectRoot;
	const tag = args?.tag;

	// If explicit path is provided and absolute, use it directly
	if (explicitPath && path.isAbsolute(explicitPath)) {
		return explicitPath;
	}

	// Normalize project root if provided
	const projectRoot = rawProjectRoot
		? normalizeProjectRoot(rawProjectRoot)
		: null;

	// If explicit path is relative, resolve it relative to normalized projectRoot
	if (explicitPath && projectRoot) {
		return path.resolve(projectRoot, explicitPath);
	}

	// Use core findComplexityReportPath with explicit path and normalized projectRoot context
	if (projectRoot) {
		return coreFindComplexityReportPath(
			explicitPath,
			{ projectRoot, tag },
			log
		);
	}

	// Fallback to core function without projectRoot context
	return coreFindComplexityReportPath(explicitPath, null, log);
}

/**
 * Resolve any project-relative path from arguments
 * @param {string} relativePath - Relative path to resolve
 * @param {Object} args - Arguments object containing projectRoot
 * @returns {string} - Resolved absolute path
 */
export function resolveProjectPath(relativePath, args) {
	// Ensure we have a projectRoot from args
	if (!args?.projectRoot) {
		throw new Error('projectRoot is required in args to resolve project paths');
	}

	// Normalize the project root to prevent double .taskmaster paths
	const projectRoot = normalizeProjectRoot(args.projectRoot);

	// If already absolute, return as-is
	if (path.isAbsolute(relativePath)) {
		return relativePath;
	}

	// Resolve relative to normalized projectRoot
	return path.resolve(projectRoot, relativePath);
}

/**
 * Find project root using core utility
 * @param {string} [startDir] - Directory to start searching from
 * @returns {string|null} - Project root path or null if not found
 */
export function findProjectRoot(startDir) {
	return coreFindProjectRoot(startDir);
}

// MAIN EXPORTS FOR MCP TOOLS - these are the functions MCP tools should use

/**
 * Find tasks.json path from arguments - primary MCP function
 * @param {Object} args - Arguments object containing projectRoot and optional file path
 * @param {Object} [log] - Log function to prevent console logging
 * @returns {string|null} - Resolved path to tasks.json or null if not found
 */
export function findTasksPath(args, log = silentLogger) {
	return resolveTasksPath(args, log);
}

/**
 * Find complexity report path from arguments - primary MCP function
 * @param {Object} args - Arguments object containing projectRoot and optional complexityReport path
 * @param {Object} [log] - Log function to prevent console logging
 * @returns {string|null} - Resolved path to complexity report or null if not found
 */
export function findComplexityReportPath(args, log = silentLogger) {
	return resolveComplexityReportPath(args, log);
}

/**
 * Find PRD path - primary MCP function
 * @param {string} [explicitPath] - Explicit path to PRD file
 * @param {Object} [args] - Arguments object for context (not used in current implementation)
 * @param {Object} [log] - Logger object to prevent console logging
 * @returns {string|null} - Resolved path to PRD file or null if not found
 */
export function findPRDPath(explicitPath, args = null, log = silentLogger) {
	return findPrdPath(explicitPath, args, log);
}

// Legacy aliases for backward compatibility - DEPRECATED
export const findTasksJsonPath = findTasksPath;
export const findComplexityReportJsonPath = findComplexityReportPath;

// Re-export PROJECT_MARKERS for MCP tools that import it from this module
export { PROJECT_MARKERS };

// Roo Code conversion profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { isSilentMode, log } from '../../scripts/modules/utils.js';
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';
import { ROO_MODES } from '../constants/profiles.js';

// Lifecycle functions for Roo profile
function onAddRulesProfile(targetDir, assetsDir) {
	// Use the provided assets directory to find the roocode directory
	const sourceDir = path.join(assetsDir, 'roocode');

	if (!fs.existsSync(sourceDir)) {
		log('error', `[Roo] Source directory does not exist: ${sourceDir}`);
		return;
	}

	copyRecursiveSync(sourceDir, targetDir);
	log('debug', `[Roo] Copied roocode directory to ${targetDir}`);

	const rooModesDir = path.join(sourceDir, '.roo');

	// Copy .roomodes to project root
	const roomodesSrc = path.join(sourceDir, '.roomodes');
	const roomodesDest = path.join(targetDir, '.roomodes');
	if (fs.existsSync(roomodesSrc)) {
		try {
			fs.copyFileSync(roomodesSrc, roomodesDest);
			log('debug', `[Roo] Copied .roomodes to ${roomodesDest}`);
		} catch (err) {
			log('error', `[Roo] Failed to copy .roomodes: ${err.message}`);
		}
	}

	for (const mode of ROO_MODES) {
		const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`);
		const dest = path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`);
		if (fs.existsSync(src)) {
			try {
				const destDir = path.dirname(dest);
				if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
				fs.copyFileSync(src, dest);
				log('debug', `[Roo] Copied ${mode}-rules to ${dest}`);
			} catch (err) {
				log('error', `[Roo] Failed to copy ${src} to ${dest}: ${err.message}`);
			}
		}
	}
}

function copyRecursiveSync(src, dest) {
	const exists = fs.existsSync(src);
	const stats = exists && fs.statSync(src);
	const isDirectory = exists && stats.isDirectory();
	if (isDirectory) {
		if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
		fs.readdirSync(src).forEach((childItemName) => {
			copyRecursiveSync(
				path.join(src, childItemName),
				path.join(dest, childItemName)
			);
		});
	} else {
		fs.copyFileSync(src, dest);
	}
}

function onRemoveRulesProfile(targetDir) {
	const roomodesPath = path.join(targetDir, '.roomodes');
	if (fs.existsSync(roomodesPath)) {
		try {
			fs.rmSync(roomodesPath, { force: true });
			log('debug', `[Roo] Removed .roomodes from ${roomodesPath}`);
		} catch (err) {
			log('error', `[Roo] Failed to remove .roomodes: ${err.message}`);
		}
	}

	const rooDir = path.join(targetDir, '.roo');
	if (fs.existsSync(rooDir)) {
		fs.readdirSync(rooDir).forEach((entry) => {
			if (entry.startsWith('rules-')) {
				const modeDir = path.join(rooDir, entry);
				try {
					fs.rmSync(modeDir, { recursive: true, force: true });
					log('debug', `[Roo] Removed ${entry} directory from ${modeDir}`);
				} catch (err) {
					log('error', `[Roo] Failed to remove ${modeDir}: ${err.message}`);
				}
			}
		});
		if (fs.readdirSync(rooDir).length === 0) {
			try {
				fs.rmSync(rooDir, { recursive: true, force: true });
				log('debug', `[Roo] Removed empty .roo directory from ${rooDir}`);
			} catch (err) {
				log('error', `[Roo] Failed to remove .roo directory: ${err.message}`);
			}
		}
	}
}

function onPostConvertRulesProfile(targetDir, assetsDir) {
	onAddRulesProfile(targetDir, assetsDir);
}

// Create and export roo profile using the base factory
export const rooProfile = createProfile({
	name: 'roo',
	displayName: 'Roo Code',
	url: 'roocode.com',
	docsUrl: 'docs.roocode.com',
	toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE,
	onAdd: onAddRulesProfile,
	onRemove: onRemoveRulesProfile,
	onPostConvert: onPostConvertRulesProfile
});

// Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile };

import { jest } from '@jest/globals';
import { ContextManager } from '../context-manager.js';

describe('ContextManager', () => {
	let contextManager;

	beforeEach(() => {
		contextManager = new ContextManager({
			maxCacheSize: 10,
			ttl: 1000, // 1 second for testing
			maxContextSize: 1000
		});
	});

	describe('getContext', () => {
		it('should create a new context when not in cache', async () => {
			const context = await contextManager.getContext('test-id', {
				test: true
			});
			expect(context.id).toBe('test-id');
			expect(context.metadata.test).toBe(true);
			expect(contextManager.stats.misses).toBe(1);
			expect(contextManager.stats.hits).toBe(0);
		});

		it('should return cached context when available', async () => {
			// First call creates the context
			await contextManager.getContext('test-id', { test: true });

			// Second call should hit cache
			const context = await contextManager.getContext('test-id', {
				test: true
			});
			expect(context.id).toBe('test-id');
			expect(context.metadata.test).toBe(true);
			expect(contextManager.stats.hits).toBe(1);
			expect(contextManager.stats.misses).toBe(1);
		});

		it('should respect TTL settings', async () => {
			// Create context
			await contextManager.getContext('test-id', { test: true });

			// Wait for TTL to expire
			await new Promise((resolve) => setTimeout(resolve, 1100));

			// Should create new context
			await contextManager.getContext('test-id', { test: true });
			expect(contextManager.stats.misses).toBe(2);
			expect(contextManager.stats.hits).toBe(0);
		});
	});

	describe('updateContext', () => {
		it('should update existing context metadata', async () => {
			await contextManager.getContext('test-id', { initial: true });
			const updated = await contextManager.updateContext('test-id', {
				updated: true
			});

			expect(updated.metadata.initial).toBe(true);
			expect(updated.metadata.updated).toBe(true);
		});
	});

	describe('invalidateContext', () => {
		it('should remove context from cache', async () => {
			await contextManager.getContext('test-id', { test: true });
			contextManager.invalidateContext('test-id', { test: true });

			// Should be a cache miss
			await contextManager.getContext('test-id', { test: true });
			expect(contextManager.stats.invalidations).toBe(1);
			expect(contextManager.stats.misses).toBe(2);
		});
	});

	describe('getStats', () => {
		it('should return current cache statistics', async () => {
			await contextManager.getContext('test-id', { test: true });
			const stats = contextManager.getStats();

			expect(stats.hits).toBe(0);
			expect(stats.misses).toBe(1);
			expect(stats.invalidations).toBe(0);
			expect(stats.size).toBe(1);
			expect(stats.maxSize).toBe(10);
			expect(stats.ttl).toBe(1000);
		});
	});
});

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { initializeProjectDirect } from '../core/task-master-core.js';
import { RULE_PROFILES } from '../../../src/constants/profiles.js';

export function registerInitializeProjectTool(server) {
	server.addTool({
		name: 'initialize_project',
		description:
			'Initializes a new Task Master project structure by calling the core initialization logic. Creates necessary folders and configuration files for Task Master in the current directory.',
		parameters: z.object({
			skipInstall: z
				.boolean()
				.optional()
				.default(false)
				.describe(
					'Skip installing dependencies automatically. Never do this unless you are sure the project is already installed.'
				),
			addAliases: z
				.boolean()
				.optional()
				.default(true)
				.describe('Add shell aliases (tm, taskmaster) to shell config file.'),
			initGit: z
				.boolean()
				.optional()
				.default(true)
				.describe('Initialize Git repository in project root.'),
			storeTasksInGit: z
				.boolean()
				.optional()
				.default(true)
				.describe('Store tasks in Git (tasks.json and tasks/ directory).'),
			yes: z
				.boolean()
				.optional()
				.default(true)
				.describe(
					'Skip prompts and use default values. Always set to true for MCP tools.'
				),
			projectRoot: z
				.string()
				.describe(
					'The root directory for the project. ALWAYS SET THIS TO THE PROJECT ROOT DIRECTORY. IF NOT SET, THE TOOL WILL NOT WORK.'
				),
			rules: z
				.array(z.enum(RULE_PROFILES))
				.optional()
				.describe(
					`List of rule profiles to include at initialization. If omitted, defaults to Cursor profile only. Available options: ${RULE_PROFILES.join(', ')}`
				)
		}),
		execute: withNormalizedProjectRoot(async (args, context) => {
			const { log } = context;
			const session = context.session;

			try {
				log.info(
					`Executing initialize_project tool with args: ${JSON.stringify(args)}`
				);

				const result = await initializeProjectDirect(args, log, { session });

				return handleApiResult(
					result,
					log,
					'Initialization failed',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				const errorMessage = `Project initialization tool failed: ${error.message || 'Unknown error'}`;
				log.error(errorMessage, error);
				return createErrorResponse(errorMessage, { details: error.stack });
			}
		})
	});
}

/**
 * create-tag-from-branch.js
 * Direct function implementation for creating tags from git branches
 */

import { createTagFromBranch } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
	getCurrentBranch,
	isGitRepository
} from '../../../../scripts/modules/utils/git-utils.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for creating tags from git branches with error handling.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.tasksJsonPath - Path to the tasks.json file (resolved by tool)
 * @param {string} [args.branchName] - Git branch name (optional, uses current branch if not provided)
 * @param {boolean} [args.copyFromCurrent] - Copy tasks from current tag
 * @param {string} [args.copyFromTag] - Copy tasks from specific tag
 * @param {string} [args.description] - Custom description for the tag
 * @param {boolean} [args.autoSwitch] - Automatically switch to the new tag
 * @param {string} [args.projectRoot] - Project root path
 * @param {Object} log - Logger object
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function createTagFromBranchDirect(args, log, context = {}) {
	// Destructure expected args
	const {
		tasksJsonPath,
		branchName,
		copyFromCurrent,
		copyFromTag,
		description,
		autoSwitch,
		projectRoot
	} = args;
	const { session } = context;

	// Enable silent mode to prevent console logs from interfering with JSON response
	enableSilentMode();

	// Create logger wrapper using the utility
	const mcpLog = createLogWrapper(log);

	try {
		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('createTagFromBranchDirect called without tasksJsonPath');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Check if projectRoot was provided
		if (!projectRoot) {
			log.error('createTagFromBranchDirect called without projectRoot');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'projectRoot is required'
				}
			};
		}

		// Check if we're in a git repository
		if (!(await isGitRepository(projectRoot))) {
			log.error('Not in a git repository');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'NOT_GIT_REPOSITORY',
					message: 'Not in a git repository. Cannot create tag from branch.'
				}
			};
		}

		// Determine branch name
		let targetBranch = branchName;
		if (!targetBranch) {
			targetBranch = await getCurrentBranch(projectRoot);
			if (!targetBranch) {
				log.error('Could not determine current git branch');
				disableSilentMode();
				return {
					success: false,
					error: {
						code: 'NO_CURRENT_BRANCH',
						message: 'Could not determine current git branch'
					}
				};
			}
		}

		log.info(`Creating tag from git branch: ${targetBranch}`);

		// Prepare options
		const options = {
			copyFromCurrent: copyFromCurrent || false,
			copyFromTag,
			description:
				description || `Tag created from git branch "${targetBranch}"`,
			autoSwitch: autoSwitch || false
		};

		// Call the createTagFromBranch function
		const result = await createTagFromBranch(
			tasksJsonPath,
			targetBranch,
			options,
			{
				session,
				mcpLog,
				projectRoot
			},
			'json' // outputFormat - use 'json' to suppress CLI UI
		);

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				branchName: result.branchName,
				tagName: result.tagName,
				created: result.created,
				mappingUpdated: result.mappingUpdated,
				autoSwitched: result.autoSwitched,
				message: `Successfully created tag "${result.tagName}" from branch "${result.branchName}"`
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in createTagFromBranchDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: error.code || 'CREATE_TAG_FROM_BRANCH_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * use-tag.js
 * Direct function implementation for switching to a tag
 */

import { useTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for switching to a tag with error handling.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.name - Name of the tag to switch to
 * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
 * @param {string} [args.projectRoot] - Project root path
 * @param {Object} log - Logger object
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function useTagDirect(args, log, context = {}) {
	// Destructure expected args
	const { tasksJsonPath, name, projectRoot } = args;
	const { session } = context;

	// Enable silent mode to prevent console logs from interfering with JSON response
	enableSilentMode();

	// Create logger wrapper using the utility
	const mcpLog = createLogWrapper(log);

	try {
		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('useTagDirect called without tasksJsonPath');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Check required parameters
		if (!name || typeof name !== 'string') {
			log.error('Missing required parameter: name');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_PARAMETER',
					message: 'Tag name is required and must be a string'
				}
			};
		}

		log.info(`Switching to tag: ${name}`);

		// Call the useTag function
		const result = await useTag(
			tasksJsonPath,
			name,
			{}, // options (empty for now)
			{
				session,
				mcpLog,
				projectRoot
			},
			'json' // outputFormat - use 'json' to suppress CLI UI
		);

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				tagName: result.currentTag,
				switched: result.switched,
				previousTag: result.previousTag,
				taskCount: result.taskCount,
				message: `Successfully switched to tag "${result.currentTag}"`
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in useTagDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: error.code || 'USE_TAG_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * copy-tag.js
 * Direct function implementation for copying a tag
 */

import { copyTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for copying a tag with error handling.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.sourceName - Name of the source tag to copy from
 * @param {string} args.targetName - Name of the new tag to create
 * @param {string} [args.description] - Optional description for the new tag
 * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
 * @param {string} [args.projectRoot] - Project root path
 * @param {Object} log - Logger object
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function copyTagDirect(args, log, context = {}) {
	// Destructure expected args
	const { tasksJsonPath, sourceName, targetName, description, projectRoot } =
		args;
	const { session } = context;

	// Enable silent mode to prevent console logs from interfering with JSON response
	enableSilentMode();

	// Create logger wrapper using the utility
	const mcpLog = createLogWrapper(log);

	try {
		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('copyTagDirect called without tasksJsonPath');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Check required parameters
		if (!sourceName || typeof sourceName !== 'string') {
			log.error('Missing required parameter: sourceName');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_PARAMETER',
					message: 'Source tag name is required and must be a string'
				}
			};
		}

		if (!targetName || typeof targetName !== 'string') {
			log.error('Missing required parameter: targetName');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_PARAMETER',
					message: 'Target tag name is required and must be a string'
				}
			};
		}

		log.info(`Copying tag from "${sourceName}" to "${targetName}"`);

		// Prepare options
		const options = {
			description
		};

		// Call the copyTag function
		const result = await copyTag(
			tasksJsonPath,
			sourceName,
			targetName,
			options,
			{
				session,
				mcpLog,
				projectRoot
			},
			'json' // outputFormat - use 'json' to suppress CLI UI
		);

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				sourceName: result.sourceName,
				targetName: result.targetName,
				copied: result.copied,
				tasksCopied: result.tasksCopied,
				description: result.description,
				message: `Successfully copied tag from "${result.sourceName}" to "${result.targetName}"`
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in copyTagDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: error.code || 'COPY_TAG_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * generate-task-files.js
 * Direct function implementation for generating task files from tasks.json
 */

import { generateTaskFiles } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';

/**
 * Direct function wrapper for generateTaskFiles with error handling.
 *
 * @param {Object} args - Command arguments containing tasksJsonPath and outputDir.
 * @param {string} args.tasksJsonPath - Path to the tasks.json file.
 * @param {string} args.outputDir - Path to the output directory.
 * @param {string} args.projectRoot - Project root path (for MCP/env fallback)
 * @param {string} args.tag - Tag for the task (optional)
 * @param {Object} log - Logger object.
 * @returns {Promise<Object>} - Result object with success status and data/error information.
 */
export async function generateTaskFilesDirect(args, log) {
	// Destructure expected args
	const { tasksJsonPath, outputDir, projectRoot, tag } = args;
	try {
		log.info(`Generating task files with args: ${JSON.stringify(args)}`);

		// Check if paths were provided
		if (!tasksJsonPath) {
			const errorMessage = 'tasksJsonPath is required but was not provided.';
			log.error(errorMessage);
			return {
				success: false,
				error: { code: 'MISSING_ARGUMENT', message: errorMessage }
			};
		}
		if (!outputDir) {
			const errorMessage = 'outputDir is required but was not provided.';
			log.error(errorMessage);
			return {
				success: false,
				error: { code: 'MISSING_ARGUMENT', message: errorMessage }
			};
		}

		// Use the provided paths
		const tasksPath = tasksJsonPath;
		const resolvedOutputDir = outputDir;

		log.info(`Generating task files from ${tasksPath} to ${resolvedOutputDir}`);

		// Execute core generateTaskFiles function in a separate try/catch
		try {
			// Enable silent mode to prevent logs from being written to stdout
			enableSilentMode();

			// Pass projectRoot and tag so the core respects context
			generateTaskFiles(tasksPath, resolvedOutputDir, {
				projectRoot,
				tag,
				mcpLog: log
			});

			// Restore normal logging after task generation
			disableSilentMode();
		} catch (genError) {
			// Make sure to restore normal logging even if there's an error
			disableSilentMode();

			log.error(`Error in generateTaskFiles: ${genError.message}`);
			return {
				success: false,
				error: { code: 'GENERATE_FILES_ERROR', message: genError.message }
			};
		}

		// Return success with file paths
		return {
			success: true,
			data: {
				message: `Successfully generated task files`,
				tasksPath: tasksPath,
				outputDir: resolvedOutputDir,
				taskFiles:
					'Individual task files have been generated in the output directory'
			}
		};
	} catch (error) {
		// Make sure to restore normal logging if an outer error occurs
		disableSilentMode();

		log.error(`Error generating task files: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'GENERATE_TASKS_ERROR',
				message: error.message || 'Unknown error generating task files'
			}
		};
	}
}

/**
 * rename-tag.js
 * Direct function implementation for renaming a tag
 */

import { renameTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for renaming a tag with error handling.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.oldName - Current name of the tag to rename
 * @param {string} args.newName - New name for the tag
 * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
 * @param {string} [args.projectRoot] - Project root path
 * @param {Object} log - Logger object
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function renameTagDirect(args, log, context = {}) {
	// Destructure expected args
	const { tasksJsonPath, oldName, newName, projectRoot } = args;
	const { session } = context;

	// Enable silent mode to prevent console logs from interfering with JSON response
	enableSilentMode();

	// Create logger wrapper using the utility
	const mcpLog = createLogWrapper(log);

	try {
		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('renameTagDirect called without tasksJsonPath');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Check required parameters
		if (!oldName || typeof oldName !== 'string') {
			log.error('Missing required parameter: oldName');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_PARAMETER',
					message: 'Old tag name is required and must be a string'
				}
			};
		}

		if (!newName || typeof newName !== 'string') {
			log.error('Missing required parameter: newName');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_PARAMETER',
					message: 'New tag name is required and must be a string'
				}
			};
		}

		log.info(`Renaming tag from "${oldName}" to "${newName}"`);

		// Call the renameTag function
		const result = await renameTag(
			tasksJsonPath,
			oldName,
			newName,
			{}, // options (empty for now)
			{
				session,
				mcpLog,
				projectRoot
			},
			'json' // outputFormat - use 'json' to suppress CLI UI
		);

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				oldName: result.oldName,
				newName: result.newName,
				renamed: result.renamed,
				taskCount: result.taskCount,
				wasCurrentTag: result.wasCurrentTag,
				message: `Successfully renamed tag from "${result.oldName}" to "${result.newName}"`
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in renameTagDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: error.code || 'RENAME_TAG_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * delete-tag.js
 * Direct function implementation for deleting a tag
 */

import { deleteTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

/**
 * Direct function wrapper for deleting a tag with error handling.
 *
 * @param {Object} args - Command arguments
 * @param {string} args.name - Name of the tag to delete
 * @param {boolean} [args.yes=false] - Skip confirmation prompts
 * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
 * @param {string} [args.projectRoot] - Project root path
 * @param {Object} log - Logger object
 * @param {Object} context - Additional context (session)
 * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 */
export async function deleteTagDirect(args, log, context = {}) {
	// Destructure expected args
	const { tasksJsonPath, name, yes = false, projectRoot } = args;
	const { session } = context;

	// Enable silent mode to prevent console logs from interfering with JSON response
	enableSilentMode();

	// Create logger wrapper using the utility
	const mcpLog = createLogWrapper(log);

	try {
		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('deleteTagDirect called without tasksJsonPath');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Check required parameters
		if (!name || typeof name !== 'string') {
			log.error('Missing required parameter: name');
			disableSilentMode();
			return {
				success: false,
				error: {
					code: 'MISSING_PARAMETER',
					message: 'Tag name is required and must be a string'
				}
			};
		}

		log.info(`Deleting tag: ${name}`);

		// Prepare options
		const options = {
			yes // For MCP, we always skip confirmation prompts
		};

		// Call the deleteTag function
		const result = await deleteTag(
			tasksJsonPath,
			name,
			options,
			{
				session,
				mcpLog,
				projectRoot
			},
			'json' // outputFormat - use 'json' to suppress CLI UI
		);

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				tagName: result.tagName,
				deleted: result.deleted,
				tasksDeleted: result.tasksDeleted,
				wasCurrentTag: result.wasCurrentTag,
				switchedToMaster: result.switchedToMaster,
				message: `Successfully deleted tag "${result.tagName}"`
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error in deleteTagDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: error.code || 'DELETE_TAG_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * Provider Registry - Singleton for managing AI providers
 *
 * This module implements a singleton registry that allows dynamic registration
 * of AI providers at runtime, while maintaining compatibility with the existing
 * static PROVIDERS object in ai-services-unified.js.
 */

// Singleton instance
let instance = null;

/**
 * Provider Registry class - Manages dynamic provider registration
 */
class ProviderRegistry {
	constructor() {
		// Private provider map
		this._providers = new Map();

		// Flag to track initialization
		this._initialized = false;
	}

	/**
	 * Get the singleton instance
	 * @returns {ProviderRegistry} The singleton instance
	 */
	static getInstance() {
		if (!instance) {
			instance = new ProviderRegistry();
		}
		return instance;
	}

	/**
	 * Initialize the registry
	 * @returns {ProviderRegistry} The singleton instance
	 */
	initialize() {
		if (this._initialized) {
			return this;
		}

		this._initialized = true;
		return this;
	}

	/**
	 * Register a provider with the registry
	 * @param {string} providerName - The name of the provider
	 * @param {object} provider - The provider instance
	 * @param {object} options - Additional options for registration
	 * @returns {ProviderRegistry} The singleton instance for chaining
	 */
	registerProvider(providerName, provider, options = {}) {
		if (!providerName || typeof providerName !== 'string') {
			throw new Error('Provider name must be a non-empty string');
		}

		if (!provider) {
			throw new Error('Provider instance is required');
		}

		// Validate that provider implements the required interface
		if (
			typeof provider.generateText !== 'function' ||
			typeof provider.streamText !== 'function' ||
			typeof provider.generateObject !== 'function'
		) {
			throw new Error('Provider must implement BaseAIProvider interface');
		}

		// Add provider to the registry
		this._providers.set(providerName, {
			instance: provider,
			options,
			registeredAt: new Date()
		});

		return this;
	}

	/**
	 * Check if a provider exists in the registry
	 * @param {string} providerName - The name of the provider
	 * @returns {boolean} True if the provider exists
	 */
	hasProvider(providerName) {
		return this._providers.has(providerName);
	}

	/**
	 * Get a provider from the registry
	 * @param {string} providerName - The name of the provider
	 * @returns {object|null} The provider instance or null if not found
	 */
	getProvider(providerName) {
		const providerEntry = this._providers.get(providerName);
		return providerEntry ? providerEntry.instance : null;
	}

	/**
	 * Get all registered providers
	 * @returns {Map} Map of all registered providers
	 */
	getAllProviders() {
		return new Map(this._providers);
	}

	/**
	 * Remove a provider from the registry
	 * @param {string} providerName - The name of the provider
	 * @returns {boolean} True if the provider was removed
	 */
	unregisterProvider(providerName) {
		if (this._providers.has(providerName)) {
			this._providers.delete(providerName);
			return true;
		}
		return false;
	}

	/**
	 * Reset the registry (primarily for testing)
	 */
	reset() {
		this._providers.clear();
		this._initialized = false;
	}
}

ProviderRegistry.getInstance().initialize(); // Ensure singleton is initialized on import
// Export singleton getter
export default ProviderRegistry;

export default {
	// Use Node.js environment for testing
	testEnvironment: 'node',

	// Automatically clear mock calls between every test
	clearMocks: true,

	// Indicates whether the coverage information should be collected while executing the test
	collectCoverage: false,

	// The directory where Jest should output its coverage files
	coverageDirectory: 'coverage',

	// A list of paths to directories that Jest should use to search for files in
	roots: ['<rootDir>/tests'],

	// The glob patterns Jest uses to detect test files
	testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],

	// Transform files
	transform: {},

	// Disable transformations for node_modules
	transformIgnorePatterns: ['/node_modules/'],

	// Set moduleNameMapper for absolute paths
	moduleNameMapper: {
		'^@/(.*)$': '<rootDir>/$1'
	},

	// Setup module aliases
	moduleDirectories: ['node_modules', '<rootDir>'],

	// Configure test coverage thresholds
	coverageThreshold: {
		global: {
			branches: 80,
			functions: 80,
			lines: 80,
			statements: 80
		}
	},

	// Generate coverage report in these formats
	coverageReporters: ['text', 'lcov'],

	// Verbose output
	verbose: true,

	// Setup file
	setupFilesAfterEnv: ['<rootDir>/tests/setup.js']
};

/**
 * src/ai-providers/anthropic.js
 *
 * Implementation for interacting with Anthropic models (e.g., Claude)
 * using the Vercel AI SDK.
 */

import { createAnthropic } from '@ai-sdk/anthropic';
import { BaseAIProvider } from './base-provider.js';

// TODO: Implement standardized functions for generateText, streamText, generateObject

// --- Client Instantiation ---
// Note: API key resolution should ideally happen closer to the call site
// using the config manager/resolver which checks process.env and session.env.
// This is a placeholder for basic functionality.
// Remove the global variable and caching logic
// let anthropicClient;

export class AnthropicAIProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'Anthropic';
	}

	/**
	 * Returns the environment variable name required for this provider's API key.
	 * @returns {string} The environment variable name for the Anthropic API key
	 */
	getRequiredApiKeyName() {
		return 'ANTHROPIC_API_KEY';
	}

	/**
	 * Creates and returns an Anthropic client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} params.apiKey - Anthropic API key
	 * @param {string} [params.baseURL] - Optional custom API endpoint
	 * @returns {Function} Anthropic client function
	 * @throws {Error} If API key is missing or initialization fails
	 */
	getClient(params) {
		try {
			const { apiKey, baseURL } = params;

			if (!apiKey) {
				throw new Error('Anthropic API key is required.');
			}

			return createAnthropic({
				apiKey,
				...(baseURL && { baseURL }),
				headers: {
					'anthropic-beta': 'output-128k-2025-02-19'
				}
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}
}

// TODO: Implement streamAnthropicObject if needed and supported well by the SDK for Anthropic.
// The basic structure would be similar to generateAnthropicObject but using streamObject.

/**
 * update-config-tokens.js
 * Updates config.json with correct maxTokens values from supported-models.json
 */

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

/**
 * Updates the config file with correct maxTokens values from supported-models.json
 * @param {string} configPath - Path to the config.json file to update
 * @returns {boolean} True if successful, false otherwise
 */
export function updateConfigMaxTokens(configPath) {
	try {
		// Load supported models
		const supportedModelsPath = path.join(__dirname, 'supported-models.json');
		const supportedModels = JSON.parse(
			fs.readFileSync(supportedModelsPath, 'utf-8')
		);

		// Load config
		const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));

		// Update each role's maxTokens if the model exists in supported-models.json
		const roles = ['main', 'research', 'fallback'];

		for (const role of roles) {
			if (config.models && config.models[role]) {
				const provider = config.models[role].provider;
				const modelId = config.models[role].modelId;

				// Find the model in supported models
				if (supportedModels[provider]) {
					const modelData = supportedModels[provider].find(
						(m) => m.id === modelId
					);
					if (modelData && modelData.max_tokens) {
						config.models[role].maxTokens = modelData.max_tokens;
					}
				}
			}
		}

		// Write back the updated config
		fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
		return true;
	} catch (error) {
		console.error('Error updating config maxTokens:', error.message);
		return false;
	}
}

import {
	checkForUpdate,
	displayUpgradeNotification,
	compareVersions
} from './scripts/modules/commands.js';
import fs from 'fs';
import path from 'path';

// Force our current version for testing
process.env.FORCE_VERSION = '0.9.30';

// Create a mock package.json in memory for testing
const mockPackageJson = {
	name: 'task-master-ai',
	version: '0.9.30'
};

// Modified version of checkForUpdate that doesn't use HTTP for testing
async function testCheckForUpdate(simulatedLatestVersion) {
	// Get current version - use our forced version
	const currentVersion = process.env.FORCE_VERSION || '0.9.30';

	console.log(`Using simulated current version: ${currentVersion}`);
	console.log(`Using simulated latest version: ${simulatedLatestVersion}`);

	// Compare versions
	const needsUpdate =
		compareVersions(currentVersion, simulatedLatestVersion) < 0;

	return {
		currentVersion,
		latestVersion: simulatedLatestVersion,
		needsUpdate
	};
}

// Test with current version older than latest (should show update notice)
async function runTest() {
	console.log('=== Testing version check scenarios ===\n');

	// Scenario 1: Update available
	console.log(
		'\n--- Scenario 1: Update available (Current: 0.9.30, Latest: 1.0.0) ---'
	);
	const updateInfo1 = await testCheckForUpdate('1.0.0');
	console.log('Update check results:');
	console.log(`- Current version: ${updateInfo1.currentVersion}`);
	console.log(`- Latest version: ${updateInfo1.latestVersion}`);
	console.log(`- Update needed: ${updateInfo1.needsUpdate}`);

	if (updateInfo1.needsUpdate) {
		console.log('\nDisplaying upgrade notification:');
		displayUpgradeNotification(
			updateInfo1.currentVersion,
			updateInfo1.latestVersion
		);
	}

	// Scenario 2: No update needed (versions equal)
	console.log(
		'\n--- Scenario 2: No update needed (Current: 0.9.30, Latest: 0.9.30) ---'
	);
	const updateInfo2 = await testCheckForUpdate('0.9.30');
	console.log('Update check results:');
	console.log(`- Current version: ${updateInfo2.currentVersion}`);
	console.log(`- Latest version: ${updateInfo2.latestVersion}`);
	console.log(`- Update needed: ${updateInfo2.needsUpdate}`);

	// Scenario 3: Development version (current newer than latest)
	console.log(
		'\n--- Scenario 3: Development version (Current: 0.9.30, Latest: 0.9.0) ---'
	);
	const updateInfo3 = await testCheckForUpdate('0.9.0');
	console.log('Update check results:');
	console.log(`- Current version: ${updateInfo3.currentVersion}`);
	console.log(`- Latest version: ${updateInfo3.latestVersion}`);
	console.log(`- Update needed: ${updateInfo3.needsUpdate}`);

	console.log('\n=== Test complete ===');
}

// Run all tests
runTest();

/**
 * Direct function wrapper for validateDependenciesCommand
 */

import { validateDependenciesCommand } from '../../../../scripts/modules/dependency-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import fs from 'fs';

/**
 * Validate dependencies in tasks.json
 * @param {Object} args - Function arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} args.projectRoot - Project root path (for MCP/env fallback)
 * @param {string} args.tag - Tag for the task (optional)
 * @param {Object} log - Logger object
 * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
 */
export async function validateDependenciesDirect(args, log) {
	// Destructure the explicit tasksJsonPath
	const { tasksJsonPath, projectRoot, tag } = args;

	if (!tasksJsonPath) {
		log.error('validateDependenciesDirect called without tasksJsonPath');
		return {
			success: false,
			error: {
				code: 'MISSING_ARGUMENT',
				message: 'tasksJsonPath is required'
			}
		};
	}

	try {
		log.info(`Validating dependencies in tasks: ${tasksJsonPath}`);

		// Use the provided tasksJsonPath
		const tasksPath = tasksJsonPath;

		// Verify the file exists
		if (!fs.existsSync(tasksPath)) {
			return {
				success: false,
				error: {
					code: 'FILE_NOT_FOUND',
					message: `Tasks file not found at ${tasksPath}`
				}
			};
		}

		// Enable silent mode to prevent console logs from interfering with JSON response
		enableSilentMode();

		const options = { projectRoot, tag };
		// Call the original command function using the provided tasksPath
		await validateDependenciesCommand(tasksPath, options);

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				message: 'Dependencies validated successfully',
				tasksPath
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error validating dependencies: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'VALIDATION_ERROR',
				message: error.message
			}
		};
	}
}

#!/usr/bin/env node

import { Config } from 'fastmcp';
import path from 'path';
import fs from 'fs';

// Log the current directory
console.error(`Current working directory: ${process.cwd()}`);

try {
	console.error('Attempting to load FastMCP Config...');

	// Check if .cursor/mcp.json exists
	const mcpPath = path.join(process.cwd(), '.cursor', 'mcp.json');
	console.error(`Checking if mcp.json exists at: ${mcpPath}`);

	if (fs.existsSync(mcpPath)) {
		console.error('mcp.json file found');
		console.error(
			`File content: ${JSON.stringify(JSON.parse(fs.readFileSync(mcpPath, 'utf8')), null, 2)}`
		);
	} else {
		console.error('mcp.json file not found');
	}

	// Try to create Config
	const config = new Config();
	console.error('Config created successfully');

	// Check if env property exists
	if (config.env) {
		console.error(
			`Config.env exists with keys: ${Object.keys(config.env).join(', ')}`
		);

		// Print each env var value (careful with sensitive values)
		for (const [key, value] of Object.entries(config.env)) {
			if (key.includes('KEY')) {
				console.error(`${key}: [value hidden]`);
			} else {
				console.error(`${key}: ${value}`);
			}
		}
	} else {
		console.error('Config.env does not exist');
	}
} catch (error) {
	console.error(`Error loading Config: ${error.message}`);
	console.error(`Stack trace: ${error.stack}`);
}

// Log process.env to see if values from mcp.json were loaded automatically
console.error('\nChecking if process.env already has values from mcp.json:');
const envVars = [
	'ANTHROPIC_API_KEY',
	'PERPLEXITY_API_KEY',
	'MODEL',
	'PERPLEXITY_MODEL',
	'MAX_TOKENS',
	'TEMPERATURE',
	'DEFAULT_SUBTASKS',
	'DEFAULT_PRIORITY'
];

for (const varName of envVars) {
	if (process.env[varName]) {
		if (varName.includes('KEY')) {
			console.error(`${varName}: [value hidden]`);
		} else {
			console.error(`${varName}: ${process.env[varName]}`);
		}
	} else {
		console.error(`${varName}: not set`);
	}
}

/**
 * Temporarily sets environment variables from session.env, executes an action,
 * and restores the original environment variables.
 * @param {object | undefined} sessionEnv - The environment object from the session.
 * @param {Function} actionFn - An async function to execute with the temporary environment.
 * @returns {Promise<any>} The result of the actionFn.
 */
export async function withSessionEnv(sessionEnv, actionFn) {
	if (
		!sessionEnv ||
		typeof sessionEnv !== 'object' ||
		Object.keys(sessionEnv).length === 0
	) {
		// If no sessionEnv is provided, just run the action directly
		return await actionFn();
	}

	const originalEnv = {};
	const keysToRestore = [];

	// Set environment variables from sessionEnv
	for (const key in sessionEnv) {
		if (Object.prototype.hasOwnProperty.call(sessionEnv, key)) {
			// Store original value if it exists, otherwise mark for deletion
			if (process.env[key] !== undefined) {
				originalEnv[key] = process.env[key];
			}
			keysToRestore.push(key);
			process.env[key] = sessionEnv[key];
		}
	}

	try {
		// Execute the provided action function
		return await actionFn();
	} finally {
		// Restore original environment variables
		for (const key of keysToRestore) {
			if (Object.prototype.hasOwnProperty.call(originalEnv, key)) {
				process.env[key] = originalEnv[key];
			} else {
				// If the key didn't exist originally, delete it
				delete process.env[key];
			}
		}
	}
}

import {
	getConfig,
	isConfigFilePresent,
	writeConfig
} from '../config-manager.js';
import { findConfigPath } from '../../../src/utils/path-utils.js';
import { log } from '../utils.js';

function setResponseLanguage(lang, options = {}) {
	const { mcpLog, projectRoot } = options;

	const report = (level, ...args) => {
		if (mcpLog && typeof mcpLog[level] === 'function') {
			mcpLog[level](...args);
		}
	};

	// Use centralized config path finding instead of hardcoded path
	const configPath = findConfigPath(null, { projectRoot });
	const configExists = isConfigFilePresent(projectRoot);

	log(
		'debug',
		`Checking for config file using findConfigPath, found: ${configPath}`
	);
	log(
		'debug',
		`Checking config file using isConfigFilePresent(), exists: ${configExists}`
	);

	if (!configExists) {
		return {
			success: false,
			error: {
				code: 'CONFIG_MISSING',
				message:
					'The configuration file is missing. Run "task-master init" to create it.'
			}
		};
	}

	// Validate response language
	if (typeof lang !== 'string' || lang.trim() === '') {
		return {
			success: false,
			error: {
				code: 'INVALID_RESPONSE_LANGUAGE',
				message: `Invalid response language: ${lang}. Must be a non-empty string.`
			}
		};
	}

	try {
		const currentConfig = getConfig(projectRoot);
		currentConfig.global.responseLanguage = lang;
		const writeResult = writeConfig(currentConfig, projectRoot);

		if (!writeResult) {
			return {
				success: false,
				error: {
					code: 'WRITE_ERROR',
					message: 'Error writing updated configuration to configuration file'
				}
			};
		}

		const successMessage = `Successfully set response language to: ${lang}`;
		report('info', successMessage);
		return {
			success: true,
			data: {
				responseLanguage: lang,
				message: successMessage
			}
		};
	} catch (error) {
		report('error', `Error setting response language: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'SET_RESPONSE_LANGUAGE_ERROR',
				message: error.message
			}
		};
	}
}

export default setResponseLanguage;

/**
 * src/ai-providers/custom-sdk/mcp/index.js
 *
 * AI SDK factory function for MCP provider.
 * Creates MCP language model instances with session-based AI operations.
 */

import { MCPLanguageModel } from './language-model.js';

/**
 * Create MCP provider factory function following AI SDK patterns
 * @param {object} options - Provider options
 * @param {object} options.session - MCP session object
 * @param {object} options.defaultSettings - Default settings for the provider
 * @returns {Function} Provider factory function
 */
export function createMCP(options = {}) {
	if (!options.session) {
		throw new Error('MCP provider requires session object');
	}

	// Return the provider factory function that AI SDK expects
	const provider = function (modelId, settings = {}) {
		if (new.target) {
			throw new Error(
				'The MCP model function cannot be called with the new keyword.'
			);
		}

		return new MCPLanguageModel({
			session: options.session,
			modelId: modelId || 'claude-3-5-sonnet-20241022',
			settings: {
				temperature: settings.temperature,
				maxTokens: settings.maxTokens,
				...options.defaultSettings,
				...settings
			}
		});
	};

	// Add required methods for AI SDK compatibility
	provider.languageModel = (modelId, settings) => provider(modelId, settings);
	provider.chat = (modelId, settings) => provider(modelId, settings);

	return provider;
}

/**
 * mcp-server/src/providers/mcp-provider.js
 *
 * Implementation for MCP custom AI SDK provider that integrates with
 * the existing MCP server infrastructure and provider registry.
 * Follows the Claude Code provider pattern for session-based providers.
 */

import { createMCP } from '../custom-sdk/index.js';
import { BaseAIProvider } from '../../../src/ai-providers/base-provider.js';

export class MCPProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'mcp';
		this.session = null; // MCP server session object
	}

	getRequiredApiKeyName() {
		return 'MCP_API_KEY';
	}

	isRequiredApiKey() {
		return false;
	}

	/**
	 * Override validateAuth to validate MCP session instead of API key
	 * @param {object} params - Parameters to validate
	 */
	validateAuth(params) {
		// Validate MCP session instead of API key
		if (!this.session) {
			throw new Error('MCP Provider requires active MCP session');
		}

		if (!this.session.clientCapabilities?.sampling) {
			throw new Error('MCP session must have client sampling capabilities');
		}
	}

	/**
	 * Creates and returns an MCP AI SDK client instance.
	 * @param {object} params - Parameters for client initialization
	 * @returns {Function} MCP AI SDK client function
	 * @throws {Error} If initialization fails
	 */
	getClient(params) {
		try {
			// Pass MCP session to AI SDK implementation
			return createMCP({
				session: this.session,
				defaultSettings: {
					temperature: params.temperature,
					maxTokens: params.maxTokens
				}
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}

	/**
	 * Method called by MCP server on connect events
	 * @param {object} session - MCP session object
	 */
	setSession(session) {
		this.session = session;

		if (!session) {
			this.logger?.warn('Set null session on MCP Provider');
		} else {
			this.logger?.debug('Updated MCP Provider session');
		}
	}

	/**
	 * Get current session status
	 * @returns {boolean} True if session is available and valid
	 */
	hasValidSession() {
		return !!(this.session && this.session.clientCapabilities?.sampling);
	}
}

/**
 * Direct function wrapper for fixDependenciesCommand
 */

import { fixDependenciesCommand } from '../../../../scripts/modules/dependency-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import fs from 'fs';

/**
 * Fix invalid dependencies in tasks.json automatically
 * @param {Object} args - Function arguments
 * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 * @param {string} args.projectRoot - Project root directory
 * @param {string} args.tag - Tag for the project
 * @param {Object} log - Logger object
 * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
 */
export async function fixDependenciesDirect(args, log) {
	// Destructure expected args
	const { tasksJsonPath, projectRoot, tag } = args;
	try {
		log.info(`Fixing invalid dependencies in tasks: ${tasksJsonPath}`);

		// Check if tasksJsonPath was provided
		if (!tasksJsonPath) {
			log.error('fixDependenciesDirect called without tasksJsonPath');
			return {
				success: false,
				error: {
					code: 'MISSING_ARGUMENT',
					message: 'tasksJsonPath is required'
				}
			};
		}

		// Use provided path
		const tasksPath = tasksJsonPath;

		// Verify the file exists
		if (!fs.existsSync(tasksPath)) {
			return {
				success: false,
				error: {
					code: 'FILE_NOT_FOUND',
					message: `Tasks file not found at ${tasksPath}`
				}
			};
		}

		// Enable silent mode to prevent console logs from interfering with JSON response
		enableSilentMode();

		const options = { projectRoot, tag };
		// Call the original command function using the provided path and proper context
		await fixDependenciesCommand(tasksPath, options);

		// Restore normal logging
		disableSilentMode();

		return {
			success: true,
			data: {
				message: 'Dependencies fixed successfully',
				tasksPath,
				tag: tag || 'master'
			}
		};
	} catch (error) {
		// Make sure to restore normal logging even if there's an error
		disableSilentMode();

		log.error(`Error fixing dependencies: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'FIX_DEPENDENCIES_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * tools/index.js
 * Export all Task Master CLI tools for MCP server
 */

import { registerListTasksTool } from './get-tasks.js';
import logger from '../logger.js';
import { registerSetTaskStatusTool } from './set-task-status.js';
import { registerParsePRDTool } from './parse-prd.js';
import { registerUpdateTool } from './update.js';
import { registerUpdateTaskTool } from './update-task.js';
import { registerUpdateSubtaskTool } from './update-subtask.js';
import { registerGenerateTool } from './generate.js';
import { registerShowTaskTool } from './get-task.js';
import { registerNextTaskTool } from './next-task.js';
import { registerExpandTaskTool } from './expand-task.js';
import { registerAddTaskTool } from './add-task.js';
import { registerAddSubtaskTool } from './add-subtask.js';
import { registerRemoveSubtaskTool } from './remove-subtask.js';
import { registerAnalyzeProjectComplexityTool } from './analyze.js';
import { registerClearSubtasksTool } from './clear-subtasks.js';
import { registerExpandAllTool } from './expand-all.js';
import { registerRemoveDependencyTool } from './remove-dependency.js';
import { registerValidateDependenciesTool } from './validate-dependencies.js';
import { registerFixDependenciesTool } from './fix-dependencies.js';
import { registerComplexityReportTool } from './complexity-report.js';
import { registerAddDependencyTool } from './add-dependency.js';
import { registerRemoveTaskTool } from './remove-task.js';
import { registerInitializeProjectTool } from './initialize-project.js';
import { registerModelsTool } from './models.js';
import { registerMoveTaskTool } from './move-task.js';
import { registerResponseLanguageTool } from './response-language.js';
import { registerAddTagTool } from './add-tag.js';
import { registerDeleteTagTool } from './delete-tag.js';
import { registerListTagsTool } from './list-tags.js';
import { registerUseTagTool } from './use-tag.js';
import { registerRenameTagTool } from './rename-tag.js';
import { registerCopyTagTool } from './copy-tag.js';
import { registerResearchTool } from './research.js';
import { registerRulesTool } from './rules.js';

/**
 * Register all Task Master tools with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerTaskMasterTools(server) {
	try {
		// Register each tool in a logical workflow order

		// Group 1: Initialization & Setup
		registerInitializeProjectTool(server);
		registerModelsTool(server);
		registerRulesTool(server);
		registerParsePRDTool(server);

		// Group 2: Task Analysis & Expansion
		registerAnalyzeProjectComplexityTool(server);
		registerExpandTaskTool(server);
		registerExpandAllTool(server);

		// Group 3: Task Listing & Viewing
		registerListTasksTool(server);
		registerShowTaskTool(server);
		registerNextTaskTool(server);
		registerComplexityReportTool(server);

		// Group 4: Task Status & Management
		registerSetTaskStatusTool(server);
		registerGenerateTool(server);

		// Group 5: Task Creation & Modification
		registerAddTaskTool(server);
		registerAddSubtaskTool(server);
		registerUpdateTool(server);
		registerUpdateTaskTool(server);
		registerUpdateSubtaskTool(server);
		registerRemoveTaskTool(server);
		registerRemoveSubtaskTool(server);
		registerClearSubtasksTool(server);
		registerMoveTaskTool(server);

		// Group 6: Dependency Management
		registerAddDependencyTool(server);
		registerRemoveDependencyTool(server);
		registerValidateDependenciesTool(server);
		registerFixDependenciesTool(server);
		registerResponseLanguageTool(server);

		// Group 7: Tag Management
		registerListTagsTool(server);
		registerAddTagTool(server);
		registerDeleteTagTool(server);
		registerUseTagTool(server);
		registerRenameTagTool(server);
		registerCopyTagTool(server);

		// Group 8: Research Features
		registerResearchTool(server);
	} catch (error) {
		logger.error(`Error registering Task Master tools: ${error.message}`);
		throw error;
	}
}

export default {
	registerTaskMasterTools
};

/**
 * tools/rules.js
 * Tool to add or remove rules from a project (MCP server)
 */

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { rulesDirect } from '../core/direct-functions/rules.js';
import { RULE_PROFILES } from '../../../src/constants/profiles.js';

/**
 * Register the rules tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerRulesTool(server) {
	server.addTool({
		name: 'rules',
		description: 'Add or remove rule profiles from the project.',
		parameters: z.object({
			action: z
				.enum(['add', 'remove'])
				.describe('Whether to add or remove rule profiles.'),
			profiles: z
				.array(z.enum(RULE_PROFILES))
				.min(1)
				.describe(
					`List of rule profiles to add or remove (e.g., [\"cursor\", \"roo\"]). Available options: ${RULE_PROFILES.join(', ')}`
				),
			projectRoot: z
				.string()
				.describe(
					'The root directory of the project. Must be an absolute path.'
				),
			force: z
				.boolean()
				.optional()
				.default(false)
				.describe(
					'DANGEROUS: Force removal even if it would leave no rule profiles. Only use if you are absolutely certain.'
				)
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(
					`[rules tool] Executing action: ${args.action} for profiles: ${args.profiles.join(', ')} in ${args.projectRoot}`
				);
				const result = await rulesDirect(args, log, { session });
				return handleApiResult(result, log);
			} catch (error) {
				log.error(`[rules tool] Error: ${error.message}`);
				return createErrorResponse(error.message, { details: error.stack });
			}
		})
	});
}

/**
 * tools/complexity-report.js
 * Tool for displaying the complexity analysis report
 */

import { z } from 'zod';
import {
	handleApiResult,
	createErrorResponse,
	withNormalizedProjectRoot
} from './utils.js';
import { complexityReportDirect } from '../core/task-master-core.js';
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
import { findComplexityReportPath } from '../core/utils/path-utils.js';
import { getCurrentTag } from '../../../scripts/modules/utils.js';

/**
 * Register the complexityReport tool with the MCP server
 * @param {Object} server - FastMCP server instance
 */
export function registerComplexityReportTool(server) {
	server.addTool({
		name: 'complexity_report',
		description: 'Display the complexity analysis report in a readable format',
		parameters: z.object({
			file: z
				.string()
				.optional()
				.describe(
					`Path to the report file (default: ${COMPLEXITY_REPORT_FILE})`
				),
			projectRoot: z
				.string()
				.describe('The directory of the project. Must be an absolute path.')
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(
					`Getting complexity report with args: ${JSON.stringify(args)}`
				);

				const resolvedTag = getCurrentTag(args.projectRoot);

				const pathArgs = {
					projectRoot: args.projectRoot,
					complexityReport: args.file,
					tag: resolvedTag
				};

				const reportPath = findComplexityReportPath(pathArgs, log);
				log.info('Reading complexity report from path: ', reportPath);

				if (!reportPath) {
					return createErrorResponse(
						'No complexity report found. Run task-master analyze-complexity first.'
					);
				}

				const result = await complexityReportDirect(
					{
						reportPath: reportPath
					},
					log
				);

				if (result.success) {
					log.info('Successfully retrieved complexity report');
				} else {
					log.error(
						`Failed to retrieve complexity report: ${result.error.message}`
					);
				}

				return handleApiResult(
					result,
					log,
					'Error retrieving complexity report',
					undefined,
					args.projectRoot
				);
			} catch (error) {
				log.error(`Error in complexity-report tool: ${error.message}`);
				return createErrorResponse(
					`Failed to retrieve complexity report: ${error.message}`
				);
			}
		})
	});
}

import { z } from 'zod';
import {
	createErrorResponse,
	handleApiResult,
	withNormalizedProjectRoot
} from './utils.js';
import { responseLanguageDirect } from '../core/direct-functions/response-language.js';

export function registerResponseLanguageTool(server) {
	server.addTool({
		name: 'response-language',
		description: 'Get or set the response language for the project',
		parameters: z.object({
			projectRoot: z
				.string()
				.describe(
					'The root directory for the project. ALWAYS SET THIS TO THE PROJECT ROOT DIRECTORY. IF NOT SET, THE TOOL WILL NOT WORK.'
				),
			language: z
				.string()
				.describe(
					'The new response language to set. like "中文" "English" or "español".'
				)
		}),
		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
			try {
				log.info(
					`Executing response-language tool with args: ${JSON.stringify(args)}`
				);

				const result = await responseLanguageDirect(
					{
						...args,
						projectRoot: args.projectRoot
					},
					log,
					{ session }
				);
				return handleApiResult(result, log, 'Error setting response language');
			} catch (error) {
				log.error(`Error in response-language tool: ${error.message}`);
				return createErrorResponse(error.message);
			}
		})
	});
}

/**
 * @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'kiro' | 'opencode' | 'roo' | 'trae' | 'windsurf' | 'vscode' | 'zed'} RulesProfile
 */

/**
 * Available rule profiles for project initialization and rules command
 *
 * ⚠  SINGLE SOURCE OF TRUTH: This is the authoritative list of all supported rule profiles.
 * This constant is used directly throughout the codebase (previously aliased as PROFILE_NAMES).
 *
 * @type {RulesProfile[]}
 * @description Defines possible rule profile sets:
 * - amp: Amp Code integration
 * - claude: Claude Code integration
 * - cline: Cline IDE rules
 * - codex: Codex integration
 * - cursor: Cursor IDE rules
 * - gemini: Gemini integration
 * - kiro: Kiro IDE rules
 * - opencode: OpenCode integration
 * - roo: Roo Code IDE rules
 * - trae: Trae IDE rules
 * - vscode: VS Code with GitHub Copilot integration
 * - windsurf: Windsurf IDE rules
 * - zed: Zed IDE rules
 *
 * To add a new rule profile:
 * 1. Add the profile name to this array
 * 2. Create a profile file in src/profiles/{profile}.js
 * 3. Export it as {profile}Profile in src/profiles/index.js
 */
export const RULE_PROFILES = [
	'amp',
	'claude',
	'cline',
	'codex',
	'cursor',
	'gemini',
	'kiro',
	'opencode',
	'roo',
	'trae',
	'vscode',
	'windsurf',
	'zed'
];

/**
 * Centralized enum for all supported Roo agent modes
 * @type {string[]}
 * @description Available Roo Code IDE modes for rule generation
 */
export const ROO_MODES = [
	'architect',
	'ask',
	'orchestrator',
	'code',
	'debug',
	'test'
];

/**
 * Check if a given rule profile is valid
 * @param {string} rulesProfile - The rule profile to check
 * @returns {boolean} True if the rule profile is valid, false otherwise
 */
export function isValidRulesProfile(rulesProfile) {
	return RULE_PROFILES.includes(rulesProfile);
}

import {
	displayUpgradeNotification,
	compareVersions
} from './scripts/modules/commands.js';

// Simulate different version scenarios
console.log('=== Simulating version check ===\n');

// 1. Current version is older than latest (should show update notice)
console.log('Scenario 1: Current version older than latest');
displayUpgradeNotification('0.9.30', '1.0.0');

// 2. Current version same as latest (no update needed)
console.log(
	'\nScenario 2: Current version same as latest (this would not normally show a notice)'
);
console.log('Current: 1.0.0, Latest: 1.0.0');
console.log('compareVersions result:', compareVersions('1.0.0', '1.0.0'));
console.log(
	'Update needed:',
	compareVersions('1.0.0', '1.0.0') < 0 ? 'Yes' : 'No'
);

// 3. Current version newer than latest (e.g., development version, would not show notice)
console.log(
	'\nScenario 3: Current version newer than latest (this would not normally show a notice)'
);
console.log('Current: 1.1.0, Latest: 1.0.0');
console.log('compareVersions result:', compareVersions('1.1.0', '1.0.0'));
console.log(
	'Update needed:',
	compareVersions('1.1.0', '1.0.0') < 0 ? 'Yes' : 'No'
);

console.log('\n=== Test complete ===');

/**
 * @typedef {'high' | 'medium' | 'low'} TaskPriority
 */

/**
 * Task priority options
 * @type {TaskPriority[]}
 * @description Defines possible task priorities:
 * - high: Critical tasks that need immediate attention
 * - medium: Standard priority tasks (default)
 * - low: Tasks that can be deferred or are nice-to-have
 */
export const TASK_PRIORITY_OPTIONS = ['high', 'medium', 'low'];

/**
 * Default task priority
 * @type {TaskPriority}
 */
export const DEFAULT_TASK_PRIORITY = 'medium';

/**
 * Check if a given priority is valid
 * @param {string} priority - The priority to check
 * @returns {boolean} True if the priority is valid, false otherwise
 */
export function isValidTaskPriority(priority) {
	return TASK_PRIORITY_OPTIONS.includes(priority?.toLowerCase());
}

/**
 * Normalize a priority value to lowercase
 * @param {string} priority - The priority to normalize
 * @returns {TaskPriority|null} The normalized priority or null if invalid
 */
export function normalizeTaskPriority(priority) {
	if (!priority) return null;
	const normalized = priority.toLowerCase();
	return isValidTaskPriority(normalized) ? normalized : null;
}

/**
 * complexity-report.js
 * Direct function implementation for displaying complexity analysis report
 */

import {
	readComplexityReport,
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';

/**
 * Direct function wrapper for displaying the complexity report with error handling and caching.
 *
 * @param {Object} args - Command arguments containing reportPath.
 * @param {string} args.reportPath - Explicit path to the complexity report file.
 * @param {Object} log - Logger object
 * @returns {Promise<Object>} - Result object with success status and data/error information
 */
export async function complexityReportDirect(args, log) {
	// Destructure expected args
	const { reportPath } = args;
	try {
		log.info(`Getting complexity report with args: ${JSON.stringify(args)}`);

		// Check if reportPath was provided
		if (!reportPath) {
			log.error('complexityReportDirect called without reportPath');
			return {
				success: false,
				error: { code: 'MISSING_ARGUMENT', message: 'reportPath is required' }
			};
		}

		// Use the provided report path
		log.info(`Looking for complexity report at: ${reportPath}`);

		// Generate cache key based on report path
		const cacheKey = `complexityReport:${reportPath}`;

		// Define the core action function to read the report
		const coreActionFn = async () => {
			try {
				// Enable silent mode to prevent console logs from interfering with JSON response
				enableSilentMode();

				const report = readComplexityReport(reportPath);

				// Restore normal logging
				disableSilentMode();

				if (!report) {
					log.warn(`No complexity report found at ${reportPath}`);
					return {
						success: false,
						error: {
							code: 'FILE_NOT_FOUND_ERROR',
							message: `No complexity report found at ${reportPath}. Run 'analyze-complexity' first.`
						}
					};
				}

				return {
					success: true,
					data: {
						report,
						reportPath
					}
				};
			} catch (error) {
				// Make sure to restore normal logging even if there's an error
				disableSilentMode();

				log.error(`Error reading complexity report: ${error.message}`);
				return {
					success: false,
					error: {
						code: 'READ_ERROR',
						message: error.message
					}
				};
			}
		};

		// Use the caching utility
		try {
			const result = await coreActionFn();
			log.info('complexityReportDirect completed');
			return result;
		} catch (error) {
			// Ensure silent mode is disabled
			disableSilentMode();

			log.error(`Unexpected error during complexityReport: ${error.message}`);
			return {
				success: false,
				error: {
					code: 'UNEXPECTED_ERROR',
					message: error.message
				}
			};
		}
	} catch (error) {
		// Ensure silent mode is disabled if an outer error occurs
		disableSilentMode();

		log.error(`Error in complexityReportDirect: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'UNEXPECTED_ERROR',
				message: error.message
			}
		};
	}
}

/**
 * src/ai-providers/custom-sdk/mcp/errors.js
 *
 * Error handling utilities for MCP AI SDK provider.
 * Maps MCP errors to AI SDK compatible error types.
 */

/**
 * MCP-specific error class
 */
export class MCPError extends Error {
	constructor(message, options = {}) {
		super(message);
		this.name = 'MCPError';
		this.code = options.code;
		this.cause = options.cause;
		this.mcpResponse = options.mcpResponse;
	}
}

/**
 * Session-related error
 */
export class MCPSessionError extends MCPError {
	constructor(message, options = {}) {
		super(message, options);
		this.name = 'MCPSessionError';
	}
}

/**
 * Sampling-related error
 */
export class MCPSamplingError extends MCPError {
	constructor(message, options = {}) {
		super(message, options);
		this.name = 'MCPSamplingError';
	}
}

/**
 * Map MCP errors to AI SDK compatible error types
 * @param {Error} error - Original error
 * @returns {Error} Mapped error
 */
export function mapMCPError(error) {
	// If already an MCP error, return as-is
	if (error instanceof MCPError) {
		return error;
	}

	const message = error.message || 'Unknown MCP error';
	const originalError = error;

	// Map common error patterns
	if (message.includes('session') || message.includes('connection')) {
		return new MCPSessionError(message, {
			cause: originalError,
			code: 'SESSION_ERROR'
		});
	}

	if (message.includes('sampling') || message.includes('timeout')) {
		return new MCPSamplingError(message, {
			cause: originalError,
			code: 'SAMPLING_ERROR'
		});
	}

	if (message.includes('capabilities') || message.includes('not supported')) {
		return new MCPSessionError(message, {
			cause: originalError,
			code: 'CAPABILITY_ERROR'
		});
	}

	// Default to generic MCP error
	return new MCPError(message, {
		cause: originalError,
		code: 'UNKNOWN_ERROR'
	});
}

/**
 * Check if error is retryable
 * @param {Error} error - Error to check
 * @returns {boolean} True if error might be retryable
 */
export function isRetryableError(error) {
	if (error instanceof MCPSamplingError && error.code === 'SAMPLING_ERROR') {
		return true;
	}

	if (error instanceof MCPSessionError && error.code === 'SESSION_ERROR') {
		// Session errors are generally not retryable
		return false;
	}

	// Check for common retryable patterns
	const message = error.message?.toLowerCase() || '';
	return (
		message.includes('timeout') ||
		message.includes('network') ||
		message.includes('temporary')
	);
}

/**
 * azure.js
 * AI provider implementation for Azure OpenAI models using Vercel AI SDK.
 */

import { createAzure } from '@ai-sdk/azure';
import { BaseAIProvider } from './base-provider.js';

export class AzureProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'Azure OpenAI';
	}

	/**
	 * Returns the environment variable name required for this provider's API key.
	 * @returns {string} The environment variable name for the Azure OpenAI API key
	 */
	getRequiredApiKeyName() {
		return 'AZURE_OPENAI_API_KEY';
	}

	/**
	 * Validates Azure-specific authentication parameters
	 * @param {object} params - Parameters to validate
	 * @throws {Error} If required parameters are missing
	 */
	validateAuth(params) {
		if (!params.apiKey) {
			throw new Error('Azure API key is required');
		}

		if (!params.baseURL) {
			throw new Error(
				'Azure endpoint URL is required. Set it in .taskmasterconfig global.azureBaseURL or models.[role].baseURL'
			);
		}
	}

	/**
	 * Creates and returns an Azure OpenAI client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} params.apiKey - Azure OpenAI API key
	 * @param {string} params.baseURL - Azure OpenAI endpoint URL (from .taskmasterconfig global.azureBaseURL or models.[role].baseURL)
	 * @returns {Function} Azure OpenAI client function
	 * @throws {Error} If required parameters are missing or initialization fails
	 */
	getClient(params) {
		try {
			const { apiKey, baseURL } = params;

			return createAzure({
				apiKey,
				baseURL
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}
}

/**
 * task-master-core.js
 * Central module that imports and re-exports all direct function implementations
 * for improved organization and maintainability.
 */

// Import direct function implementations
import { listTasksDirect } from './direct-functions/list-tasks.js';
import { getCacheStatsDirect } from './direct-functions/cache-stats.js';
import { parsePRDDirect } from './direct-functions/parse-prd.js';
import { updateTasksDirect } from './direct-functions/update-tasks.js';
import { updateTaskByIdDirect } from './direct-functions/update-task-by-id.js';
import { updateSubtaskByIdDirect } from './direct-functions/update-subtask-by-id.js';
import { generateTaskFilesDirect } from './direct-functions/generate-task-files.js';
import { setTaskStatusDirect } from './direct-functions/set-task-status.js';
import { showTaskDirect } from './direct-functions/show-task.js';
import { nextTaskDirect } from './direct-functions/next-task.js';
import { expandTaskDirect } from './direct-functions/expand-task.js';
import { addTaskDirect } from './direct-functions/add-task.js';
import { addSubtaskDirect } from './direct-functions/add-subtask.js';
import { removeSubtaskDirect } from './direct-functions/remove-subtask.js';
import { analyzeTaskComplexityDirect } from './direct-functions/analyze-task-complexity.js';
import { clearSubtasksDirect } from './direct-functions/clear-subtasks.js';
import { expandAllTasksDirect } from './direct-functions/expand-all-tasks.js';
import { removeDependencyDirect } from './direct-functions/remove-dependency.js';
import { validateDependenciesDirect } from './direct-functions/validate-dependencies.js';
import { fixDependenciesDirect } from './direct-functions/fix-dependencies.js';
import { complexityReportDirect } from './direct-functions/complexity-report.js';
import { addDependencyDirect } from './direct-functions/add-dependency.js';
import { removeTaskDirect } from './direct-functions/remove-task.js';
import { initializeProjectDirect } from './direct-functions/initialize-project.js';
import { modelsDirect } from './direct-functions/models.js';
import { moveTaskDirect } from './direct-functions/move-task.js';
import { researchDirect } from './direct-functions/research.js';
import { addTagDirect } from './direct-functions/add-tag.js';
import { deleteTagDirect } from './direct-functions/delete-tag.js';
import { listTagsDirect } from './direct-functions/list-tags.js';
import { useTagDirect } from './direct-functions/use-tag.js';
import { renameTagDirect } from './direct-functions/rename-tag.js';
import { copyTagDirect } from './direct-functions/copy-tag.js';

// Re-export utility functions
export { findTasksPath } from './utils/path-utils.js';

// Use Map for potential future enhancements like introspection or dynamic dispatch
export const directFunctions = new Map([
	['listTasksDirect', listTasksDirect],
	['getCacheStatsDirect', getCacheStatsDirect],
	['parsePRDDirect', parsePRDDirect],
	['updateTasksDirect', updateTasksDirect],
	['updateTaskByIdDirect', updateTaskByIdDirect],
	['updateSubtaskByIdDirect', updateSubtaskByIdDirect],
	['generateTaskFilesDirect', generateTaskFilesDirect],
	['setTaskStatusDirect', setTaskStatusDirect],
	['showTaskDirect', showTaskDirect],
	['nextTaskDirect', nextTaskDirect],
	['expandTaskDirect', expandTaskDirect],
	['addTaskDirect', addTaskDirect],
	['addSubtaskDirect', addSubtaskDirect],
	['removeSubtaskDirect', removeSubtaskDirect],
	['analyzeTaskComplexityDirect', analyzeTaskComplexityDirect],
	['clearSubtasksDirect', clearSubtasksDirect],
	['expandAllTasksDirect', expandAllTasksDirect],
	['removeDependencyDirect', removeDependencyDirect],
	['validateDependenciesDirect', validateDependenciesDirect],
	['fixDependenciesDirect', fixDependenciesDirect],
	['complexityReportDirect', complexityReportDirect],
	['addDependencyDirect', addDependencyDirect],
	['removeTaskDirect', removeTaskDirect],
	['initializeProjectDirect', initializeProjectDirect],
	['modelsDirect', modelsDirect],
	['moveTaskDirect', moveTaskDirect],
	['researchDirect', researchDirect],
	['addTagDirect', addTagDirect],
	['deleteTagDirect', deleteTagDirect],
	['listTagsDirect', listTagsDirect],
	['useTagDirect', useTagDirect],
	['renameTagDirect', renameTagDirect],
	['copyTagDirect', copyTagDirect]
]);

// Re-export all direct function implementations
export {
	listTasksDirect,
	getCacheStatsDirect,
	parsePRDDirect,
	updateTasksDirect,
	updateTaskByIdDirect,
	updateSubtaskByIdDirect,
	generateTaskFilesDirect,
	setTaskStatusDirect,
	showTaskDirect,
	nextTaskDirect,
	expandTaskDirect,
	addTaskDirect,
	addSubtaskDirect,
	removeSubtaskDirect,
	analyzeTaskComplexityDirect,
	clearSubtasksDirect,
	expandAllTasksDirect,
	removeDependencyDirect,
	validateDependenciesDirect,
	fixDependenciesDirect,
	complexityReportDirect,
	addDependencyDirect,
	removeTaskDirect,
	initializeProjectDirect,
	modelsDirect,
	moveTaskDirect,
	researchDirect,
	addTagDirect,
	deleteTagDirect,
	listTagsDirect,
	useTagDirect,
	renameTagDirect,
	copyTagDirect
};

/**
 * @fileoverview Claude Code provider factory and exports
 */

import { NoSuchModelError } from '@ai-sdk/provider';
import { ClaudeCodeLanguageModel } from './language-model.js';

/**
 * @typedef {import('./types.js').ClaudeCodeSettings} ClaudeCodeSettings
 * @typedef {import('./types.js').ClaudeCodeModelId} ClaudeCodeModelId
 * @typedef {import('./types.js').ClaudeCodeProvider} ClaudeCodeProvider
 * @typedef {import('./types.js').ClaudeCodeProviderSettings} ClaudeCodeProviderSettings
 */

/**
 * Create a Claude Code provider using the official SDK
 * @param {ClaudeCodeProviderSettings} [options={}] - Provider configuration options
 * @returns {ClaudeCodeProvider} Claude Code provider instance
 */
export function createClaudeCode(options = {}) {
	/**
	 * Create a language model instance
	 * @param {ClaudeCodeModelId} modelId - Model ID
	 * @param {ClaudeCodeSettings} [settings={}] - Model settings
	 * @returns {ClaudeCodeLanguageModel}
	 */
	const createModel = (modelId, settings = {}) => {
		return new ClaudeCodeLanguageModel({
			id: modelId,
			settings: {
				...options.defaultSettings,
				...settings
			}
		});
	};

	/**
	 * Provider function
	 * @param {ClaudeCodeModelId} modelId - Model ID
	 * @param {ClaudeCodeSettings} [settings] - Model settings
	 * @returns {ClaudeCodeLanguageModel}
	 */
	const provider = function (modelId, settings) {
		if (new.target) {
			throw new Error(
				'The Claude Code model function cannot be called with the new keyword.'
			);
		}

		return createModel(modelId, settings);
	};

	provider.languageModel = createModel;
	provider.chat = createModel; // Alias for languageModel

	// Add textEmbeddingModel method that throws NoSuchModelError
	provider.textEmbeddingModel = (modelId) => {
		throw new NoSuchModelError({
			modelId,
			modelType: 'textEmbeddingModel'
		});
	};

	return /** @type {ClaudeCodeProvider} */ (provider);
}

/**
 * Default Claude Code provider instance
 */
export const claudeCode = createClaudeCode();

// Provider exports
export { ClaudeCodeLanguageModel } from './language-model.js';

// Error handling exports
export {
	isAuthenticationError,
	isTimeoutError,
	getErrorMetadata,
	createAPICallError,
	createAuthenticationError,
	createTimeoutError
} from './errors.js';

/**
 * xai.js
 * AI provider implementation for xAI models using Vercel AI SDK.
 */

import { createXai } from '@ai-sdk/xai';
import { BaseAIProvider } from './base-provider.js';

export class XAIProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'xAI';
	}

	/**
	 * Returns the environment variable name required for this provider's API key.
	 * @returns {string} The environment variable name for the xAI API key
	 */
	getRequiredApiKeyName() {
		return 'XAI_API_KEY';
	}

	/**
	 * Creates and returns an xAI client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} params.apiKey - xAI API key
	 * @param {string} [params.baseURL] - Optional custom API endpoint
	 * @returns {Function} xAI client function
	 * @throws {Error} If API key is missing or initialization fails
	 */
	getClient(params) {
		try {
			const { apiKey, baseURL } = params;

			if (!apiKey) {
				throw new Error('xAI API key is required.');
			}

			return createXai({
				apiKey,
				baseURL: baseURL || 'https://api.x.ai/v1'
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}
}

/**
 * @fileoverview Extract JSON from Claude's response, handling markdown blocks and other formatting
 */

/**
 * Extract JSON from Claude's response
 * @param {string} text - The text to extract JSON from
 * @returns {string} - The extracted JSON string
 */
export function extractJson(text) {
	// Remove markdown code blocks if present
	let jsonText = text.trim();

	// Remove ```json blocks
	jsonText = jsonText.replace(/^```json\s*/gm, '');
	jsonText = jsonText.replace(/^```\s*/gm, '');
	jsonText = jsonText.replace(/```\s*$/gm, '');

	// Remove common TypeScript/JavaScript patterns
	jsonText = jsonText.replace(/^const\s+\w+\s*=\s*/, ''); // Remove "const varName = "
	jsonText = jsonText.replace(/^let\s+\w+\s*=\s*/, ''); // Remove "let varName = "
	jsonText = jsonText.replace(/^var\s+\w+\s*=\s*/, ''); // Remove "var varName = "
	jsonText = jsonText.replace(/;?\s*$/, ''); // Remove trailing semicolons

	// Try to extract JSON object or array
	const objectMatch = jsonText.match(/{[\s\S]*}/);
	const arrayMatch = jsonText.match(/\[[\s\S]*\]/);

	if (objectMatch) {
		jsonText = objectMatch[0];
	} else if (arrayMatch) {
		jsonText = arrayMatch[0];
	}

	// First try to parse as valid JSON
	try {
		JSON.parse(jsonText);
		return jsonText;
	} catch {
		// If it's not valid JSON, it might be a JavaScript object literal
		// Try to convert it to valid JSON
		try {
			// This is a simple conversion that handles basic cases
			// Replace unquoted keys with quoted keys
			const converted = jsonText
				.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
				// Replace single quotes with double quotes
				.replace(/'/g, '"');

			// Validate the converted JSON
			JSON.parse(converted);
			return converted;
		} catch {
			// If all else fails, return the original text
			// The AI SDK will handle the error appropriately
			return text;
		}
	}
}

/**
 * perplexity.js
 * AI provider implementation for Perplexity models using Vercel AI SDK.
 */

import { createPerplexity } from '@ai-sdk/perplexity';
import { BaseAIProvider } from './base-provider.js';

export class PerplexityAIProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'Perplexity';
	}

	/**
	 * Returns the environment variable name required for this provider's API key.
	 * @returns {string} The environment variable name for the Perplexity API key
	 */
	getRequiredApiKeyName() {
		return 'PERPLEXITY_API_KEY';
	}

	/**
	 * Creates and returns a Perplexity client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} params.apiKey - Perplexity API key
	 * @param {string} [params.baseURL] - Optional custom API endpoint
	 * @returns {Function} Perplexity client function
	 * @throws {Error} If API key is missing or initialization fails
	 */
	getClient(params) {
		try {
			const { apiKey, baseURL } = params;

			if (!apiKey) {
				throw new Error('Perplexity API key is required.');
			}

			return createPerplexity({
				apiKey,
				baseURL: baseURL || 'https://api.perplexity.ai'
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}
}

/**
 * Logger utility functions for Task Master
 * Provides standardized logging patterns for both CLI and utility contexts
 */

import { log as utilLog } from '../../scripts/modules/utils.js';

/**
 * Creates a standard logger object that wraps the utility log function
 * This provides a consistent logger interface across different parts of the application
 * @returns {Object} A logger object with standard logging methods (info, warn, error, debug, success)
 */
export function createStandardLogger() {
	return {
		info: (msg, ...args) => utilLog('info', msg, ...args),
		warn: (msg, ...args) => utilLog('warn', msg, ...args),
		error: (msg, ...args) => utilLog('error', msg, ...args),
		debug: (msg, ...args) => utilLog('debug', msg, ...args),
		success: (msg, ...args) => utilLog('success', msg, ...args)
	};
}

/**
 * Creates a logger using either the provided logger or a default standard logger
 * This is the recommended pattern for functions that accept an optional logger parameter
 * @param {Object|null} providedLogger - Optional logger object passed from caller
 * @returns {Object} A logger object with standard logging methods
 */
export function getLoggerOrDefault(providedLogger = null) {
	return providedLogger || createStandardLogger();
}

/**
 * ollama.js
 * AI provider implementation for Ollama models using the ollama-ai-provider package.
 */

import { createOllama } from 'ollama-ai-provider';
import { BaseAIProvider } from './base-provider.js';

export class OllamaAIProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'Ollama';
	}

	/**
	 * Override auth validation - Ollama doesn't require API keys
	 * @param {object} params - Parameters to validate
	 */
	validateAuth(_params) {
		// Ollama runs locally and doesn't require API keys
		// No authentication validation needed
	}

	/**
	 * Creates and returns an Ollama client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} [params.baseURL] - Optional Ollama base URL (defaults to http://localhost:11434)
	 * @returns {Function} Ollama client function
	 * @throws {Error} If initialization fails
	 */
	getClient(params) {
		try {
			const { baseURL } = params;

			return createOllama({
				...(baseURL && { baseURL })
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}

	isRequiredApiKey() {
		return false;
	}

	/**
	 * Returns the required API key environment variable name for Ollama.
	 * @returns {string} The environment variable name
	 */
	getRequiredApiKeyName() {
		return 'OLLAMA_API_KEY';
	}
}

/**
 * google.js
 * AI provider implementation for Google AI models using Vercel AI SDK.
 */

import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { BaseAIProvider } from './base-provider.js';

export class GoogleAIProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'Google';
	}

	/**
	 * Returns the environment variable name required for this provider's API key.
	 * @returns {string} The environment variable name for the Google API key
	 */
	getRequiredApiKeyName() {
		return 'GOOGLE_API_KEY';
	}

	/**
	 * Creates and returns a Google AI client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} params.apiKey - Google API key
	 * @param {string} [params.baseURL] - Optional custom API endpoint
	 * @returns {Function} Google AI client function
	 * @throws {Error} If API key is missing or initialization fails
	 */
	getClient(params) {
		try {
			const { apiKey, baseURL } = params;

			if (!apiKey) {
				throw new Error('Google API key is required.');
			}

			return createGoogleGenerativeAI({
				apiKey,
				...(baseURL && { baseURL })
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}
}

/**
 * openai.js
 * AI provider implementation for OpenAI models using Vercel AI SDK.
 */

import { createOpenAI } from '@ai-sdk/openai';
import { BaseAIProvider } from './base-provider.js';

export class OpenAIProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'OpenAI';
	}

	/**
	 * Returns the environment variable name required for this provider's API key.
	 * @returns {string} The environment variable name for the OpenAI API key
	 */
	getRequiredApiKeyName() {
		return 'OPENAI_API_KEY';
	}

	/**
	 * Creates and returns an OpenAI client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} params.apiKey - OpenAI API key
	 * @param {string} [params.baseURL] - Optional custom API endpoint
	 * @returns {Function} OpenAI client function
	 * @throws {Error} If API key is missing or initialization fails
	 */
	getClient(params) {
		try {
			const { apiKey, baseURL } = params;

			if (!apiKey) {
				throw new Error('OpenAI API key is required.');
			}

			return createOpenAI({
				apiKey,
				...(baseURL && { baseURL })
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}
}

/**
 * Path constants for Task Master application
 */

// .taskmaster directory structure paths
export const TASKMASTER_DIR = '.taskmaster';
export const TASKMASTER_TASKS_DIR = '.taskmaster/tasks';
export const TASKMASTER_DOCS_DIR = '.taskmaster/docs';
export const TASKMASTER_REPORTS_DIR = '.taskmaster/reports';
export const TASKMASTER_TEMPLATES_DIR = '.taskmaster/templates';

// Task Master configuration files
export const TASKMASTER_CONFIG_FILE = '.taskmaster/config.json';
export const TASKMASTER_STATE_FILE = '.taskmaster/state.json';
export const LEGACY_CONFIG_FILE = '.taskmasterconfig';

// Task Master report files
export const COMPLEXITY_REPORT_FILE =
	'.taskmaster/reports/task-complexity-report.json';
export const LEGACY_COMPLEXITY_REPORT_FILE =
	'scripts/task-complexity-report.json';

// Task Master PRD file paths
export const PRD_FILE = '.taskmaster/docs/prd.txt';
export const LEGACY_PRD_FILE = 'scripts/prd.txt';

// Task Master template files
export const EXAMPLE_PRD_FILE = '.taskmaster/templates/example_prd.txt';
export const LEGACY_EXAMPLE_PRD_FILE = 'scripts/example_prd.txt';

// Task Master task file paths
export const TASKMASTER_TASKS_FILE = '.taskmaster/tasks/tasks.json';
export const LEGACY_TASKS_FILE = 'tasks/tasks.json';

// General project files (not Task Master specific but commonly used)
export const ENV_EXAMPLE_FILE = '.env.example';
export const GITIGNORE_FILE = '.gitignore';

// Task file naming pattern
export const TASK_FILE_PREFIX = 'task_';
export const TASK_FILE_EXTENSION = '.txt';

/**
 * Project markers used to identify a task-master project root
 * These files/directories indicate that a directory is a Task Master project
 */
export const PROJECT_MARKERS = [
	'.taskmaster', // New taskmaster directory
	LEGACY_CONFIG_FILE, // .taskmasterconfig
	'tasks.json', // Generic tasks file
	LEGACY_TASKS_FILE, // tasks/tasks.json (legacy location)
	TASKMASTER_TASKS_FILE, // .taskmaster/tasks/tasks.json (new location)
	'.git', // Git repository
	'.svn' // SVN repository
];

/**
 * src/ai-providers/groq.js
 *
 * Implementation for interacting with Groq models
 * using the Vercel AI SDK.
 */

import { createGroq } from '@ai-sdk/groq';
import { BaseAIProvider } from './base-provider.js';

export class GroqProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'Groq';
	}

	/**
	 * Returns the environment variable name required for this provider's API key.
	 * @returns {string} The environment variable name for the Groq API key
	 */
	getRequiredApiKeyName() {
		return 'GROQ_API_KEY';
	}

	/**
	 * Creates and returns a Groq client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} params.apiKey - Groq API key
	 * @param {string} [params.baseURL] - Optional custom API endpoint
	 * @returns {Function} Groq client function
	 * @throws {Error} If API key is missing or initialization fails
	 */
	getClient(params) {
		try {
			const { apiKey, baseURL } = params;

			if (!apiKey) {
				throw new Error('Groq API key is required.');
			}

			return createGroq({
				apiKey,
				...(baseURL && { baseURL })
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}
}

/**
 * openrouter.js
 * AI provider implementation for OpenRouter models using Vercel AI SDK.
 */

import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { BaseAIProvider } from './base-provider.js';

export class OpenRouterAIProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'OpenRouter';
	}

	/**
	 * Returns the environment variable name required for this provider's API key.
	 * @returns {string} The environment variable name for the OpenRouter API key
	 */
	getRequiredApiKeyName() {
		return 'OPENROUTER_API_KEY';
	}

	/**
	 * Creates and returns an OpenRouter client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} params.apiKey - OpenRouter API key
	 * @param {string} [params.baseURL] - Optional custom API endpoint
	 * @returns {Function} OpenRouter client function
	 * @throws {Error} If API key is missing or initialization fails
	 */
	getClient(params) {
		try {
			const { apiKey, baseURL } = params;

			if (!apiKey) {
				throw new Error('OpenRouter API key is required.');
			}

			return createOpenRouter({
				apiKey,
				...(baseURL && { baseURL })
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}
}

// mcp-server/src/tools/get-operation-status.js
import { z } from 'zod';
import { createErrorResponse, createContentResponse } from './utils.js'; // Assuming these utils exist

/**
 * Register the get_operation_status tool.
 * @param {FastMCP} server - FastMCP server instance.
 * @param {AsyncOperationManager} asyncManager - The async operation manager.
 */
export function registerGetOperationStatusTool(server, asyncManager) {
	server.addTool({
		name: 'get_operation_status',
		description:
			'Retrieves the status and result/error of a background operation.',
		parameters: z.object({
			operationId: z.string().describe('The ID of the operation to check.')
		}),
		execute: async (args, { log }) => {
			try {
				const { operationId } = args;
				log.info(`Checking status for operation ID: ${operationId}`);

				const status = asyncManager.getStatus(operationId);

				// Status will now always return an object, but it might have status='not_found'
				if (status.status === 'not_found') {
					log.warn(`Operation ID not found: ${operationId}`);
					return createErrorResponse(
						status.error?.message || `Operation ID not found: ${operationId}`,
						status.error?.code || 'OPERATION_NOT_FOUND'
					);
				}

				log.info(`Status for ${operationId}: ${status.status}`);
				return createContentResponse(status);
			} catch (error) {
				log.error(`Error in get_operation_status tool: ${error.message}`, {
					stack: error.stack
				});
				return createErrorResponse(
					`Failed to get operation status: ${error.message}`,
					'GET_STATUS_ERROR'
				);
			}
		}
	});
}

import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { BaseAIProvider } from './base-provider.js';

export class BedrockAIProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'Bedrock';
	}

	isRequiredApiKey() {
		return false;
	}

	/**
	 * Returns the required API key environment variable name for Bedrock.
	 * Bedrock uses AWS credentials, so we return the AWS access key identifier.
	 * @returns {string} The environment variable name
	 */
	getRequiredApiKeyName() {
		return 'AWS_ACCESS_KEY_ID';
	}

	/**
	 * Override auth validation - Bedrock uses AWS credentials instead of API keys
	 * @param {object} params - Parameters to validate
	 */
	validateAuth(params) {}

	/**
	 * Creates and returns a Bedrock client instance.
	 * See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
	 * for AWS SDK environment variables and configuration options.
	 */
	getClient(params) {
		try {
			const credentialProvider = fromNodeProviderChain();

			return createAmazonBedrock({
				credentialProvider
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}
}

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { log } from '../../scripts/modules/utils.js';

/**
 * Reads the version from the nearest package.json relative to this file.
 * Returns 'unknown' if not found or on error.
 * @returns {string} The version string or 'unknown'.
 */
export function getTaskMasterVersion() {
	let version = 'unknown';
	try {
		// Get the directory of the current module (getPackageVersion.js)
		const currentModuleFilename = fileURLToPath(import.meta.url);
		const currentModuleDirname = path.dirname(currentModuleFilename);
		// Construct the path to package.json relative to this file (../../package.json)
		const packageJsonPath = path.join(
			currentModuleDirname,
			'..',
			'..',
			'package.json'
		);

		if (fs.existsSync(packageJsonPath)) {
			const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
			const packageJson = JSON.parse(packageJsonContent);
			version = packageJson.version;
		}
	} catch (error) {
		// Silently fall back to default version
		log('warn', 'Could not read own package.json for version info.', error);
	}
	return version;
}

/**
 * task-manager.js
 * Task management functions for the Task Master CLI
 */

import { findTaskById } from './utils.js';
import parsePRD from './task-manager/parse-prd.js';
import updateTasks from './task-manager/update-tasks.js';
import updateTaskById from './task-manager/update-task-by-id.js';
import generateTaskFiles from './task-manager/generate-task-files.js';
import setTaskStatus from './task-manager/set-task-status.js';
import updateSingleTaskStatus from './task-manager/update-single-task-status.js';
import listTasks from './task-manager/list-tasks.js';
import expandTask from './task-manager/expand-task.js';
import expandAllTasks from './task-manager/expand-all-tasks.js';
import clearSubtasks from './task-manager/clear-subtasks.js';
import addTask from './task-manager/add-task.js';
import analyzeTaskComplexity from './task-manager/analyze-task-complexity.js';
import findNextTask from './task-manager/find-next-task.js';
import addSubtask from './task-manager/add-subtask.js';
import removeSubtask from './task-manager/remove-subtask.js';
import updateSubtaskById from './task-manager/update-subtask-by-id.js';
import removeTask from './task-manager/remove-task.js';
import taskExists from './task-manager/task-exists.js';
import isTaskDependentOn from './task-manager/is-task-dependent.js';
import setResponseLanguage from './task-manager/response-language.js';
import moveTask from './task-manager/move-task.js';
import { migrateProject } from './task-manager/migrate.js';
import { performResearch } from './task-manager/research.js';
import { readComplexityReport } from './utils.js';

// Export task manager functions
export {
	parsePRD,
	updateTasks,
	updateTaskById,
	updateSubtaskById,
	generateTaskFiles,
	setTaskStatus,
	updateSingleTaskStatus,
	listTasks,
	expandTask,
	expandAllTasks,
	clearSubtasks,
	addTask,
	addSubtask,
	removeSubtask,
	findNextTask,
	analyzeTaskComplexity,
	removeTask,
	findTaskById,
	taskExists,
	isTaskDependentOn,
	setResponseLanguage,
	moveTask,
	readComplexityReport,
	migrateProject,
	performResearch
};

/**
 * @fileoverview Type definitions for Claude Code AI SDK provider
 * These JSDoc types mirror the TypeScript interfaces from the original provider
 */

/**
 * Claude Code provider settings
 * @typedef {Object} ClaudeCodeSettings
 * @property {string} [pathToClaudeCodeExecutable='claude'] - Custom path to Claude Code CLI executable
 * @property {string} [customSystemPrompt] - Custom system prompt to use
 * @property {string} [appendSystemPrompt] - Append additional content to the system prompt
 * @property {number} [maxTurns] - Maximum number of turns for the conversation
 * @property {number} [maxThinkingTokens] - Maximum thinking tokens for the model
 * @property {string} [cwd] - Working directory for CLI operations
 * @property {'bun'|'deno'|'node'} [executable='node'] - JavaScript runtime to use
 * @property {string[]} [executableArgs] - Additional arguments for the JavaScript runtime
 * @property {'default'|'acceptEdits'|'bypassPermissions'|'plan'} [permissionMode='default'] - Permission mode for tool usage
 * @property {string} [permissionPromptToolName] - Custom tool name for permission prompts
 * @property {boolean} [continue] - Continue the most recent conversation
 * @property {string} [resume] - Resume a specific session by ID
 * @property {string[]} [allowedTools] - Tools to explicitly allow during execution (e.g., ['Read', 'LS', 'Bash(git log:*)'])
 * @property {string[]} [disallowedTools] - Tools to disallow during execution (e.g., ['Write', 'Edit', 'Bash(rm:*)'])
 * @property {Object.<string, MCPServerConfig>} [mcpServers] - MCP server configuration
 * @property {boolean} [verbose] - Enable verbose logging for debugging
 */

/**
 * MCP Server configuration
 * @typedef {Object} MCPServerConfig
 * @property {'stdio'|'sse'} [type='stdio'] - Server type
 * @property {string} command - Command to execute (for stdio type)
 * @property {string[]} [args] - Arguments for the command
 * @property {Object.<string, string>} [env] - Environment variables
 * @property {string} url - URL for SSE type servers
 * @property {Object.<string, string>} [headers] - Headers for SSE type servers
 */

/**
 * Model ID type - either 'opus', 'sonnet', or any string
 * @typedef {'opus'|'sonnet'|string} ClaudeCodeModelId
 */

/**
 * Language model options
 * @typedef {Object} ClaudeCodeLanguageModelOptions
 * @property {ClaudeCodeModelId} id - The model ID
 * @property {ClaudeCodeSettings} [settings] - Optional settings
 */

/**
 * Error metadata for Claude Code errors
 * @typedef {Object} ClaudeCodeErrorMetadata
 * @property {string} [code] - Error code
 * @property {number} [exitCode] - Process exit code
 * @property {string} [stderr] - Standard error output
 * @property {string} [promptExcerpt] - Excerpt of the prompt that caused the error
 */

/**
 * Claude Code provider interface
 * @typedef {Object} ClaudeCodeProvider
 * @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} languageModel - Create a language model
 * @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} chat - Alias for languageModel
 * @property {function(string): never} textEmbeddingModel - Throws NoSuchModelError (not supported)
 */

/**
 * Claude Code provider settings
 * @typedef {Object} ClaudeCodeProviderSettings
 * @property {ClaudeCodeSettings} [defaultSettings] - Default settings to use for all models
 */

export {}; // This ensures the file is treated as a module

/**
 * src/ai-providers/claude-code.js
 *
 * Implementation for interacting with Claude models via Claude Code CLI
 * using a custom AI SDK implementation.
 */

import { createClaudeCode } from './custom-sdk/claude-code/index.js';
import { BaseAIProvider } from './base-provider.js';
import { getClaudeCodeSettingsForCommand } from '../../scripts/modules/config-manager.js';

export class ClaudeCodeProvider extends BaseAIProvider {
	constructor() {
		super();
		this.name = 'Claude Code';
	}

	getRequiredApiKeyName() {
		return 'CLAUDE_CODE_API_KEY';
	}

	isRequiredApiKey() {
		return false;
	}

	/**
	 * Override validateAuth to skip API key validation for Claude Code
	 * @param {object} params - Parameters to validate
	 */
	validateAuth(params) {
		// Claude Code doesn't require an API key
		// No validation needed
	}

	/**
	 * Creates and returns a Claude Code client instance.
	 * @param {object} params - Parameters for client initialization
	 * @param {string} [params.commandName] - Name of the command invoking the service
	 * @param {string} [params.baseURL] - Optional custom API endpoint (not used by Claude Code)
	 * @returns {Function} Claude Code client function
	 * @throws {Error} If initialization fails
	 */
	getClient(params) {
		try {
			// Claude Code doesn't use API keys or base URLs
			// Just return the provider factory
			return createClaudeCode({
				defaultSettings: getClaudeCodeSettingsForCommand(params?.commandName)
			});
		} catch (error) {
			this.handleError('client initialization', error);
		}
	}
}

// test-config-manager.js
console.log('=== ENVIRONMENT TEST ===');
console.log('Working directory:', process.cwd());
console.log('NODE_PATH:', process.env.NODE_PATH);

// Test basic imports
try {
	console.log('Importing config-manager');
	// Use dynamic import for ESM
	const configManagerModule = await import(
		'./scripts/modules/config-manager.js'
	);
	const configManager = configManagerModule.default || configManagerModule;
	console.log('Config manager loaded successfully');

	console.log('Loading supported models');
	// Add after line 14 (after "Config manager loaded successfully")
	console.log('Config manager exports:', Object.keys(configManager));
} catch (error) {
	console.error('Import error:', error.message);
	console.error(error.stack);
}

// Test file access
try {
	console.log('Checking for .taskmasterconfig');
	// Use dynamic import for ESM
	const { readFileSync, existsSync } = await import('fs');
	const { resolve } = await import('path');

	const configExists = existsSync('./.taskmasterconfig');
	console.log('.taskmasterconfig exists:', configExists);

	if (configExists) {
		const config = JSON.parse(readFileSync('./.taskmasterconfig', 'utf-8'));
		console.log('Config keys:', Object.keys(config));
	}

	console.log('Checking for supported-models.json');
	const modelsPath = resolve('./scripts/modules/supported-models.json');
	console.log('Models path:', modelsPath);
	const modelsExists = existsSync(modelsPath);
	console.log('supported-models.json exists:', modelsExists);
} catch (error) {
	console.error('File access error:', error.message);
}

console.log('=== TEST COMPLETE ===');

/**
 * @typedef {'pending' | 'done' | 'in-progress' | 'review' | 'deferred' | 'cancelled'} TaskStatus
 */

/**
 * Task status options list
 * @type {TaskStatus[]}
 * @description Defines possible task statuses:
 * - pending: Task waiting to start
 * - done: Task completed
 * - in-progress: Task in progress
 * - review: Task completed and waiting for review
 * - deferred: Task postponed or paused
 * - cancelled: Task cancelled and will not be completed
 */
export const TASK_STATUS_OPTIONS = [
	'pending',
	'done',
	'in-progress',
	'review',
	'deferred',
	'cancelled'
];

/**
 * Check if a given status is a valid task status
 * @param {string} status - The status to check
 * @returns {boolean} True if the status is valid, false otherwise
 */
export function isValidTaskStatus(status) {
	return TASK_STATUS_OPTIONS.includes(status);
}

/**
 * response-language.js
 * Direct function for managing response language via MCP
 */

import { setResponseLanguage } from '../../../../scripts/modules/task-manager.js';
import {
	enableSilentMode,
	disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';

export async function responseLanguageDirect(args, log, context = {}) {
	const { projectRoot, language } = args;
	const mcpLog = createLogWrapper(log);

	log.info(
		`Executing response-language_direct with args: ${JSON.stringify(args)}`
	);
	log.info(`Using project root: ${projectRoot}`);

	try {
		enableSilentMode();
		return setResponseLanguage(language, {
			mcpLog,
			projectRoot
		});
	} catch (error) {
		return {
			success: false,
			error: {
				code: 'DIRECT_FUNCTION_ERROR',
				message: error.message,
				details: error.stack
			}
		};
	} finally {
		disableSilentMode();
	}
}

#!/usr/bin/env node

import TaskMasterMCPServer from './src/index.js';
import dotenv from 'dotenv';
import logger from './src/logger.js';

// Load environment variables
dotenv.config();

/**
 * Start the MCP server
 */
async function startServer() {
	const server = new TaskMasterMCPServer();

	// Handle graceful shutdown
	process.on('SIGINT', async () => {
		await server.stop();
		process.exit(0);
	});

	process.on('SIGTERM', async () => {
		await server.stop();
		process.exit(0);
	});

	try {
		await server.start();
	} catch (error) {
		logger.error(`Failed to start MCP server: ${error.message}`);
		process.exit(1);
	}
}

// Start the server
startServer();

/**
 * cache-stats.js
 * Direct function implementation for retrieving cache statistics
 */

import { contextManager } from '../context-manager.js';

/**
 * Get cache statistics for monitoring
 * @param {Object} args - Command arguments
 * @param {Object} log - Logger object
 * @returns {Object} - Cache statistics
 */
export async function getCacheStatsDirect(args, log) {
	try {
		log.info('Retrieving cache statistics');
		const stats = contextManager.getStats();
		return {
			success: true,
			data: stats
		};
	} catch (error) {
		log.error(`Error getting cache stats: ${error.message}`);
		return {
			success: false,
			error: {
				code: 'CACHE_STATS_ERROR',
				message: error.message || 'Unknown error occurred'
			}
		};
	}
}

/**
 * @typedef {'add' | 'remove'} RulesAction
 */

/**
 * Individual rules action constants
 */
export const RULES_ACTIONS = {
	ADD: 'add',
	REMOVE: 'remove'
};

/**
 * Special rules command (not a CRUD operation)
 */
export const RULES_SETUP_ACTION = 'setup';

/**
 * Check if a given action is a valid rules action
 * @param {string} action - The action to check
 * @returns {boolean} True if the action is valid, false otherwise
 */
export function isValidRulesAction(action) {
	return Object.values(RULES_ACTIONS).includes(action);
}

#!/usr/bin/env node

/**
 * dev.js
 * Task Master CLI - AI-driven development task management
 *
 * This is the refactored entry point that uses the modular architecture.
 * It imports functionality from the modules directory and provides a CLI.
 */

import dotenv from 'dotenv';
dotenv.config();

// Add at the very beginning of the file
if (process.env.DEBUG === '1') {
	console.error('DEBUG - dev.js received args:', process.argv.slice(2));
}

import { runCLI } from './modules/commands.js';

// Run the CLI with the process arguments
runCLI(process.argv);

/**
 * Provider validation constants
 * Defines which providers should be validated against the supported-models.json file
 */

// Providers that have predefined model lists and should be validated
export const VALIDATED_PROVIDERS = [
	'anthropic',
	'openai',
	'google',
	'perplexity',
	'xai',
	'groq',
	'mistral'
];

// Custom providers object for easy named access
export const CUSTOM_PROVIDERS = {
	AZURE: 'azure',
	VERTEX: 'vertex',
	BEDROCK: 'bedrock',
	OPENROUTER: 'openrouter',
	OLLAMA: 'ollama',
	CLAUDE_CODE: 'claude-code',
	MCP: 'mcp',
	GEMINI_CLI: 'gemini-cli'
};

// Custom providers array (for backward compatibility and iteration)
export const CUSTOM_PROVIDERS_ARRAY = Object.values(CUSTOM_PROVIDERS);

// All known providers (for reference)
export const ALL_PROVIDERS = [
	...VALIDATED_PROVIDERS,
	...CUSTOM_PROVIDERS_ARRAY
];

// Gemini profile for rule-transformer
import { createProfile } from './base-profile.js';

// Create and export gemini profile using the base factory
export const geminiProfile = createProfile({
	name: 'gemini',
	displayName: 'Gemini',
	url: 'codeassist.google',
	docsUrl: 'github.com/google-gemini/gemini-cli',
	profileDir: '.gemini', // Keep .gemini for settings.json
	rulesDir: '.', // Root directory for GEMINI.md
	mcpConfigName: 'settings.json', // Override default 'mcp.json'
	includeDefaultRules: false,
	fileMap: {
		'AGENTS.md': 'GEMINI.md'
	}
});

// Codex profile for rule-transformer
import { createProfile } from './base-profile.js';

// Create and export codex profile using the base factory
export const codexProfile = createProfile({
	name: 'codex',
	displayName: 'Codex',
	url: 'codex.ai',
	docsUrl: 'platform.openai.com/docs/codex',
	profileDir: '.', // Root directory
	rulesDir: '.', // No specific rules directory needed
	mcpConfig: false,
	mcpConfigName: null,
	includeDefaultRules: false,
	fileMap: {
		'AGENTS.md': 'AGENTS.md'
	}
});

/**
 * Command related constants
 * Defines which commands trigger AI processing
 */

// Command names that trigger AI processing
export const AI_COMMAND_NAMES = [
	'add-task',
	'analyze-complexity',
	'expand-task',
	'parse-prd',
	'research',
	'research-save',
	'update-subtask',
	'update-task',
	'update-tasks'
];

// Cursor conversion profile for rule-transformer
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';

// Create and export cursor profile using the base factory
export const cursorProfile = createProfile({
	name: 'cursor',
	displayName: 'Cursor',
	url: 'cursor.so',
	docsUrl: 'docs.cursor.com',
	targetExtension: '.mdc', // Cursor keeps .mdc extension
	supportsRulesSubdirectories: true
});

// Cline conversion profile for rule-transformer
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';

// Create and export cline profile using the base factory
export const clineProfile = createProfile({
	name: 'cline',
	displayName: 'Cline',
	url: 'cline.bot',
	docsUrl: 'docs.cline.bot',
	profileDir: '.clinerules',
	rulesDir: '.clinerules',
	mcpConfig: false
});

// Trae conversion profile for rule-transformer
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';

// Create and export trae profile using the base factory
export const traeProfile = createProfile({
	name: 'trae',
	displayName: 'Trae',
	url: 'trae.ai',
	docsUrl: 'docs.trae.ai',
	mcpConfig: false
});

// Windsurf conversion profile for rule-transformer
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';

// Create and export windsurf profile using the base factory
export const windsurfProfile = createProfile({
	name: 'windsurf',
	displayName: 'Windsurf',
	url: 'windsurf.com',
	docsUrl: 'docs.windsurf.com'
});

/**
 * index.js
 * Main export point for all Task Master CLI modules
 */

// Export all modules
export * from './ui.js';
export * from './utils.js';
export * from './commands.js';
export * from './task-manager.js';
export * from './prompt-manager.js';

/**
 * src/ai-providers/index.js
 * Central export point for all AI provider classes
 */

export { AnthropicAIProvider } from './anthropic.js';
export { PerplexityAIProvider } from './perplexity.js';
export { GoogleAIProvider } from './google.js';
export { OpenAIProvider } from './openai.js';
export { XAIProvider } from './xai.js';
export { GroqProvider } from './groq.js';
export { OpenRouterAIProvider } from './openrouter.js';
export { OllamaAIProvider } from './ollama.js';
export { BedrockAIProvider } from './bedrock.js';
export { AzureProvider } from './azure.js';
export { VertexAIProvider } from './google-vertex.js';
export { ClaudeCodeProvider } from './claude-code.js';
export { GeminiCliProvider } from './gemini-cli.js';

// Profile exports for centralized importing
export { ampProfile } from './amp.js';
export { claudeProfile } from './claude.js';
export { clineProfile } from './cline.js';
export { codexProfile } from './codex.js';
export { cursorProfile } from './cursor.js';
export { geminiProfile } from './gemini.js';
export { kiroProfile } from './kiro.js';
export { opencodeProfile } from './opencode.js';
export { rooProfile } from './roo.js';
export { traeProfile } from './trae.js';
export { vscodeProfile } from './vscode.js';
export { windsurfProfile } from './windsurf.js';
export { zedProfile } from './zed.js';

class
FuzzyTaskSearch
{
constructor (tasks, searchType = 'default') {}
_prepareSearchableTasks (tasks) {}
_extractPromptWords (prompt) {}
findRelevantTasks (prompt, options = {}) {}
getTaskIds (searchResults) {}
getTaskIdsWithSubtasks (searchResults, includeSubtasks = false) {}
formatSearchSummary (searchResults, options = {}) {}
}

This code segment functions to: Implement a fuzzy search algorithm that combines multiple matching strategies and relevance scoring to retrieve structured data entries based on text input patterns, while providing configurable search parameters and result categorization.

```javascript
[TASK_BEGIN]
class FuzzyTaskSearch {
	constructor(tasks, searchType = 'default') {
		const config = SEARCH_CONFIGS[searchType] || SEARCH_CONFIGS.default;
		this.searchInstance = new Fuse(this._prepareSearchableTasks(tasks), config);
	}

	_prepareSearchableTasks(tasks) {
		return tasks.flatMap(task => {
			const searchableTask = {
				...task,
				dependencyTitles: task.dependencies?.map(d => d.title) || []
			};

			// Include subtasks in searchable collection
			return [searchableTask, ...(task.subtasks || []).map(subtask => ({
				...subtask,
				parentId: task.id,
				dependencyTitles: []
			}))];
		});
	}

	_extractPromptWords(prompt) {
		return prompt.toLowerCase()
			.split(/[^\w]+/g)
			.filter(word => word.length > 2);
	}

	findRelevantTasks(prompt, options = {}) {
		const searchQuery = this._extractPromptWords(prompt).join(' ');
		const rawResults = this.searchInstance.search(searchQuery);

		return rawResults
			.map(result => ({
				...result.item,
				score: result.score,
				matchMetadata: result.matches
			}))
			.sort((a, b) => a.score - b.score)
			.slice(0, options.maxResults || SEARCH_CONFIGS.default.limit);
	}

	getTaskIds(searchResults) {
		return [...new Set(searchResults.map(result =>
			result.parentId ? `${result.parentId}.${result.id}` : result.id
		))];
	}

	getTaskIdsWithSubtasks(searchResults, includeSubtasks = false) {
		const ids = new Set();
		searchResults.forEach(result => {
			if (!result.parentId) ids.add(result.id);
			if (includeSubtasks || result.parentId) ids.add(result.parentId ? `${result.parentId}.${result.id}` : result.id);
		});
		return Array.from(ids);
	}

	formatSearchSummary(searchResults, options = {}) {
		const categoryCounts = PURPOSE_CATEGORIES.reduce((acc, category) => {
			const count = searchResults.filter(result =>
				category.pattern.test(result.title) || category.pattern.test(result.description)
			).length;
			return {...acc, [category.label]: count};
		}, {});

		const relevanceLevel = Object.entries(RELEVANCE_THRESHOLDS)
			.find(([_, threshold]) =>
				searchResults[0]?.score <= threshold)?.[0] || 'low';

		const exampleTasks = searchResults
			.slice(0, 3)
			.map(t => t.title)
			.join(', ');

		return `Found ${searchResults.length} potentially relevant tasks (${relevanceLevel} confidence):\n` +
			`Categories: ${JSON.stringify(categoryCounts)}\n` +
			`Examples: ${exampleTasks}${options.verbose ? '\n' + searchResults.map(t => `- ${t.title} (score: ${t.score.toFixed(2)})`).join('\n') : ''}`;
	}
}
[TASK_END]
```

class FuzzyTaskSearch {
	constructor(tasks, searchType = 'default') {
		const config = SEARCH_CONFIGS[searchType] || SEARCH_CONFIGS.default;
		this.searchInstance = new Fuse(this._prepareSearchableTasks(tasks), config);
	}
	_prepareSearchableTasks(tasks) {
		return tasks.flatMap(task => {
			const searchableTask = {
				...task,
				dependencyTitles: task.dependencies?.map(d => d.title) || []
			};
			return [searchableTask, ...(task.subtasks || []).map(subtask => ({
				...subtask,
				parentId: task.id,
				dependencyTitles: []
			}))];
		});
	}
	_extractPromptWords(prompt) {
		return prompt.toLowerCase()
			.split(/[^\w]+/g)
			.filter(word => word.length > 2);
	}
	findRelevantTasks(prompt, options = {}) {
		const searchQuery = this._extractPromptWords(prompt).join(' ');
		const rawResults = this.searchInstance.search(searchQuery);
		return rawResults
			.map(result => ({
				...result.item,
				score: result.score,
				matchMetadata: result.matches
			}))
			.sort((a, b) => a.score - b.score)
			.slice(0, options.maxResults || SEARCH_CONFIGS.default.limit);
	}
	getTaskIds(searchResults) {
		return [...new Set(searchResults.map(result =>
			result.parentId ? `${result.parentId}.${result.id}` : result.id
		))];
	}
	getTaskIdsWithSubtasks(searchResults, includeSubtasks = false) {
		const ids = new Set();
		searchResults.forEach(result => {
			if (!result.parentId) ids.add(result.id);
			if (includeSubtasks || result.parentId) ids.add(result.parentId ? `${result.parentId}.${result.id}` : result.id);
		});
		return Array.from(ids);
	}
	formatSearchSummary(searchResults, options = {}) {
		const categoryCounts = PURPOSE_CATEGORIES.reduce((acc, category) => {
			const count = searchResults.filter(result =>
				category.pattern.test(result.title) || category.pattern.test(result.description)
			).length;
			return {...acc, [category.label]: count};
		}, {});
		const relevanceLevel = Object.entries(RELEVANCE_THRESHOLDS)
			.find(([_, threshold]) =>
				searchResults[0]?.score <= threshold)?.[0] || 'low';
		const exampleTasks = searchResults
			.slice(0, 3)
			.map(t => t.title)
			.join(', ');
		return `Found ${searchResults.length} potentially relevant tasks (${relevanceLevel} confidence):\n` +
			`Categories: ${JSON.stringify(categoryCounts)}\n` +
			`Examples: ${exampleTasks}${options.verbose ? '\n' + searchResults.map(t => `- ${t.title} (score: ${t.score.toFixed(2)})`).join('\n') : ''}`;
	}
}