/** * v2.3 resolved provider registry — single in-memory source of truth after * merging the hardcoded built-ins (provider-registry.ts) with the config file * (provider-config.ts). Mirrors Paseo's buildProviderRegistry/addDerivedProviders. * * Phase 1 scope: build + expose the resolved registry. `launchCommand` is null * for built-ins (the default argv is resolved at dispatch time in Phase 3) and * is the config `command` for custom ACP entries. No DB columns (design.md §3.3); * `enabled` lives in memory only. */ import type { ProviderDef } from './provider-registry.js'; import { PROVIDERS } from './provider-registry.js'; import { load, type CoderProvidersFile } from './provider-config.js'; export interface ResolvedProviderDef extends ProviderDef { id: string; enabled: boolean; isBuiltin: boolean; isCustomAcp: boolean; /** Full argv for spawn: [binary, ...args]. Null for built-ins (resolved at dispatch). */ launchCommand: [string, ...string[]] | null; env: Record | undefined; configLabel?: string; configDescription?: string; /** Config `models` — REPLACES the discovered/static model list when present. */ configModels?: Array<{ id: string; label: string }>; /** Config `additionalModels` — MERGED on top of the resolved model list. */ configAdditionalModels?: Array<{ id: string; label: string }>; } /** * Merge built-ins with config overrides into the resolved registry. * Algorithm verbatim from design.md §3.1. */ export function buildResolvedRegistry( builtins: ProviderDef[], config: CoderProvidersFile, ): Map { const out = new Map(); const overrides = config.providers ?? {}; const builtinNames = new Set(builtins.map((b) => b.name)); // 1. Built-ins, applying a config override if one is present. for (const def of builtins) { const ov = overrides[def.name]; let enabled = ov?.enabled !== false; // 3. boocode is always enabled; an enabled:false override is ignored + warned. if (def.name === 'boocode' && ov?.enabled === false) { console.warn("provider-config: ignoring enabled:false for built-in 'boocode' (always enabled)"); enabled = true; } const launchCommand = ov?.command && ov.command.length > 0 ? (ov.command as [string, ...string[]]) : null; out.set(def.name, { ...def, label: ov?.label ?? def.label, id: def.name, enabled, isBuiltin: true, isCustomAcp: false, launchCommand, env: ov?.env, configLabel: ov?.label, configDescription: ov?.description, configModels: ov?.models, configAdditionalModels: ov?.additionalModels, }); } // 2. Config ids that are not built-ins → custom ACP entries. for (const [id, ov] of Object.entries(overrides)) { if (builtinNames.has(id)) continue; // §2.2 rules: "New id without extends → Reject at load with log." if (ov.extends !== 'acp' || !ov.label || !ov.command || ov.command.length === 0) { console.warn( `provider-config: skipping custom provider '${id}' — requires extends:'acp', label, and command`, ); continue; } out.set(id, { name: id, label: ov.label, transport: 'acp', modelSource: 'probe', id, enabled: ov.enabled !== false, isBuiltin: false, isCustomAcp: true, launchCommand: ov.command as [string, ...string[]], env: ov.env, configLabel: ov.label, configDescription: ov.description, configModels: ov.models, configAdditionalModels: ov.additionalModels, }); } return out; } // --- Module singleton --------------------------------------------------------- let cachedRegistry: Map | null = null; let cachedPath: string | null = null; /** Load the config file at `path`, rebuild, and cache the resolved registry. */ export function loadProviderConfig(path: string): Map { cachedPath = path; cachedRegistry = buildResolvedRegistry(PROVIDERS, load(path)); return cachedRegistry; } /** Re-read the last-loaded config file and rebuild (Phase 4 calls this after PATCH). */ export function reloadProviderConfig(): Map { if (cachedPath == null) { cachedRegistry = buildResolvedRegistry(PROVIDERS, { providers: {} }); return cachedRegistry; } return loadProviderConfig(cachedPath); } /** The cached resolved registry (built-ins only if nothing has been loaded yet). */ export function getResolvedRegistry(): Map { return cachedRegistry ?? buildResolvedRegistry(PROVIDERS, { providers: {} }); } /** Resolved provider ids in registry order. */ export function getResolvedProviderIds(): string[] { return [...getResolvedRegistry().keys()]; }