fix: Improve MCP server robustness and debugging
- Refactor for more reliable project root detection, particularly when running within integrated environments like Cursor IDE. Includes deriving root from script path and avoiding fallback to '/'.
- Enhance error handling in :
- Add detailed debug information (paths searched, CWD, etc.) to the error message when is not found in the provided project root.
- Improve clarity of error messages and potential solutions.
- Add verbose logging in to trace session object content and the finally resolved project root path, aiding in debugging path-related issues.
- Add default values for and to the example environment configuration.
This commit is contained in:
@@ -10,8 +10,8 @@
|
||||
|
||||
- **Refactor project root handling for MCP Server:**
|
||||
- **Prioritize Session Roots**: MCP tools now extract the project root path directly from `session.roots[0].uri` provided by the client (e.g., Cursor).
|
||||
- **New Utility `getProjectRootFromSession`**: Added to `mcp-server/src/tools/utils.js` to encapsulate session root extraction and decoding.
|
||||
- **Simplify `findTasksJsonPath`**: The core path finding utility in `mcp-server/src/core/utils/path-utils.js` now prioritizes the `projectRoot` passed in `args` (originating from the session). Removed checks for `TASK_MASTER_PROJECT_ROOT` env var and package directory fallback.
|
||||
- **New Utility `getProjectRootFromSession`**: Added to `mcp-server/src/tools/utils.js` to encapsulate session root extraction and decoding. **Further refined for more reliable detection, especially in integrated environments, including deriving root from script path and avoiding fallback to '/'.**
|
||||
- **Simplify `findTasksJsonPath`**: The core path finding utility in `mcp-server/src/core/utils/path-utils.js` now prioritizes the `projectRoot` passed in `args` (originating from the session). Removed checks for `TASK_MASTER_PROJECT_ROOT` env var (we do not use this anymore) and package directory fallback. **Enhanced error handling to include detailed debug information (paths searched, CWD, server dir, etc.) and clearer potential solutions when `tasks.json` is not found.**
|
||||
- **Retain CLI Fallbacks**: Kept `lastFoundProjectRoot` cache check and CWD search in `findTasksJsonPath` for compatibility with direct CLI usage.
|
||||
|
||||
- Updated all MCP tools to use the new project root handling:
|
||||
@@ -22,7 +22,10 @@
|
||||
|
||||
- Add comprehensive PROJECT_MARKERS array for detecting common project files (used in CLI fallback logic).
|
||||
- Improved error messages with specific troubleshooting guidance.
|
||||
- Enhanced logging to indicate the source of project root selection.
|
||||
- **Enhanced logging:**
|
||||
- Indicate the source of project root selection more clearly.
|
||||
- **Add verbose logging in `get-task.js` to trace session object content and resolved project root path, aiding debugging.**
|
||||
|
||||
- DRY refactoring by centralizing path utilities in `core/utils/path-utils.js` and session handling in `tools/utils.js`.
|
||||
- Keep caching of `lastFoundProjectRoot` for CLI performance.
|
||||
|
||||
@@ -59,6 +62,7 @@
|
||||
- Update tool descriptions to better reflect their actual behavior and capabilities.
|
||||
- Add cross-references between related tools and commands.
|
||||
- Include troubleshooting guidance in tool descriptions.
|
||||
- **Add default values for `DEFAULT_SUBTASKS` and `DEFAULT_PRIORITY` to the example `.cursor/mcp.json` configuration.**
|
||||
|
||||
- Document MCP server naming conventions in architecture.mdc and mcp.mdc files (file names use kebab-case, direct functions use camelCase with Direct suffix, tool registration functions use camelCase with Tool suffix, and MCP tool names use snake_case).
|
||||
- Update MCP tool naming to follow more intuitive conventions that better align with natural language requests in client chat applications.
|
||||
|
||||
@@ -4,7 +4,17 @@
|
||||
"command": "node",
|
||||
"args": [
|
||||
"./mcp-server/server.js"
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "%ANTHROPIC_API_KEY%",
|
||||
"PERPLEXITY_API_KEY": "%PERPLEXITY_API_KEY%",
|
||||
"MODEL": "%MODEL%",
|
||||
"PERPLEXITY_MODEL": "%PERPLEXITY_MODEL%",
|
||||
"MAX_TOKENS": "%MAX_TOKENS%",
|
||||
"TEMPERATURE": "%TEMPERATURE%",
|
||||
"DEFAULT_SUBTASKS": 5,
|
||||
"DEFAULT_PRIORITY": "medium"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,8 +91,23 @@ export function findTasksJsonPath(args, log) {
|
||||
if (args.projectRoot) {
|
||||
const projectRoot = args.projectRoot;
|
||||
log.info(`Using explicitly provided project root: ${projectRoot}`);
|
||||
// This will throw if tasks.json isn't found within this root
|
||||
return findTasksJsonInDirectory(projectRoot, args.file, log);
|
||||
try {
|
||||
// This will throw if tasks.json isn't found within this root
|
||||
return findTasksJsonInDirectory(projectRoot, args.file, log);
|
||||
} catch (error) {
|
||||
// Include debug info in error
|
||||
const debugInfo = {
|
||||
projectRoot,
|
||||
currentDir: process.cwd(),
|
||||
serverDir: path.dirname(process.argv[1]),
|
||||
possibleProjectRoot: path.resolve(path.dirname(process.argv[1]), '../..'),
|
||||
lastFoundProjectRoot,
|
||||
searchedPaths: error.message
|
||||
};
|
||||
|
||||
error.message = `Tasks file not found in any of the expected locations relative to project root "${projectRoot}" (from session).\nDebug Info: ${JSON.stringify(debugInfo, null, 2)}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fallback logic primarily for CLI or when projectRoot isn't passed ---
|
||||
@@ -120,7 +135,7 @@ export function findTasksJsonPath(args, log) {
|
||||
return findTasksJsonWithParentSearch(startDir, args.file, log);
|
||||
} catch (error) {
|
||||
// If all attempts fail, augment and throw the original error from CWD search
|
||||
error.message = `${error.message}\n\nPossible solutions:\n1. Run the command from your project directory containing tasks.json\n2. Use --project-root=/path/to/project to specify the project location (if using CLI)\n3. Ensure the project root is correctly passed from the client (if using MCP)`;
|
||||
error.message = `${error.message}\n\nPossible solutions:\n1. Run the command from your project directory containing tasks.json\n2. Use --project-root=/path/to/project to specify the project location (if using CLI)\n3. Ensure the project root is correctly passed from the client (if using MCP)\n\nCurrent working directory: ${startDir}\nLast known project root: ${lastFoundProjectRoot}\nProject root from args: ${args.projectRoot}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function registerAddDependencyTool(server) {
|
||||
const result = await addDependencyDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log);
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
reportProgress({ progress: 100 });
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { addSubtaskDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -30,21 +31,28 @@ export function registerAddSubtaskTool(server) {
|
||||
skipGenerate: z.boolean().optional().describe("Skip regenerating task files"),
|
||||
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Adding subtask with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await addSubtaskDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await addSubtaskDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Subtask added successfully: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to add subtask: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error adding subtask');
|
||||
} catch (error) {
|
||||
log.error(`Error in addSubtask tool: ${error.message}`);
|
||||
|
||||
@@ -43,7 +43,7 @@ export function registerAddTaskTool(server) {
|
||||
const result = await addTaskDirect({
|
||||
projectRoot: rootFolder, // Pass the resolved root
|
||||
...args
|
||||
}, log);
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
return handleApiResult(result, log);
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { analyzeTaskComplexityDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -26,14 +27,25 @@ 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 }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await analyzeTaskComplexityDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await analyzeTaskComplexityDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Task complexity analysis complete: ${result.data.message}`);
|
||||
log.info(`Report summary: ${JSON.stringify(result.data.reportSummary)}`);
|
||||
@@ -41,7 +53,6 @@ export function registerAnalyzeTool(server) {
|
||||
log.error(`Failed to analyze task complexity: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error analyzing task complexity');
|
||||
} catch (error) {
|
||||
log.error(`Error in analyze tool: ${error.message}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { clearSubtasksDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -27,21 +28,31 @@ export function registerClearSubtasksTool(server) {
|
||||
message: "Either 'id' or 'all' parameter must be provided",
|
||||
path: ["id", "all"]
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await clearSubtasksDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await clearSubtasksDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
reportProgress({ progress: 100 });
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Subtasks cleared successfully: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to clear subtasks: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error clearing subtasks');
|
||||
} catch (error) {
|
||||
log.error(`Error in clearSubtasks tool: ${error.message}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { complexityReportDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -22,21 +23,31 @@ export function registerComplexityReportTool(server) {
|
||||
file: z.string().optional().describe("Path to the report file (default: scripts/task-complexity-report.json)"),
|
||||
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Getting complexity report with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await complexityReportDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await complexityReportDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Successfully retrieved complexity report${result.fromCache ? ' (from cache)' : ''}`);
|
||||
} else {
|
||||
log.error(`Failed to retrieve complexity report: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error retrieving complexity report');
|
||||
} catch (error) {
|
||||
log.error(`Error in complexity-report tool: ${error.message}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { expandAllTasksDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -26,21 +27,31 @@ export function registerExpandAllTool(server) {
|
||||
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 }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await expandAllTasksDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await expandAllTasksDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`All tasks expanded successfully: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to expand tasks: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error expanding tasks');
|
||||
} catch (error) {
|
||||
log.error(`Error in expandAll tool: ${error.message}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { expandTaskDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -27,25 +28,36 @@ export function registerExpandTaskTool(server) {
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Expanding task with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await expandTaskDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await expandTaskDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Successfully expanded task ID: ${args.id} with ${result.data.subtasksAdded} new subtasks${result.data.hasExistingSubtasks ? ' (appended to existing subtasks)' : ''}`);
|
||||
} else {
|
||||
log.error(`Failed to expand task: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error expanding task');
|
||||
} catch (error) {
|
||||
log.error(`Error in expand-task tool: ${error.message}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { fixDependenciesDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -22,21 +23,31 @@ export function registerFixDependenciesTool(server) {
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Fixing dependencies with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await fixDependenciesDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await fixDependenciesDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Successfully fixed dependencies: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to fix dependencies: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error fixing dependencies');
|
||||
} catch (error) {
|
||||
log.error(`Error in fixDependencies tool: ${error.message}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { generateTaskFilesDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -23,21 +24,36 @@ export function registerGenerateTool(server) {
|
||||
output: z.string().optional().describe("Output directory (default: same directory as tasks file)"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Generating task files with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await generateTaskFilesDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
// Log result
|
||||
log.info(`${result.success ? 'Successfully generated task files' : 'Failed to generate task files'}`);
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await generateTaskFilesDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
if (result.success) {
|
||||
log.info(`Successfully generated task files: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to generate task files: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error generating task files');
|
||||
} catch (error) {
|
||||
log.error(`Error in generate tool: ${error.message}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { showTaskDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -28,24 +29,43 @@ export function registerShowTaskTool(server) {
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
// Log the session right at the start of execute
|
||||
log.info(`Session object received in execute: ${JSON.stringify(session)}`); // Use JSON.stringify for better visibility
|
||||
|
||||
try {
|
||||
log.info(`Getting task details for ID: ${args.id}`);
|
||||
|
||||
log.info(`Session object received in execute: ${JSON.stringify(session)}`); // Use JSON.stringify for better visibility
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await showTaskDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
} else if (!rootFolder) {
|
||||
// Ensure we always have *some* root, even if session failed and args didn't provide one
|
||||
rootFolder = process.cwd();
|
||||
log.warn(`Session and args failed to provide root, using CWD: ${rootFolder}`);
|
||||
}
|
||||
|
||||
log.info(`Attempting to use project root: ${rootFolder}`); // Log the final resolved root
|
||||
|
||||
log.info(`Root folder: ${rootFolder}`); // Log the final resolved root
|
||||
const result = await showTaskDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log);
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Successfully retrieved task details for ID: ${args.id}${result.fromCache ? ' (from cache)' : ''}`);
|
||||
} else {
|
||||
log.error(`Failed to get task: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error retrieving task details');
|
||||
} catch (error) {
|
||||
log.error(`Error in get-task tool: ${error.message}`);
|
||||
log.error(`Error in get-task tool: ${error.message}\n${error.stack}`); // Add stack trace
|
||||
return createErrorResponse(`Failed to get task: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
createErrorResponse,
|
||||
handleApiResult
|
||||
handleApiResult,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { listTasksDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -16,31 +17,42 @@ import { listTasksDirect } from "../core/task-master-core.js";
|
||||
*/
|
||||
export function registerListTasksTool(server) {
|
||||
server.addTool({
|
||||
name: "get-tasks",
|
||||
description: "Get all tasks from Task Master",
|
||||
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"),
|
||||
status: z.string().optional().describe("Filter tasks by status (e.g., 'pending', 'done')"),
|
||||
withSubtasks: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Include subtasks in the response"),
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
.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)"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Root directory of the project (default: automatically detected)"
|
||||
"Root directory of the project (default: automatically detected from session or CWD)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Getting tasks with filters: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call core function - args contains projectRoot which is handled internally
|
||||
const result = await listTasksDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
// Log result and use handleApiResult utility
|
||||
log.info(`Retrieved ${result.success ? (result.data?.tasks?.length || 0) : 0} tasks`);
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await listTasksDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log);
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
log.info(`Retrieved ${result.success ? (result.data?.tasks?.length || 0) : 0} tasks${result.fromCache ? ' (from cache)' : ''}`);
|
||||
return handleApiResult(result, log, 'Error getting tasks');
|
||||
} catch (error) {
|
||||
log.error(`Error getting tasks: ${error.message}`);
|
||||
|
||||
@@ -30,9 +30,7 @@ import { registerAddDependencyTool } from "./add-dependency.js";
|
||||
* Register all Task Master tools with the MCP server
|
||||
* @param {Object} server - FastMCP server instance
|
||||
*/
|
||||
export function registerTaskMasterTools(server) {
|
||||
logger.info("Registering Task Master tools with MCP server");
|
||||
|
||||
export function registerTaskMasterTools(server) {
|
||||
try {
|
||||
// Register each tool
|
||||
registerListTasksTool(server);
|
||||
@@ -56,8 +54,6 @@ export function registerTaskMasterTools(server) {
|
||||
registerFixDependenciesTool(server);
|
||||
registerComplexityReportTool(server);
|
||||
registerAddDependencyTool(server);
|
||||
|
||||
logger.info("Successfully registered all Task Master tools");
|
||||
} catch (error) {
|
||||
logger.error(`Error registering Task Master tools: ${error.message}`);
|
||||
throw error;
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { nextTaskDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -22,18 +23,30 @@ export function registerNextTaskTool(server) {
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Finding next task with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await nextTaskDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await nextTaskDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
if (result.data.nextTask) {
|
||||
log.info(`Successfully found next task ID: ${result.data.nextTask.id}${result.fromCache ? ' (from cache)' : ''}`);
|
||||
@@ -44,7 +57,6 @@ export function registerNextTaskTool(server) {
|
||||
log.error(`Failed to find next task: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error finding next task');
|
||||
} catch (error) {
|
||||
log.error(`Error in next-task tool: ${error.message}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { parsePRDDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -16,32 +17,47 @@ import { parsePRDDirect } from "../core/task-master-core.js";
|
||||
*/
|
||||
export function registerParsePRDTool(server) {
|
||||
server.addTool({
|
||||
name: "parse_prd_document",
|
||||
description: "Parse PRD document and generate tasks",
|
||||
name: "parse_prd",
|
||||
description: "Parse a Product Requirements Document (PRD) or text file to automatically generate initial tasks.",
|
||||
parameters: z.object({
|
||||
input: z.string().describe("Path to the PRD document file"),
|
||||
numTasks: z.union([z.number(), z.string()]).optional().describe("Number of tasks to generate (default: 10)"),
|
||||
output: z.string().optional().describe("Output path for tasks.json file (default: tasks/tasks.json)"),
|
||||
input: z.string().default("tasks/tasks.json").describe("Path to the PRD document file (relative to project root or absolute)"),
|
||||
numTasks: z.union([z.number(), z.string()]).optional().describe("Approximate number of top-level tasks to generate (default: 10)"),
|
||||
output: z.string().optional().describe("Output path for tasks.json file (relative to project root or absolute, default: tasks/tasks.json)"),
|
||||
force: z.boolean().optional().describe("Allow overwriting an existing tasks.json file."),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Root directory of the project (default: current working directory)"
|
||||
"Root directory of the project (default: automatically detected from session or CWD)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await parsePRDDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
// Log result
|
||||
log.info(`${result.success ? `Successfully generated ${result.data?.taskCount || 0} tasks` : 'Failed to parse PRD'}`);
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await parsePRDDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
if (result.success) {
|
||||
log.info(`Successfully generated ${result.data?.taskCount || 0} tasks from PRD at ${result.data?.outputPath}`);
|
||||
} else {
|
||||
log.error(`Failed to parse PRD: ${result.error?.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error parsing PRD document');
|
||||
} catch (error) {
|
||||
log.error(`Error in parsePRD tool: ${error.message}`);
|
||||
log.error(`Error in parse_prd tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { removeDependencyDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -24,26 +25,36 @@ export function registerRemoveDependencyTool(server) {
|
||||
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 }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await removeDependencyDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await removeDependencyDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Successfully removed dependency: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to remove dependency: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error removing dependency');
|
||||
} catch (error) {
|
||||
log.error(`Error in removeDependency tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { removeSubtaskDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -25,21 +26,31 @@ export function registerRemoveSubtaskTool(server) {
|
||||
skipGenerate: z.boolean().optional().describe("Skip regenerating task files"),
|
||||
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Removing subtask with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await removeSubtaskDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await removeSubtaskDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Subtask removed successfully: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to remove subtask: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error removing subtask');
|
||||
} catch (error) {
|
||||
log.error(`Error in removeSubtask tool: ${error.message}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { setTaskStatusDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -17,14 +18,14 @@ import { setTaskStatusDirect } from "../core/task-master-core.js";
|
||||
export function registerSetTaskStatusTool(server) {
|
||||
server.addTool({
|
||||
name: "set_task_status",
|
||||
description: "Set the status of a task",
|
||||
description: "Set the status of one or more tasks or subtasks.",
|
||||
parameters: z.object({
|
||||
id: z
|
||||
.string()
|
||||
.describe("Task ID (can be comma-separated for multiple tasks)"),
|
||||
.describe("Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated for multiple updates."),
|
||||
status: z
|
||||
.string()
|
||||
.describe("New status (todo, in-progress, review, done)"),
|
||||
.describe("New status to set (e.g., 'pending', 'done', 'in-progress', 'review', 'deferred', 'cancelled'."),
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
@@ -33,17 +34,31 @@ export function registerSetTaskStatusTool(server) {
|
||||
"Root directory of the project (default: automatically detected)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Setting status of task(s) ${args.id} to: ${args.status}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await setTaskStatusDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
// Log result
|
||||
log.info(`${result.success ? `Successfully updated task ${args.id} status to "${args.status}"` : 'Failed to update task status'}`);
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await setTaskStatusDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
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'}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error setting task status');
|
||||
} catch (error) {
|
||||
log.error(`Error in setTaskStatus tool: ${error.message}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { updateSubtaskByIdDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -16,7 +17,7 @@ import { updateSubtaskByIdDirect } from "../core/task-master-core.js";
|
||||
*/
|
||||
export function registerUpdateSubtaskTool(server) {
|
||||
server.addTool({
|
||||
name: "update-subtask",
|
||||
name: "update_subtask",
|
||||
description: "Appends additional information to a specific subtask without replacing existing content",
|
||||
parameters: z.object({
|
||||
id: z.string().describe("ID of the subtask to update in format \"parentId.subtaskId\" (e.g., \"5.2\")"),
|
||||
@@ -30,20 +31,34 @@ export function registerUpdateSubtaskTool(server) {
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Updating subtask with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await updateSubtaskByIdDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
// Log result
|
||||
log.info(`${result.success ? `Successfully updated subtask with ID ${args.id}` : 'Failed to update subtask'}`);
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await updateSubtaskByIdDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
if (result.success) {
|
||||
log.info(`Successfully updated subtask with ID ${args.id}`);
|
||||
} else {
|
||||
log.error(`Failed to update subtask: ${result.error?.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error updating subtask');
|
||||
} catch (error) {
|
||||
log.error(`Error in update-subtask tool: ${error.message}`);
|
||||
log.error(`Error in update_subtask tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { updateTaskByIdDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -16,11 +17,11 @@ import { updateTaskByIdDirect } from "../core/task-master-core.js";
|
||||
*/
|
||||
export function registerUpdateTaskTool(server) {
|
||||
server.addTool({
|
||||
name: "update-task",
|
||||
description: "Updates a single task by ID with new information",
|
||||
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 to update"),
|
||||
prompt: z.string().describe("New information or context to update the task"),
|
||||
id: z.union([z.number(), 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"),
|
||||
projectRoot: z
|
||||
@@ -30,20 +31,34 @@ export function registerUpdateTaskTool(server) {
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Updating task with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await updateTaskByIdDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
// Log result
|
||||
log.info(`${result.success ? `Successfully updated task with ID ${args.id}` : 'Failed to update task'}`);
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await updateTaskByIdDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
if (result.success) {
|
||||
log.info(`Successfully updated task with ID ${args.id}`);
|
||||
} else {
|
||||
log.error(`Failed to update task: ${result.error?.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error updating task');
|
||||
} catch (error) {
|
||||
log.error(`Error in update-task tool: ${error.message}`);
|
||||
log.error(`Error in update_task tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { updateTasksDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -17,10 +18,10 @@ import { updateTasksDirect } from "../core/task-master-core.js";
|
||||
export function registerUpdateTool(server) {
|
||||
server.addTool({
|
||||
name: "update",
|
||||
description: "Update tasks with ID >= specified ID based on the provided prompt",
|
||||
description: "Update multiple upcoming tasks (with ID >= 'from' ID) based on new context or changes provided in the prompt.",
|
||||
parameters: z.object({
|
||||
from: z.union([z.number(), z.string()]).describe("Task ID from which to start updating"),
|
||||
prompt: z.string().describe("Explanation of changes or new context"),
|
||||
from: z.union([z.number(), z.string()]).describe("Task ID from which to start updating (inclusive)"),
|
||||
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"),
|
||||
projectRoot: z
|
||||
@@ -30,17 +31,31 @@ export function registerUpdateTool(server) {
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await updateTasksDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
// Log result
|
||||
log.info(`${result.success ? `Successfully updated tasks from ID ${args.from}` : 'Failed to update tasks'}`);
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await updateTasksDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
if (result.success) {
|
||||
log.info(`Successfully updated tasks from ID ${args.from}: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to update tasks: ${result.error?.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error updating tasks');
|
||||
} catch (error) {
|
||||
log.error(`Error in update tool: ${error.message}`);
|
||||
|
||||
@@ -5,12 +5,11 @@
|
||||
|
||||
import { spawnSync } from "child_process";
|
||||
import path from "path";
|
||||
import { contextManager } from '../core/context-manager.js'; // Import the singleton
|
||||
import fs from 'fs';
|
||||
import { decodeURIComponent } from 'querystring'; // Added for URI decoding
|
||||
import { contextManager } from '../core/context-manager.js'; // Import the singleton
|
||||
|
||||
// Import path utilities to ensure consistent path resolution
|
||||
import { lastFoundProjectRoot, getPackagePath, PROJECT_MARKERS } from '../core/utils/path-utils.js';
|
||||
import { lastFoundProjectRoot, PROJECT_MARKERS } from '../core/utils/path-utils.js';
|
||||
|
||||
/**
|
||||
* Get normalized project root path
|
||||
@@ -18,7 +17,7 @@ import { lastFoundProjectRoot, getPackagePath, PROJECT_MARKERS } from '../core/u
|
||||
* @param {Object} log - Logger object
|
||||
* @returns {string} - Normalized absolute path to project root
|
||||
*/
|
||||
export function getProjectRoot(projectRootRaw, log) {
|
||||
function getProjectRoot(projectRootRaw, log) {
|
||||
// PRECEDENCE ORDER:
|
||||
// 1. Environment variable override
|
||||
// 2. Explicitly provided projectRoot in args
|
||||
@@ -74,28 +73,69 @@ export function getProjectRoot(projectRootRaw, log) {
|
||||
* @param {Object} log - Logger object.
|
||||
* @returns {string|null} - The absolute path to the project root, or null if not found.
|
||||
*/
|
||||
export function getProjectRootFromSession(session, log) {
|
||||
if (session && session.roots && session.roots.length > 0) {
|
||||
const firstRoot = session.roots[0];
|
||||
if (firstRoot && firstRoot.uri) {
|
||||
try {
|
||||
const rootUri = firstRoot.uri;
|
||||
const rootPath = rootUri.startsWith('file://')
|
||||
? decodeURIComponent(rootUri.slice(7)) // Remove 'file://' and decode
|
||||
: rootUri; // Assume it's a path if no scheme
|
||||
log.info(`Extracted project root from session: ${rootPath}`);
|
||||
return rootPath;
|
||||
} catch (e) {
|
||||
log.error(`Error decoding project root URI from session: ${firstRoot.uri}`, e);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
log.info('Session exists, but first root or its URI is missing.');
|
||||
function getProjectRootFromSession(session, log) {
|
||||
try {
|
||||
// If we have a session with roots array
|
||||
if (session?.roots?.[0]?.uri) {
|
||||
const rootUri = session.roots[0].uri;
|
||||
const rootPath = rootUri.startsWith('file://')
|
||||
? decodeURIComponent(rootUri.slice(7))
|
||||
: rootUri;
|
||||
return rootPath;
|
||||
}
|
||||
} else {
|
||||
log.info('No session or session roots found to extract project root.');
|
||||
|
||||
// If we have a session with roots.roots array (different structure)
|
||||
if (session?.roots?.roots?.[0]?.uri) {
|
||||
const rootUri = session.roots.roots[0].uri;
|
||||
const rootPath = rootUri.startsWith('file://')
|
||||
? decodeURIComponent(rootUri.slice(7))
|
||||
: rootUri;
|
||||
return rootPath;
|
||||
}
|
||||
|
||||
// Get the server's location and try to find project root -- this is a fallback necessary in Cursor IDE
|
||||
const serverPath = process.argv[1]; // This should be the path to server.js, which is in mcp-server/
|
||||
if (serverPath && serverPath.includes('mcp-server')) {
|
||||
// Find the mcp-server directory first
|
||||
const mcpServerIndex = serverPath.indexOf('mcp-server');
|
||||
if (mcpServerIndex !== -1) {
|
||||
// Get the path up to mcp-server, which should be the project root
|
||||
const projectRoot = serverPath.substring(0, mcpServerIndex - 1); // -1 to remove trailing slash
|
||||
|
||||
// Verify this looks like our project root by checking for key files/directories
|
||||
if (fs.existsSync(path.join(projectRoot, '.cursor')) ||
|
||||
fs.existsSync(path.join(projectRoot, 'mcp-server')) ||
|
||||
fs.existsSync(path.join(projectRoot, 'package.json'))) {
|
||||
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');
|
||||
} catch (e) {
|
||||
// If we have a server path, use it as a basis for project root
|
||||
const serverPath = process.argv[1];
|
||||
if (serverPath && serverPath.includes('mcp-server')) {
|
||||
const mcpServerIndex = serverPath.indexOf('mcp-server');
|
||||
return mcpServerIndex !== -1 ? serverPath.substring(0, mcpServerIndex - 1) : process.cwd();
|
||||
}
|
||||
|
||||
// Only use cwd if it's not "/"
|
||||
const cwd = process.cwd();
|
||||
return cwd !== '/' ? cwd : '/';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +146,7 @@ export function getProjectRootFromSession(session, log) {
|
||||
* @param {Function} processFunction - Optional function to process successful result data
|
||||
* @returns {Object} - Standardized MCP response object
|
||||
*/
|
||||
export function handleApiResult(result, log, errorPrefix = 'API error', processFunction = processMCPResponseData) {
|
||||
function handleApiResult(result, log, errorPrefix = 'API error', processFunction = processMCPResponseData) {
|
||||
if (!result.success) {
|
||||
const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
|
||||
// Include cache status in error logs
|
||||
@@ -138,7 +178,7 @@ export function handleApiResult(result, log, errorPrefix = 'API error', processF
|
||||
* @param {string|undefined} projectRootRaw - Optional raw project root path (will be normalized internally)
|
||||
* @returns {Object} - The result of the command execution
|
||||
*/
|
||||
export function executeTaskMasterCommand(
|
||||
function executeTaskMasterCommand(
|
||||
command,
|
||||
log,
|
||||
args = [],
|
||||
@@ -215,7 +255,7 @@ export function executeTaskMasterCommand(
|
||||
* @returns {Promise<Object>} - An object containing the result, indicating if it was from cache.
|
||||
* Format: { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }
|
||||
*/
|
||||
export async function getCachedOrExecute({ cacheKey, actionFn, log }) {
|
||||
async function getCachedOrExecute({ cacheKey, actionFn, log }) {
|
||||
// Check cache first
|
||||
const cachedResult = contextManager.getCachedData(cacheKey);
|
||||
|
||||
@@ -259,7 +299,7 @@ export async function getCachedOrExecute({ cacheKey, actionFn, log }) {
|
||||
* @param {string[]} fieldsToRemove - An array of field names to remove.
|
||||
* @returns {Object|Array} - The processed data with specified fields removed.
|
||||
*/
|
||||
export function processMCPResponseData(taskOrData, fieldsToRemove = ['details', 'testStrategy']) {
|
||||
function processMCPResponseData(taskOrData, fieldsToRemove = ['details', 'testStrategy']) {
|
||||
if (!taskOrData) {
|
||||
return taskOrData;
|
||||
}
|
||||
@@ -316,7 +356,7 @@ export function processMCPResponseData(taskOrData, fieldsToRemove = ['details',
|
||||
* @param {string|Object} content - Content to include in response
|
||||
* @returns {Object} - Content response object in FastMCP format
|
||||
*/
|
||||
export function createContentResponse(content) {
|
||||
function createContentResponse(content) {
|
||||
// FastMCP requires text type, so we format objects as JSON strings
|
||||
return {
|
||||
content: [
|
||||
@@ -351,10 +391,11 @@ export function createErrorResponse(errorMessage) {
|
||||
|
||||
// Ensure all functions are exported
|
||||
export {
|
||||
getProjectRoot,
|
||||
getProjectRootFromSession,
|
||||
handleApiResult,
|
||||
executeTaskMasterCommand,
|
||||
getCachedOrExecute,
|
||||
processMCPResponseData,
|
||||
createContentResponse,
|
||||
createErrorResponse
|
||||
};
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse
|
||||
createErrorResponse,
|
||||
getProjectRootFromSession
|
||||
} from "./utils.js";
|
||||
import { validateDependenciesDirect } from "../core/task-master-core.js";
|
||||
|
||||
@@ -17,26 +18,36 @@ import { validateDependenciesDirect } from "../core/task-master-core.js";
|
||||
export function registerValidateDependenciesTool(server) {
|
||||
server.addTool({
|
||||
name: "validate_dependencies",
|
||||
description: "Identify invalid dependencies in tasks without fixing them",
|
||||
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("Path to the tasks file"),
|
||||
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
execute: async (args, { log, session, reportProgress }) => {
|
||||
try {
|
||||
log.info(`Validating dependencies with args: ${JSON.stringify(args)}`);
|
||||
await reportProgress({ progress: 0 });
|
||||
|
||||
// Call the direct function wrapper
|
||||
const result = await validateDependenciesDirect(args, log);
|
||||
let rootFolder = getProjectRootFromSession(session, log);
|
||||
|
||||
if (!rootFolder && args.projectRoot) {
|
||||
rootFolder = args.projectRoot;
|
||||
log.info(`Using project root from args as fallback: ${rootFolder}`);
|
||||
}
|
||||
|
||||
const result = await validateDependenciesDirect({
|
||||
projectRoot: rootFolder,
|
||||
...args
|
||||
}, log, { reportProgress, mcpLog: log, session});
|
||||
|
||||
await reportProgress({ progress: 100 });
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Successfully validated dependencies: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to validate dependencies: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error validating dependencies');
|
||||
} catch (error) {
|
||||
log.error(`Error in validateDependencies tool: ${error.message}`);
|
||||
@@ -44,4 +55,4 @@ export function registerValidateDependenciesTool(server) {
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -136,9 +136,13 @@ function handleClaudeError(error) {
|
||||
* @param {string} prdPath - Path to the PRD file
|
||||
* @param {number} numTasks - Number of tasks to generate
|
||||
* @param {number} retryCount - Retry count
|
||||
* @param {Object} options - Options object containing:
|
||||
* - reportProgress: Function to report progress to MCP server (optional)
|
||||
* - mcpLog: MCP logger object (optional)
|
||||
* - session: Session object from MCP server (optional)
|
||||
* @returns {Object} Claude's response
|
||||
*/
|
||||
async function callClaude(prdContent, prdPath, numTasks, retryCount = 0) {
|
||||
async function callClaude(prdContent, prdPath, numTasks, retryCount = 0, { reportProgress, mcpLog, session } = {}) {
|
||||
try {
|
||||
log('info', 'Calling Claude...');
|
||||
|
||||
@@ -190,7 +194,7 @@ Expected output format:
|
||||
Important: Your response must be valid JSON only, with no additional explanation or comments.`;
|
||||
|
||||
// Use streaming request to handle large responses and show progress
|
||||
return await handleStreamingRequest(prdContent, prdPath, numTasks, CONFIG.maxTokens, systemPrompt);
|
||||
return await handleStreamingRequest(prdContent, prdPath, numTasks, CONFIG.maxTokens, systemPrompt, { reportProgress, mcpLog, session } = {});
|
||||
} catch (error) {
|
||||
// Get user-friendly error message
|
||||
const userMessage = handleClaudeError(error);
|
||||
@@ -224,19 +228,24 @@ Important: Your response must be valid JSON only, with no additional explanation
|
||||
* @param {number} numTasks - Number of tasks to generate
|
||||
* @param {number} maxTokens - Maximum tokens
|
||||
* @param {string} systemPrompt - System prompt
|
||||
* @param {Object} options - Options object containing:
|
||||
* - reportProgress: Function to report progress to MCP server (optional)
|
||||
* - mcpLog: MCP logger object (optional)
|
||||
* - session: Session object from MCP server (optional)
|
||||
* @returns {Object} Claude's response
|
||||
*/
|
||||
async function handleStreamingRequest(prdContent, prdPath, numTasks, maxTokens, systemPrompt) {
|
||||
async function handleStreamingRequest(prdContent, prdPath, numTasks, maxTokens, systemPrompt, { reportProgress, mcpLog, session } = {}) {
|
||||
const loadingIndicator = startLoadingIndicator('Generating tasks from PRD...');
|
||||
if (reportProgress) { await reportProgress({ progress: 0 }); }
|
||||
let responseText = '';
|
||||
let streamingInterval = null;
|
||||
|
||||
try {
|
||||
// Use streaming for handling large responses
|
||||
const stream = await anthropic.messages.create({
|
||||
model: CONFIG.model,
|
||||
max_tokens: maxTokens,
|
||||
temperature: CONFIG.temperature,
|
||||
model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
|
||||
max_tokens: session?.env?.MAX_TOKENS || maxTokens,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
@@ -261,6 +270,12 @@ async function handleStreamingRequest(prdContent, prdPath, numTasks, maxTokens,
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
responseText += chunk.delta.text;
|
||||
}
|
||||
if (reportProgress) {
|
||||
await reportProgress({ progress: (responseText.length / maxTokens) * 100 });
|
||||
}
|
||||
if (mcpLog) {
|
||||
mcpLog.info(`Progress: ${responseText.length / maxTokens * 100}%`);
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingInterval) clearInterval(streamingInterval);
|
||||
@@ -355,9 +370,13 @@ function processClaudeResponse(textContent, numTasks, retryCount, prdContent, pr
|
||||
* @param {number} numSubtasks - Number of subtasks to generate
|
||||
* @param {number} nextSubtaskId - Next subtask ID
|
||||
* @param {string} additionalContext - Additional context
|
||||
* @param {Object} options - Options object containing:
|
||||
* - reportProgress: Function to report progress to MCP server (optional)
|
||||
* - mcpLog: MCP logger object (optional)
|
||||
* - session: Session object from MCP server (optional)
|
||||
* @returns {Array} Generated subtasks
|
||||
*/
|
||||
async function generateSubtasks(task, numSubtasks, nextSubtaskId, additionalContext = '') {
|
||||
async function generateSubtasks(task, numSubtasks, nextSubtaskId, additionalContext = '', { reportProgress, mcpLog, session } = {}) {
|
||||
try {
|
||||
log('info', `Generating ${numSubtasks} subtasks for task ${task.id}: ${task.title}`);
|
||||
|
||||
@@ -418,12 +437,14 @@ Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use
|
||||
process.stdout.write(`Generating subtasks for task ${task.id}${'.'.repeat(dotCount)}`);
|
||||
dotCount = (dotCount + 1) % 4;
|
||||
}, 500);
|
||||
|
||||
// TODO: MOVE THIS TO THE STREAM REQUEST FUNCTION (DRY)
|
||||
|
||||
// Use streaming API call
|
||||
const stream = await anthropic.messages.create({
|
||||
model: CONFIG.model,
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
temperature: CONFIG.temperature,
|
||||
model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
|
||||
max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
@@ -439,6 +460,12 @@ Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
responseText += chunk.delta.text;
|
||||
}
|
||||
if (reportProgress) {
|
||||
await reportProgress({ progress: (responseText.length / CONFIG.maxTokens) * 100 });
|
||||
}
|
||||
if (mcpLog) {
|
||||
mcpLog.info(`Progress: ${responseText.length / CONFIG.maxTokens * 100}%`);
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingInterval) clearInterval(streamingInterval);
|
||||
@@ -464,15 +491,19 @@ Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use
|
||||
* @param {number} numSubtasks - Number of subtasks to generate
|
||||
* @param {number} nextSubtaskId - Next subtask ID
|
||||
* @param {string} additionalContext - Additional context
|
||||
* @param {Object} options - Options object containing:
|
||||
* - reportProgress: Function to report progress to MCP server (optional)
|
||||
* - mcpLog: MCP logger object (optional)
|
||||
* - session: Session object from MCP server (optional)
|
||||
* @returns {Array} Generated subtasks
|
||||
*/
|
||||
async function generateSubtasksWithPerplexity(task, numSubtasks = 3, nextSubtaskId = 1, additionalContext = '') {
|
||||
async function generateSubtasksWithPerplexity(task, numSubtasks = 3, nextSubtaskId = 1, additionalContext = '', { reportProgress, mcpLog, session } = {}) {
|
||||
try {
|
||||
// First, perform research to get context
|
||||
log('info', `Researching context for task ${task.id}: ${task.title}`);
|
||||
const perplexityClient = getPerplexityClient();
|
||||
|
||||
const PERPLEXITY_MODEL = process.env.PERPLEXITY_MODEL || 'sonar-pro';
|
||||
const PERPLEXITY_MODEL = process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro';
|
||||
const researchLoadingIndicator = startLoadingIndicator('Researching best practices with Perplexity AI...');
|
||||
|
||||
// Formulate research query based on task
|
||||
@@ -566,9 +597,9 @@ Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use
|
||||
|
||||
// Use streaming API call
|
||||
const stream = await anthropic.messages.create({
|
||||
model: CONFIG.model,
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
temperature: CONFIG.temperature,
|
||||
model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
|
||||
max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
@@ -584,6 +615,12 @@ Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
responseText += chunk.delta.text;
|
||||
}
|
||||
if (reportProgress) {
|
||||
await reportProgress({ progress: (responseText.length / CONFIG.maxTokens) * 100 });
|
||||
}
|
||||
if (mcpLog) {
|
||||
mcpLog.info(`Progress: ${responseText.length / CONFIG.maxTokens * 100}%`);
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingInterval) clearInterval(streamingInterval);
|
||||
|
||||
@@ -49,19 +49,19 @@ import {
|
||||
|
||||
// Initialize Anthropic client
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
apiKey: process.env.ANTHROPIC_API_KEY || session?.env?.ANTHROPIC_API_KEY,
|
||||
});
|
||||
|
||||
// Import perplexity if available
|
||||
let perplexity;
|
||||
|
||||
try {
|
||||
if (process.env.PERPLEXITY_API_KEY) {
|
||||
if (process.env.PERPLEXITY_API_KEY || session?.env?.PERPLEXITY_API_KEY) {
|
||||
// Using the existing approach from ai-services.js
|
||||
const OpenAI = (await import('openai')).default;
|
||||
|
||||
perplexity = new OpenAI({
|
||||
apiKey: process.env.PERPLEXITY_API_KEY,
|
||||
apiKey: process.env.PERPLEXITY_API_KEY || session?.env?.PERPLEXITY_API_KEY,
|
||||
baseURL: 'https://api.perplexity.ai',
|
||||
});
|
||||
|
||||
@@ -77,8 +77,11 @@ try {
|
||||
* @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 {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)
|
||||
*/
|
||||
async function parsePRD(prdPath, tasksPath, numTasks) {
|
||||
async function parsePRD(prdPath, tasksPath, numTasks, { reportProgress, mcpLog, session } = {}) {
|
||||
try {
|
||||
log('info', `Parsing PRD file: ${prdPath}`);
|
||||
|
||||
@@ -86,22 +89,20 @@ async function parsePRD(prdPath, tasksPath, numTasks) {
|
||||
const prdContent = fs.readFileSync(prdPath, 'utf8');
|
||||
|
||||
// Call Claude to generate tasks
|
||||
const tasksData = await callClaude(prdContent, prdPath, numTasks);
|
||||
const tasksData = await callClaude(prdContent, prdPath, numTasks, { reportProgress, mcpLog, session } = {});
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
const tasksDir = path.dirname(tasksPath);
|
||||
if (!fs.existsSync(tasksDir)) {
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the tasks to the file
|
||||
writeJSON(tasksPath, tasksData);
|
||||
|
||||
log('success', `Successfully generated ${tasksData.tasks.length} tasks from PRD`);
|
||||
log('info', `Tasks saved to: ${tasksPath}`);
|
||||
|
||||
// Generate individual task files
|
||||
await generateTaskFiles(tasksPath, tasksDir);
|
||||
await generateTaskFiles(tasksPath, tasksDir, { reportProgress, mcpLog, session } = {});
|
||||
|
||||
console.log(boxen(
|
||||
chalk.green(`Successfully generated ${tasksData.tasks.length} tasks from PRD`),
|
||||
@@ -132,13 +133,16 @@ async function parsePRD(prdPath, tasksPath, numTasks) {
|
||||
* @param {number} fromId - Task ID to start updating from
|
||||
* @param {string} prompt - Prompt with new context
|
||||
* @param {boolean} useResearch - Whether to use Perplexity AI for research
|
||||
* @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)
|
||||
*/
|
||||
async function updateTasks(tasksPath, fromId, prompt, useResearch = false) {
|
||||
async function updateTasks(tasksPath, fromId, prompt, useResearch = false, { reportProgress, mcpLog, session } = {}) {
|
||||
try {
|
||||
log('info', `Updating tasks from ID ${fromId} with prompt: "${prompt}"`);
|
||||
|
||||
// Validate research flag
|
||||
if (useResearch && (!perplexity || !process.env.PERPLEXITY_API_KEY)) {
|
||||
if (useResearch && (!perplexity || !process.env.PERPLEXITY_API_KEY || session?.env?.PERPLEXITY_API_KEY)) {
|
||||
log('warn', 'Perplexity AI is not available. Falling back to Claude AI.');
|
||||
console.log(chalk.yellow('Perplexity AI is not available (API key may be missing). Falling back to Claude AI.'));
|
||||
useResearch = false;
|
||||
@@ -224,7 +228,7 @@ The changes described in the prompt should be applied to ALL tasks in the list.`
|
||||
log('info', 'Using Perplexity AI for research-backed task updates');
|
||||
|
||||
// Call Perplexity AI using format consistent with ai-services.js
|
||||
const perplexityModel = process.env.PERPLEXITY_MODEL || 'sonar-pro';
|
||||
const perplexityModel = process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro';
|
||||
const result = await perplexity.chat.completions.create({
|
||||
model: perplexityModel,
|
||||
messages: [
|
||||
@@ -245,8 +249,8 @@ IMPORTANT: In the tasks JSON above, any subtasks with "status": "done" or "statu
|
||||
Return only the updated tasks as a valid JSON array.`
|
||||
}
|
||||
],
|
||||
temperature: parseFloat(process.env.TEMPERATURE || CONFIG.temperature),
|
||||
max_tokens: parseInt(process.env.MAX_TOKENS || CONFIG.maxTokens),
|
||||
temperature: parseFloat(process.env.TEMPERATURE || session?.env?.TEMPERATURE || CONFIG.temperature),
|
||||
max_tokens: parseInt(process.env.MAX_TOKENS || session?.env?.MAX_TOKENS || CONFIG.maxTokens),
|
||||
});
|
||||
|
||||
const responseText = result.choices[0].message.content;
|
||||
@@ -278,9 +282,9 @@ Return only the updated tasks as a valid JSON array.`
|
||||
|
||||
// Use streaming API call
|
||||
const stream = await anthropic.messages.create({
|
||||
model: CONFIG.model,
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
temperature: CONFIG.temperature,
|
||||
model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
|
||||
max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
@@ -304,6 +308,13 @@ Return only the updated tasks as a valid JSON array.`
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
responseText += chunk.delta.text;
|
||||
}
|
||||
if (reportProgress) {
|
||||
await reportProgress({ progress: (responseText.length / CONFIG.maxTokens) * 100 });
|
||||
}
|
||||
|
||||
if (mcpLog) {
|
||||
mcpLog.info(`Progress: ${responseText.length / CONFIG.maxTokens * 100}%`);
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingInterval) clearInterval(streamingInterval);
|
||||
@@ -366,9 +377,12 @@ Return only the updated tasks as a valid JSON array.`
|
||||
* @param {number} taskId - Task ID to update
|
||||
* @param {string} prompt - Prompt with new context
|
||||
* @param {boolean} useResearch - Whether to use Perplexity AI for research
|
||||
* @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)
|
||||
* @returns {Object} - Updated task data or null if task wasn't updated
|
||||
*/
|
||||
async function updateTaskById(tasksPath, taskId, prompt, useResearch = false) {
|
||||
async function updateTaskById(tasksPath, taskId, prompt, useResearch = false, { reportProgress, mcpLog, session } = {}) {
|
||||
try {
|
||||
log('info', `Updating single task ${taskId} with prompt: "${prompt}"`);
|
||||
|
||||
@@ -383,7 +397,7 @@ async function updateTaskById(tasksPath, taskId, prompt, useResearch = false) {
|
||||
}
|
||||
|
||||
// Validate research flag
|
||||
if (useResearch && (!perplexity || !process.env.PERPLEXITY_API_KEY)) {
|
||||
if (useResearch && (!perplexity || !process.env.PERPLEXITY_API_KEY || session?.env?.PERPLEXITY_API_KEY)) {
|
||||
log('warn', 'Perplexity AI is not available. Falling back to Claude AI.');
|
||||
console.log(chalk.yellow('Perplexity AI is not available (API key may be missing). Falling back to Claude AI.'));
|
||||
useResearch = false;
|
||||
@@ -484,13 +498,13 @@ The changes described in the prompt should be thoughtfully applied to make the t
|
||||
log('info', 'Using Perplexity AI for research-backed task update');
|
||||
|
||||
// Verify Perplexity API key exists
|
||||
if (!process.env.PERPLEXITY_API_KEY) {
|
||||
if (!process.env.PERPLEXITY_API_KEY || session?.env?.PERPLEXITY_API_KEY) {
|
||||
throw new Error('PERPLEXITY_API_KEY environment variable is missing but --research flag was used.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Call Perplexity AI
|
||||
const perplexityModel = process.env.PERPLEXITY_MODEL || 'sonar-pro';
|
||||
const perplexityModel = process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro';
|
||||
const result = await perplexity.chat.completions.create({
|
||||
model: perplexityModel,
|
||||
messages: [
|
||||
@@ -511,8 +525,8 @@ IMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status
|
||||
Return only the updated task as a valid JSON object.`
|
||||
}
|
||||
],
|
||||
temperature: parseFloat(process.env.TEMPERATURE || CONFIG.temperature),
|
||||
max_tokens: parseInt(process.env.MAX_TOKENS || CONFIG.maxTokens),
|
||||
temperature: parseFloat(process.env.TEMPERATURE || session?.env?.TEMPERATURE || CONFIG.temperature),
|
||||
max_tokens: parseInt(process.env.MAX_TOKENS || session?.env?.MAX_TOKENS || CONFIG.maxTokens),
|
||||
});
|
||||
|
||||
const responseText = result.choices[0].message.content;
|
||||
@@ -542,7 +556,7 @@ Return only the updated task as a valid JSON object.`
|
||||
|
||||
try {
|
||||
// Verify Anthropic API key exists
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
if (!process.env.ANTHROPIC_API_KEY || session?.env?.ANTHROPIC_API_KEY) {
|
||||
throw new Error('ANTHROPIC_API_KEY environment variable is missing. Required for task updates.');
|
||||
}
|
||||
|
||||
@@ -557,9 +571,9 @@ Return only the updated task as a valid JSON object.`
|
||||
|
||||
// Use streaming API call
|
||||
const stream = await anthropic.messages.create({
|
||||
model: CONFIG.model,
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
temperature: CONFIG.temperature,
|
||||
model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
|
||||
max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
@@ -583,6 +597,12 @@ Return only the updated task as a valid JSON object.`
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
responseText += chunk.delta.text;
|
||||
}
|
||||
if (reportProgress) {
|
||||
await reportProgress({ progress: (responseText.length / CONFIG.maxTokens) * 100 });
|
||||
}
|
||||
if (mcpLog) {
|
||||
mcpLog.info(`Progress: ${responseText.length / CONFIG.maxTokens * 100}%`);
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingInterval) clearInterval(streamingInterval);
|
||||
@@ -2034,9 +2054,12 @@ function clearSubtasks(tasksPath, taskIds) {
|
||||
* @param {string} prompt - Description of the task to add
|
||||
* @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)
|
||||
* @returns {number} The new task ID
|
||||
*/
|
||||
async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium') {
|
||||
async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium', { reportProgress, mcpLog, session } = {}) {
|
||||
displayBanner();
|
||||
|
||||
// Read the existing tasks
|
||||
@@ -2112,9 +2135,9 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
|
||||
try {
|
||||
// Call Claude with streaming enabled
|
||||
const stream = await anthropic.messages.create({
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
model: CONFIG.model,
|
||||
temperature: CONFIG.temperature,
|
||||
max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
|
||||
model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
messages: [{ role: "user", content: userPrompt }],
|
||||
system: systemPrompt,
|
||||
stream: true
|
||||
@@ -2133,6 +2156,13 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
fullResponse += chunk.delta.text;
|
||||
}
|
||||
|
||||
if (reportProgress) {
|
||||
await reportProgress({ progress: (fullResponse.length / CONFIG.maxTokens) * 100 });
|
||||
}
|
||||
if (mcpLog) {
|
||||
mcpLog.info(`Progress: ${fullResponse.length / CONFIG.maxTokens * 100}%`);
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingInterval) clearInterval(streamingInterval);
|
||||
@@ -2213,8 +2243,11 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
|
||||
/**
|
||||
* Analyzes task complexity and generates expansion recommendations
|
||||
* @param {Object} options Command options
|
||||
* @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)
|
||||
*/
|
||||
async function analyzeTaskComplexity(options) {
|
||||
async function analyzeTaskComplexity(options, { reportProgress, mcpLog, session } = {}) {
|
||||
const tasksPath = options.file || 'tasks/tasks.json';
|
||||
const outputPath = options.output || 'scripts/task-complexity-report.json';
|
||||
const modelOverride = options.model;
|
||||
@@ -2274,7 +2307,7 @@ Your response must be a clean JSON array only, following exactly this format:
|
||||
DO NOT include any text before or after the JSON array. No explanations, no markdown formatting.`;
|
||||
|
||||
const result = await perplexity.chat.completions.create({
|
||||
model: process.env.PERPLEXITY_MODEL || 'sonar-pro',
|
||||
model: process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro',
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
@@ -2285,8 +2318,8 @@ DO NOT include any text before or after the JSON array. No explanations, no mark
|
||||
content: researchPrompt
|
||||
}
|
||||
],
|
||||
temperature: CONFIG.temperature,
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
|
||||
});
|
||||
|
||||
// Extract the response text
|
||||
@@ -2315,9 +2348,9 @@ DO NOT include any text before or after the JSON array. No explanations, no mark
|
||||
async function useClaudeForComplexityAnalysis() {
|
||||
// Call the LLM API with streaming
|
||||
const stream = await anthropic.messages.create({
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
model: modelOverride || CONFIG.model,
|
||||
temperature: CONFIG.temperature,
|
||||
max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
|
||||
model: modelOverride || CONFIG.model || session?.env?.ANTHROPIC_MODEL,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
system: "You are an expert software architect and project manager analyzing task complexity. Respond only with valid JSON.",
|
||||
stream: true
|
||||
@@ -2336,6 +2369,12 @@ DO NOT include any text before or after the JSON array. No explanations, no mark
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
fullResponse += chunk.delta.text;
|
||||
}
|
||||
if (reportProgress) {
|
||||
await reportProgress({ progress: (fullResponse.length / CONFIG.maxTokens) * 100 });
|
||||
}
|
||||
if (mcpLog) {
|
||||
mcpLog.info(`Progress: ${fullResponse.length / CONFIG.maxTokens * 100}%`);
|
||||
}
|
||||
}
|
||||
|
||||
clearInterval(streamingInterval);
|
||||
@@ -2530,7 +2569,7 @@ Your response must be a clean JSON array only, following exactly this format:
|
||||
DO NOT include any text before or after the JSON array. No explanations, no markdown formatting.`;
|
||||
|
||||
const result = await perplexity.chat.completions.create({
|
||||
model: process.env.PERPLEXITY_MODEL || 'sonar-pro',
|
||||
model: process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro',
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
@@ -2541,8 +2580,8 @@ DO NOT include any text before or after the JSON array. No explanations, no mark
|
||||
content: missingTasksResearchPrompt
|
||||
}
|
||||
],
|
||||
temperature: CONFIG.temperature,
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
|
||||
});
|
||||
|
||||
// Extract the response
|
||||
@@ -2550,9 +2589,9 @@ DO NOT include any text before or after the JSON array. No explanations, no mark
|
||||
} else {
|
||||
// Use Claude
|
||||
const stream = await anthropic.messages.create({
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
model: modelOverride || CONFIG.model,
|
||||
temperature: CONFIG.temperature,
|
||||
max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
|
||||
model: modelOverride || CONFIG.model || session?.env?.ANTHROPIC_MODEL,
|
||||
temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
|
||||
messages: [{ role: "user", content: missingTasksPrompt }],
|
||||
system: "You are an expert software architect and project manager analyzing task complexity. Respond only with valid JSON.",
|
||||
stream: true
|
||||
@@ -2563,6 +2602,12 @@ DO NOT include any text before or after the JSON array. No explanations, no mark
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
missingAnalysisResponse += chunk.delta.text;
|
||||
}
|
||||
if (reportProgress) {
|
||||
await reportProgress({ progress: (missingAnalysisResponse.length / CONFIG.maxTokens) * 100 });
|
||||
}
|
||||
if (mcpLog) {
|
||||
mcpLog.info(`Progress: ${missingAnalysisResponse.length / CONFIG.maxTokens * 100}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3063,9 +3108,12 @@ async function removeSubtask(tasksPath, subtaskId, convertToTask = false, genera
|
||||
* @param {string} subtaskId - ID of the subtask to update in format "parentId.subtaskId"
|
||||
* @param {string} prompt - Prompt for generating additional information
|
||||
* @param {boolean} useResearch - Whether to use Perplexity AI for research-backed updates
|
||||
* @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)
|
||||
* @returns {Object|null} - The updated subtask or null if update failed
|
||||
*/
|
||||
async function updateSubtaskById(tasksPath, subtaskId, prompt, useResearch = false) {
|
||||
async function updateSubtaskById(tasksPath, subtaskId, prompt, useResearch = false, { reportProgress, mcpLog, session } = {} ) {
|
||||
let loadingIndicator = null;
|
||||
try {
|
||||
log('info', `Updating subtask ${subtaskId} with prompt: "${prompt}"`);
|
||||
@@ -3194,15 +3242,15 @@ Provide concrete examples, code snippets, or implementation details when relevan
|
||||
|
||||
if (modelType === 'perplexity') {
|
||||
// Construct Perplexity payload
|
||||
const perplexityModel = process.env.PERPLEXITY_MODEL || 'sonar-pro';
|
||||
const perplexityModel = process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro';
|
||||
const response = await client.chat.completions.create({
|
||||
model: perplexityModel,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userMessageContent }
|
||||
],
|
||||
temperature: parseFloat(process.env.TEMPERATURE || CONFIG.temperature),
|
||||
max_tokens: parseInt(process.env.MAX_TOKENS || CONFIG.maxTokens),
|
||||
temperature: parseFloat(process.env.TEMPERATURE || session?.env?.TEMPERATURE || CONFIG.temperature),
|
||||
max_tokens: parseInt(process.env.MAX_TOKENS || session?.env?.MAX_TOKENS || CONFIG.maxTokens),
|
||||
});
|
||||
additionalInformation = response.choices[0].message.content.trim();
|
||||
} else { // Claude
|
||||
@@ -3234,6 +3282,12 @@ Provide concrete examples, code snippets, or implementation details when relevan
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
responseText += chunk.delta.text;
|
||||
}
|
||||
if (reportProgress) {
|
||||
await reportProgress({ progress: (responseText.length / CONFIG.maxTokens) * 100 });
|
||||
}
|
||||
if (mcpLog) {
|
||||
mcpLog.info(`Progress: ${responseText.length / CONFIG.maxTokens * 100}%`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (streamingInterval) clearInterval(streamingInterval);
|
||||
|
||||
Reference in New Issue
Block a user