Skip to main content
Glama
batch.ts6.9 kB
/** * Batch Task Operations Tool * * Mark multiple tasks as complete at once. * Supports array format, range format, and mixed format. */ import * as fs from 'fs'; import * as path from 'path'; import { z } from 'zod'; import { parseTasksFile, type Task } from '../speckit/taskParser.js'; // Zod schema for tasks_tick_range tool export const TasksTickRangeSchema = z.object({ workspacePath: z.string().optional().describe('Path to workspace directory containing .dincoder folder'), tasksPath: z.string().optional().describe('Direct path to tasks.md file (overrides workspacePath)'), taskIds: z.union([ z.array(z.string()), z.string(), ]).describe('Task IDs to complete. Supports array ["T001", "T003"], range "T001-T005", or mixed ["T001-T005", "T010"]'), notes: z.string().optional().describe('Optional completion notes'), strict: z.boolean().default(false).describe('If true, fail all if any task is invalid. If false, skip invalid tasks and continue.'), }); export type TasksTickRangeParams = z.infer<typeof TasksTickRangeSchema>; interface CompletionReport { completed: string[]; failed: { id: string; reason: string }[]; skipped: { id: string; reason: string }[]; } /** * Mark multiple tasks as complete */ export async function tasksTickRange(params: TasksTickRangeParams): Promise<string> { // Resolve tasks.md path const tasksPath = params.tasksPath || path.join( params.workspacePath || process.cwd(), '.dincoder', 'tasks.md' ); if (!fs.existsSync(tasksPath)) { throw new Error(`Tasks file not found: ${tasksPath}`); } // Parse task IDs const taskIds = parseTaskIds(params.taskIds); // Validate task IDs before making any changes const tasks = parseTasksFile(tasksPath); const validation = validateTaskIds(taskIds, tasks, params.strict); if (params.strict && validation.failed.length > 0) { throw new Error( `Strict mode: Cannot complete tasks due to validation errors:\n` + validation.failed.map(f => `- ${f.id}: ${f.reason}`).join('\n') ); } // Read tasks.md content const content = fs.readFileSync(tasksPath, 'utf-8'); const lines = content.split('\n'); const report: CompletionReport = { completed: [], failed: [], skipped: [], }; // Mark tasks as complete for (const taskId of taskIds) { const task = tasks.find(t => t.id === taskId); if (!task) { report.failed.push({ id: taskId, reason: 'Task not found' }); continue; } if (task.status === 'completed') { report.skipped.push({ id: taskId, reason: 'Already completed' }); continue; } // Update the line (convert [ ] or [~] to [x]) const lineIndex = task.line - 1; if (lineIndex >= 0 && lineIndex < lines.length) { const line = lines[lineIndex]; const updatedLine = line.replace(/\[([ ~])\]/, '[x]'); lines[lineIndex] = updatedLine; report.completed.push(taskId); } else { report.failed.push({ id: taskId, reason: 'Invalid line number' }); } } // Write back to file if any tasks were completed if (report.completed.length > 0) { const updatedContent = lines.join('\n'); fs.writeFileSync(tasksPath, updatedContent, 'utf-8'); } // Add to report's failed list report.failed.push(...validation.failed); // Format report return formatCompletionReport(report, params, tasksPath); } /** * Parse task IDs from various formats */ function parseTaskIds(input: string | string[]): string[] { const taskIds: string[] = []; const inputs = Array.isArray(input) ? input : [input]; for (const item of inputs) { // Check if it's a range format (T001-T005) const rangeMatch = item.match(/^([A-Z]\d+)-([A-Z]\d+)$/i); if (rangeMatch) { const [, start, end] = rangeMatch; const startNum = parseInt(start.substring(1), 10); const endNum = parseInt(end.substring(1), 10); const prefix = start.charAt(0); if (startNum <= endNum) { for (let i = startNum; i <= endNum; i++) { taskIds.push(`${prefix}${String(i).padStart(3, '0')}`); } } else { throw new Error(`Invalid range: ${item} (start > end)`); } } else if (/^[A-Z]\d+$/i.test(item)) { // Single task ID taskIds.push(item.toUpperCase()); } else { throw new Error(`Invalid task ID format: ${item}`); } } return [...new Set(taskIds)]; // Remove duplicates } /** * Validate task IDs */ function validateTaskIds( taskIds: string[], tasks: Task[], strict: boolean ): { failed: { id: string; reason: string }[] } { const failed: { id: string; reason: string }[] = []; const taskMap = new Map(tasks.map(t => [t.id, t])); for (const taskId of taskIds) { const task = taskMap.get(taskId); if (!task) { failed.push({ id: taskId, reason: 'Task not found in tasks.md' }); } else if (task.status === 'completed' && strict) { failed.push({ id: taskId, reason: 'Already completed (strict mode)' }); } } return { failed }; } /** * Format completion report */ function formatCompletionReport( report: CompletionReport, params: TasksTickRangeParams, tasksPath: string ): string { let output = '# Batch Task Completion Report\n\n'; const total = report.completed.length + report.failed.length + report.skipped.length; output += `**Summary:**\n`; output += `- Total tasks processed: ${total}\n`; output += `- ✅ Completed: ${report.completed.length}\n`; output += `- ⏭️ Skipped: ${report.skipped.length}\n`; output += `- ❌ Failed: ${report.failed.length}\n`; output += `- Mode: ${params.strict ? 'strict (all-or-nothing)' : 'lenient (skip invalid)'}\n`; output += `\n`; if (params.notes) { output += `**Notes:** ${params.notes}\n\n`; } output += `**Updated file:** ${tasksPath}\n\n`; output += '---\n\n'; if (report.completed.length > 0) { output += `## ✅ Completed (${report.completed.length})\n\n`; for (const taskId of report.completed) { output += `- ${taskId}\n`; } output += '\n'; } if (report.skipped.length > 0) { output += `## ⏭️ Skipped (${report.skipped.length})\n\n`; for (const { id, reason } of report.skipped) { output += `- ${id}: *${reason}*\n`; } output += '\n'; } if (report.failed.length > 0) { output += `## ❌ Failed (${report.failed.length})\n\n`; for (const { id, reason } of report.failed) { output += `- ${id}: *${reason}*\n`; } output += '\n'; } // Calculate completion percentage const tasksFile = parseTasksFile(tasksPath); const completedCount = tasksFile.filter(t => t.status === 'completed').length; const percentage = Math.round((completedCount / tasksFile.length) * 100); output += `**Overall Progress:** ${completedCount}/${tasksFile.length} tasks complete (${percentage}%)\n`; return output; }

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