import { readFileSync, readdirSync, writeFileSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const repoRoot = resolve(__dirname, ".."); const featuresPath = join(repoRoot, "FEATURES.md"); const workflowToolsPath = join(repoRoot, "packages", "mcp-server", "src", "workflow-tools.ts"); const providersPath = join(repoRoot, "packages", "pi-ai", "src", "types.ts"); const extensionsRoot = join(repoRoot, "src", "resources", "extensions"); const searchToolPath = join(repoRoot, "src", "resources", "extensions", "search-the-web", "tool-search.ts"); export const START = ""; export const END = ""; function uniqueSorted(values) { return [...new Set(values)].sort((a, b) => a.localeCompare(b)); } export function parseWorkflowToolNames() { const src = readFileSync(workflowToolsPath, "utf8"); const matches = [...src.matchAll(/server\.tool\(\s*"([^"]+)"/g)].map((m) => m[1]); return uniqueSorted(matches); } export function parseKnownProviders() { const src = readFileSync(providersPath, "utf8"); const match = src.match(/export type KnownProvider =([\s\S]*?);/); if (!match) throw new Error("Could not find KnownProvider in packages/pi-ai/src/types.ts"); const providers = [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]); return uniqueSorted(providers); } export function parseBundledExtensions() { const entries = readdirSync(extensionsRoot, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) .filter((name) => { try { const manifestPath = join(extensionsRoot, name, "extension-manifest.json"); readFileSync(manifestPath, "utf8"); return true; } catch { return false; } }); return uniqueSorted(entries); } export function parseSearchProviders() { const src = readFileSync(searchToolPath, "utf8"); const providers = [ ...src.matchAll(/providers\.push\('([^']+)'\)/g), ...src.matchAll(/provider\?: '([^']+)'/g), ] .map((m) => m[1]) .filter((p) => p !== "combosearch"); return uniqueSorted(providers); } function formatBullets(values, formatter = (value) => `- \`${value}\``) { return values.map((value) => formatter(value)).join("\n"); } export function buildSection() { const workflowTools = parseWorkflowToolNames(); const extensions = parseBundledExtensions(); const searchProviders = parseSearchProviders(); const knownProviders = parseKnownProviders(); return [ "### Workflow Tools", "", "Generated from `packages/mcp-server/src/workflow-tools.ts`.", "", formatBullets(workflowTools), "", "### Bundled Extensions", "", "Generated from `src/resources/extensions/*/extension-manifest.json`.", "", formatBullets( extensions, (value) => `- \`${value}\` — [extension-manifest.json](${relative(repoRoot, join(extensionsRoot, value, "extension-manifest.json"))})`, ), "", "### Search Providers", "", "Generated from the `search-the-web` extension provider declarations.", "", formatBullets(searchProviders), "", "### Known Model Providers", "", "Generated from `packages/pi-ai/src/types.ts` (`KnownProvider`).", "", formatBullets(knownProviders), "", ].join("\n"); } export function updateFeaturesContent(features) { const startIndex = features.indexOf(START); const endIndex = features.indexOf(END); if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) { throw new Error("FEATURES.md is missing generated inventory markers"); } const before = features.slice(0, startIndex + START.length); const after = features.slice(endIndex); const section = `\n\n${buildSection()}`; return `${before}${section}\n${after}`; } export function main() { const features = readFileSync(featuresPath, "utf8"); const updated = updateFeaturesContent(features); writeFileSync(featuresPath, updated); process.stdout.write(`Updated ${relative(repoRoot, featuresPath)}\n`); } if (process.argv[1] && resolve(process.argv[1]) === __filename) { main(); }