import * as path from "path" import * as crypto from "crypto" import * as fs from "fs" import { collectGitContext, type GitContext } from "./git-context" import { buildWorkspaceSnapshot, renderWorkspaceSection, type WorkspaceSnapshot } from "./workspace" import { loadProjectInstructions } from "./llmtune-md" export interface ContextResult { prompt: string cacheKey: string cacheHit: boolean sections: string[] } const CACHE_DIR = () => { const base = process.env.LLMTUNE_CACHE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || "~", ".llmtune", "cache") return base } function getCachePath(cacheKey: string): string { return path.join(CACHE_DIR(), `${cacheKey}.json`) } export function computeCacheKey(workspaceRoot: string, cwd: string): string { const git = collectGitContext(workspaceRoot) const base = `${workspaceRoot}:${cwd}` if (git.available && git.recentCommit) { return crypto.createHash("sha256").update(`${base}:${git.recentCommit}`).digest("hex").slice(0, 16) } return crypto.createHash("sha256").update(base).digest("hex").slice(0, 16) } export function readCache(cacheKey: string): ContextResult | null { try { const cachePath = getCachePath(cacheKey) if (fs.existsSync(cachePath)) { const stat = fs.statSync(cachePath) const ageMs = Date.now() - stat.mtimeMs if (ageMs > 30 * 60 * 1000) return null const data = JSON.parse(fs.readFileSync(cachePath, "utf-8")) return { ...data, cacheHit: true } } } catch {} return null } export function writeCache(cacheKey: string, result: ContextResult): void { try { const cacheDir = CACHE_DIR() if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }) } const cachePath = getCachePath(cacheKey) fs.writeFileSync(cachePath, JSON.stringify({ ...result, cacheHit: false }, null, 2)) } catch {} } export async function buildContextPrompt( workspaceRoot: string, cwd: string, options?: { useCache?: boolean } ): Promise { const cacheKey = computeCacheKey(workspaceRoot, cwd) if (options?.useCache !== false) { const cached = readCache(cacheKey) if (cached) return cached } const sections: string[] = [] // Workspace section (async) const workspace = await buildWorkspaceSnapshot(workspaceRoot, cwd) const workspaceSection = renderWorkspaceSection(workspace) if (workspaceSection) sections.push(workspaceSection) // Git section const git = collectGitContext(workspaceRoot) const gitSection = renderGitSection(git) if (gitSection) sections.push(gitSection) // LLMTUNE.md / CLAUDE.md section const mdFiles = loadProjectInstructions(workspaceRoot, cwd) const mdSection = renderMdSection(mdFiles, workspaceRoot) if (mdSection) sections.push(mdSection) const prompt = sections.join("\n\n") const result: ContextResult = { prompt, cacheKey, cacheHit: false, sections, } writeCache(cacheKey, result) return result } function renderGitSection(git: GitContext): string { if (!git.available) return "" const lines = ["## Git Context"] if (git.branch) lines.push(`- Current branch: ${git.branch}`) if (git.recentCommit) lines.push(`- Latest commit: ${git.recentCommit}`) if (git.status) { lines.push("- Git status snapshot:", "```text", git.status.slice(0, 2000), "```") } return lines.join("\n") } function renderMdSection( files: Array<{ path: string; content: string }>, workspaceRoot: string ): string { if (files.length === 0) return "" const lines = ["## Project Instructions"] for (const f of files) { const relative = path.relative(workspaceRoot, f.path) const label = relative ? `./${relative}` : f.path lines.push(`### ${label}`, "```md", f.content.slice(0, 4000), "```") } return lines.join("\n") }