import { readFileSync } from "node:fs"; import { join } from "node:path"; import { CONFIG_DIR_NAME, getAgentDir, } from "@earendil-works/pi-coding-agent"; import { DEFAULT_CONFIG, EXTENSION_ID, type LoadedConfig, type MidrunCompactConfig, } from "./types.ts"; const CONFIG_KEYS = new Set([ "enabled", "thresholdPercent", "autoResume", "notify", "customCompactionInstructions", ]); export interface ConfigPaths { globalPath: string; projectPath: string; } export interface LoadConfigOptions { cwd: string; projectTrusted: boolean; globalPath?: string; projectPath?: string; } interface ParsedConfigFile { exists: boolean; patch: Partial; errors: string[]; warnings: string[]; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function describeReadError(error: unknown): string { return error instanceof Error ? error.message : String(error); } function isMissingFileError(error: unknown): boolean { return isRecord(error) && error.code === "ENOENT"; } function parseBoolean( value: unknown, key: "enabled" | "autoResume" | "notify", filePath: string, errors: string[], ): boolean | undefined { if (typeof value === "boolean") return value; errors.push(`${filePath}: ${key} must be a boolean.`); return undefined; } function parseThreshold(value: unknown, filePath: string, errors: string[]): number | undefined { if (typeof value === "number" && Number.isFinite(value) && value > 0 && value < 100) { return value; } errors.push(`${filePath}: thresholdPercent must be a finite number greater than 0 and less than 100.`); return undefined; } function parseInstructions(value: unknown, filePath: string, errors: string[]): string | undefined { if (typeof value === "string") return value; errors.push(`${filePath}: customCompactionInstructions must be a string.`); return undefined; } function parseConfigFile(filePath: string): ParsedConfigFile { let text: string; try { text = readFileSync(filePath, "utf8"); } catch (error) { if (isMissingFileError(error)) { return { exists: false, patch: {}, errors: [], warnings: [] }; } return { exists: true, patch: {}, errors: [`Unable to read ${filePath}: ${describeReadError(error)}`], warnings: [], }; } let parsed: unknown; try { parsed = JSON.parse(text); } catch (error) { return { exists: true, patch: {}, errors: [`Unable to parse ${filePath}: ${describeReadError(error)}`], warnings: [], }; } if (!isRecord(parsed)) { return { exists: true, patch: {}, errors: [`${filePath}: expected a JSON object at the top level.`], warnings: [], }; } const patch: Partial = {}; const errors: string[] = []; const warnings: string[] = []; for (const key of Object.keys(parsed)) { if (!CONFIG_KEYS.has(key as keyof MidrunCompactConfig)) { warnings.push(`${filePath}: unknown configuration key "${key}" was ignored.`); } } if ("enabled" in parsed) { const enabled = parseBoolean(parsed.enabled, "enabled", filePath, errors); if (enabled !== undefined) patch.enabled = enabled; } if ("thresholdPercent" in parsed) { const threshold = parseThreshold(parsed.thresholdPercent, filePath, errors); if (threshold !== undefined) patch.thresholdPercent = threshold; } if ("autoResume" in parsed) { const autoResume = parseBoolean(parsed.autoResume, "autoResume", filePath, errors); if (autoResume !== undefined) patch.autoResume = autoResume; } if ("notify" in parsed) { const notify = parseBoolean(parsed.notify, "notify", filePath, errors); if (notify !== undefined) patch.notify = notify; } if ("customCompactionInstructions" in parsed) { const instructions = parseInstructions(parsed.customCompactionInstructions, filePath, errors); if (instructions !== undefined) patch.customCompactionInstructions = instructions; } return { exists: true, patch, errors, warnings }; } export function resolveConfigPaths(cwd: string): ConfigPaths { return { globalPath: join(getAgentDir(), "extensions", EXTENSION_ID, "config.json"), projectPath: join(cwd, CONFIG_DIR_NAME, "extensions", EXTENSION_ID, "config.json"), }; } export function loadMidrunCompactConfig(options: LoadConfigOptions): LoadedConfig { const defaults = resolveConfigPaths(options.cwd); const globalPath = options.globalPath ?? defaults.globalPath; const projectPath = options.projectPath ?? defaults.projectPath; const config: MidrunCompactConfig = { ...DEFAULT_CONFIG }; const loadedSources: string[] = []; const errors: string[] = []; const warnings: string[] = []; const globalConfig = parseConfigFile(globalPath); if (globalConfig.exists) loadedSources.push(globalPath); Object.assign(config, globalConfig.patch); errors.push(...globalConfig.errors); warnings.push(...globalConfig.warnings); let projectConfigSkippedAsUntrusted = false; if (options.projectTrusted) { const projectConfig = parseConfigFile(projectPath); if (projectConfig.exists) loadedSources.push(projectPath); Object.assign(config, projectConfig.patch); errors.push(...projectConfig.errors); warnings.push(...projectConfig.warnings); } else { projectConfigSkippedAsUntrusted = true; } const valid = errors.length === 0; return { config, valid, automaticEnabled: valid && config.enabled, globalPath, projectPath, loadedSources, errors, warnings, projectConfigSkippedAsUntrusted, }; }