Skip to main content
Glama
WritersAid.tsโ€ข23.5 kB
/** * WritersAid - Main orchestrator for Writer's Aid MCP * Coordinates all writing-focused components */ import { WritingStorage } from "./storage/WritingStorage.js"; import { SQLiteManager, type SQLiteConfig } from "./storage/SQLiteManager.js"; import { MarkdownParser } from "./parsers/MarkdownParser.js"; import { MarkdownChunker } from "./parsers/MarkdownChunker.js"; import { LinkAnalyzer } from "./parsers/LinkAnalyzer.js"; import { ManuscriptSearch } from "./search/ManuscriptSearch.js"; import { ThemeExtractor } from "./search/ThemeExtractor.js"; import { TerminologyChecker } from "./analysis/TerminologyChecker.js"; import { StructureValidator } from "./analysis/StructureValidator.js"; import { ReadabilityAnalyzer } from "./analysis/ReadabilityAnalyzer.js"; import { DuplicateFinder } from "./analysis/DuplicateFinder.js"; import { GapFinder } from "./analysis/GapFinder.js"; import { TodoExtractor } from "./quality/TodoExtractor.js"; import { LinkHealthChecker } from "./quality/LinkHealthChecker.js"; import { ConsistencyChecker } from "./quality/ConsistencyChecker.js"; import { SessionManager } from "./memory/SessionManager.js"; import { DecisionExtractor } from "./memory/DecisionExtractor.js"; import { SessionIndexer } from "./memory/SessionIndexer.js"; import { MistakeTracker } from "./memory/MistakeTracker.js"; import { RequirementsManager } from "./memory/RequirementsManager.js"; import { GitIntegrator } from "./memory/GitIntegrator.js"; import { ConceptTracker } from "./memory/ConceptTracker.js"; import { HolisticSearcher } from "./memory/HolisticSearcher.js"; import fs from "fs"; import path from "path"; export interface WritersAidConfig { projectPath: string; dbPath?: string; enableCache?: boolean; } export class WritersAid { private storage: WritingStorage; private parser: MarkdownParser; private chunker: MarkdownChunker; private linkAnalyzer: LinkAnalyzer; private search: ManuscriptSearch; private themeExtractor: ThemeExtractor; private terminologyChecker: TerminologyChecker; private structureValidator: StructureValidator; private readabilityAnalyzer: ReadabilityAnalyzer; private duplicateFinder: DuplicateFinder; private gapFinder: GapFinder; private todoExtractor: TodoExtractor; private linkHealthChecker: LinkHealthChecker; private consistencyChecker: ConsistencyChecker; private sessionManager: SessionManager; private decisionExtractor: DecisionExtractor; private sessionIndexer: SessionIndexer; private mistakeTracker: MistakeTracker; private requirementsManager: RequirementsManager; private gitIntegrator: GitIntegrator; private conceptTracker: ConceptTracker; private holisticSearcher: HolisticSearcher; constructor(private config: WritersAidConfig) { // Determine database path const dbPath = config.dbPath || path.join(config.projectPath, ".writers-aid", "manuscript.db"); // Ensure .writers-aid directory exists const writersAidDir = path.dirname(dbPath); if (!fs.existsSync(writersAidDir)) { fs.mkdirSync(writersAidDir, { recursive: true }); } // Initialize storage with SQLiteManager const sqliteConfig: SQLiteConfig = { dbPath }; const sqliteManager = new SQLiteManager(sqliteConfig); this.storage = new WritingStorage(sqliteManager); // Initialize parsers this.parser = new MarkdownParser(); this.chunker = new MarkdownChunker(); this.linkAnalyzer = new LinkAnalyzer(); // Initialize search this.search = new ManuscriptSearch(this.storage); this.themeExtractor = new ThemeExtractor(this.storage); // Initialize analysis tools this.terminologyChecker = new TerminologyChecker(this.storage); this.structureValidator = new StructureValidator(this.storage); this.readabilityAnalyzer = new ReadabilityAnalyzer(this.storage); this.duplicateFinder = new DuplicateFinder(this.storage); this.gapFinder = new GapFinder(this.storage); // Initialize quality tools this.todoExtractor = new TodoExtractor(this.storage); this.linkHealthChecker = new LinkHealthChecker(this.storage); this.consistencyChecker = new ConsistencyChecker(this.storage); // Initialize holistic memory components this.sessionManager = new SessionManager(sqliteManager); this.decisionExtractor = new DecisionExtractor(sqliteManager); this.sessionIndexer = new SessionIndexer(sqliteManager); this.mistakeTracker = new MistakeTracker(sqliteManager); this.requirementsManager = new RequirementsManager(sqliteManager); this.gitIntegrator = new GitIntegrator(sqliteManager, config.projectPath); this.conceptTracker = new ConceptTracker(sqliteManager); // Initialize unified search this.holisticSearcher = new HolisticSearcher( sqliteManager, this.search, this.sessionManager, this.decisionExtractor, this.mistakeTracker, this.conceptTracker, this.gitIntegrator ); } /** * Index all markdown files in the project */ async indexManuscript(options: { scope?: string } = {}): Promise<{ filesIndexed: number; chunksCreated: number; }> { const { scope = "**/*.md" } = options; // Find all markdown files const files = this.findMarkdownFiles(this.config.projectPath, scope); let filesIndexed = 0; let chunksCreated = 0; for (const filePath of files) { const content = fs.readFileSync(filePath, "utf-8"); const relativePath = path.relative(this.config.projectPath, filePath); // Parse markdown const parsed = this.parser.parse(relativePath, content); // Add file to storage await this.storage.addFile({ filePath: relativePath, content, title: parsed.headings[0]?.text || path.basename(filePath, ".md"), }); // Create chunks const chunks = this.chunker.chunk(relativePath, content, parsed.headings); for (const chunk of chunks) { await this.storage.addChunk(relativePath, { heading: chunk.heading || "", content: chunk.content, chunkIndex: chunk.chunk_index, tokenCount: chunk.token_count, }); chunksCreated++; } // Analyze links const links = this.linkAnalyzer.extractLinks(relativePath, relativePath, content); for (const link of links) { await this.storage.addLink({ sourceFile: relativePath, targetFile: link.target_file_path || "", linkText: link.link_text || "", linkType: link.link_type, }); } filesIndexed++; } return { filesIndexed, chunksCreated }; } /** * Search manuscript content */ async searchContent(query: string, options?: { scope?: string; limit?: number }) { return this.search.search(query, options); } /** * Extract main themes from manuscript */ async extractThemes(options?: { scope?: string; numThemes?: number }) { return this.themeExtractor.extractThemes(options); } /** * Check terminology consistency */ async checkTerminology(options?: { scope?: string; autoDetect?: boolean; terms?: string[]; limit?: number; examplesPerVariant?: number; }) { return this.terminologyChecker.checkTerminology(options || {}); } /** * Validate document structure */ async validateStructure(options?: { filePath?: string; checks?: string[] }) { return this.structureValidator.validateStructure(options || {}); } /** * Analyze readability */ async analyzeReadability(filePath?: string) { return this.readabilityAnalyzer.analyzeReadability(filePath); } /** * Find duplicate content */ async findDuplicates(options?: { scope?: string; similarityThreshold?: number; minLength?: number; limit?: number; }) { return this.duplicateFinder.findDuplicates(options || {}); } /** * Find content gaps (undefined terms) */ async findGaps(options?: { scope?: string; limit?: number }) { return this.gapFinder.findGaps(options || {}); } /** * Extract TODOs */ async findTodos(options?: { scope?: string; markers?: string[]; groupBy?: "file" | "priority" | "marker"; limit?: number; }) { return this.todoExtractor.findTodos(options || {}); } /** * Check link health */ async checkLinks(options?: { checkExternal?: boolean; scope?: string; limit?: number }) { return this.linkHealthChecker.checkLinks(options || {}); } /** * Check cross-document consistency */ async checkConsistency(options?: { scope?: string }) { return this.consistencyChecker.checkConsistency(options || {}); } /** * Get writing statistics */ async getStats(_options?: { scope?: string }) { const files = await this.storage.getAllFiles(); const totalWords = files.reduce((sum, f) => sum + (f.word_count || 0), 0); const totalFiles = files.length; return { totalWords, totalFiles, averageWordsPerFile: totalFiles > 0 ? Math.round(totalWords / totalFiles) : 0, files: files.map((f) => ({ path: f.file_path, words: f.word_count || 0, lastModified: f.last_modified, })), }; } /** * Find markdown files matching glob pattern */ private findMarkdownFiles(dir: string, _pattern: string): string[] { const files: string[] = []; const walk = (currentDir: string) => { const entries = fs.readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); // Skip hidden directories and node_modules if (entry.name.startsWith(".") || entry.name === "node_modules") { continue; } if (entry.isDirectory()) { walk(fullPath); } else if (entry.isFile() && entry.name.endsWith(".md")) { files.push(fullPath); } } }; walk(dir); return files; } // ============================================================================ // Holistic Memory Tools // ============================================================================ /** * Recall writing sessions by date range or file */ async recallWritingSessions(options: { startDate?: Date; endDate?: Date; filePath?: string; limit?: number; }) { const query = { projectPath: this.config.projectPath, startDate: options.startDate, endDate: options.endDate, fileInvolved: options.filePath, limit: options.limit || 10, }; const sessions = this.sessionManager.findSessions(query); return { sessions: sessions.map((session) => ({ id: session.id, startedAt: new Date(session.startedAt).toISOString(), endedAt: session.endedAt ? new Date(session.endedAt).toISOString() : null, filesTouched: session.filesTouched || [], summary: session.summary || "Writing session", conversationFile: session.conversationFile, })), total: sessions.length, }; } /** * Get session context for a file */ async getSessionContext(options: { filePath?: string; limit?: number }) { if (!options.filePath) { return { sessions: [], decisions: [], message: "No file path provided", }; } const sessions = this.sessionManager.getSessionsForFile( options.filePath, options.limit || 5 ); const decisions = this.decisionExtractor.getDecisionsByFile( options.filePath, options.limit || 5 ); return { file: options.filePath, sessions: sessions.map((session) => ({ id: session.id, startedAt: new Date(session.startedAt).toISOString(), summary: session.summary || "Writing session", })), decisions: decisions.map((decision) => ({ decisionText: decision.decisionText, rationale: decision.rationale, decisionType: decision.decisionType, timestamp: new Date(decision.timestamp).toISOString(), })), }; } /** * List writing decisions by file or type */ async listWritingDecisions(options: { filePath?: string; decisionType?: "structure" | "content" | "terminology" | "style"; limit?: number; }) { let decisions; if (options.filePath) { decisions = this.decisionExtractor.getDecisionsByFile( options.filePath, options.limit || 20 ); } else if (options.decisionType) { decisions = this.decisionExtractor.getDecisionsByType( options.decisionType, options.limit || 20 ); } else { // Get all recent decisions decisions = this.decisionExtractor.getDecisionsByType( "structure", options.limit || 20 ); } return { decisions: decisions.map((decision) => ({ id: decision.id, decisionText: decision.decisionText, rationale: decision.rationale, decisionType: decision.decisionType, filePath: decision.filePath, section: decision.section, timestamp: new Date(decision.timestamp).toISOString(), alternativesConsidered: decision.alternativesConsidered, })), total: decisions.length, }; } /** * Index writing sessions from conversation history */ async indexWritingSessions(options?: { conversationsDir?: string; }): Promise<{ sessionsIndexed: number; decisionsExtracted: number; filesProcessed: number; }> { const result = await this.sessionIndexer.indexProject({ projectPath: this.config.projectPath, conversationsDir: options?.conversationsDir, }); return { sessionsIndexed: result.sessionsIndexed, decisionsExtracted: result.decisionsExtracted, filesProcessed: result.filesProcessed, }; } // ============================================================================ // Mistake Tracking (Phase 2) // ============================================================================ /** * Mark a writing mistake */ markMistake(options: { filePath: string; lineRange?: string; mistakeType: | "logical_fallacy" | "factual_error" | "poor_structure" | "inconsistency" | "unclear_writing" | "citation_error" | "redundancy" | "other"; description: string; correction?: string; }) { const mistake = this.mistakeTracker.markMistake({ filePath: options.filePath, lineRange: options.lineRange, mistakeType: options.mistakeType, description: options.description, correction: options.correction, timestamp: Date.now(), }); return { id: mistake.id, filePath: mistake.filePath, mistakeType: mistake.mistakeType, description: mistake.description, timestamp: new Date(mistake.timestamp).toISOString(), }; } /** * Search for similar mistakes */ searchSimilarMistakes(options: { description: string; limit?: number }) { const mistakes = this.mistakeTracker.searchSimilarMistakes( options.description, options.limit || 5 ); return { matches: mistakes.map((m) => ({ id: m.id, filePath: m.filePath, mistakeType: m.mistakeType, description: m.description, correction: m.correction, howFixed: m.howFixed, timestamp: new Date(m.timestamp).toISOString(), })), total: mistakes.length, }; } // ============================================================================ // Requirements Management (Phase 2) // ============================================================================ /** * Set a publisher or style requirement */ setRequirement(options: { requirementType: | "word_count" | "citation_style" | "formatting" | "deadline" | "target_audience" | "tone" | "reading_level" | "chapter_count" | "other"; description: string; value?: string; enforced?: boolean; }) { const requirement = this.requirementsManager.addRequirement({ requirementType: options.requirementType, description: options.description, value: options.value, enforced: options.enforced || false, }); return { id: requirement.id, requirementType: requirement.requirementType, description: requirement.description, value: requirement.value, enforced: requirement.enforced, }; } /** * Get requirements */ getRequirements(options?: { requirementType?: | "word_count" | "citation_style" | "formatting" | "deadline" | "target_audience" | "tone" | "reading_level" | "chapter_count" | "other"; enforcedOnly?: boolean; }) { let requirements; if (options?.requirementType) { requirements = this.requirementsManager.getRequirementsByType( options.requirementType ); } else if (options?.enforcedOnly) { requirements = this.requirementsManager.getAllRequirements(true); } else { requirements = this.requirementsManager.getAllRequirements(); } return { requirements: requirements.map((r) => ({ id: r.id, requirementType: r.requirementType, description: r.description, value: r.value, enforced: r.enforced, })), total: requirements.length, }; } /** * Add a style decision */ addStyleDecision(options: { category: | "terminology" | "formatting" | "citations" | "tone" | "headings" | "lists" | "code_blocks" | "quotes" | "other"; canonicalChoice: string; rationale?: string; examples?: string[]; }) { const decision = this.requirementsManager.addStyleDecision({ category: options.category, canonicalChoice: options.canonicalChoice, rationale: options.rationale, examples: options.examples, }); return { id: decision.id, category: decision.category, canonicalChoice: decision.canonicalChoice, rationale: decision.rationale, examples: decision.examples, }; } // ============================================================================ // Git Integration & Evolution (Phase 3) // ============================================================================ /** * Track file evolution through git commits */ async trackFileEvolution(options: { filePath: string; limit?: number }) { const evolution = await this.gitIntegrator.getFileEvolution(options.filePath); const limited = evolution.slice(0, options.limit || 10); return { filePath: options.filePath, revisions: limited.map(({ revision, commit }) => ({ commitHash: commit.commitHash, timestamp: new Date(commit.timestamp * 1000).toISOString(), author: commit.author, message: commit.message, linesAdded: revision.linesAdded, linesRemoved: revision.linesRemoved, rationale: revision.rationale, sessionLinked: !!commit.sessionId, })), total: evolution.length, }; } /** * Track concept evolution over time */ trackConceptEvolution(options: { conceptName: string; limit?: number }) { const evolution = this.conceptTracker.getConceptEvolution(options.conceptName); if (!evolution) { return { conceptName: options.conceptName, found: false, message: "No versions found for this concept", }; } // Trim versions to limit (default handled by pagination resolver in handler) const versionsToReturn = options.limit ? evolution.versions.slice(0, options.limit) : evolution.versions; return { conceptName: evolution.conceptName, found: true, versions: versionsToReturn.map((v) => ({ versionNumber: v.versionNumber, definition: v.definition, filePath: v.filePath, timestamp: new Date(v.timestamp).toISOString(), changeRationale: v.changeRationale, commitHash: v.commitHash, })), totalVersions: evolution.totalVersions, changeCount: evolution.changeCount, firstDefinition: evolution.firstDefinition.definition, latestDefinition: evolution.latestDefinition.definition, }; } /** * Find concept contradictions */ findConceptContradictions(options: { conceptName: string }) { const contradictions = this.conceptTracker.findContradictions(options.conceptName); return { conceptName: options.conceptName, contradictions: contradictions.map((c) => ({ version1: { versionNumber: c.version1.versionNumber, definition: c.version1.definition, timestamp: new Date(c.version1.timestamp).toISOString(), }, version2: { versionNumber: c.version2.versionNumber, definition: c.version2.definition, timestamp: new Date(c.version2.timestamp).toISOString(), }, contradiction: c.contradiction, })), total: contradictions.length, }; } /** * Link commits to sessions */ async linkCommitsToSessions(options?: { since?: string; limit?: number }) { const since = options?.since ? new Date(options.since) : undefined; const result = await this.gitIntegrator.indexCommits({ since, filePattern: "**/*.md", }); // Auto-link commits to sessions based on timestamp proximity // (Future enhancement: use conversation files for precise linking) return { commitsIndexed: result.commitsIndexed, revisionsCreated: result.revisionsCreated, message: "Git commits indexed successfully", }; } // ============================================================================ // Holistic Memory - Phase 4: Unified Search // ============================================================================ /** * Unified search across all memory layers */ async holisticSearch(options: { query: string; layers?: Array<"content" | "decisions" | "mistakes" | "concepts" | "sessions" | "commits">; startDate?: string; endDate?: string; limit?: number; minRelevance?: number; }) { const dateRange = { start: options.startDate ? new Date(options.startDate) : undefined, end: options.endDate ? new Date(options.endDate) : undefined, }; return this.holisticSearcher.search({ query: options.query, layers: options.layers, dateRange, limit: options.limit, minRelevance: options.minRelevance, }); } /** * Get all context for a file (sessions, decisions, mistakes, commits) */ async getFileContext(options: { filePath: string }) { return this.holisticSearcher.getFileContext(options.filePath); } // ============================================================================ // Holistic Memory - Phase 5: Before-Edit Integration // ============================================================================ /** * Check context before editing a file */ async checkBeforeEdit(options: { filePath: string }) { return this.holisticSearcher.checkBeforeEdit(options.filePath); } /** * Close storage connection */ close() { this.storage.close(); } }

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/xiaolai/claude-writers-aid-mcp'

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