@pinkpixel/taskflow-mcp
Version:
Task manager Model Context Protocol (MCP) server for planning and executing tasks.
1,172 lines (1,171 loc) • 78.2 kB
JavaScript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";
import { z } from "zod";
import * as yaml from "js-yaml";
const DEFAULT_PATH = path.join(os.homedir(), "Documents", "tasks.json");
const TASK_FILE_PATH = process.env.TASK_MANAGER_FILE_PATH || DEFAULT_PATH;
// Zod Schemas
const DependencySchema = z.object({
name: z.string(),
version: z.string().optional(),
url: z.string().optional(),
description: z.string().optional(),
});
const NoteSchema = z.object({
title: z.string(),
content: z.string(),
});
const SubtaskSchema = z.object({
title: z.string(),
description: z.string(),
});
const RequestPlanningSchema = z.object({
originalRequest: z.string(),
splitDetails: z.string().optional(),
outputPath: z.string().optional(),
dependencies: z.array(DependencySchema).optional(),
notes: z.array(NoteSchema).optional(),
tasks: z.array(z.object({
title: z.string(),
description: z.string(),
subtasks: z.array(SubtaskSchema).optional(),
dependencies: z.array(DependencySchema).optional(),
})),
});
const GetNextTaskSchema = z.object({
requestId: z.string(),
});
const MarkTaskDoneSchema = z.object({
requestId: z.string(),
taskId: z.string(),
completedDetails: z.string().optional(),
});
const OpenTaskDetailsSchema = z.object({
taskId: z.string(),
});
const ListRequestsSchema = z.object({});
const AddTasksToRequestSchema = z.object({
requestId: z.string(),
tasks: z.array(z.object({
title: z.string(),
description: z.string(),
subtasks: z.array(SubtaskSchema).optional(),
dependencies: z.array(DependencySchema).optional(),
})),
});
const UpdateTaskSchema = z.object({
requestId: z.string(),
taskId: z.string(),
title: z.string().optional(),
description: z.string().optional(),
});
const DeleteTaskSchema = z.object({
requestId: z.string(),
taskId: z.string(),
});
const AddSubtasksSchema = z.object({
requestId: z.string(),
taskId: z.string(),
subtasks: z.array(SubtaskSchema),
});
const MarkSubtaskDoneSchema = z.object({
requestId: z.string(),
taskId: z.string(),
subtaskId: z.string(),
});
const UpdateSubtaskSchema = z.object({
requestId: z.string(),
taskId: z.string(),
subtaskId: z.string(),
title: z.string().optional(),
description: z.string().optional(),
});
const DeleteSubtaskSchema = z.object({
requestId: z.string(),
taskId: z.string(),
subtaskId: z.string(),
});
const ExportTaskStatusSchema = z.object({
requestId: z.string(),
outputPath: z.string().optional(),
filename: z.string().optional(),
format: z.enum(["markdown", "json", "html"]).default("markdown"),
});
const AddNoteSchema = z.object({
requestId: z.string(),
title: z.string(),
content: z.string(),
});
const UpdateNoteSchema = z.object({
requestId: z.string(),
noteId: z.string(),
title: z.string().optional(),
content: z.string().optional(),
});
const DeleteNoteSchema = z.object({
requestId: z.string(),
noteId: z.string(),
});
const AddDependencySchema = z.object({
requestId: z.string(),
taskId: z.string().optional(), // If not provided, add to request
dependency: DependencySchema,
});
// Tools
const PLAN_TASK_TOOL = {
name: "plan_task",
description: "Register a new user request and plan its associated tasks. You must provide 'originalRequest' and 'tasks', and optionally 'splitDetails'.\n\n" +
"Tasks can now include subtasks, which are smaller units of work that make up a task. All subtasks must be completed before a task can be marked as done.\n\n" +
"You can also include:\n" +
"- 'dependencies': List of project or task-specific dependencies (libraries, tools, etc.)\n" +
"- 'notes': General notes about the project (preferences, guidelines, etc.)\n" +
"- 'outputPath': Path to save a Markdown file with the task plan for reference. It's recommended to use absolute paths (e.g., 'C:/Users/username/Documents/task-plan.md') rather than relative paths for more reliable file creation.\n\n" +
"This tool initiates a new workflow for handling a user's request. The workflow is as follows:\n" +
"1. Use 'plan_task' to register a request and its tasks (with optional subtasks, dependencies, and notes).\n" +
"2. After adding tasks, you MUST use 'get_next_task' to retrieve the first task. A progress table will be displayed.\n" +
"3. Use 'get_next_task' to retrieve the next uncompleted task.\n" +
"4. If the task has subtasks, complete each subtask using 'mark_subtask_done' before marking the task as done.\n" +
"5. **IMPORTANT:** After marking a task as done, a progress table will be displayed showing the updated status of all tasks. The assistant MUST NOT proceed to another task without the user's approval. Ask the user for approval before proceeding.\n" +
"6. Once the user approves the completed task, you can proceed to 'get_next_task' again to fetch the next pending task.\n" +
"7. Repeat this cycle until all tasks are done.\n" +
"8. After all tasks are completed, 'get_next_task' will indicate that all tasks are done. At this point, ask the user for confirmation that the entire request has been completed satisfactorily.\n" +
"9. If the user wants more tasks, you can use 'add_tasks_to_request' or 'plan_task' to add new tasks and continue the cycle.\n\n" +
"The critical point is to always wait for user approval after completing each task and after all tasks are done. Do not proceed automatically, UNLESS the user has explicitly told you to continue with all tasks and that you do not need approval.",
inputSchema: {
type: "object",
properties: {
originalRequest: { type: "string" },
splitDetails: { type: "string" },
outputPath: { type: "string" },
dependencies: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
version: { type: "string" },
url: { type: "string" },
description: { type: "string" },
},
required: ["name"],
},
},
notes: {
type: "array",
items: {
type: "object",
properties: {
title: { type: "string" },
content: { type: "string" },
},
required: ["title", "content"],
},
},
tasks: {
type: "array",
items: {
type: "object",
properties: {
title: { type: "string" },
description: { type: "string" },
dependencies: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
version: { type: "string" },
url: { type: "string" },
description: { type: "string" },
},
required: ["name"],
},
},
subtasks: {
type: "array",
items: {
type: "object",
properties: {
title: { type: "string" },
description: { type: "string" },
},
required: ["title", "description"],
},
},
},
required: ["title", "description"],
},
},
},
required: ["originalRequest", "tasks"],
},
};
const GET_NEXT_TASK_TOOL = {
name: "get_next_task",
description: "Given a 'requestId', return the next pending task (not done yet). If all tasks are completed, it will indicate that no more tasks are left and that you must ask the user what to do next.\n\n" +
"A progress table showing the current status of all tasks will be displayed with each response.\n\n" +
"If the same task is returned again or if no new task is provided after a task was marked as done, you MUST NOT proceed. In such a scenario, you must prompt the user for approval before calling 'get_next_task' again. Do not skip the user's approval step.\n" +
"In other words:\n" +
"- After calling 'mark_task_done', do not call 'get_next_task' again until the user has given approval for the completed task.\n" +
"- If 'get_next_task' returns 'all_tasks_done', it means all tasks have been completed. At this point, confirm with the user that all tasks have been completed, and optionally add more tasks via 'add_tasks_to_request' or 'plan_task'.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
},
required: ["requestId"],
},
};
const MARK_TASK_DONE_TOOL = {
name: "mark_task_done",
description: "Mark a given task as done after you've completed it. Provide 'requestId' and 'taskId', and optionally 'completedDetails'.\n\n" +
"After marking a task as done, a progress table will be displayed showing the updated status of all tasks.\n\n" +
"After this, DO NOT proceed to 'get_next_task' again until the user has explicitly approved the completed task. Ask the user for approval before continuing.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
taskId: { type: "string" },
completedDetails: { type: "string" },
},
required: ["requestId", "taskId"],
},
};
const OPEN_TASK_DETAILS_TOOL = {
name: "open_task_details",
description: "Get details of a specific task by 'taskId'. This is for inspecting task information at any point.",
inputSchema: {
type: "object",
properties: {
taskId: { type: "string" },
},
required: ["taskId"],
},
};
const LIST_REQUESTS_TOOL = {
name: "list_requests",
description: "List all requests with their basic information and summary of tasks. This provides a quick overview of all requests in the system.",
inputSchema: {
type: "object",
properties: {},
},
};
const ADD_TASKS_TO_REQUEST_TOOL = {
name: "add_tasks_to_request",
description: "Add new tasks to an existing request. This allows extending a request with additional tasks.\n\n" +
"Tasks can include subtasks and dependencies. A progress table will be displayed showing all tasks including the newly added ones.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
tasks: {
type: "array",
items: {
type: "object",
properties: {
title: { type: "string" },
description: { type: "string" },
dependencies: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
version: { type: "string" },
url: { type: "string" },
description: { type: "string" },
},
required: ["name"],
},
},
subtasks: {
type: "array",
items: {
type: "object",
properties: {
title: { type: "string" },
description: { type: "string" },
},
required: ["title", "description"],
},
},
},
required: ["title", "description"],
},
},
},
required: ["requestId", "tasks"],
},
};
const UPDATE_TASK_TOOL = {
name: "update_task",
description: "Update an existing task's title and/or description. Only uncompleted tasks can be updated.\n\n" +
"A progress table will be displayed showing the updated task information.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
taskId: { type: "string" },
title: { type: "string" },
description: { type: "string" },
},
required: ["requestId", "taskId"],
},
};
const DELETE_TASK_TOOL = {
name: "delete_task",
description: "Delete a specific task from a request. Only uncompleted tasks can be deleted.\n\n" +
"A progress table will be displayed showing the remaining tasks after deletion.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
taskId: { type: "string" },
},
required: ["requestId", "taskId"],
},
};
const ADD_SUBTASKS_TOOL = {
name: "add_subtasks",
description: "Add subtasks to an existing task. Provide 'requestId', 'taskId', and 'subtasks' array.\n\n" +
"Subtasks are smaller units of work that make up a task. All subtasks must be completed before a task can be marked as done.\n\n" +
"A progress table will be displayed showing the updated task with its subtasks.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
taskId: { type: "string" },
subtasks: {
type: "array",
items: {
type: "object",
properties: {
title: { type: "string" },
description: { type: "string" },
},
required: ["title", "description"],
},
},
},
required: ["requestId", "taskId", "subtasks"],
},
};
const MARK_SUBTASK_DONE_TOOL = {
name: "mark_subtask_done",
description: "Mark a subtask as done. Provide 'requestId', 'taskId', and 'subtaskId'.\n\n" +
"A progress table will be displayed showing the updated status of all tasks and subtasks.\n\n" +
"All subtasks must be completed before a task can be marked as done.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
taskId: { type: "string" },
subtaskId: { type: "string" },
},
required: ["requestId", "taskId", "subtaskId"],
},
};
const UPDATE_SUBTASK_TOOL = {
name: "update_subtask",
description: "Update a subtask's title or description. Provide 'requestId', 'taskId', 'subtaskId', and optionally 'title' and/or 'description'.\n\n" +
"Only uncompleted subtasks can be updated.\n\n" +
"A progress table will be displayed showing the updated task with its subtasks.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
taskId: { type: "string" },
subtaskId: { type: "string" },
title: { type: "string" },
description: { type: "string" },
},
required: ["requestId", "taskId", "subtaskId"],
},
};
const DELETE_SUBTASK_TOOL = {
name: "delete_subtask",
description: "Delete a subtask from a task. Provide 'requestId', 'taskId', and 'subtaskId'.\n\n" +
"Only uncompleted subtasks can be deleted.\n\n" +
"A progress table will be displayed showing the updated task with its remaining subtasks.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
taskId: { type: "string" },
subtaskId: { type: "string" },
},
required: ["requestId", "taskId", "subtaskId"],
},
};
const EXPORT_TASK_STATUS_TOOL = {
name: "export_task_status",
description: "Export the current status of all tasks in a request to a file.\n\n" +
"This tool saves the current state of tasks, subtasks, dependencies, and notes to a file for reference.\n\n" +
"You can specify:\n" +
"- 'format': 'markdown', 'json', or 'html'\n" +
"- 'outputPath': Full path to save the file, or just a directory path\n" +
"- 'filename': Optional custom filename (auto-generated if not provided)\n\n" +
"Path handling:\n" +
"- If outputPath is a directory, filename will be auto-generated as '{project-name}_tasks.{ext}'\n" +
"- If outputPath includes filename, it will be used as-is\n" +
"- Relative paths are resolved from current working directory\n" +
"- If no path specified, saves to current working directory",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
outputPath: {
type: "string",
description: "Directory or full file path where to save the export"
},
filename: {
type: "string",
description: "Optional custom filename (auto-generated if not provided)"
},
format: {
type: "string",
enum: ["markdown", "json", "html"],
default: "markdown"
},
},
required: ["requestId"],
},
};
const ADD_NOTE_TOOL = {
name: "add_note",
description: "Add a note to a request. Notes can contain important information about the project, such as user preferences or guidelines.\n\n" +
"Notes are displayed in the task progress table and can be referenced when working on tasks.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
title: { type: "string" },
content: { type: "string" },
},
required: ["requestId", "title", "content"],
},
};
const UPDATE_NOTE_TOOL = {
name: "update_note",
description: "Update an existing note's title or content.\n\n" +
"Provide the 'requestId' and 'noteId', and optionally 'title' and/or 'content' to update.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
noteId: { type: "string" },
title: { type: "string" },
content: { type: "string" },
},
required: ["requestId", "noteId"],
},
};
const DELETE_NOTE_TOOL = {
name: "delete_note",
description: "Delete a note from a request.\n\n" +
"Provide the 'requestId' and 'noteId' of the note to delete.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
noteId: { type: "string" },
},
required: ["requestId", "noteId"],
},
};
const ADD_DEPENDENCY_TOOL = {
name: "add_dependency",
description: "Add a dependency to a request or task.\n\n" +
"Dependencies can be libraries, tools, or other requirements needed for the project or specific tasks.\n\n" +
"If 'taskId' is provided, the dependency will be added to that specific task. Otherwise, it will be added to the request.",
inputSchema: {
type: "object",
properties: {
requestId: { type: "string" },
taskId: { type: "string" },
dependency: {
type: "object",
properties: {
name: { type: "string" },
version: { type: "string" },
url: { type: "string" },
description: { type: "string" },
},
required: ["name"],
},
},
required: ["requestId", "dependency"],
},
};
class TaskFlowServer {
requestCounter = 0;
taskCounter = 0;
data = { requests: [] };
constructor() {
this.loadTasks();
}
/**
* Sanitize and fix newline characters in strings
* This addresses the issue where \n characters get corrupted during transmission
*/
sanitizeString(input) {
if (typeof input !== 'string') {
return String(input);
}
// Fix common newline corruption patterns found in issue #4
// Replace corrupted patterns like 'nn' or 'n-' back to proper newlines
return input
// Fix the specific pattern from issue #4: "instructions.mdnnFiles" -> "instructions.md\n\nFiles"
.replace(/\.md(nn|n)(?=[A-Z])/g, '.md\n\n')
// Fix other 'nn' patterns that should be double newlines
.replace(/(\w)(nn)(?=[A-Z])/g, '$1\n\n')
// Fix single 'n' followed by dash (common pattern)
.replace(/(\w)(n)(-)/g, '$1\n$3')
// Fix standalone 'n' that should be newlines when followed by list items
.replace(/(\w)(n)(?=[-*•])/g, '$1\n')
// General cleanup: if we have literal \n sequences, preserve them
.replace(/\\n/g, '\n');
}
/**
* Sanitize task data to fix newline issues
*/
sanitizeTaskData(data) {
if (typeof data === 'string') {
return this.sanitizeString(data);
}
if (Array.isArray(data)) {
return data.map(item => this.sanitizeTaskData(item));
}
if (data && typeof data === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(data)) {
sanitized[key] = this.sanitizeTaskData(value);
}
return sanitized;
}
return data;
}
async loadTasks() {
try {
const data = await fs.readFile(TASK_FILE_PATH, "utf-8");
const extension = path.extname(TASK_FILE_PATH).toLowerCase();
if (extension === '.yaml' || extension === '.yml') {
// Parse as YAML
const parsed = yaml.load(data);
if (typeof parsed === 'object' && parsed !== null) {
this.data = parsed;
}
else {
throw new Error('Invalid YAML structure');
}
}
else {
// Parse as JSON (default)
this.data = JSON.parse(data);
}
const allTaskIds = [];
const allRequestIds = [];
for (const req of this.data.requests) {
const reqNum = Number.parseInt(req.requestId.replace("req-", ""), 10);
if (!Number.isNaN(reqNum)) {
allRequestIds.push(reqNum);
}
for (const t of req.tasks) {
const tNum = Number.parseInt(t.id.replace("task-", ""), 10);
if (!Number.isNaN(tNum)) {
allTaskIds.push(tNum);
}
}
}
this.requestCounter =
allRequestIds.length > 0 ? Math.max(...allRequestIds) : 0;
this.taskCounter = allTaskIds.length > 0 ? Math.max(...allTaskIds) : 0;
}
catch (error) {
console.warn(`Error loading tasks from ${TASK_FILE_PATH}:`, error instanceof Error ? error.message : error);
this.data = { requests: [] };
}
}
async saveTasks() {
try {
const extension = path.extname(TASK_FILE_PATH).toLowerCase();
let content;
if (extension === '.yaml' || extension === '.yml') {
// Save as YAML with proper multiline handling
content = yaml.dump(this.data, {
indent: 2,
lineWidth: -1, // Don't wrap long lines
noRefs: true, // Don't use references
sortKeys: false // Keep original key order
});
}
else {
// Save as JSON (default)
content = JSON.stringify(this.data, null, 2);
}
await fs.writeFile(TASK_FILE_PATH, content, "utf-8");
}
catch (error) {
if (error instanceof Error && error.message.includes("EROFS")) {
console.error("EROFS: read-only file system. Cannot save tasks.");
throw error;
}
throw error;
}
}
formatTaskProgressTable(requestId) {
const req = this.data.requests.find((r) => r.requestId === requestId);
if (!req)
return "Request not found";
let table = "\nProgress Status:\n";
table += "| Task ID | Title | Description | Status | Subtasks |\n";
table += "|----------|----------|------|------|----------|\n";
for (const task of req.tasks) {
const status = task.done ? "✅ Done" : "🔄 In Progress";
const subtaskCount = task.subtasks.length;
const completedSubtasks = task.subtasks.filter(s => s.done).length;
const subtaskStatus = subtaskCount > 0
? `${completedSubtasks}/${subtaskCount}`
: "None";
table += `| ${task.id} | ${task.title} | ${task.description} | ${status} | ${subtaskStatus} |\n`;
// Add subtasks with indentation if they exist
if (subtaskCount > 0) {
for (const subtask of task.subtasks) {
const subtaskStatus = subtask.done ? "✅ Done" : "🔄 In Progress";
table += `| └─ ${subtask.id} | ${subtask.title} | ${subtask.description} | ${subtaskStatus} | - |\n`;
}
}
}
return table;
}
/**
* Export tasks to a Markdown file
*/
async exportTasksToMarkdown(requestId, outputPath) {
const req = this.data.requests.find((r) => r.requestId === requestId);
if (!req)
throw new Error("Request not found");
let markdown = `# Project Plan: ${req.originalRequest}\n\n`;
// Add split details if available
if (req.splitDetails && req.splitDetails !== req.originalRequest) {
markdown += `## Details\n${req.splitDetails}\n\n`;
}
// Add dependencies if available
if (req.dependencies && req.dependencies.length > 0) {
markdown += "## Dependencies\n\n";
for (const dep of req.dependencies) {
markdown += `- **${dep.name}**`;
if (dep.version)
markdown += ` (${dep.version})`;
if (dep.description)
markdown += `: ${dep.description}`;
if (dep.url)
markdown += ` - [Link](${dep.url})`;
markdown += "\n";
}
markdown += "\n";
}
// Add notes if available
if (req.notes && req.notes.length > 0) {
markdown += "## Notes\n\n";
for (const note of req.notes) {
markdown += `### ${note.title}\n${note.content}\n\n`;
}
}
// Add tasks overview with checkboxes
markdown += "## Tasks Overview\n";
for (const task of req.tasks) {
markdown += `- [ ] ${task.title}\n`;
// Add subtasks with indentation
if (task.subtasks.length > 0) {
for (const subtask of task.subtasks) {
markdown += ` - [ ] ${subtask.title}\n`;
}
}
// Add task dependencies if available
if (task.dependencies && task.dependencies.length > 0) {
markdown += ` - Dependencies: `;
markdown += task.dependencies.map(d => d.name + (d.version ? ` (${d.version})` : "")).join(", ");
markdown += "\n";
}
}
markdown += "\n";
// Add detailed tasks
markdown += "## Detailed Tasks\n\n";
for (let i = 0; i < req.tasks.length; i++) {
const task = req.tasks[i];
markdown += `### ${i + 1}. ${task.title}\n`;
markdown += `**Description:** ${task.description}\n\n`;
// Add task dependencies if available
if (task.dependencies && task.dependencies.length > 0) {
markdown += "**Dependencies:**\n";
for (const dep of task.dependencies) {
markdown += `- ${dep.name}`;
if (dep.version)
markdown += ` (${dep.version})`;
if (dep.description)
markdown += `: ${dep.description}`;
if (dep.url)
markdown += ` - [Link](${dep.url})`;
markdown += "\n";
}
markdown += "\n";
}
// Add subtasks if available
if (task.subtasks.length > 0) {
markdown += "**Subtasks:**\n";
for (const subtask of task.subtasks) {
markdown += `- [ ] ${subtask.title}\n`;
markdown += ` - Description: ${subtask.description}\n`;
}
markdown += "\n";
}
}
// Add progress tracking section
markdown += "## Progress Tracking\n\n";
markdown += "| Task | Status | Completion Date |\n";
markdown += "|------|--------|----------------|\n";
for (const task of req.tasks) {
markdown += `| ${task.title} | ${task.done ? "✅ Done" : "🔄 In Progress"} | ${task.done ? "YYYY-MM-DD" : ""} |\n`;
}
// Write to file
try {
await fs.writeFile(outputPath, markdown, "utf-8");
}
catch (error) {
console.error(`Error writing to file ${outputPath}:`, error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to write to file: ${errorMessage}`);
}
}
/**
* Generate a safe filename from a string
*/
generateSafeFilename(text) {
return text
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters except spaces and hyphens
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
.substring(0, 50); // Limit length
}
/**
* Resolve the final export path with proper filename
*/
async resolveExportPath(req, outputPath, filename, format = "markdown") {
// Get file extension based on format
const extensions = {
markdown: 'md',
json: 'json',
html: 'html'
};
const ext = extensions[format];
// Generate default filename based on project name
const projectName = this.generateSafeFilename(req.originalRequest);
const defaultFilename = `${projectName}_tasks.${ext}`;
// If no outputPath provided, use current working directory
if (!outputPath) {
return path.resolve(process.cwd(), filename || defaultFilename);
}
// Resolve the output path
const resolvedPath = path.resolve(outputPath);
try {
// Check if the path is a directory
const stats = await fs.stat(resolvedPath).catch(() => null);
const isDirectory = stats?.isDirectory() ?? false;
// If it's an existing directory, or if it ends with / or \, treat as directory
if (isDirectory || outputPath.endsWith('/') || outputPath.endsWith('\\')) {
return path.join(resolvedPath, filename || defaultFilename);
}
// If path has an extension, use as-is
if (path.extname(resolvedPath)) {
return resolvedPath;
}
// Otherwise, treat as directory and add filename
return path.join(resolvedPath, filename || defaultFilename);
}
catch (error) {
// If we can't stat the path, check if it looks like a directory
if (outputPath.endsWith('/') || outputPath.endsWith('\\')) {
return path.join(resolvedPath, filename || defaultFilename);
}
// If it has no extension, treat as directory
if (!path.extname(outputPath)) {
return path.join(resolvedPath, filename || defaultFilename);
}
return resolvedPath;
}
}
/**
* Export task status to a file in the specified format
*/
async exportTaskStatus(requestId, outputPath, filename, format = "markdown") {
const req = this.data.requests.find((r) => r.requestId === requestId);
if (!req)
throw new Error("Request not found");
// Resolve the final export path
const finalPath = await this.resolveExportPath(req, outputPath, filename, format);
let content = "";
switch (format) {
case "markdown":
content = await this.generateMarkdownStatus(req);
break;
case "json":
content = JSON.stringify(req, null, 2);
break;
case "html":
content = await this.generateHtmlStatus(req);
break;
default:
throw new Error(`Unsupported format: ${format}`);
}
try {
// Ensure directory exists
const dir = path.dirname(finalPath);
await fs.mkdir(dir, { recursive: true });
// Write the file
await fs.writeFile(finalPath, content, "utf-8");
return { outputPath: finalPath, format };
}
catch (error) {
console.error(`Error writing to file ${finalPath}:`, error);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to write to file ${finalPath}: ${errorMessage}`);
}
}
/**
* Generate Markdown status report
*/
async generateMarkdownStatus(req) {
const now = new Date().toISOString().split("T")[0];
let markdown = `# Task Status Report: ${req.originalRequest}\n\n`;
markdown += `*Generated on: ${now}*\n\n`;
// Overall progress
const totalTasks = req.tasks.length;
const completedTasks = req.tasks.filter(t => t.done).length;
const progressPercent = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
markdown += `## Overall Progress: ${progressPercent}%\n\n`;
markdown += `- **Total Tasks:** ${totalTasks}\n`;
markdown += `- **Completed Tasks:** ${completedTasks}\n`;
markdown += `- **Remaining Tasks:** ${totalTasks - completedTasks}\n\n`;
// Add notes if available
if (req.notes && req.notes.length > 0) {
markdown += "## Notes\n\n";
for (const note of req.notes) {
markdown += `### ${note.title}\n${note.content}\n\n`;
markdown += `*Last updated: ${new Date(note.updatedAt).toLocaleString()}*\n\n`;
}
}
// Task status
markdown += "## Task Status\n\n";
for (let i = 0; i < req.tasks.length; i++) {
const task = req.tasks[i];
const taskStatus = task.done ? "✅ Done" : "🔄 In Progress";
markdown += `### ${i + 1}. ${task.title} (${taskStatus})\n`;
markdown += `**Description:** ${task.description}\n\n`;
markdown += `**Status:** ${taskStatus}\n`;
if (task.done && task.completedDetails) {
markdown += `**Completion Details:** ${task.completedDetails}\n\n`;
}
// Add subtasks if available
if (task.subtasks.length > 0) {
const totalSubtasks = task.subtasks.length;
const completedSubtasks = task.subtasks.filter(s => s.done).length;
const subtaskProgress = totalSubtasks > 0 ? Math.round((completedSubtasks / totalSubtasks) * 100) : 0;
markdown += `**Subtask Progress:** ${subtaskProgress}% (${completedSubtasks}/${totalSubtasks})\n\n`;
markdown += "| Subtask | Description | Status |\n";
markdown += "|---------|-------------|--------|\n";
for (const subtask of task.subtasks) {
const subtaskStatus = subtask.done ? "✅ Done" : "🔄 In Progress";
markdown += `| ${subtask.title} | ${subtask.description} | ${subtaskStatus} |\n`;
}
markdown += "\n";
}
// Add dependencies if available
if (task.dependencies && task.dependencies.length > 0) {
markdown += "**Dependencies:**\n";
for (const dep of task.dependencies) {
markdown += `- ${dep.name}`;
if (dep.version)
markdown += ` (${dep.version})`;
if (dep.description)
markdown += `: ${dep.description}`;
markdown += "\n";
}
markdown += "\n";
}
}
return markdown;
}
/**
* Generate HTML status report
*/
async generateHtmlStatus(req) {
const now = new Date().toISOString().split("T")[0];
const totalTasks = req.tasks.length;
const completedTasks = req.tasks.filter(t => t.done).length;
const progressPercent = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
let html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Status: ${req.originalRequest}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; max-width: 1000px; margin: 0 auto; padding: 20px; }
h1, h2, h3 { color: #333; }
.progress-bar { background-color: #f0f0f0; border-radius: 4px; height: 20px; margin-bottom: 20px; }
.progress-bar-fill { background-color: #4CAF50; height: 100%; border-radius: 4px; width: ${progressPercent}%; }
.task { border: 1px solid #ddd; padding: 15px; margin-bottom: 15px; border-radius: 4px; }
.task-header { display: flex; justify-content: space-between; align-items: center; }
.task-status { padding: 5px 10px; border-radius: 4px; font-size: 14px; }
.status-done { background-color: #E8F5E9; color: #2E7D32; }
.status-progress { background-color: #E3F2FD; color: #1565C0; }
.status-approved { background-color: #E8F5E9; color: #2E7D32; }
.status-pending { background-color: #FFF8E1; color: #F57F17; }
.subtasks { margin-top: 10px; }
.subtask { padding: 8px; border-bottom: 1px solid #eee; }
.subtask:last-child { border-bottom: none; }
table { border-collapse: collapse; width: 100%; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
th { background-color: #f2f2f2; }
.note { background-color: #FFF8E1; padding: 10px; border-left: 4px solid #FFC107; margin-bottom: 15px; }
</style>
</head>
<body>
<h1>Task Status: ${req.originalRequest}</h1>
<p><em>Generated on: ${now}</em></p>
<h2>Overall Progress: ${progressPercent}%</h2>
<div class="progress-bar">
<div class="progress-bar-fill"></div>
</div>
<p>
<strong>Total Tasks:</strong> ${totalTasks} |
<strong>Completed:</strong> ${completedTasks} |
<strong>Remaining:</strong> ${totalTasks - completedTasks}
</p>
`;
// Add notes if available
if (req.notes && req.notes.length > 0) {
html += `<h2>Notes</h2>`;
for (const note of req.notes) {
html += `
<div class="note">
<h3>${note.title}</h3>
<p>${note.content}</p>
<p><small>Last updated: ${new Date(note.updatedAt).toLocaleString()}</small></p>
</div>`;
}
}
// Task status
html += `<h2>Task Status</h2>`;
for (let i = 0; i < req.tasks.length; i++) {
const task = req.tasks[i];
const taskStatusClass = task.done ? "status-done" : "status-progress";
const taskStatus = task.done ? "Done" : "In Progress";
html += `
<div class="task">
<div class="task-header">
<h3>${i + 1}. ${task.title}</h3>
<span class="task-status ${taskStatusClass}">${taskStatus}</span>
</div>
<p><strong>Description:</strong> ${task.description}</p>`;
if (task.done && task.completedDetails) {
html += `<p><strong>Completion Details:</strong> ${task.completedDetails}</p>`;
}
// Add subtasks if available
if (task.subtasks.length > 0) {
const totalSubtasks = task.subtasks.length;
const completedSubtasks = task.subtasks.filter(s => s.done).length;
const subtaskProgress = totalSubtasks > 0 ? Math.round((completedSubtasks / totalSubtasks) * 100) : 0;
html += `
<p><strong>Subtask Progress:</strong> ${subtaskProgress}% (${completedSubtasks}/${totalSubtasks})</p>
<table>
<tr>
<th>Subtask</th>
<th>Description</th>
<th>Status</th>
</tr>`;
for (const subtask of task.subtasks) {
const subtaskStatus = subtask.done ? "Done" : "In Progress";
const subtaskStatusClass = subtask.done ? "status-done" : "status-progress";
html += `
<tr>
<td>${subtask.title}</td>
<td>${subtask.description}</td>
<td><span class="task-status ${subtaskStatusClass}">${subtaskStatus}</span></td>
</tr>`;
}
html += `
</table>`;
}
// Add dependencies if available
if (task.dependencies && task.dependencies.length > 0) {
html += `
<p><strong>Dependencies:</strong></p>
<ul>`;
for (const dep of task.dependencies) {
html += `
<li>${dep.name}${dep.version ? ` (${dep.version})` : ""}${dep.description ? `: ${dep.description}` : ""}</li>`;
}
html += `
</ul>`;
}
html += `
</div>`;
}
html += `
</body>
</html>`;
return html;
}
formatRequestsList() {
let output = "\nRequests List:\n";
output +=
"| Request ID | Original Request | Total Tasks | Completed |\n";
output +=
"|------------|------------------|-------------|-----------|\n";
for (const req of this.data.requests) {
const totalTasks = req.tasks.length;
const completedTasks = req.tasks.filter((t) => t.done).length;
output += `| ${req.requestId} | ${req.originalRequest.substring(0, 30)}${req.originalRequest.length > 30 ? "..." : ""} | ${totalTasks} | ${completedTasks} |\n`;
}
return output;
}
async requestPlanning(originalRequest, tasks, splitDetails, outputPath, dependencies, notes) {
await this.loadTasks();
this.requestCounter += 1;
const requestId = `req-${this.requestCounter}`;
// Sanitize input data to fix newline issues
const sanitizedOriginalRequest = this.sanitizeString(originalRequest);
const sanitizedSplitDetails = splitDetails ? this.sanitizeString(splitDetails) : undefined;
const sanitizedTasks = this.sanitizeTaskData(tasks);
const sanitizedNotes = notes ? this.sanitizeTaskData(notes) : undefined;
const newTasks = [];
for (const taskDef of sanitizedTasks) {
this.taskCounter += 1;
// Process subtasks if they exist
const subtasks = [];
if (taskDef.subtasks && taskDef.subtasks.length > 0) {
for (const subtaskDef of taskDef.subtasks) {
this.taskCounter += 1;
subtasks.push({
id: `subtask-${this.taskCounter}`,
title: this.sanitizeString(subtaskDef.title),
description: this.sanitizeString(subtaskDef.description),
done: false,
});
}
}
newTasks.push({
id: `task-${this.taskCounter}`,
title: this.sanitizeString(taskDef.title),
description: this.sanitizeString(taskDef.description),
done: false,
approved: false,
completedDetails: "",
subtasks: subtasks,
dependencies: taskDef.dependencies,
});
}
// Process notes if they exist
const processedNotes = [];
if (sanitizedNotes && sanitizedNotes.length > 0) {
for (const noteDef of sanitizedNotes) {
const now = new Date().toISOString();
processedNotes.push({
id: `note-${this.taskCounter++}`,
title: this.sanitizeString(noteDef.title),
content: this.sanitizeString(noteDef.content),
createdAt: now,
updatedAt: now,
});
}
}
this.data.requests.push({
requestId,
originalRequest: sanitizedOriginalRequest,
splitDetails: sanitizedSplitDetails || sanitizedOriginalRequest,
tasks: newTasks,
completed: false,
dependencies: dependencies,
notes: processedNotes,
});
await this.saveTasks();
// Generate Markdown file if outputPath is provided
if (outputPath) {
await this.exportTasksToMarkdown(requestId, outputPath);
}
const progressTable = this.formatTaskProgressTable(requestId);
return {
status: "planned",
requestId,
totalTasks: newTasks.length,
outputPath: outputPath ? outputPath : undefined,
tasks: newTasks.map((t) => ({
id: t.id,
title: t.title,
description: t.description,
})),
message: `Tasks have been successfully added. Please use 'get_next_task' to retrieve the first task.\n${progressTable}`,
};
}
async getNextTask(requestId) {
await this.loadTasks();
const req = this.data.requests.find((r) => r.requestId === requestId);
if (!req) {
return { status: "error", message: "Request not found" };
}
if (req.completed) {
return {
status: "already_completed",
message: "Request already completed.",
};
}
const nextTask = req.tasks.find((t) => !t.done);
if (!nextTask) {
// all tasks done?
const allDone = req.tasks.every((t) => t.done);
if (allDone && !req.completed) {
const progressTable = this.formatTaskProgressTable(requestId);
return {
status: "all_tasks_done",
message: `All tasks have been completed. Awaiting completion approval.\n${progressTable}`,
};
}
return { status: "no_next_task", message: "No undone tasks found." };
}
const progressTable = this.formatTaskProgressTable(requestId);
return {
status: "next_task",
task: {
id: nextTask.id,
title: nextTask.title,
description: nextTask.description,
},
message: `Next task is ready. Task approval will be required after completion.\n${progressTable}`,
};
}
async markTaskDone(requestId, taskId, completedDetails) {
await this.loadTasks();
const req = this.data.requests.find((r) => r.requestId === requestId);
if (!req)
return { status: "error", message: "Request not found" };
const task = req.tasks.find((t) => t.id === taskId);
if (!task)
return { status: "error", message: "Task not found" };
if (task.done)
return {
status: "already_done",
message: "Task is already marked done.",
};
// Check if all subtasks are done
const hasSubtasks = task.subtasks.length > 0;
const allSubtasksDone = task.subtasks.every(s => s.done);
if (hasSubtas