import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; export const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'); export const CODEX_CONFIG_PATH = path.join(CODEX_HOME, 'config.toml'); export const CODEX_GLOBAL_STATE_PATH = path.join(CODEX_HOME, '.codex-global-state.json'); export const CODEX_MODELS_CACHE_PATH = path.join(CODEX_HOME, 'models_cache.json'); export const CODEX_SESSIONS_DIR = path.join(CODEX_HOME, 'sessions'); export const CODEX_SESSION_INDEX = path.join(CODEX_HOME, 'session_index.jsonl'); function stripQuotes(value) { const trimmed = String(value || '').trim(); if ( (trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'")) ) { return trimmed.slice(1, -1); } return trimmed; } function shortModelName(model) { if (!model) { return '5.5 中'; } return model .replace(/^gpt-/i, '') .replace(/-codex.*$/i, '') .replace(/-mini$/i, ' mini') + ' 中'; } function publicModel(entry) { if (!entry?.slug) { return null; } if (entry.visibility && entry.visibility !== 'list') { return null; } return { value: entry.slug, label: entry.display_name || entry.slug }; } export async function readCodexModels(currentModel = 'gpt-5.5') { const models = new Map(); try { const raw = await fs.readFile(CODEX_MODELS_CACHE_PATH, 'utf8'); const parsed = JSON.parse(raw); const entries = Array.isArray(parsed.models) ? parsed.models : []; for (const entry of entries) { const model = publicModel(entry); if (model && !models.has(model.value)) { models.set(model.value, model); } } } catch (error) { if (error.code !== 'ENOENT') { console.warn('[config] Failed to read Codex model cache:', error.message); } } if (currentModel && !models.has(currentModel)) { models.set(currentModel, { value: currentModel, label: currentModel }); } return [...models.values()]; } export async function readCodexWorkspaceState() { try { const raw = await fs.readFile(CODEX_GLOBAL_STATE_PATH, 'utf8'); const parsed = JSON.parse(raw); const labels = parsed['electron-workspace-root-labels'] || {}; const orderedRoots = [ ...(Array.isArray(parsed['project-order']) ? parsed['project-order'] : []), ...(Array.isArray(parsed['electron-saved-workspace-roots']) ? parsed['electron-saved-workspace-roots'] : []) ]; const seen = new Set(); const projects = []; for (const root of orderedRoots) { if (!root || typeof root !== 'string') { continue; } const key = process.platform === 'win32' ? root.toLowerCase() : root; if (seen.has(key)) { continue; } seen.add(key); projects.push({ path: root, label: typeof labels[root] === 'string' ? labels[root] : null }); } return { projects }; } catch (error) { if (error.code !== 'ENOENT') { console.warn('[config] Failed to read Codex workspace state:', error.message); } return { projects: [] }; } } export async function readCodexConfig() { const fallback = { provider: 'codex', model: 'gpt-5.5', modelShort: '5.5 中', reasoningEffort: null, baseUrl: null, models: [{ value: 'gpt-5.5', label: 'gpt-5.5' }], projects: [] }; let raw; try { raw = await fs.readFile(CODEX_CONFIG_PATH, 'utf8'); } catch (error) { if (error.code !== 'ENOENT') { console.warn('[config] Failed to read Codex config:', error.message); } fallback.models = await readCodexModels(fallback.model); return fallback; } const config = { ...fallback, projects: [] }; const projectMap = new Map(); const providerBaseUrls = new Map(); let currentProject = null; let currentProvider = null; for (const rawLine of raw.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) { continue; } const projectMatch = line.match(/^\[projects\.(?:'([^']+)'|"([^"]+)")\]$/); if (projectMatch) { currentProject = stripQuotes(projectMatch[1] || projectMatch[2]); currentProvider = null; if (!projectMap.has(currentProject)) { projectMap.set(currentProject, { path: currentProject, trustLevel: null }); } continue; } const providerMatch = line.match(/^\[model_providers\.(?:'([^']+)'|"([^"]+)"|([^\]]+))\]$/); if (providerMatch) { currentProject = null; currentProvider = stripQuotes(providerMatch[1] || providerMatch[2] || providerMatch[3]); continue; } if (line.startsWith('[')) { currentProject = null; currentProvider = null; continue; } const assignment = line.match(/^([A-Za-z0-9_]+)\s*=\s*(.+)$/); if (!assignment) { continue; } const key = assignment[1]; const value = stripQuotes(assignment[2]); if (currentProject) { if (key === 'trust_level') { projectMap.get(currentProject).trustLevel = value; } continue; } if (currentProvider) { if (key === 'base_url') { providerBaseUrls.set(currentProvider, value); } continue; } if (key === 'model_provider') { config.provider = value; } else if (key === 'model') { config.model = value; } else if (key === 'model_reasoning_effort') { config.reasoningEffort = value; } } const cwd = process.cwd(); if (!projectMap.has(cwd)) { projectMap.set(cwd, { path: cwd, trustLevel: 'trusted' }); } config.modelShort = shortModelName(config.model); config.baseUrl = providerBaseUrls.get(config.provider) || (config.provider === 'cliproxyapi' ? 'http://127.0.0.1:8317/v1' : null); config.models = await readCodexModels(config.model); config.projects = [...projectMap.values()]; return config; }