UNPKG

@ttaqt/novel-workflow-mcp

Version:

MCP server for AI-assisted novel writing workflow with real-time web dashboard

132 lines 4.8 kB
import { readdir, readFile, stat } from 'fs/promises'; import { join } from 'path'; import { PathUtils } from './path-utils.js'; import { parseTaskProgress } from './task-parser.js'; export class SpecParser { projectPath; constructor(projectPath) { this.projectPath = projectPath; } async getAllSpecs() { const specs = []; const specsPath = PathUtils.getSpecPath(this.projectPath, ''); try { const entries = await readdir(specsPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const spec = await this.getSpec(entry.name); if (spec) { specs.push(spec); } } } } catch (error) { // Directory doesn't exist yet return []; } return specs; } async getSpec(name) { const specPath = PathUtils.getSpecPath(this.projectPath, name); try { const stats = await stat(specPath); if (!stats.isDirectory()) { return null; } // Read all phase files (using story document names) // Map old names to new names for novel-workflow const requirements = await this.getPhaseStatus(specPath, 'outline-brief.md'); const design = await this.getPhaseStatus(specPath, 'outline-detailed.md'); const tasks = await this.getPhaseStatus(specPath, 'scenes.md'); // Parse scene progress using unified parser let taskProgress = undefined; if (tasks.exists) { try { const scenesContent = await readFile(join(specPath, 'scenes.md'), 'utf-8'); taskProgress = parseTaskProgress(scenesContent); } catch { // Error reading scenes file } } return { name, createdAt: stats.birthtime.toISOString(), lastModified: stats.mtime.toISOString(), phases: { requirements, design, tasks, implementation: { exists: taskProgress ? taskProgress.completed > 0 : false } }, taskProgress }; } catch (error) { return null; } } async getProjectSteeringStatus() { const steeringPath = PathUtils.getSteeringPath(this.projectPath); try { const stats = await stat(steeringPath); // Check for new novel-workflow steering documents const storyConceptExists = await this.fileExists(join(steeringPath, 'story-concept.md')); const worldBuildingExists = await this.fileExists(join(steeringPath, 'world-building.md')); const characterProfilesExists = await this.fileExists(join(steeringPath, 'character-profiles.md')); // Also check legacy names for backward compatibility const productExists = await this.fileExists(join(steeringPath, 'product.md')); const techExists = await this.fileExists(join(steeringPath, 'tech.md')); const structureExists = await this.fileExists(join(steeringPath, 'structure.md')); return { exists: stats.isDirectory(), documents: { product: storyConceptExists || productExists, tech: worldBuildingExists || techExists, structure: characterProfilesExists || structureExists }, lastModified: stats.mtime.toISOString() }; } catch (error) { return { exists: false, documents: { product: false, tech: false, structure: false } }; } } async getPhaseStatus(basePath, filename) { const filePath = join(basePath, filename); try { const stats = await stat(filePath); const content = await readFile(filePath, 'utf-8'); return { exists: true, lastModified: stats.mtime.toISOString(), content }; } catch (error) { return { exists: false }; } } async fileExists(filePath) { try { await stat(filePath); return true; } catch { return false; } } } //# sourceMappingURL=parser.js.map