Merge branch 'next' of https://github.com/eyaltoledano/claude-task-master into joedanz/flexible-brand-rules

# Conflicts:
#	mcp-server/src/tools/index.js
#	scripts/modules/commands.js
This commit is contained in:
Joe Danziger
2025-05-26 19:19:07 -04:00
55 changed files with 8442 additions and 666 deletions

View File

@@ -9,6 +9,7 @@ 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 ora from 'ora'; // Import ora
@@ -30,7 +31,8 @@ import {
updateSubtaskById,
removeTask,
findTaskById,
taskExists
taskExists,
moveTask
} from './task-manager.js';
import {
@@ -47,7 +49,8 @@ import {
writeConfig,
ConfigurationError,
isConfigFilePresent,
getAvailableModels
getAvailableModels,
getBaseUrlForRole
} from './config-manager.js';
import {
@@ -162,6 +165,64 @@ async function runInteractiveSetup(projectRoot) {
});
}
// 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
@@ -189,6 +250,11 @@ async function runInteractiveSetup(projectRoot) {
value: '__CUSTOM_OPENROUTER__'
};
const customOllamaOption = {
name: '* Custom Ollama model', // Symbol updated
value: '__CUSTOM_OLLAMA__'
};
let choices = [];
let defaultIndex = 0; // Default to 'Cancel'
@@ -234,6 +300,7 @@ async function runInteractiveSetup(projectRoot) {
}
commonPrefix.push(cancelOption);
commonPrefix.push(customOpenRouterOption);
commonPrefix.push(customOllamaOption);
let prefixLength = commonPrefix.length; // Initial prefix length
@@ -364,6 +431,47 @@ async function runInteractiveSetup(projectRoot) {
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 = '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 &&
typeof selectedValue === 'object' &&
@@ -517,6 +625,10 @@ function registerCommands(programInstance) {
'--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'
)
.action(async (file, options) => {
// Use input option if file argument not provided
const inputFile = file || options.input;
@@ -525,6 +637,7 @@ function registerCommands(programInstance) {
const outputPath = options.output;
const force = options.force || false;
const append = options.append || false;
const research = options.research || false;
let useForce = force;
let useAppend = append;
@@ -557,7 +670,8 @@ function registerCommands(programInstance) {
spinner = ora('Parsing PRD and generating tasks...\n').start();
await parsePRD(defaultPrdPath, outputPath, numTasks, {
append: useAppend, // Changed key from useAppend to append
force: useForce // Changed key from useForce to force
force: useForce, // Changed key from useForce to force
research: research
});
spinner.succeed('Tasks generated successfully!');
return;
@@ -581,13 +695,15 @@ function registerCommands(programInstance) {
' -o, --output <file> Output file path (default: "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\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\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 scripts/prd.txt by default\n' +
@@ -615,11 +731,19 @@ function registerCommands(programInstance) {
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();
await parsePRD(inputFile, outputPath, numTasks, {
useAppend: useAppend,
useForce: useForce
append: useAppend,
force: useForce,
research: research
});
spinner.succeed('Tasks generated successfully!');
} catch (error) {
@@ -1041,6 +1165,8 @@ function registerCommands(programInstance) {
// set-status command
programInstance
.command('set-status')
.alias('mark')
.alias('set')
.description('Set the status of a task')
.option(
'-i, --id <id>',
@@ -1221,6 +1347,12 @@ function registerCommands(programInstance) {
'-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')
.action(async (options) => {
const tasksPath = options.file || 'tasks/tasks.json';
const outputPath = options.output;
@@ -1231,6 +1363,16 @@ function registerCommands(programInstance) {
console.log(chalk.blue(`Analyzing task complexity from: ${tasksPath}`));
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(
@@ -2377,8 +2519,137 @@ Examples:
return; // Stop execution here
});
// Add/remove profile rules command
// 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', 'tasks/tasks.json')
.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'
)
.action(async (options) => {
const tasksPath = options.file;
const sourceId = options.from;
const destinationId = options.to;
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(tasksPath);
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(
tasksPath,
fromId,
toId,
i === sourceIds.length - 1
);
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(
tasksPath,
sourceId,
destinationId,
true
);
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 (e.g., task-master rules add windsurf roo)'