/** * Repo scan for `.md` files the cockpit should know about (DRAFT-001 v0.3.0). * * Conservative scope: repo-root well-known files + repo-root `docs/`, * plus each `packages/*` package's well-known files + its `docs/`. * No fancy globbing, no recursion past `docs/`. If any directory is * absent we skip silently — this extension is opt-in, never noisy. */ import * as fs from "node:fs"; import * as path from "node:path"; const ROOT_FILES = ["SPEC.md", "CONSTITUTION.md", "AGENTS.md", "CHANGELOG.md", "README.md"]; const PACKAGE_FILES = ["SPEC.md", "README.md", "CHANGELOG.md"]; /** Returns absolute paths to every `.md` file the cockpit should track. */ export function discoverSpecFiles(repoRoot: string): string[] { const found = new Set(); // Repo root well-known files. for (const name of ROOT_FILES) { const p = path.join(repoRoot, name); if (existsAsFile(p)) found.add(p); } // Repo root docs/ (recursive). collectMdRecursive(path.join(repoRoot, "docs"), found); // Per-package files + docs/. const packagesDir = path.join(repoRoot, "packages"); if (existsAsDir(packagesDir)) { for (const pkgName of safeReaddir(packagesDir)) { const pkgDir = path.join(packagesDir, pkgName); if (!existsAsDir(pkgDir)) continue; for (const name of PACKAGE_FILES) { const p = path.join(pkgDir, name); if (existsAsFile(p)) found.add(p); } collectMdRecursive(path.join(pkgDir, "docs"), found); } } return Array.from(found).sort(); } function collectMdRecursive(dir: string, sink: Set) { if (!existsAsDir(dir)) return; for (const entry of safeReaddirWithTypes(dir)) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { collectMdRecursive(full, sink); } else if (entry.isFile() && entry.name.endsWith(".md")) { sink.add(full); } } } function existsAsFile(p: string): boolean { try { return fs.statSync(p).isFile(); } catch { return false; } } function existsAsDir(p: string): boolean { try { return fs.statSync(p).isDirectory(); } catch { return false; } } function safeReaddir(dir: string): string[] { try { return fs.readdirSync(dir); } catch { return []; } } function safeReaddirWithTypes(dir: string): fs.Dirent[] { try { return fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; } } /** * Extract a `.md` path from a tool_call/tool_result event input. Returns * the absolute path if the tool is one we recognise as a file mutation * targeting a markdown file; otherwise undefined. */ export function extractMdPathFromTool( toolName: string | undefined, input: unknown, cwd: string, ): string | undefined { if (!toolName) return undefined; if (!isFileWriteTool(toolName)) return undefined; if (!input || typeof input !== "object") return undefined; const obj = input as Record; const raw = obj.path ?? obj.file_path ?? obj.filePath; if (typeof raw !== "string" || raw.length === 0) return undefined; if (!raw.endsWith(".md")) return undefined; return path.isAbsolute(raw) ? raw : path.resolve(cwd, raw); } function isFileWriteTool(name: string): boolean { // Cover pi built-ins and common Anthropic-style tool names. return name === "write" || name === "edit" || name === "str_replace" || name === "str_replace_editor" || name === "create"; }