import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import { dirname, isAbsolute, resolve } from "node:path"; import { withFileMutationQueue } from "@earendil-works/pi-coding-agent"; import type { QuietToolsConfig } from "./config"; export interface ArtifactRecord { timestamp: string; toolName: string; toolCallId: string; artifactPath: string; chars: number; lines: number; compacted: boolean; } function safePart(value: string): string { return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "tool"; } function resolveUnderCwd(cwd: string, path: string): string { return isAbsolute(path) ? path : resolve(cwd, path); } export function artifactRoot(cwd: string, config: QuietToolsConfig): string { return resolveUnderCwd(cwd, config.artifactDir); } export function indexPath(cwd: string, config: QuietToolsConfig): string { return resolveUnderCwd(cwd, config.indexFile); } export async function writeArtifact(args: { cwd: string; config: QuietToolsConfig; toolName: string; toolCallId: string; text: string; lines: number; }): Promise { const root = artifactRoot(args.cwd, args.config); await mkdir(root, { recursive: true }); const timestamp = new Date().toISOString(); const stamp = timestamp.replace(/[:.]/g, "-"); const hash = createHash("sha256").update(`${args.toolName}\n${args.toolCallId}\n${timestamp}`).digest("hex").slice(0, 10); const file = resolve(root, `${stamp}-${safePart(args.toolName)}-${safePart(args.toolCallId || hash)}-${hash}.txt`); await withFileMutationQueue(file, async () => { await writeFile(file, args.text, "utf8"); }); const record: ArtifactRecord = { timestamp, toolName: args.toolName, toolCallId: args.toolCallId, artifactPath: file, chars: args.text.length, lines: args.lines, compacted: true, }; const idx = indexPath(args.cwd, args.config); await mkdir(dirname(idx), { recursive: true }); await withFileMutationQueue(idx, async () => { await appendFile(idx, `${JSON.stringify(record)}\n`, "utf8"); }); return record; } export async function readLastArtifacts(cwd: string, config: QuietToolsConfig, count: number): Promise { const idx = indexPath(cwd, config); try { const text = await readFile(idx, "utf8"); return text .trim() .split("\n") .filter(Boolean) .slice(-count) .map((line) => JSON.parse(line) as ArtifactRecord); } catch { return []; } }