UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

441 lines (397 loc) 12.1 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import * as fs from "fs"; import * as path from "path"; import { logger } from "../core/monitoring/logger.js"; const SPEC_DIR = "docs/specs"; const SPEC_CONFIGS = { "one-pager": { filename: "ONE_PAGER.md", title: "One-Pager", sections: [ "Problem", "Audience", "Platform", "Core Flow", "MVP Features", "Non-Goals", "Metrics" ], inputs: [] }, "dev-spec": { filename: "DEV_SPEC.md", title: "Development Specification", sections: [ "Architecture", "Tech Stack", "API Contracts", "Data Models", "Auth", "Error Handling", "Deployment" ], inputs: ["one-pager"] }, "prompt-plan": { filename: "PROMPT_PLAN.md", title: "Prompt Plan", sections: [ "Stage A: Project Setup", "Stage B: Core Data Models", "Stage C: API Layer", "Stage D: Business Logic", "Stage E: Frontend / UI", "Stage F: Integration & Testing", "Stage G: Deploy & Polish" ], inputs: ["one-pager", "dev-spec"] }, agents: { filename: "AGENTS.md", title: "AGENTS.md", sections: [ "Repo Files", "Responsibilities", "Guardrails", "Testing", "When to Ask" ], inputs: ["one-pager", "dev-spec", "prompt-plan"] } }; function onePagerTemplate(title) { return `# ${title} \u2014 One-Pager ## Problem <!-- What problem does this solve? Who has this problem? --> ## Audience <!-- Primary users and their context --> ## Platform <!-- Web / Mobile / CLI / API \u2014 and why --> ## Core Flow <!-- Happy-path user journey in 3-5 steps --> 1. 2. 3. ## MVP Features <!-- Minimum set of features for first release --> - [ ] - [ ] - [ ] ## Non-Goals <!-- Explicitly out of scope for MVP --> - ## Metrics <!-- How will you measure success? --> - `; } function devSpecTemplate(title, onePagerContent) { return `# ${title} \u2014 Development Specification > Generated from ONE_PAGER.md <details><summary>Source: ONE_PAGER.md</summary> ${onePagerContent} </details> ## Architecture <!-- High-level system diagram / component breakdown --> ## Tech Stack <!-- Languages, frameworks, databases, infra --> | Layer | Choice | Rationale | |-------|--------|-----------| | Frontend | | | | Backend | | | | Database | | | | Hosting | | | ## API Contracts <!-- Key endpoints with request/response shapes --> ## Data Models <!-- Core entities and relationships --> ## Auth <!-- Authentication and authorization strategy --> ## Error Handling <!-- Error codes, retry strategies, user-facing messages --> ## Deployment <!-- CI/CD, environments, rollback strategy --> `; } function promptPlanTemplate(title, onePagerContent, devSpecContent) { return `# ${title} \u2014 Prompt Plan > Generated from ONE_PAGER.md and DEV_SPEC.md > Each stage has TDD checkboxes \u2014 check off as tasks complete. <details><summary>Source: ONE_PAGER.md</summary> ${onePagerContent} </details> <details><summary>Source: DEV_SPEC.md</summary> ${devSpecContent} </details> ## Stage A: Project Setup - [ ] Initialize repository and tooling - [ ] Configure CI/CD pipeline - [ ] Set up development environment ## Stage B: Core Data Models - [ ] Define database schema - [ ] Create model layer - [ ] Write model tests ## Stage C: API Layer - [ ] Implement API endpoints - [ ] Add input validation - [ ] Write API tests ## Stage D: Business Logic - [ ] Implement core business rules - [ ] Add edge case handling - [ ] Write integration tests ## Stage E: Frontend / UI - [ ] Build core UI components - [ ] Implement user flows - [ ] Write UI tests ## Stage F: Integration & Testing - [ ] End-to-end test suite - [ ] Performance testing - [ ] Security audit ## Stage G: Deploy & Polish - [ ] Production deployment - [ ] Monitoring and alerting - [ ] Documentation `; } function agentsTemplate(title, inputs) { const sourceBlocks = Object.entries(inputs).map( ([name, content]) => `<details><summary>Source: ${name}</summary> ${content} </details>` ).join("\n\n"); return `# ${title} \u2014 AGENTS.md > Auto-generated agent configuration for Claude Code / Cursor / Windsurf. ${sourceBlocks} ## Repo Files <!-- Key files and their purpose --> | File | Purpose | |------|---------| | | | ## Responsibilities <!-- What this agent should and shouldn't do --> ### DO - ### DON'T - ## Guardrails <!-- Safety constraints and limits --> - Never commit secrets or credentials - Always run tests before committing - Keep changes focused and atomic ## Testing <!-- How to validate changes --> \`\`\`bash npm test npm run lint npm run build \`\`\` ## When to Ask <!-- Situations where the agent should ask for human input --> - Architectural changes affecting multiple systems - Security-sensitive modifications - Breaking API changes - Ambiguous requirements `; } class SpecGeneratorSkill { constructor(context) { this.context = context; this.baseDir = process.cwd(); } baseDir; /** Generate a spec document by type */ async generate(type, title, opts) { const config = SPEC_CONFIGS[type]; if (!config) { return { success: false, message: `Unknown spec type: ${type}` }; } const specDir = path.join(this.baseDir, SPEC_DIR); const outputPath = path.join(specDir, config.filename); if (fs.existsSync(outputPath) && !opts?.force) { return { success: false, message: `${config.filename} already exists. Use --force to overwrite.`, data: { path: outputPath } }; } const inputContents = {}; for (const inputType of config.inputs) { const inputConfig = SPEC_CONFIGS[inputType]; const inputPath = path.join(specDir, inputConfig.filename); if (fs.existsSync(inputPath)) { inputContents[inputConfig.filename] = fs.readFileSync( inputPath, "utf-8" ); } } const content = this.renderTemplate(type, title, inputContents); fs.mkdirSync(specDir, { recursive: true }); fs.writeFileSync(outputPath, content, "utf-8"); logger.info(`Generated spec: ${config.filename}`, { type, title }); return { success: true, message: `Created ${config.filename}`, data: { path: outputPath, type, sections: config.sections, inputsUsed: Object.keys(inputContents) }, action: `Generated ${SPEC_DIR}/${config.filename}` }; } /** List existing spec documents */ async list() { const specDir = path.join(this.baseDir, SPEC_DIR); const specs = []; for (const [type, config] of Object.entries(SPEC_CONFIGS)) { const filePath = path.join(specDir, config.filename); specs.push({ type, filename: config.filename, exists: fs.existsSync(filePath), path: filePath }); } const existing = specs.filter((s) => s.exists); const missing = specs.filter((s) => !s.exists); return { success: true, message: `${existing.length}/${specs.length} specs exist`, data: { specs, existing, missing } }; } /** Update a spec — primarily for checking off PROMPT_PLAN items */ async update(filePath, changes) { const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(this.baseDir, filePath); if (!fs.existsSync(resolvedPath)) { return { success: false, message: `File not found: ${resolvedPath}` }; } let content = fs.readFileSync(resolvedPath, "utf-8"); const checkboxPattern = /^(Stage [A-G])(?::(\d+))?$/; const match = changes.match(checkboxPattern); if (match) { const [, stageName, itemNum] = match; content = this.checkItem( content, stageName, itemNum ? parseInt(itemNum) : void 0 ); } else { const escaped = changes.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const re = new RegExp(`- \\[ \\] ${escaped}`, "i"); if (re.test(content)) { content = content.replace(re, `- [x] ${changes}`); } else { return { success: false, message: `No unchecked item matching "${changes}" found` }; } } const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " "); const changelog = ` <!-- Updated: ${timestamp} | ${changes} --> `; content += changelog; fs.writeFileSync(resolvedPath, content, "utf-8"); logger.info("Updated spec", { path: resolvedPath, changes }); return { success: true, message: `Updated: ${changes}`, data: { path: resolvedPath, changes }, action: `Checked off item in ${path.basename(resolvedPath)}` }; } /** Validate completeness of a spec */ async validate(filePath) { const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(this.baseDir, filePath); if (!fs.existsSync(resolvedPath)) { return { success: false, message: `File not found: ${resolvedPath}` }; } const content = fs.readFileSync(resolvedPath, "utf-8"); const unchecked = (content.match(/- \[ \]/g) || []).length; const checked = (content.match(/- \[x\]/gi) || []).length; const total = checked + unchecked; const emptySections = []; const sectionRegex = /^## (.+)$/gm; let sectionMatch; while ((sectionMatch = sectionRegex.exec(content)) !== null) { const sectionName = sectionMatch[1]; const sectionStart = content.indexOf(sectionMatch[0]); const nextSection = content.indexOf("\n## ", sectionStart + 1); const sectionContent = nextSection === -1 ? content.slice(sectionStart + sectionMatch[0].length) : content.slice(sectionStart + sectionMatch[0].length, nextSection); const stripped = sectionContent.replace(/<!--.*?-->/gs, "").replace(/\s+/g, "").trim(); if (stripped.length === 0 || stripped === "||") { emptySections.push(sectionName); } } const isComplete = unchecked === 0 && emptySections.length === 0; return { success: true, message: isComplete ? "Spec is complete" : `Spec incomplete: ${unchecked} unchecked items, ${emptySections.length} empty sections`, data: { path: resolvedPath, checkboxes: { checked, unchecked, total }, emptySections, isComplete, completionPercent: total > 0 ? Math.round(checked / total * 100) : 100 } }; } // --- Private helpers --- renderTemplate(type, title, inputs) { switch (type) { case "one-pager": return onePagerTemplate(title); case "dev-spec": return devSpecTemplate( title, inputs["ONE_PAGER.md"] || "*ONE_PAGER.md not found \u2014 generate it first.*" ); case "prompt-plan": return promptPlanTemplate( title, inputs["ONE_PAGER.md"] || "*ONE_PAGER.md not found*", inputs["DEV_SPEC.md"] || "*DEV_SPEC.md not found \u2014 generate it first.*" ); case "agents": return agentsTemplate(title, inputs); default: return `# ${title} Unknown spec type: ${type} `; } } /** Check off a specific checkbox item in a stage */ checkItem(content, stageName, itemIndex) { const lines = content.split("\n"); let inStage = false; let itemCount = 0; for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith("## ") && lines[i].includes(stageName)) { inStage = true; itemCount = 0; continue; } if (inStage && lines[i].startsWith("## ")) { break; } if (inStage && lines[i].match(/^- \[ \]/)) { itemCount++; if (itemIndex === void 0 || itemCount === itemIndex) { lines[i] = lines[i].replace("- [ ]", "- [x]"); if (itemIndex !== void 0) break; } } } return lines.join("\n"); } } export { SpecGeneratorSkill };