// Server-only. Kept out of the package index so browser bundles never load `node:fs`. import { readFile } from "node:fs/promises"; import path from "node:path"; /** Upper bound on a project summary, so it stays a short brief for the project's main agent. */ export const PROJECT_SUMMARY_MAX_LENGTH = 600; const FILE_READ_LIMIT = 64 * 1024; const README_NAMES = ["README.md", "README.markdown", "README.rst", "README.txt", "README"]; export interface ProjectFiles { readme?: string | null; packageJson?: string | null; pyproject?: string | null; cargoToml?: string | null; } function clamp(text: string): string { const flat = text.replace(/\s+/g, " ").trim(); if (flat.length <= PROJECT_SUMMARY_MAX_LENGTH) return flat; return `${flat.slice(0, PROJECT_SUMMARY_MAX_LENGTH - 1).trimEnd()}…`; } /** The first prose paragraph of a README: no headings, badges, images, HTML, or code. */ function readmeParagraph(readme: string): string | null { let inFence = false; const paragraph: string[] = []; for (const raw of readme.split(/\r?\n/)) { const line = raw.trim(); if (line.startsWith("```") || line.startsWith("~~~")) { inFence = !inFence; if (paragraph.length) break; continue; } if (inFence) continue; const prose = line && !/^(#|=+$|-+$|<|!\[|\[!\[|\||>)/.test(line) && // A line of only links is navigation, not a description. line.replace(/\[[^\]]*\]\([^)]*\)/g, "").replace(/[\s|·•-]/g, "") !== ""; if (prose) { paragraph.push(line); } else if (paragraph.length) { break; } } if (!paragraph.length) return null; const text = paragraph .join(" ") .replace(/!\[[^\]]*\]\([^)]*\)/g, "") .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") .replace(/[*_`]/g, ""); return text.trim() || null; } function manifestDescription(files: ProjectFiles): string | null { if (files.packageJson) { try { const parsed = JSON.parse(files.packageJson) as { description?: unknown }; if (typeof parsed.description === "string" && parsed.description.trim()) { return parsed.description.trim(); } } catch { // An unreadable manifest has no description. } } for (const toml of [files.pyproject, files.cargoToml]) { const match = toml?.match(/^\s*description\s*=\s*"([^"\n]+)"/m); if (match?.[1]?.trim()) return match[1].trim(); } return null; } /** * What a project is about, from the files a folder already has: the README's first paragraph, * else a manifest description. Returns null when neither says anything. */ export function summarizeProjectFiles(files: ProjectFiles): string | null { const summary = (files.readme ? readmeParagraph(files.readme) : null) ?? manifestDescription(files); return summary ? clamp(summary) : null; } async function readSmallFile(file: string): Promise { try { const content = await readFile(file, "utf8"); return content.slice(0, FILE_READ_LIMIT); } catch { return null; } } /** Reads a linked project folder's README and manifests; nothing else in it is opened. */ export async function readProjectSummary(directory: string): Promise { let readme: string | null = null; for (const name of README_NAMES) { readme = await readSmallFile(path.join(directory, name)); if (readme) break; } const [packageJson, pyproject, cargoToml] = await Promise.all([ readSmallFile(path.join(directory, "package.json")), readSmallFile(path.join(directory, "pyproject.toml")), readSmallFile(path.join(directory, "Cargo.toml")), ]); return summarizeProjectFiles({ readme, packageJson, pyproject, cargoToml }); }