@ttaqt/novel-workflow-mcp
Version:
MCP server for AI-assisted novel writing workflow with real-time web dashboard
132 lines • 4.8 kB
JavaScript
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