import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; /** * Extensions known to also rewrite compaction results or filter model * context. If one of these is installed alongside pi-context-epochs, the * last-registered session_before_compact handler wins, which can silently * replace our epoch boundary. We warn at startup and fail closed at * session_compact verification time (no continuation is sent for a boundary * we do not own). */ export const KNOWN_CONFLICTING_PACKAGES = [ "pi-agenticoding", "pi-context", "pi-context-prune", "billion-context-pi", "pi-codex-compaction", "pi-codex-compact", "pi-session-continuity", "pi-continue", ]; function settingsPackageSources(settings: unknown): string[] { if (!settings || typeof settings !== "object" || Array.isArray(settings)) return []; const packages = (settings as { packages?: unknown }).packages; if (!Array.isArray(packages)) return []; const sources: string[] = []; for (const entry of packages) { if (typeof entry === "string") sources.push(entry); else if (entry && typeof entry === "object" && typeof (entry as { source?: unknown }).source === "string") { sources.push((entry as { source: string }).source); } } return sources; } function sourceMatches(source: string): string | null { const lower = source.toLowerCase(); for (const name of KNOWN_CONFLICTING_PACKAGES) { // npm:, git:<...>/, bare path ending in if (lower === name || lower.endsWith(`/${name}`) || lower.includes(`:${name}`) || lower.includes(`npm:${name}@`) || lower.startsWith(name)) { return name; } const base = lower.split("/").pop()?.replace(/\.jsonl$/, "") ?? lower; if (base === name) return name; } return null; } export interface ConflictScanResult { conflicts: string[]; scanned: string[]; } /** Best-effort scan of global and project settings for competing extensions. */ export function scanForConflictingPackages(projectCwd: string | undefined): ConflictScanResult { const candidates = [join(homedidSafe(), ".pi", "agent", "settings.json")]; if (projectCwd) candidates.push(join(projectCwd, ".pi", "settings.json")); const conflicts = new Set(); const scanned: string[] = []; for (const path of candidates) { if (!existsSync(path)) continue; scanned.push(path); try { const settings = JSON.parse(readFileSync(path, "utf8")); for (const source of settingsPackageSources(settings)) { const match = sourceMatches(source); if (match) conflicts.add(match); } } catch { // unreadable settings: not fatal } } return { conflicts: [...conflicts], scanned }; } function homedidSafe(): string { try { return homedir(); } catch { return ""; } }