import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; export interface OverflowSettings { enabled: boolean; thresholdChars: number; headLines: number; tailLines: number; maxSignalLines: number; signalPattern: string; signalRegex: RegExp; maxLineChars: number; exemptTools: string[]; retentionDays: number; maxStoreBytes: number; maxEntryBytes: number; } export const DEFAULT_SIGNAL_PATTERN = "\\b(error|errors|fail|failed|failure|warn|warning|exception|traceback|panic|fatal)\\b"; export const OVERFLOW_DEFAULTS = { enabled: true, thresholdChars: 24_000, headLines: 40, tailLines: 120, maxSignalLines: 40, signalPattern: DEFAULT_SIGNAL_PATTERN, maxLineChars: 500, exemptTools: ["read", "edit", "write", "overflow_query", "ask_user_question"], retentionDays: 7, maxStoreBytes: 1_073_741_824, maxEntryBytes: 67_108_864, } as const; // Bundle-level `config/` is the single source of truth: the extension reads // `config/pi-overflow.json` from inside the installed bundle (resolved // relative to this package-root source file), never `settings.json`. export const OVERFLOW_CONFIG_PATH = fileURLToPath( new URL("../config/pi-overflow.json", import.meta.url), ); function isObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function bool(value: unknown, fallback: boolean): boolean { return typeof value === "boolean" ? value : fallback; } function intInRange(value: unknown, fallback: number, min: number, max: number): number { return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max ? value : fallback; } function compileSignalPattern(value: unknown): { pattern: string; regex: RegExp } { const pattern = typeof value === "string" && value.length > 0 ? value : DEFAULT_SIGNAL_PATTERN; try { return { pattern, regex: new RegExp(pattern, "i") }; } catch { return { pattern: DEFAULT_SIGNAL_PATTERN, regex: new RegExp(DEFAULT_SIGNAL_PATTERN, "i") }; } } function stringList(value: unknown, fallback: readonly string[]): string[] { if (!Array.isArray(value)) return [...fallback]; const list = value.filter((item): item is string => typeof item === "string" && item.length > 0); return [...new Set(list)]; } /** * Read `config/pi-overflow.json`. Every field is validated independently and * falls back to its bundle default when missing or invalid; an unreadable or * non-object file yields all defaults. Tests inject an explicit path. */ export function readOverflowSettings(configPath: string = OVERFLOW_CONFIG_PATH): OverflowSettings { let parsed: unknown = {}; try { parsed = JSON.parse(readFileSync(configPath, "utf8")); } catch { parsed = {}; } const config = isObject(parsed) ? parsed : {}; const signal = compileSignalPattern(config.signalPattern); return { enabled: bool(config.enabled, OVERFLOW_DEFAULTS.enabled), thresholdChars: intInRange(config.thresholdChars, OVERFLOW_DEFAULTS.thresholdChars, 1, 10_000_000), headLines: intInRange(config.headLines, OVERFLOW_DEFAULTS.headLines, 0, 10_000), tailLines: intInRange(config.tailLines, OVERFLOW_DEFAULTS.tailLines, 0, 10_000), maxSignalLines: intInRange(config.maxSignalLines, OVERFLOW_DEFAULTS.maxSignalLines, 0, 1_000), signalPattern: signal.pattern, signalRegex: signal.regex, maxLineChars: intInRange(config.maxLineChars, OVERFLOW_DEFAULTS.maxLineChars, 10, 100_000), exemptTools: stringList(config.exemptTools, OVERFLOW_DEFAULTS.exemptTools), retentionDays: intInRange(config.retentionDays, OVERFLOW_DEFAULTS.retentionDays, 0, 3_650), maxStoreBytes: intInRange(config.maxStoreBytes, OVERFLOW_DEFAULTS.maxStoreBytes, 1_024, 1_099_511_627_776), maxEntryBytes: intInRange(config.maxEntryBytes, OVERFLOW_DEFAULTS.maxEntryBytes, 1_024, 1_099_511_627_776), }; }