UNPKG

@iflow-mcp/ejmockler-brutalist

Version:

Deploy Claude, Codex & Gemini CLI agents to demolish your work before users do. Real file analysis. Brutal honesty. Now with conversation continuation & intelligent pagination.

184 lines 7.38 kB
import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; import { logger } from './logger.js'; /** * ModelResolver — Runtime model discovery and migration resolution. * * Instead of hardcoding model lists that rot, this reads each CLI's * own config at startup to discover: * - The user's configured default model * - Codex's model migration table (old → new mappings) * * Any model string is accepted and passed through to the CLI. * For codex, deprecated model names are resolved through the * migration chain before invocation. */ export class ModelResolver { cliModels = { claude: { migrations: new Map() }, codex: { migrations: new Map() }, gemini: { migrations: new Map() }, }; initialized = false; initTime = 0; CACHE_TTL = 300_000; // 5 minutes async initialize() { const results = await Promise.allSettled([ this.loadCodexConfig(), this.loadClaudeConfig(), ]); for (const r of results) { if (r.status === 'rejected') { logger.debug('ModelResolver: config load failed', r.reason); } } this.initialized = true; this.initTime = Date.now(); logger.info('🔍 ModelResolver initialized', { claude: this.cliModels.claude.defaultModel || '(cli default)', codex: this.cliModels.codex.defaultModel || '(cli default)', gemini: '(cli default)', codexMigrations: this.cliModels.codex.migrations.size, }); } /** Re-read configs if cache has expired. */ async refreshIfStale() { if (this.initialized && Date.now() - this.initTime < this.CACHE_TTL) return; try { await this.initialize(); } catch (err) { logger.warn('ModelResolver: refresh failed, using stale data', err); } } /** * Resolve a requested model for a given CLI. * - Returns undefined when no model was requested (let CLI use its own default). * - For codex, follows the migration chain to the current model name. */ resolveModel(cli, requestedModel) { if (!requestedModel) return undefined; if (cli === 'codex') return this.followMigrationChain(requestedModel); return requestedModel; } /** Return discovered default models for each CLI. */ getDefaults() { return { claude: this.cliModels.claude.defaultModel, codex: this.cliModels.codex.defaultModel, gemini: this.cliModels.gemini.defaultModel, }; } /** Build a dynamic schema description for the models parameter. */ getModelsDescription() { const parts = []; for (const cli of ['claude', 'codex', 'gemini']) { const def = this.cliModels[cli].defaultModel; parts.push(`${cli}: ${def ? `default ${def}` : 'uses CLI default'}`); } return `Per-CLI model override. Pass any model the CLI supports. Omit to use each CLI's configured default. Current defaults — ${parts.join(', ')}`; } /** Build roster text for cli_agent_roster. */ getRosterModelInfo() { const defaults = this.getDefaults(); const migrations = this.cliModels.codex.migrations; let info = '## Model Configuration (auto-discovered)\n'; info += `**Claude:** ${defaults.claude || '(CLI default)'}\n`; info += `**Codex:** ${defaults.codex || '(CLI default)'}`; if (migrations.size > 0) { info += ` — ${migrations.size} migration(s) tracked`; } info += '\n'; info += `**Gemini:** ${defaults.gemini || '(CLI default)'}\n\n`; info += '*Pass any model name via the `models` parameter. Deprecated codex model names are auto-resolved through the migration chain.*\n'; return info; } // ── Config parsers ────────────────────────────────────────────── async loadCodexConfig() { const configPath = path.join(os.homedir(), '.codex', 'config.toml'); try { const raw = await fs.readFile(configPath, 'utf-8'); this.cliModels.codex = this.parseCodexToml(raw); } catch (err) { if (err?.code === 'ENOENT') { logger.debug(`ModelResolver: codex config not found at ${configPath}`); } else { logger.warn(`ModelResolver: failed to read codex config: ${err?.message}`); } } } async loadClaudeConfig() { const configPath = path.join(os.homedir(), '.claude', 'settings.json'); try { const raw = await fs.readFile(configPath, 'utf-8'); const settings = JSON.parse(raw); if (typeof settings.model === 'string') { this.cliModels.claude.defaultModel = settings.model; } } catch (err) { if (err?.code === 'ENOENT') { logger.debug(`ModelResolver: claude config not found at ${configPath}`); } else { logger.warn(`ModelResolver: failed to read claude config: ${err?.message}`); } } } /** * Lightweight TOML parser for codex config. * Extracts top-level `model = "..."` and `[notice.model_migrations]` entries. * Not a general TOML parser — handles only the structure codex actually writes. */ parseCodexToml(raw) { const info = { migrations: new Map() }; let inMigrations = false; for (const line of raw.split('\n')) { const trimmed = line.trim(); // Section headers if (trimmed.startsWith('[')) { inMigrations = trimmed === '[notice.model_migrations]'; // Any other section header ends the migrations block if (!inMigrations && trimmed.startsWith('[')) continue; } // Top-level model = "..." if (!inMigrations) { const topModel = trimmed.match(/^model\s*=\s*"([^"]+)"/); if (topModel) { info.defaultModel = topModel[1]; } continue; } // Migration entries: "old-model" = "new-model" const migration = trimmed.match(/^"([^"]+)"\s*=\s*"([^"]+)"/); if (migration) { info.migrations.set(migration[1], migration[2]); } } return info; } /** Follow codex migration chain to resolve deprecated model names. */ followMigrationChain(model) { const migrations = this.cliModels.codex.migrations; let current = model; const seen = new Set(); while (migrations.has(current) && !seen.has(current)) { seen.add(current); const next = migrations.get(current); logger.debug(`ModelResolver: migrating codex model ${current}${next}`); current = next; } if (current !== model) { logger.info(`🔄 Resolved deprecated codex model: ${model}${current}`); } return current; } } //# sourceMappingURL=model-resolver.js.map