import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent"; export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; /** Session persistence configuration for headless memory-agent sub-sessions. */ export interface SessionPersistenceConfig { /** Enable disk persistence (default: false = in-memory). */ enabled: boolean; /** Custom session directory. Defaults to memoryDir/sessions/. */ sessionDir?: string; } /** Shared defaults that per-task configs inherit. Per-task fields override these. */ export interface DefaultsConfig { model?: string; sessionPersistence?: SessionPersistenceConfig; } export interface AutoSurfacingConfig { enabled: boolean; model?: string; thinkLevel: ThinkLevel; maxFiles: number; maxTopicBytes: number; maxInjectionBytes: number; } export interface ExtractMemoriesConfig { enabled: boolean; model?: string; thinkLevel: ThinkLevel; maxContextTokens: number; } export interface MemoryConfig { enabled: boolean; /** Shared defaults for model and sessionPersistence. Per-task configs override. */ defaults?: DefaultsConfig; memoryDir: string; /** Write capacity: max entries in MEMORY.md index. */ memIndexMaxLines: number; /** Write capacity: max bytes of serialized MEMORY.md index. */ memIndexMaxBytes: number; /** Injection truncation: max lines of MEMORY.md injected into system prompt. */ memIndexInjectMaxLines: number; /** Injection truncation: max bytes of MEMORY.md injected into system prompt. */ memIndexInjectMaxBytes: number; dream: { nudgeAfterSessions: number; nudgeAfterHours: number; model?: string; thinkLevel: ThinkLevel; sessionPersistence?: SessionPersistenceConfig; }; sessionSearch: { maxSessions: number; maxMatches: number }; autoSurfacing: AutoSurfacingConfig & { sessionPersistence?: SessionPersistenceConfig; }; extractMemories: ExtractMemoriesConfig & { sessionPersistence?: SessionPersistenceConfig; }; } export const DEFAULT_CONFIG: MemoryConfig = { enabled: true, memoryDir: join(homedir(), CONFIG_DIR_NAME, "memory"), memIndexMaxLines: 200, memIndexMaxBytes: 25600, memIndexInjectMaxLines: 20, memIndexInjectMaxBytes: 3072, dream: { nudgeAfterSessions: 5, nudgeAfterHours: 24, thinkLevel: "high" }, sessionSearch: { maxSessions: 10, maxMatches: 5 }, autoSurfacing: { enabled: true, thinkLevel: "off", maxFiles: 3, maxTopicBytes: 3072, maxInjectionBytes: 10240, }, extractMemories: { enabled: true, thinkLevel: "high", maxContextTokens: 2000, }, }; function expandTilde(p: string): string { if (p === "~") return homedir(); if (p.startsWith("~/")) return join(homedir(), p.slice(2)); return p; } function deepMerge(base: T, over: Partial): T { // biome-ignore lint/suspicious/noExplicitAny: generic deep merge const out: any = { ...base }; for (const k of Object.keys(over) as (keyof T)[]) { // biome-ignore lint/suspicious/noExplicitAny: generic deep merge const ov = over[k] as any; // biome-ignore lint/suspicious/noExplicitAny: generic deep merge if (ov && typeof ov === "object" && !Array.isArray(ov) && typeof (out as any)[k] === "object") { // biome-ignore lint/suspicious/noExplicitAny: generic deep merge (out as any)[k] = deepMerge((out as any)[k], ov); } else if (ov !== undefined) { // biome-ignore lint/suspicious/noExplicitAny: generic deep merge (out as any)[k] = ov; } } return out; } function readJsonSafe(path: string): Partial { try { if (existsSync(path)) return JSON.parse(readFileSync(path, "utf-8")) as Partial; } catch { // ignore malformed } return {}; } export interface LoadConfigContext { cwd: string; isProjectTrusted(): boolean; _globalDir?: string; _configDirName?: string; } export async function loadConfig(ctx: LoadConfigContext): Promise { const agentDir = ctx._globalDir ?? getAgentDir(); const configDirName = ctx._configDirName ?? CONFIG_DIR_NAME; let cfg: MemoryConfig = { ...DEFAULT_CONFIG }; const globalFile = join(agentDir, "memory.json"); cfg = deepMerge(cfg, readJsonSafe(globalFile)); if (ctx.isProjectTrusted()) { const projectFile = join(ctx.cwd, configDirName, "memory.json"); cfg = deepMerge(cfg, readJsonSafe(projectFile)); } cfg.memoryDir = expandTilde(cfg.memoryDir); return cfg; }