Skip to main content
Glama
tasks.ts10.7 kB
import { z } from 'zod'; import * as fs from 'fs/promises'; import * as path from 'path'; import { detectProjectType, findSpecsDirectory } from '../speckit/detector.js'; import { generateTasksFromTemplate } from '../speckit/templates.js'; import { parseTasks } from '../speckit/parser.js'; /** * Spec Kit tasks tools - Real Spec Kit Integration */ // Tool schemas export const TasksGenerateSchema = z.object({ scope: z.string().describe('Scope or context for task generation'), planPath: z.string().optional().describe('Path to plan file'), workspacePath: z.string().optional().describe('Workspace directory path'), }); export const TasksTickSchema = z.object({ taskId: z.string().describe('Task ID to mark as complete'), tasksPath: z.string().optional().describe('Path to tasks file'), workspacePath: z.string().optional().describe('Workspace directory path'), }); // Task structure export interface Task { id: string; title: string; description: string; status: 'pending' | 'in-progress' | 'completed'; dependencies: string[]; storyPoints: number; createdAt: string; completedAt?: string; } /** * Generate actionable tasks from plan */ export async function tasksGenerate(params: z.infer<typeof TasksGenerateSchema>) { const { scope, planPath, workspacePath = process.cwd() } = params; // Validate workspace path const resolvedPath = path.resolve(workspacePath); await validateWorkspacePath(resolvedPath); try { // Check project type const detection = await detectProjectType(resolvedPath); // Find the specs directory const specsDir = await findSpecsDirectory(resolvedPath); // Find the plan.md file - either from provided path or most recent feature let actualPlanPath = planPath; let featurePath: string | null = null; if (!actualPlanPath) { // Find most recent feature with plan.md const entries = await fs.readdir(specsDir); const featureDirs = entries .filter(entry => /^\d{3}-/.test(entry)) .sort() .reverse(); for (const dir of featureDirs) { const testPath = path.join(specsDir, dir, 'plan.md'); const exists = await fs.access(testPath).then(() => true).catch(() => false); if (exists) { actualPlanPath = testPath; featurePath = path.join(specsDir, dir); break; } } } else if (planPath) { // Use provided plan path actualPlanPath = path.resolve(resolvedPath, planPath); featurePath = path.dirname(actualPlanPath); } if (!actualPlanPath || !featurePath) { throw new Error('No plan found. Please create a plan first using plan_create.'); } // Generate tasks from template const tasksContent = await generateTasksFromTemplate(actualPlanPath, scope); // Write tasks.md const tasksPath = path.join(featurePath, 'tasks.md'); await fs.writeFile(tasksPath, tasksContent, 'utf-8'); // Parse the generated tasks to extract structured data const taskDoc = await parseTasks(tasksPath); // Also update .dincoder compatibility files const dincoderPath = path.join(resolvedPath, '.dincoder'); const dincoderExists = await fs.access(dincoderPath).then(() => true).catch(() => false); if (dincoderExists) { // Create compatibility tasks.json const tasksJson = { projectName: path.basename(featurePath!), createdAt: new Date().toISOString(), specKitPath: tasksPath, tasks: taskDoc.tasks.map((task, index) => ({ id: (index + 1).toString(), title: task.id, description: task.description, status: task.status, dependencies: task.dependencies || [], storyPoints: 1, createdAt: new Date().toISOString(), phase: task.phase, parallel: task.parallel })), nextTaskId: taskDoc.tasks.length + 1, completedCount: taskDoc.tasks.filter(t => t.status === 'complete').length, totalCount: taskDoc.tasks.length }; await fs.writeFile( path.join(dincoderPath, 'tasks.json'), JSON.stringify(tasksJson, null, 2), 'utf-8' ); } return { success: true, tasksPath, message: 'Generated implementation tasks', details: { location: featurePath, projectType: detection.type, taskCount: taskDoc.tasks.length, phases: { setup: taskDoc.tasks.filter(t => t.phase === 'setup').length, tests: taskDoc.tasks.filter(t => t.phase === 'tests').length, implementation: taskDoc.tasks.filter(t => t.phase === 'implementation').length, integration: taskDoc.tasks.filter(t => t.phase === 'integration').length, polish: taskDoc.tasks.filter(t => t.phase === 'polish').length }, filesCreated: { tasks: tasksPath, compatibility: dincoderExists ? path.join(dincoderPath, 'tasks.json') : undefined }, nextSteps: [ 'Review tasks.md for accuracy', 'Begin implementation with setup tasks', 'Use tasks_tick to mark tasks as complete' ] }, }; } catch (error) { throw new Error(`Failed to generate tasks: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Mark a task as complete */ export async function tasksTick(params: z.infer<typeof TasksTickSchema>) { const { taskId, tasksPath, workspacePath = process.cwd() } = params; // Validate workspace path const resolvedPath = path.resolve(workspacePath); await validateWorkspacePath(resolvedPath); try { // Find the tasks.md file - either from provided path or most recent feature let actualTasksPath = tasksPath; if (!actualTasksPath) { // Find the specs directory const specsDir = await findSpecsDirectory(resolvedPath); // Find most recent feature with tasks.md const entries = await fs.readdir(specsDir); const featureDirs = entries .filter(entry => /^\d{3}-/.test(entry)) .sort() .reverse(); for (const dir of featureDirs) { const testPath = path.join(specsDir, dir, 'tasks.md'); const exists = await fs.access(testPath).then(() => true).catch(() => false); if (exists) { actualTasksPath = testPath; break; } } } else if (tasksPath) { actualTasksPath = path.resolve(resolvedPath, tasksPath); } if (!actualTasksPath) { throw new Error('No tasks file found. Please generate tasks first using tasks_generate.'); } // Read the tasks.md file let tasksContent = await fs.readFile(actualTasksPath, 'utf-8'); // Update task status in markdown // Tasks are in format: - [ ] T001 [P] Description // Mark as complete: - [x] T001 [P] Description const taskPattern = new RegExp(`^(\\s*- \\[)[ ](\\] ${taskId}\\b.*)$`, 'm'); if (taskPattern.test(tasksContent)) { tasksContent = tasksContent.replace(taskPattern, '$1x$2'); // Write updated tasks back await fs.writeFile(actualTasksPath, tasksContent, 'utf-8'); // Parse updated tasks const taskDoc = await parseTasks(actualTasksPath); const completedTask = taskDoc.tasks.find(t => t.id === taskId); // Also update .dincoder compatibility files const dincoderPath = path.join(resolvedPath, '.dincoder'); const tasksJsonPath = path.join(dincoderPath, 'tasks.json'); const jsonExists = await fs.access(tasksJsonPath).then(() => true).catch(() => false); if (jsonExists) { const content = await fs.readFile(tasksJsonPath, 'utf-8'); const tasksJson = JSON.parse(content); // Find and update task const task = tasksJson.tasks.find((t: any) => t.title === taskId); if (task) { task.status = 'completed'; task.completedAt = new Date().toISOString(); tasksJson.completedCount = tasksJson.tasks.filter((t: any) => t.status === 'completed').length; } await fs.writeFile(tasksJsonPath, JSON.stringify(tasksJson, null, 2), 'utf-8'); } // Count remaining tasks const remainingTasks = taskDoc.tasks.filter(t => t.status === 'pending').length; const completedTasks = taskDoc.tasks.filter(t => t.status === 'complete').length; return { success: true, message: `Marked task ${taskId} as complete`, details: { taskId, description: completedTask?.description || 'Unknown task', tasksPath: actualTasksPath, progress: { completed: completedTasks, remaining: remainingTasks, total: taskDoc.tasks.length, percentage: Math.round((completedTasks / taskDoc.tasks.length) * 100) }, nextTasks: taskDoc.tasks .filter(t => t.status === 'pending') .slice(0, 3) .map(t => `${t.id}: ${t.description}`) }, }; } else { // Task not found or already completed const taskDoc = await parseTasks(actualTasksPath); const task = taskDoc.tasks.find(t => t.id === taskId); if (task && task.status === 'complete') { return { success: false, message: `Task ${taskId} is already marked as complete`, details: { taskId, description: task.description } }; } else { throw new Error(`Task ${taskId} not found in tasks file`); } } } catch (error) { throw new Error(`Failed to update task: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Validate workspace path for security */ async function validateWorkspacePath(workspacePath: string): Promise<void> { // Check if path exists try { const stat = await fs.stat(workspacePath); if (!stat.isDirectory()) { throw new Error('Workspace path is not a directory'); } } catch (error) { if ((error as any).code === 'ENOENT') { throw new Error('Workspace path does not exist'); } throw error; } // Prevent access to system directories const restrictedPaths = ['/etc', '/usr', '/bin', '/sbin', '/var', '/tmp']; const normalizedPath = path.normalize(workspacePath).toLowerCase(); for (const restricted of restrictedPaths) { if (normalizedPath.startsWith(restricted)) { throw new Error(`Access to system directory ${restricted} is not allowed`); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/flight505/MCP_DinCoder'

If you have feedback or need assistance with the MCP directory API, please join our Discord server