import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; export const CONFIG_FILE_NAME = "wj-pi-auto-compact.json"; export const DEFAULT_CONFIG = Object.freeze({ enabled: true, maxContextPercent: 90, }); export interface AutoCompactConfig { readonly enabled: boolean; readonly maxContextPercent: number; readonly customInstructions?: string; } export type ConfigLoadResult = | { readonly kind: "valid"; readonly config: AutoCompactConfig; readonly source: "default" | "file" } | { readonly kind: "invalid"; readonly config: AutoCompactConfig; readonly reason: string }; export function defaultConfigPath(): string { return join(homedir(), ".pi", "agent", CONFIG_FILE_NAME); } export function loadAutoCompactConfig(path = defaultConfigPath()): ConfigLoadResult { let bytes: Buffer; try { bytes = readFileSync(path); } catch (error) { if (isNodeError(error) && error.code === "ENOENT") { return Object.freeze({ kind: "valid", config: DEFAULT_CONFIG, source: "default" }); } return invalid("Configuration file could not be read"); } let text: string; try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch { return invalid("Configuration file is not valid UTF-8"); } let parsed: unknown; try { parsed = JSON.parse(text) as unknown; } catch { return invalid("Configuration file is not valid JSON"); } if (!isRecord(parsed)) return invalid("Configuration root value must be an object"); const keys = Object.keys(parsed); if (keys.some((key) => key !== "enabled" && key !== "maxContextPercent" && key !== "customInstructions")) { return invalid("Configuration file contains unknown fields"); } if (parsed.enabled !== undefined && typeof parsed.enabled !== "boolean") { return invalid("enabled must be a boolean"); } if ( parsed.maxContextPercent !== undefined && ( typeof parsed.maxContextPercent !== "number" || !Number.isFinite(parsed.maxContextPercent) || parsed.maxContextPercent <= 0 || parsed.maxContextPercent > 100 ) ) return invalid("maxContextPercent must be greater than 0 and no more than 100"); if (parsed.customInstructions !== undefined && typeof parsed.customInstructions !== "string") { return invalid("customInstructions must be a string"); } const customInstructions = typeof parsed.customInstructions === "string" && parsed.customInstructions.trim().length > 0 ? parsed.customInstructions : undefined; return Object.freeze({ kind: "valid", source: "file", config: Object.freeze({ enabled: parsed.enabled ?? DEFAULT_CONFIG.enabled, maxContextPercent: parsed.maxContextPercent ?? DEFAULT_CONFIG.maxContextPercent, ...(customInstructions === undefined ? {} : { customInstructions }), }), }); } function invalid(reason: string): ConfigLoadResult { return Object.freeze({ kind: "invalid", config: Object.freeze({ ...DEFAULT_CONFIG, enabled: false }), reason, }); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function isNodeError(value: unknown): value is NodeJS.ErrnoException { return value instanceof Error && "code" in value; }