import { execFile, spawn } from "node:child_process"; import { mkdir, mkdtemp, readFile, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, dirname, extname, join } from "node:path"; import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); function isMarkdownPath(path: string): boolean { return [".md", ".markdown", ".txt"].includes(extname(path).toLowerCase()); } function isLatexPath(path: string): boolean { return extname(path).toLowerCase() === ".tex"; } function wrapCodeAsMarkdown(source: string, filePath: string): string { const language = extname(filePath).replace(/^\./, "") || "text"; return `# ${basename(filePath)}\n\n\`\`\`${language}\n${source}\n\`\`\`\n`; } export async function openWithDefaultApp(targetPath: string): Promise { const target = pathToFileURL(targetPath).href; if (process.platform === "darwin") { await execFileAsync("open", [target]); return; } if (process.platform === "win32") { await execFileAsync("cmd", ["/c", "start", "", target]); return; } await execFileAsync("xdg-open", [target]); } async function runCommandWithInput( command: string, args: string[], input: string, ): Promise<{ stdout: string; stderr: string }> { return await new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] }); const stdoutChunks: Buffer[] = []; const stderrChunks: Buffer[] = []; child.stdout.on("data", (chunk: Buffer | string) => { stdoutChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); }); child.stderr.on("data", (chunk: Buffer | string) => { stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); }); child.once("error", reject); child.once("close", (code) => { const stdout = Buffer.concat(stdoutChunks).toString("utf8"); const stderr = Buffer.concat(stderrChunks).toString("utf8"); if (code === 0) { resolve({ stdout, stderr }); return; } reject(new Error(`${command} failed with exit code ${code}${stderr ? `: ${stderr.trim()}` : ""}`)); }); child.stdin.end(input); }); } export async function renderHtmlPreview(filePath: string): Promise { const source = await readFile(filePath, "utf8"); const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc"; const inputFormat = isLatexPath(filePath) ? "latex" : "markdown+lists_without_preceding_blankline+tex_math_dollars+autolink_bare_uris-raw_html"; const markdown = isLatexPath(filePath) || isMarkdownPath(filePath) ? source : wrapCodeAsMarkdown(source, filePath); const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", `--resource-path=${dirname(filePath)}`]; const { stdout } = await runCommandWithInput(pandocCommand, args, markdown); const html = `${basename(filePath)}
${stdout}
`; const tempDir = await mkdtemp(join(tmpdir(), "feynman-preview-")); const htmlPath = join(tempDir, `${basename(filePath)}.html`); await writeFile(htmlPath, html, "utf8"); return htmlPath; } export async function renderPdfPreview(filePath: string): Promise { const source = await readFile(filePath, "utf8"); const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc"; const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex"; const inputFormat = isLatexPath(filePath) ? "latex" : "markdown+lists_without_preceding_blankline+tex_math_dollars+autolink_bare_uris-raw_html"; const markdown = isLatexPath(filePath) || isMarkdownPath(filePath) ? source : wrapCodeAsMarkdown(source, filePath); const tempDir = await mkdtemp(join(tmpdir(), "feynman-preview-")); const pdfPath = join(tempDir, `${basename(filePath)}.pdf`); const args = [ "-f", inputFormat, "-o", pdfPath, `--pdf-engine=${pdfEngine}`, `--resource-path=${dirname(filePath)}`, ]; await runCommandWithInput(pandocCommand, args, markdown); return pdfPath; } export async function pathExists(path: string): Promise { try { await stat(path); return true; } catch { return false; } } export function buildProjectAgentsTemplate(): string { return `# Feynman Project Guide This file is read automatically at startup. It is the durable project memory for Feynman. ## Project Overview - State the research question, target artifact, target venue, and key datasets or benchmarks here. ## AI Research Context - Problem statement: - Core hypothesis: - Closest prior work: - Required baselines: - Required ablations: - Primary metrics: - Datasets / benchmarks: ## Ground Rules - Do not modify raw data in \`Data/Raw/\` or equivalent raw-data folders. - Read first, act second: inspect project structure and existing notes before making changes. - Prefer durable artifacts in \`notes/\`, \`outputs/\`, \`experiments/\`, and \`papers/\`. - Keep strong claims source-grounded. Include direct URLs in final writeups. ## Current Status - Replace this section with the latest project status, known issues, and next steps. ## Session Logging - Use \`/log\` at the end of meaningful sessions to write a durable session note into \`notes/session-logs/\`. ## Review Readiness - Known reviewer concerns: - Missing experiments: - Missing writing or framing work: `; } export function buildSessionLogsReadme(): string { return `# Session Logs Use \`/log\` to write one durable note per meaningful Feynman session. Recommended contents: - what was done - strongest findings - artifacts written - unresolved questions - next steps `; }