Skip to main content
Glama
visualize.ts7.79 kB
/** * Task Visualization Tool * * Generate visual dependency graphs from tasks.md files. * Supports Mermaid, Graphviz DOT, and ASCII tree formats. */ import * as fs from 'fs'; import * as path from 'path'; import { z } from 'zod'; import { parseTasksFile, buildDependencyGraph, detectCircularDependencies, type Task, type DependencyGraph, } from '../speckit/taskParser.js'; // Zod schema for tasks_visualize tool export const TasksVisualizeSchema = 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)'), format: z.enum(['mermaid', 'graphviz', 'ascii']).default('mermaid').describe('Output format'), includeCompleted: z.boolean().default(false).describe('Include completed tasks in visualization'), groupByPhase: z.boolean().default(false).describe('Group tasks by phase using subgraphs'), highlightCriticalPath: z.boolean().default(false).describe('Highlight the critical path'), }); export type TasksVisualizeParams = z.infer<typeof TasksVisualizeSchema>; /** * Generate task dependency visualization */ export async function tasksVisualize(params: TasksVisualizeParams): 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 tasks let tasks = parseTasksFile(tasksPath); // Filter out completed tasks if requested if (!params.includeCompleted) { tasks = tasks.filter(t => t.status !== 'completed'); } // Check for circular dependencies const graph = buildDependencyGraph(tasks); const circularTasks = detectCircularDependencies(graph); if (circularTasks.length > 0) { throw new Error( `Circular dependencies detected in tasks: ${circularTasks.join(', ')}\n` + `Please resolve these before visualizing the dependency graph.` ); } // Generate visualization based on format switch (params.format) { case 'mermaid': return generateMermaidDiagram(tasks, graph, params); case 'graphviz': return generateGraphvizDot(tasks, graph, params); case 'ascii': return generateAsciiTree(tasks, graph, params); default: throw new Error(`Unsupported format: ${params.format}`); } } /** * Generate Mermaid flowchart diagram */ function generateMermaidDiagram( tasks: Task[], graph: DependencyGraph, params: TasksVisualizeParams ): string { let diagram = '```mermaid\nflowchart LR\n'; // Define style classes diagram += ' classDef pending fill:#e0e0e0,stroke:#666,stroke-width:2px\n'; diagram += ' classDef inProgress fill:#fff59d,stroke:#f57f17,stroke-width:3px\n'; diagram += ' classDef completed fill:#c8e6c9,stroke:#388e3c,stroke-width:2px\n\n'; if (params.groupByPhase) { // Group by phase const phases = new Map<string, Task[]>(); for (const task of tasks) { const phase = task.metadata.phase || 'default'; if (!phases.has(phase)) { phases.set(phase, []); } phases.get(phase)!.push(task); } // Generate subgraphs for (const [phase, phaseTasks] of phases.entries()) { diagram += ` subgraph ${phase}\n`; for (const task of phaseTasks) { diagram += generateMermaidNode(task, ' '); } diagram += ` end\n\n`; } } else { // No grouping for (const task of tasks) { diagram += generateMermaidNode(task, ' '); } diagram += '\n'; } // Add edges (dependencies) for (const task of tasks) { const dependencies = graph.edges.get(task.id) || []; for (const depId of dependencies) { if (graph.nodes.has(depId)) { diagram += ` ${depId} --> ${task.id}\n`; } } } diagram += '```'; return diagram; } /** * Generate Mermaid node definition */ function generateMermaidNode(task: Task, indent: string): string { const escapedDesc = task.description.replace(/"/g, '#quot;').replace(/\[/g, '#91;').replace(/\]/g, '#93;'); const label = `${task.id}: ${escapedDesc.substring(0, 40)}${escapedDesc.length > 40 ? '...' : ''}`; let styleClass = ''; switch (task.status) { case 'pending': styleClass = ':::pending'; break; case 'in_progress': styleClass = ':::inProgress'; break; case 'completed': styleClass = ':::completed'; break; } return `${indent}${task.id}["${label}"]${styleClass}\n`; } /** * Generate Graphviz DOT format */ function generateGraphvizDot( tasks: Task[], graph: DependencyGraph, _params: TasksVisualizeParams ): string { let dot = 'digraph TaskDependencies {\n'; dot += ' rankdir=LR;\n'; dot += ' node [shape=box, style=rounded];\n\n'; // Define nodes with colors for (const task of tasks) { let color = '#e0e0e0'; // pending (gray) if (task.status === 'in_progress') { color = '#fff59d'; // yellow } else if (task.status === 'completed') { color = '#c8e6c9'; // green } const escapedDesc = task.description.replace(/"/g, '\\"'); const label = `${task.id}: ${escapedDesc.substring(0, 40)}${escapedDesc.length > 40 ? '...' : ''}`; dot += ` ${task.id} [label="${label}", fillcolor="${color}", style=filled];\n`; } dot += '\n'; // Add edges for (const task of tasks) { const dependencies = graph.edges.get(task.id) || []; for (const depId of dependencies) { if (graph.nodes.has(depId)) { dot += ` ${depId} -> ${task.id};\n`; } } } dot += '}\n'; return dot; } /** * Generate ASCII tree format */ function generateAsciiTree( tasks: Task[], graph: DependencyGraph, _params: TasksVisualizeParams ): string { const visited = new Set<string>(); let output = 'Task Dependency Tree:\n\n'; // Find root tasks (no dependencies) const rootTasks = tasks.filter(task => { const deps = graph.edges.get(task.id) || []; return deps.length === 0; }); function printTask(taskId: string, depth: number, isLast: boolean, prefix: string) { if (visited.has(taskId)) { return; // Already printed } visited.add(taskId); const task = graph.nodes.get(taskId); if (!task) { return; } const connector = isLast ? '└── ' : '├── '; const statusIcon = task.status === 'completed' ? '✓' : task.status === 'in_progress' ? '~' : '○'; const label = `${statusIcon} ${task.id}: ${task.description.substring(0, 50)}${task.description.length > 50 ? '...' : ''}`; output += `${prefix}${connector}${label}\n`; // Find dependents (tasks that depend on this task) const dependents = tasks.filter(t => { const deps = graph.edges.get(t.id) || []; return deps.includes(taskId); }); const newPrefix = prefix + (isLast ? ' ' : '│ '); dependents.forEach((dependent, index) => { const isLastChild = index === dependents.length - 1; printTask(dependent.id, depth + 1, isLastChild, newPrefix); }); } // Print from root tasks rootTasks.forEach((task, index) => { const isLast = index === rootTasks.length - 1; printTask(task.id, 0, isLast, ''); }); // Print any orphaned tasks (tasks with broken dependencies) const orphaned = tasks.filter(t => !visited.has(t.id)); if (orphaned.length > 0) { output += '\nOrphaned tasks (broken dependencies):\n'; orphaned.forEach(task => { const statusIcon = task.status === 'completed' ? '✓' : task.status === 'in_progress' ? '~' : '○'; output += ` ${statusIcon} ${task.id}: ${task.description}\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