import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, loadSkills, } from "@earendil-works/pi-coding-agent";
import { getSupportedThinkingLevels, } from "@earendil-works/pi-ai";
import { config } from "./config.js";
import { makeLock } from "./concurrency.js";
import { discoverExtensionResources } from "./extensions-discovery.js";
import { getProjectSkillState, readSkillOverrides, setProjectSkillOverride, } from "./skill-overrides.js";
import { getProjectPromptState, readPromptOverrides, setProjectPromptOverride, } from "./prompt-overrides.js";
const MODELS_FILE = () => join(config.piConfigDir, "models.json");
const AUTH_FILE = () => join(config.piConfigDir, "auth.json");
const SETTINGS_FILE = () => join(config.piConfigDir, "settings.json");
async function ensureConfigDir() {
    await mkdir(config.piConfigDir, { recursive: true });
}
/**
 * Keys we refuse to allow user-supplied input to set on any JSON-shaped
 * config blob. Without filtering, a request body like
 * `{"__proto__": {"polluted": true}}` flows through `JSON.parse` (where
 * Node decodes `__proto__` as an own data property — safe) and then
 * through a property-write somewhere downstream that *does* hit the
 * prototype chain — corrupting `Object.prototype` process-wide.
 *
 * We filter at every JSON-write boundary as defense in depth, not just
 * at the one route the original audit caught.
 */
const DANGEROUS_KEYS = new Set(["__proto__", "prototype", "constructor"]);
/**
 * Recursively strip dangerous keys from a value before persisting. Used
 * by `writeModelsJson` and any other path that round-trips
 * user-supplied JSON to disk.
 */
function stripDangerousKeys(input) {
    if (Array.isArray(input)) {
        return input.map((v) => stripDangerousKeys(v));
    }
    if (typeof input !== "object" || input === null)
        return input;
    const cleaned = {};
    for (const [k, v] of Object.entries(input)) {
        if (DANGEROUS_KEYS.has(k))
            continue;
        Object.defineProperty(cleaned, k, {
            value: stripDangerousKeys(v),
            enumerable: true,
            writable: true,
            configurable: true,
        });
    }
    return cleaned;
}
async function atomicWriteJson(path, data) {
    await ensureConfigDir();
    const tmp = `${path}.${randomUUID()}.tmp`;
    await writeFile(tmp, JSON.stringify(data, null, 2), "utf8");
    try {
        await rename(tmp, path);
    }
    catch (err) {
        // Cross-fs rename, perms, source vanished — clean up the leftover
        // tmp file before rethrowing. Without this, repeated failures would
        // leave `<path>.<uuid>.tmp` files accumulating in the config dir.
        await unlink(tmp).catch(() => undefined);
        throw err;
    }
}
async function readJsonOr(path, fallback) {
    try {
        const raw = await readFile(path, "utf8");
        if (raw.trim().length === 0)
            return fallback;
        return JSON.parse(raw);
    }
    catch (err) {
        if (err.code === "ENOENT")
            return fallback;
        throw err;
    }
}
// ---------------------------------------------------------------------------
// models.json
export async function readModelsJson() {
    const data = await readJsonOr(MODELS_FILE(), { providers: {} });
    if (typeof data !== "object" || data === null || !("providers" in data)) {
        return { providers: {} };
    }
    const r = data;
    if (typeof r.providers !== "object" || r.providers === null) {
        return { providers: {} };
    }
    return { providers: r.providers };
}
/**
 * Like readModelsJson but with secret-shaped fields replaced with a literal
 * sentinel. Used by the GET /config/models route so an inline `apiKey` in
 * models.json (the pi SDK accepts both inline keys and `apiKeyCommand`) is
 * never echoed back to a browser or to an operator's log shipper.
 *
 * The persisted file is unchanged — writeModelsJson takes the actual
 * shape; this redaction is purely on the read path.
 */
const SECRET_PLACEHOLDER = "***REDACTED***";
export async function readModelsJsonRedacted() {
    const raw = await readModelsJson();
    const out = {};
    for (const [name, provider] of Object.entries(raw.providers)) {
        out[name] = redactProviderConfig(provider);
    }
    return { providers: out };
}
function redactProviderConfig(p) {
    const { apiKey, apiKeyCommand, ...rest } = p;
    const redacted = { ...rest };
    if (apiKey !== undefined)
        redacted.apiKey = SECRET_PLACEHOLDER;
    if (apiKeyCommand !== undefined)
        redacted.apiKeyCommand = SECRET_PLACEHOLDER;
    return redacted;
}
export async function writeModelsJson(data) {
    // Round-trip secret protection: GET /config/models redacts inline
    // `apiKey` / `apiKeyCommand` to a sentinel string. If the editor
    // PUTs the body back unchanged, the literal sentinel would
    // overwrite the real secret on disk and the next request would go
    // out with `Authorization: Bearer ***REDACTED***`. Pre-merge here
    // so the sentinel means "keep the existing value" — same semantics
    // auth.json already uses for its presence-only API.
    const existing = await readModelsJson().catch(() => ({ providers: {} }));
    const safe = { providers: {} };
    for (const [name, provider] of Object.entries(data.providers ?? {})) {
        if (DANGEROUS_KEYS.has(name))
            continue;
        const cleaned = stripDangerousKeys(provider);
        const prior = existing.providers[name];
        if (cleaned.apiKey === SECRET_PLACEHOLDER) {
            if (prior?.apiKey !== undefined)
                cleaned.apiKey = prior.apiKey;
            else
                delete cleaned.apiKey;
        }
        if (cleaned.apiKeyCommand === SECRET_PLACEHOLDER) {
            if (prior?.apiKeyCommand !== undefined)
                cleaned.apiKeyCommand = prior.apiKeyCommand;
            else
                delete cleaned.apiKeyCommand;
        }
        safe.providers[name] = cleaned;
    }
    await atomicWriteJson(MODELS_FILE(), safe);
}
// ---------------------------------------------------------------------------
// auth.json — uses the SDK's AuthStorage for locking + presence semantics.
function authStorage() {
    return AuthStorage.create(AUTH_FILE());
}
/**
 * Build a fresh ModelRegistry seeded with the on-disk auth + models.json.
 * Exposed so route handlers can resolve a provider+modelId pair to a typed
 * Model<Api> WITHOUT going through pi-ai's static `getModel`, which only
 * knows built-in providers and silently returns undefined for anything
 * defined in models.json.
 */
export function liveModelRegistry() {
    return ModelRegistry.create(authStorage(), MODELS_FILE());
}
export function readAuthSummary() {
    const store = authStorage();
    const providers = {};
    // `list()` enumerates providers stored in auth.json. Augment each with the
    // typed `getAuthStatus` shape so the response surface matches what the UI
    // would render (configured + source + label, no key value).
    for (const provider of store.list()) {
        const status = store.getAuthStatus(provider);
        providers[provider] = {
            configured: status.configured,
            source: status.source,
            label: status.label,
        };
    }
    return { providers };
}
export function writeApiKey(provider, apiKey) {
    if (provider.length === 0)
        throw new Error("provider name cannot be empty");
    if (apiKey.length === 0)
        throw new Error("api key cannot be empty");
    const store = authStorage();
    store.set(provider, { type: "api_key", key: apiKey });
}
export class AuthProviderNotFoundError extends Error {
    constructor(provider) {
        super(`auth provider not found: ${provider}`);
        this.name = "AuthProviderNotFoundError";
    }
}
export function removeApiKey(provider) {
    const store = authStorage();
    if (!store.has(provider))
        throw new AuthProviderNotFoundError(provider);
    store.remove(provider);
}
// ---------------------------------------------------------------------------
// settings.json
export async function readSettings() {
    return readJsonOr(SETTINGS_FILE(), {});
}
/**
 * Serialise all read-modify-write sequences over settings.json. Without
 * this, two concurrent PUT /config/settings requests can read the same
 * baseline and race the rename(), losing one write. Also covers the
 * snapshot+restore dance in routes/control.ts:setModel — exported so
 * that route can wrap the entire snapshot → setModel → restore sequence
 * as a single critical section. Single-process / single-tenant only.
 */
export const withSettingsLock = makeLock();
/**
 * Atomically replace settings.json with `settings`. Used by the
 * per-session model route to roll back the SDK's side effects on
 * `session.setModel(...)`. The SDK touches more keys than just
 * defaultProvider/defaultModel (defaultThinkingLevel, etc.), so a
 * key-by-key restore was leaking SDK-written values into the file
 * and resetting users' manually-curated settings to whatever the
 * SDK happened to write.
 *
 * Note: this function does NOT take `withSettingsLock`. The Promise-
 * chain lock is non-reentrant, so callers that need to write under an
 * already-held lock (e.g. `routes/control.ts:setModel` doing a
 * snapshot+restore inside its own critical section) would deadlock.
 * `atomicWriteJson` is itself crash-safe; the lock only matters for
 * read-modify-write coherency, which is owned by the caller.
 */
export async function writeSettings(settings) {
    await atomicWriteJson(SETTINGS_FILE(), settings);
}
/**
 * Partial-merge update: shallow merge of `patch` over the existing settings.
 * Pass `null` for any key in `patch` to delete that key. Atomic write.
 *
 * Refuses prototype-pollution keys (`__proto__`, `prototype`,
 * `constructor`) — `JSON.parse` itself decodes these as own-properties
 * (which is why the simple `next[k] = v` write would actually corrupt
 * `Object.prototype`); we filter them at the boundary.
 */
export async function updateSettings(patch) {
    return withSettingsLock(async () => {
        const current = await readSettings();
        const next = { ...current };
        for (const [k, v] of Object.entries(patch)) {
            if (DANGEROUS_KEYS.has(k))
                continue;
            if (v === null) {
                delete next[k];
            }
            else {
                // defineProperty avoids a setter-trap if the prototype chain
                // somehow contains an accessor for this key.
                Object.defineProperty(next, k, {
                    value: v,
                    writable: true,
                    enumerable: true,
                    configurable: true,
                });
            }
        }
        await atomicWriteJson(SETTINGS_FILE(), next);
        return next;
    });
}
let providersListingCache = null;
const PROVIDERS_LISTING_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
export function clearProvidersListingCache() {
    providersListingCache = null;
}
export async function liveProvidersListing() {
    const now = Date.now();
    if (providersListingCache !== null &&
        now - providersListingCache.ts < PROVIDERS_LISTING_CACHE_TTL) {
        return providersListingCache.data;
    }
    const store = authStorage();
    const registry = ModelRegistry.create(store, MODELS_FILE());
    const all = registry.getAll();
    // When HIDE_BUILTIN_PROVIDERS is on, restrict to providers whose
    // name appears as a key in models.json. Built-ins (anthropic,
    // openai, etc. the SDK ships with) drop out, leaving only the
    // operator-added custom providers.
    const customOnly = config.hideBuiltinProviders
        ? new Set(Object.keys((await readModelsJson()).providers))
        : undefined;
    const grouped = new Map();
    for (const m of all) {
        if (customOnly !== undefined && !customOnly.has(m.provider))
            continue;
        let entry = grouped.get(m.provider);
        if (entry === undefined) {
            entry = { provider: m.provider, models: [] };
            grouped.set(m.provider, entry);
        }
        entry.models.push({
            id: m.id,
            name: m.name,
            contextWindow: m.contextWindow,
            maxTokens: m.maxTokens,
            reasoning: m.reasoning,
            input: m.input,
            hasAuth: registry.hasConfiguredAuth(m),
            supportedThinkingLevels: getSupportedThinkingLevels(m),
        });
    }
    const result = { providers: Array.from(grouped.values()) };
    providersListingCache = { data: result, ts: Date.now() };
    return result;
}
export async function listSkills(workspacePath, projectId) {
    // Pi packages can contribute skill directories or files via
    // `package.json#pi.skills`, resolved by DefaultPackageManager.
    // discoverExtensionResources returns those resolved paths so
    // loadSkills can scan them alongside the SDK's default global /
    // project dirs.
    const extResources = await discoverExtensionResources(workspacePath);
    const extensionSkillPaths = extResources.skillPaths.map((s) => s.skillPath);
    const result = loadSkills({
        cwd: workspacePath,
        agentDir: config.piConfigDir,
        skillPaths: extensionSkillPaths,
        includeDefaults: true,
    });
    const settings = await readSettings();
    // Pi's `settings.skills` is a list of override patterns, NOT a list
    // of enabled names. A skill is enabled at the global scope unless
    // an `!<name>` (or `-<name>`) pattern targets it. See the doc-comment
    // on `effectiveSkillsForProject` for the full pattern semantics.
    const globalDisabled = disabledNamesFromPatterns(settings.skills ?? []);
    const overrides = await readSkillOverrides();
    // Map registered skill paths → owning package source (e.g.
    // "pi-subagents") so each summary can attribute its origin
    // beyond the bare global / project heuristic.
    const skillPathToPackage = new Map();
    for (const s of extResources.skillPaths) {
        skillPathToPackage.set(s.skillPath, s.packageSource);
    }
    return {
        skills: result.skills.map((s) => skillSummary(s, workspacePath, globalDisabled, overrides, projectId, skillPathToPackage)),
        diagnostics: result.diagnostics,
    };
}
function skillSummary(s, workspacePath, globalDisabled, overrides, projectId, skillPathToPackage) {
    // The SDK's loadSkills puts global ones under agentDir, project ones
    // under workspacePath/.pi/skills, and package-contributed ones under
    // whatever path the package's `pi.skills` manifest entry resolved
    // to (typically `~/.pi/agent/packages/<name>/skills/...`). Match
    // against the package skill-path map FIRST — those paths usually
    // don't fall under the project / global heuristics below, but
    // checking explicitly is safer.
    const packageSource = findPackageForSkill(s.baseDir, skillPathToPackage);
    const isProject = packageSource === undefined && s.baseDir.startsWith(workspacePath);
    const source = packageSource !== undefined ? "extension" : isProject ? "project" : "global";
    const isEnabledGlobal = !globalDisabled.has(s.name);
    const projectOverride = projectId !== undefined ? getProjectSkillState(overrides, projectId, s.name) : undefined;
    const effective = projectOverride === "enabled" ? true : projectOverride === "disabled" ? false : isEnabledGlobal;
    const summary = {
        name: s.name,
        description: s.description,
        source,
        filePath: s.filePath,
        enabled: isEnabledGlobal,
        effective,
        disableModelInvocation: s.disableModelInvocation,
    };
    if (packageSource !== undefined)
        summary.extensionPath = packageSource;
    if (projectOverride !== undefined)
        summary.projectOverride = projectOverride;
    return summary;
}
/**
 * Match a skill's `baseDir` against the package-skill-path map.
 * The skill's baseDir might be exactly a registered path or a
 * subdirectory of one (loadSkills recurses). Pick the longest
 * matching prefix so the closest registering package wins. Returns
 * the package source identifier ("pi-subagents", a git URL, etc.)
 * or `undefined` when the skill didn't come from any registered
 * package.
 */
function findPackageForSkill(baseDir, skillPathToPackage) {
    let best;
    for (const [path, pkg] of skillPathToPackage) {
        if (baseDir === path || baseDir.startsWith(path + "/")) {
            if (best === undefined || path.length > best.path.length) {
                best = { path, pkg };
            }
        }
    }
    return best?.pkg;
}
/**
 * Returns the full per-project overrides map. Used by the Settings
 * UI's cascade view to render override rows for projects OTHER than
 * the active one (e.g. "this skill is disabled in 3 of 8 projects").
 */
export async function getAllSkillOverrides() {
    return readSkillOverrides();
}
export class SkillNotFoundError extends Error {
    constructor(name) {
        super(`skill not found: ${name}`);
        this.name = "SkillNotFoundError";
    }
}
/**
 * Toggle a skill's enabled state.
 *
 * - `scope: "global"` (the default; back-compat with the original
 *   one-arg form) writes to pi's `settings.skills` — the canonical
 *   global enable/disable list.
 * - `scope: "project"` writes to the Huiyu-Pi-private overrides
 *   file at `${FORGE_DATA_DIR}/skills-overrides.json` for the
 *   given `projectId`. Tri-state: `enabled` / `disabled` /
 *   (passing `enabled: undefined` clears the override = inherit
 *   from global).
 *
 * The skill must be discoverable in the `loadSkills` result — passing
 * a name that doesn't exist throws SkillNotFoundError so route
 * handlers can return a clean 404.
 */
export async function setSkillEnabled(name, enabled, workspacePath, opts) {
    const all = await listSkills(workspacePath, opts?.projectId);
    if (!all.skills.some((s) => s.name === name))
        throw new SkillNotFoundError(name);
    const scope = opts?.scope ?? "global";
    if (scope === "project") {
        if (opts?.projectId === undefined) {
            throw new Error("setSkillEnabled: scope=project requires a projectId");
        }
        // Tri-state mapping: true → "enabled", false → "disabled",
        // undefined → clear (inherit). Project writes don't touch pi's
        // settings.skills so the global list stays stable across project
        // switches and other pi clients (TUI) keep their view.
        const state = enabled === true ? "enabled" : enabled === false ? "disabled" : undefined;
        await setProjectSkillOverride(opts.projectId, name, state);
        return (await listSkills(workspacePath, opts.projectId)).skills;
    }
    // global scope (existing behaviour)
    if (enabled === undefined) {
        throw new Error("setSkillEnabled: scope=global requires enabled to be true or false");
    }
    // The skills array is read-modify-write against settings.skills, so
    // serialise the whole sequence under withSettingsLock — without this,
    // toggling two skills in rapid succession (the UI lets the user
    // click as fast as they want) can lose one toggle. We inline the
    // read+merge+write here rather than calling updateSettings (which
    // would deadlock — the lock is non-reentrant) and use atomicWriteJson
    // directly for the write.
    //
    // Pattern semantics: pi auto-discovers every skill on disk and
    // enables them by default. To DISABLE one we push `!<name>`. To
    // re-enable we drop any `!<name>` / `-<name>` / `+<name>` for that
    // name (absence = pi's default-on). We also drop bare-name entries
    // a prior buggy version of this file may have left on disk — pi
    // ignores them, so they're inert and just clutter the file.
    await withSettingsLock(async () => {
        const settings = await readSettings();
        const existing = settings.skills ?? [];
        const filtered = existing.filter((p) => {
            // Drop inert bare entries on every rewrite.
            if (!p.startsWith("!") && !p.startsWith("+") && !p.startsWith("-"))
                return false;
            // Drop any prior pattern targeting THIS skill name; we'll re-add
            // exactly the one we want below.
            if (p.slice(1) === name)
                return false;
            return true;
        });
        if (!enabled)
            filtered.push(excludePattern(name));
        const next = { ...settings, skills: filtered };
        await atomicWriteJson(SETTINGS_FILE(), next);
    });
    return (await listSkills(workspacePath, opts?.projectId)).skills;
}
/**
 * Pi's `settings.skills` is NOT an enabled-allowlist of skill names —
 * it is a list of override PATTERNS with three prefix conventions:
 *
 *   `!<name>`  → exclude (skill won't load if pattern matches)
 *   `+<name>`  → force include (overrides any `!`)
 *   `-<name>`  → force exclude (overrides everything)
 *   bare name  → silently ignored by pi's `getOverridePatterns`
 *
 * Pi auto-discovers every skill it finds under the user/project skill
 * directories and they are ALL ENABLED BY DEFAULT. The only way to
 * disable one is to push `!<name>` (or `-<name>`) into the patterns
 * list. Writing bare names accomplishes nothing.
 *
 * Helpers below codify this so callers don't need to re-derive it.
 */
const excludePattern = (name) => `!${name}`;
const forceIncludePattern = (name) => `+${name}`;
/** Names that an exclude pattern (`!name` or `-name`) targets. */
function disabledNamesFromPatterns(patterns) {
    const out = new Set();
    for (const p of patterns) {
        if (p.startsWith("!") || p.startsWith("-"))
            out.add(p.slice(1));
    }
    return out;
}
/**
 * Compute the skill-pattern list a session in `projectId` should see —
 * a merge of pi's global patterns with our per-project overrides.
 *
 * Returned values are PATTERNS (`!name` / `+name`), not names. The
 * session-registry pushes them into the SettingsManager so pi's
 * package-manager applies them when discovering skills.
 *
 * Resolution rules:
 *   - Start with whatever patterns pi already has at the global scope
 *     (these come from prior `setSkillEnabled(scope:"global")` writes).
 *   - For every skill the project marked `disable`, ensure `!<name>`
 *     is in the list — even if no global exclude exists.
 *   - For every skill the project marked `enable`, push `+<name>` so
 *     it force-includes in this project's session even if a global
 *     `!<name>` would otherwise hide it.
 */
export async function effectiveSkillsForProject(projectId) {
    const settings = await readSettings();
    const overrides = await readSkillOverrides();
    // Filter to only valid override patterns; drop any inert bare entries
    // a prior buggy version of this code might have left on disk.
    const globalPatterns = (settings.skills ?? []).filter((p) => p.startsWith("!") || p.startsWith("+") || p.startsWith("-"));
    const result = new Set(globalPatterns);
    const entry = overrides.projects[projectId];
    if (entry !== undefined) {
        for (const name of entry.disable)
            result.add(excludePattern(name));
        for (const name of entry.enable)
            result.add(forceIncludePattern(name));
    }
    return Array.from(result);
}
export async function listPrompts(workspacePath, projectId) {
    // Use a one-off DefaultResourceLoader for discovery — the SDK's
    // standalone `loadPromptTemplates` isn't exported from the package
    // index, so we go through the loader's `getPrompts()` instead. This
    // also gives us SDK-emitted diagnostics for free if/when the SDK
    // starts surfacing them for prompts (today the array is always empty).
    const loader = new DefaultResourceLoader({
        cwd: workspacePath,
        agentDir: config.piConfigDir,
        settingsManager: SettingsManager.create(workspacePath, config.piConfigDir),
    });
    await loader.reload();
    const { prompts: templates, diagnostics } = loader.getPrompts();
    const settings = await readSettings();
    const globalDisabled = disabledNamesFromPatterns(settings.prompts ?? []);
    const overrides = await readPromptOverrides();
    return {
        prompts: templates.map((t) => promptSummary(t, workspacePath, globalDisabled, overrides, projectId)),
        diagnostics,
    };
}
function promptSummary(t, workspacePath, globalDisabled, overrides, projectId) {
    // The SDK returns sourceInfo with a `scope` of "user" / "project" /
    // undefined; we fall back to a path-based heuristic when the SDK
    // didn't tag (e.g. explicit promptPaths from a third-party caller).
    const isProject = t.sourceInfo.scope === "project" ||
        (t.sourceInfo.scope === undefined && t.filePath.startsWith(workspacePath));
    const source = isProject ? "project" : "global";
    const isEnabledGlobal = !globalDisabled.has(t.name);
    const projectOverride = projectId !== undefined ? getProjectPromptState(overrides, projectId, t.name) : undefined;
    const effective = projectOverride === "enabled" ? true : projectOverride === "disabled" ? false : isEnabledGlobal;
    const summary = {
        name: t.name,
        description: t.description,
        source,
        filePath: t.filePath,
        enabled: isEnabledGlobal,
        effective,
    };
    if (t.argumentHint !== undefined)
        summary.argumentHint = t.argumentHint;
    if (projectOverride !== undefined)
        summary.projectOverride = projectOverride;
    return summary;
}
export class PromptNotFoundError extends Error {
    constructor(name) {
        super(`prompt not found: ${name}`);
        this.name = "PromptNotFoundError";
    }
}
/**
 * Toggle a prompt's enabled state at either the global scope (writes
 * pi's `settings.prompts` patterns) or the project scope (writes
 * Huiyu-Pi-private prompt-overrides.json). Mirrors `setSkillEnabled`
 * end-to-end — same tri-state semantics for project scope, same
 * pattern-rewrite for global scope.
 */
export async function setPromptEnabled(name, enabled, workspacePath, opts) {
    const all = await listPrompts(workspacePath, opts?.projectId);
    if (!all.prompts.some((p) => p.name === name))
        throw new PromptNotFoundError(name);
    const scope = opts?.scope ?? "global";
    if (scope === "project") {
        if (opts?.projectId === undefined) {
            throw new Error("setPromptEnabled: scope=project requires a projectId");
        }
        const state = enabled === true ? "enabled" : enabled === false ? "disabled" : undefined;
        await setProjectPromptOverride(opts.projectId, name, state);
        return (await listPrompts(workspacePath, opts.projectId)).prompts;
    }
    if (enabled === undefined) {
        throw new Error("setPromptEnabled: scope=global requires enabled to be true or false");
    }
    // Same lock + read-modify-write + pattern-rewrite as setSkillEnabled.
    // Pi auto-discovers every prompt and enables them by default; to
    // disable one we push `!<name>`. Drop any prior pattern targeting the
    // same name plus any inert bare entries on every rewrite.
    await withSettingsLock(async () => {
        const settings = await readSettings();
        const existing = settings.prompts ?? [];
        const filtered = existing.filter((p) => {
            if (!p.startsWith("!") && !p.startsWith("+") && !p.startsWith("-"))
                return false;
            if (p.slice(1) === name)
                return false;
            return true;
        });
        if (!enabled)
            filtered.push(excludePattern(name));
        const next = { ...settings, prompts: filtered };
        await atomicWriteJson(SETTINGS_FILE(), next);
    });
    return (await listPrompts(workspacePath, opts?.projectId)).prompts;
}
/**
 * Compute the prompt-pattern list a session in `projectId` should see.
 * Same merge semantics as `effectiveSkillsForProject` — global patterns
 * are the floor; per-project enable/disable patterns layer on top
 * via `+name`/`!name`. Pushed into the SettingsManager monkey-patch in
 * `session-registry.buildSessionSettingsManager` so pi's package-manager
 * applies them when discovering prompts.
 */
export async function effectivePromptsForProject(projectId) {
    const settings = await readSettings();
    const overrides = await readPromptOverrides();
    const globalPatterns = (settings.prompts ?? []).filter((p) => p.startsWith("!") || p.startsWith("+") || p.startsWith("-"));
    const result = new Set(globalPatterns);
    const entry = overrides.projects[projectId];
    if (entry !== undefined) {
        for (const name of entry.disable)
            result.add(excludePattern(name));
        for (const name of entry.enable)
            result.add(forceIncludePattern(name));
    }
    return Array.from(result);
}
/** Cascade-view counterpart to `getAllSkillOverrides`, for the Settings
 *  UI's per-prompt expand-and-show-all-projects affordance. */
export async function getAllPromptOverrides() {
    return readPromptOverrides();
}
//# sourceMappingURL=config-manager.js.map