Skip to main content
Glama

Spec Workflow MCP

watcher.ts4.18 kB
import { EventEmitter } from 'events'; import chokidar from 'chokidar'; import { PathUtils } from '../core/path-utils.js'; import { SpecParser, ParsedSpec } from './parser.js'; export interface SpecChangeEvent { type: 'spec' | 'steering'; action: 'created' | 'updated' | 'deleted'; name: string; data?: ParsedSpec | any; } export class SpecWatcher extends EventEmitter { private projectPath: string; private parser: SpecParser; private watcher?: chokidar.FSWatcher; constructor(projectPath: string, parser: SpecParser) { super(); this.projectPath = projectPath; this.parser = parser; } async start(): Promise<void> { const workflowRoot = PathUtils.getWorkflowRoot(this.projectPath); const specsPath = PathUtils.getSpecPath(this.projectPath, ''); const steeringPath = PathUtils.getSteeringPath(this.projectPath); // Watch for changes in specs and steering directories this.watcher = chokidar.watch([ `${specsPath}/**/*.md`, `${steeringPath}/*.md` ], { ignoreInitial: true, persistent: true, ignorePermissionErrors: true }); this.watcher.on('add', (filePath) => this.handleFileChange('created', filePath)); this.watcher.on('change', (filePath) => this.handleFileChange('updated', filePath)); this.watcher.on('unlink', (filePath) => this.handleFileChange('deleted', filePath)); // File watcher started for workflow directories } async stop(): Promise<void> { if (this.watcher) { // Remove all listeners before closing to prevent memory leaks this.watcher.removeAllListeners(); await this.watcher.close(); this.watcher = undefined; // File watcher stopped } // Clean up EventEmitter listeners this.removeAllListeners(); } private async handleFileChange(action: 'created' | 'updated' | 'deleted', filePath: string): Promise<void> { try { const normalizedPath = filePath.replace(/\\/g, '/'); // Add small delay for file creation/updates to ensure file is fully written if (action === 'created' || action === 'updated') { await new Promise(resolve => setTimeout(resolve, 100)); } // Determine if this is a spec or steering change if (normalizedPath.includes('/specs/')) { await this.handleSpecChange(action, normalizedPath); } else if (normalizedPath.includes('/steering/')) { await this.handleSteeringChange(action, normalizedPath); } } catch (error) { // Error handling file change } } private async handleSpecChange(action: 'created' | 'updated' | 'deleted', filePath: string): Promise<void> { // Extract spec name from path like: /path/to/.spec-workflow/specs/user-auth/requirements.md const pathParts = filePath.split('/'); const specsIndex = pathParts.findIndex(part => part === 'specs'); if (specsIndex === -1 || specsIndex + 1 >= pathParts.length) return; const specName = pathParts[specsIndex + 1]; const document = pathParts[specsIndex + 2]?.replace('.md', ''); let specData: ParsedSpec | null = null; if (action !== 'deleted') { specData = await this.parser.getSpec(specName); } const event: SpecChangeEvent = { type: 'spec', action, name: specName, data: specData }; // Spec change detected this.emit('change', event); // Emit specific task update event if this was a tasks.md file if (document === 'tasks') { this.emit('task-update', { specName, action }); } } private async handleSteeringChange(action: 'created' | 'updated' | 'deleted', filePath: string): Promise<void> { // Extract document name from path like: /path/to/.spec-workflow/steering/tech.md const pathParts = filePath.split('/'); const document = pathParts[pathParts.length - 1]?.replace('.md', ''); const steeringStatus = await this.parser.getProjectSteeringStatus(); const event = { type: 'steering' as const, action, name: document, steeringStatus }; // Steering change detected this.emit('steering-change', event); } }

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/Pimzino/spec-workflow-mcp'

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