// @ts-nocheck — Node built-ins; MCP tsconfig may omit @types/node in some paths. import { existsSync, readdirSync, readFileSync, appendFileSync, writeFileSync } from "node:fs"; import { basename, join } from "node:path"; import { parse as parseYaml } from "yaml"; import type { ConvoyClassification, ConvoyFileStatus } from "@tasks/shared/convoy-lifecycle"; const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/; export type ParsedConvoyFile = { filePath: string; slug: string; classification: ConvoyClassification | null; successMetric: string | null; skipFlags: string[]; fileStatus: ConvoyFileStatus | null; bodyMarkdown: string; modelPolicy: Record | null; }; export type PendingMcpSyncEntry = { ts: string; tool: string; args: Record; error: string; }; function readString(fm: Record, key: string): string | null { const value = fm[key]; return typeof value === "string" && value.trim() ? value.trim() : null; } function readStringArray(fm: Record, key: string): string[] { const value = fm[key]; if (Array.isArray(value)) { return value.filter((item): item is string => typeof item === "string"); } return []; } export function parseConvoyMarkdownFile( filePath: string, raw: string, ): ParsedConvoyFile | null { const fmMatch = raw.match(FRONTMATTER); let frontmatter: Record = {}; let bodyMarkdown = raw; if (fmMatch) { try { const parsed = parseYaml(fmMatch[1]); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { frontmatter = parsed as Record; } } catch { return null; } bodyMarkdown = raw.slice(fmMatch[0].length); } const slug = readString(frontmatter, "name") ?? basename(filePath).replace(/\.md$/i, ""); const classification = readString(frontmatter, "classification"); const fileStatus = readString(frontmatter, "status"); const modelPolicyRaw = frontmatter.model_policy; const modelPolicy = modelPolicyRaw && typeof modelPolicyRaw === "object" && !Array.isArray(modelPolicyRaw) ? (modelPolicyRaw as Record) : null; return { filePath, slug, classification: classification as ConvoyClassification | null, successMetric: readString(frontmatter, "success_metric"), skipFlags: readStringArray(frontmatter, "skip"), fileStatus: fileStatus as ConvoyFileStatus | null, bodyMarkdown, modelPolicy, }; } export function listConvoyMarkdownFiles(repoRoot: string): ParsedConvoyFile[] { const convoysDir = join(repoRoot, ".convoys"); if (!existsSync(convoysDir)) { return []; } const results: ParsedConvoyFile[] = []; for (const entry of readdirSync(convoysDir, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name === "README.md") { continue; } const filePath = join(convoysDir, entry.name); const raw = readFileSync(filePath, "utf8"); const parsed = parseConvoyMarkdownFile(filePath, raw); if (parsed) { results.push(parsed); } } return results; } export function pendingSyncPath(repoRoot: string): string { return join(repoRoot, ".convoys", ".pending-mcp-sync.jsonl"); } export function appendPendingSync( repoRoot: string, entry: Omit & { ts?: string }, ): void { const path = pendingSyncPath(repoRoot); const line = JSON.stringify({ ts: entry.ts ?? new Date().toISOString(), tool: entry.tool, args: entry.args, error: entry.error, }); appendFileSync(path, `${line}\n`, "utf8"); } export function readPendingSync(repoRoot: string): PendingMcpSyncEntry[] { const path = pendingSyncPath(repoRoot); if (!existsSync(path)) { return []; } const raw = readFileSync(path, "utf8"); const entries: PendingMcpSyncEntry[] = []; for (const line of raw.split("\n")) { if (!line.trim()) continue; try { const parsed = JSON.parse(line) as PendingMcpSyncEntry; entries.push(parsed); } catch { // skip corrupt lines } } return entries; } export function clearPendingSync(repoRoot: string): void { const path = pendingSyncPath(repoRoot); if (existsSync(path)) { writeFileSync(path, "", "utf8"); } }