Recovers lost files and commits work from the past 5-6 days. Holy shit that was a close call.

This commit is contained in:
Eyal Toledano
2025-04-07 19:55:03 -04:00
parent 15b9b0e617
commit 9a5d1de29c
42 changed files with 5180 additions and 1988 deletions

View File

@@ -5,61 +5,53 @@
import { z } from "zod";
import {
handleApiResult,
createErrorResponse,
createContentResponse,
getProjectRootFromSession
getProjectRootFromSession,
executeTaskMasterCommand,
handleApiResult
} from "./utils.js";
import { addTaskDirect } from "../core/task-master-core.js";
/**
* Register the add-task tool with the MCP server
* Register the addTask tool with the MCP server
* @param {Object} server - FastMCP server instance
* @param {AsyncOperationManager} asyncManager - The async operation manager instance.
*/
export function registerAddTaskTool(server, asyncManager) {
export function registerAddTaskTool(server) {
server.addTool({
name: "add_task",
description: "Starts adding a new task using AI in the background.",
description: "Add a new task using AI",
parameters: z.object({
prompt: z.string().describe("Description of the task to add"),
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"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
projectRoot: z.string().optional().describe("Root directory of the project"),
research: z.boolean().optional().describe("Whether to use research capabilities for task creation")
}),
execute: async (args, context) => {
const { log, reportProgress, session } = context;
execute: async (args, { log, reportProgress, session }) => {
try {
log.info(`MCP add_task request received with prompt: \"${args.prompt}\"`);
log.info(`Starting add-task with args: ${JSON.stringify(args)}`);
if (!args.prompt) {
return createErrorResponse("Prompt is required for add_task.", "VALIDATION_ERROR");
}
// Get project root from session
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const directArgs = {
projectRoot: rootFolder,
...args
};
const operationId = asyncManager.addOperation(addTaskDirect, directArgs, context);
log.info(`Started background operation for add_task. Operation ID: ${operationId}`);
return createContentResponse({
message: "Add task operation started successfully.",
operationId: operationId
});
// Call the direct function
const result = await addTaskDirect({
...args,
projectRoot: rootFolder
}, log, { reportProgress, session });
// Return the result
return handleApiResult(result, log);
} catch (error) {
log.error(`Error initiating add_task operation: ${error.message}`, { stack: error.stack });
return createErrorResponse(`Failed to start add task operation: ${error.message}`, "ADD_TASK_INIT_ERROR");
log.error(`Error in add-task tool: ${error.message}`);
return createErrorResponse(error.message);
}
}
});

View File

@@ -27,10 +27,9 @@ export function registerAnalyzeTool(server) {
research: z.boolean().optional().describe("Use Perplexity AI for research-backed complexity analysis"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -42,9 +41,7 @@ export function registerAnalyzeTool(server) {
const result = await analyzeTaskComplexityDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Task complexity analysis complete: ${result.data.message}`);

View File

@@ -20,17 +20,16 @@ export function registerExpandAllTool(server) {
name: "expand_all",
description: "Expand all pending tasks into subtasks",
parameters: z.object({
num: z.union([z.number(), z.string()]).optional().describe("Number of subtasks to generate for each task"),
num: z.string().optional().describe("Number of subtasks to generate for each task"),
research: z.boolean().optional().describe("Enable Perplexity AI for research-backed subtask generation"),
prompt: z.string().optional().describe("Additional context to guide subtask generation"),
force: z.boolean().optional().describe("Force regeneration of subtasks for tasks that already have them"),
file: z.string().optional().describe("Path to the tasks file (default: tasks/tasks.json)"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -42,9 +41,7 @@ export function registerExpandAllTool(server) {
const result = await expandAllTasksDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Successfully expanded all tasks: ${result.data.message}`);

View File

@@ -10,6 +10,8 @@ import {
getProjectRootFromSession
} from "./utils.js";
import { expandTaskDirect } from "../core/task-master-core.js";
import fs from "fs";
import path from "path";
/**
* Register the expand-task tool with the MCP server
@@ -21,10 +23,9 @@ export function registerExpandTaskTool(server) {
description: "Expand a task into subtasks for detailed implementation",
parameters: z.object({
id: z.string().describe("ID of task to expand"),
num: z.union([z.number(), z.string()]).optional().describe("Number of subtasks to generate"),
num: z.union([z.string(), z.number()]).optional().describe("Number of subtasks to generate"),
research: z.boolean().optional().describe("Use Perplexity AI for research-backed generation"),
prompt: z.string().optional().describe("Additional context for subtask generation"),
force: z.boolean().optional().describe("Force regeneration even for tasks that already have subtasks"),
file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z
.string()
@@ -33,11 +34,11 @@ export function registerExpandTaskTool(server) {
"Root directory of the project (default: current working directory)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, reportProgress, session }) => {
try {
log.info(`Expanding task with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
log.info(`Starting expand-task with args: ${JSON.stringify(args)}`);
// Get project root from session
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
@@ -45,19 +46,27 @@ export function registerExpandTaskTool(server) {
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const result = await expandTaskDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
log.info(`Project root resolved to: ${rootFolder}`);
// await reportProgress({ progress: 100 });
// Check for tasks.json in the standard locations
const tasksJsonPath = path.join(rootFolder, 'tasks', 'tasks.json');
if (result.success) {
log.info(`Successfully expanded task with ID ${args.id}`);
if (fs.existsSync(tasksJsonPath)) {
log.info(`Found tasks.json at ${tasksJsonPath}`);
// Add the file parameter directly to args
args.file = tasksJsonPath;
} else {
log.error(`Failed to expand task: ${result.error?.message || 'Unknown error'}`);
log.warn(`Could not find tasks.json at ${tasksJsonPath}`);
}
// Call direct function with only session in the context, not reportProgress
// Use the pattern recommended in the MCP guidelines
const result = await expandTaskDirect({
...args,
projectRoot: rootFolder
}, log, { session }); // Only pass session, NOT reportProgress
// Return the result
return handleApiResult(result, log, 'Error expanding task');
} catch (error) {
log.error(`Error in expand task tool: ${error.message}`);

View File

@@ -28,7 +28,6 @@ import { registerAddDependencyTool } from "./add-dependency.js";
import { registerRemoveTaskTool } from './remove-task.js';
import { registerInitializeProjectTool } from './initialize-project.js';
import { asyncOperationManager } from '../core/utils/async-manager.js';
import { registerGetOperationStatusTool } from './get-operation-status.js';
/**
* Register all Task Master tools with the MCP server
@@ -61,7 +60,6 @@ export function registerTaskMasterTools(server, asyncManager) {
registerAddDependencyTool(server);
registerRemoveTaskTool(server);
registerInitializeProjectTool(server);
registerGetOperationStatusTool(server, asyncManager);
} catch (error) {
logger.error(`Error registering Task Master tools: ${error.message}`);
throw error;

View File

@@ -31,7 +31,7 @@ export function registerParsePRDTool(server) {
"Root directory of the project (default: automatically detected from session or CWD)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Parsing PRD with args: ${JSON.stringify(args)}`);
@@ -45,9 +45,7 @@ export function registerParsePRDTool(server) {
const result = await parsePRDDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Successfully parsed PRD: ${result.data.message}`);

View File

@@ -34,11 +34,11 @@ export function registerSetTaskStatusTool(server) {
"Root directory of the project (default: automatically detected)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Setting status of task(s) ${args.id} to: ${args.status}`);
// await reportProgress({ progress: 0 });
// Get project root from session
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
@@ -46,19 +46,20 @@ export function registerSetTaskStatusTool(server) {
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
// Call the direct function with the project root
const result = await setTaskStatusDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
...args,
projectRoot: rootFolder
}, log);
// Log the result
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'}`);
}
// Format and return the result
return handleApiResult(result, log, 'Error setting task status');
} catch (error) {
log.error(`Error in setTaskStatus tool: ${error.message}`);

View File

@@ -31,10 +31,9 @@ export function registerUpdateSubtaskTool(server) {
"Root directory of the project (default: current working directory)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Updating subtask with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -46,9 +45,7 @@ export function registerUpdateSubtaskTool(server) {
const result = await updateSubtaskByIdDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Successfully updated subtask with ID ${args.id}`);

View File

@@ -20,7 +20,7 @@ export function registerUpdateTaskTool(server) {
name: "update_task",
description: "Updates a single task by ID with new information or context provided in the prompt.",
parameters: z.object({
id: z.union([z.number(), z.string()]).describe("ID of the task or subtask (e.g., '15', '15.2') to update"),
id: z.string().describe("ID of the task or subtask (e.g., '15', '15.2') to update"),
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"),
file: z.string().optional().describe("Path to the tasks file"),
@@ -31,10 +31,9 @@ export function registerUpdateTaskTool(server) {
"Root directory of the project (default: current working directory)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Updating task with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -46,9 +45,7 @@ export function registerUpdateTaskTool(server) {
const result = await updateTaskByIdDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Successfully updated task with ID ${args.id}`);

View File

@@ -18,9 +18,9 @@ import { updateTasksDirect } from "../core/task-master-core.js";
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.",
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.",
parameters: z.object({
from: z.union([z.number(), z.string()]).describe("Task ID from which to start updating (inclusive)"),
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"),
@@ -31,10 +31,9 @@ export function registerUpdateTool(server) {
"Root directory of the project (default: current working directory)"
),
}),
execute: async (args, { log, session, reportProgress }) => {
execute: async (args, { log, session }) => {
try {
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -46,9 +45,7 @@ export function registerUpdateTool(server) {
const result = await updateTasksDirect({
projectRoot: rootFolder,
...args
}, log/*, { reportProgress, mcpLog: log, session}*/);
// await reportProgress({ progress: 100 });
}, log, { session });
if (result.success) {
log.info(`Successfully updated tasks from ID ${args.from}: ${result.data.message}`);

View File

@@ -75,21 +75,43 @@ function getProjectRoot(projectRootRaw, log) {
*/
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]
})}`);
// ALWAYS ensure we return a valid path for project root
const cwd = process.cwd();
// If we have a session with roots array
if (session?.roots?.[0]?.uri) {
const rootUri = session.roots[0].uri;
log.info(`Found rootUri in session.roots[0].uri: ${rootUri}`);
const rootPath = rootUri.startsWith('file://')
? decodeURIComponent(rootUri.slice(7))
: rootUri;
log.info(`Decoded rootPath: ${rootPath}`);
return rootPath;
}
// If we have a session with roots.roots array (different structure)
if (session?.roots?.roots?.[0]?.uri) {
const rootUri = session.roots.roots[0].uri;
log.info(`Found rootUri in session.roots.roots[0].uri: ${rootUri}`);
const rootPath = rootUri.startsWith('file://')
? decodeURIComponent(rootUri.slice(7))
: rootUri;
log.info(`Decoded rootPath: ${rootPath}`);
return rootPath;
}
@@ -106,24 +128,15 @@ function getProjectRootFromSession(session, log) {
if (fs.existsSync(path.join(projectRoot, '.cursor')) ||
fs.existsSync(path.join(projectRoot, 'mcp-server')) ||
fs.existsSync(path.join(projectRoot, 'package.json'))) {
log.info(`Found project root from server path: ${projectRoot}`);
return projectRoot;
}
}
}
// If we get here, we'll try process.cwd() but only if it's not "/"
const cwd = process.cwd();
if (cwd !== '/') {
return cwd;
}
// Last resort: try to derive from the server path we found earlier
if (serverPath) {
const mcpServerIndex = serverPath.indexOf('mcp-server');
return mcpServerIndex !== -1 ? serverPath.substring(0, mcpServerIndex - 1) : cwd;
}
throw new Error('Could not determine project root');
// ALWAYS ensure we return a valid path as a last resort
log.info(`Using current working directory as ultimate fallback: ${cwd}`);
return cwd;
} catch (e) {
// If we have a server path, use it as a basis for project root
const serverPath = process.argv[1];
@@ -171,18 +184,20 @@ function handleApiResult(result, log, errorPrefix = 'API error', processFunction
}
/**
* Execute a Task Master CLI command using child_process
* @param {string} command - The command to execute
* @param {Object} log - The logger object from FastMCP
* 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
projectRootRaw = null,
customEnv = null // Changed from session to customEnv
) {
try {
// Normalize project root internally using the getProjectRoot utility
@@ -201,8 +216,13 @@ function executeTaskMasterCommand(
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);
@@ -210,6 +230,7 @@ function executeTaskMasterCommand(
// 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);
}