function createAgentRuntime(deps) { const { processEnv, CLAUDE_PATH, CODEX_PATH, MODEL_MAP, loadModelConfig, applyCustomTemplateToSettings, getDefaultCodexModel, loadCodexConfig, prepareCodexCustomRuntime, ccwebMcpServerArg, ccwebMcpServerArgs, internalMcpUrl, internalMcpToken, nodePath, wsSend, truncateObj, sanitizeToolInput, loadSession, saveSession, setRuntimeSessionId, getRuntimeSessionId, } = deps; function tomlString(value) { return JSON.stringify(String(value || '')); } function tomlStringArray(values) { return `[${values.map((value) => tomlString(value)).join(',')}]`; } function tomlKeySegment(value) { const raw = String(value || '').trim(); if (/^[A-Za-z0-9_-]+$/.test(raw)) return raw; return tomlString(raw); } function tomlValue(value) { if (Array.isArray(value)) return `[${value.map((item) => tomlValue(item)).join(',')}]`; if (value && typeof value === 'object') { return `{${Object.entries(value) .map(([key, item]) => `${tomlKeySegment(key)}=${tomlValue(item)}`) .join(',')}}`; } if (typeof value === 'boolean') return value ? 'true' : 'false'; if (typeof value === 'number' && Number.isFinite(value)) return String(value); return tomlString(value); } function createCcwebMcpEnv(session, options = {}) { if (!ccwebMcpServerArg || !internalMcpUrl || !internalMcpToken || !session?.id) return null; const rawHopCount = Number.parseInt(String(options.mcpContext?.hopCount || 0), 10); const hopCount = Number.isFinite(rawHopCount) ? Math.max(0, rawHopCount) : 0; return { CC_WEB_MCP_URL: internalMcpUrl, CC_WEB_MCP_TOKEN: internalMcpToken, CC_WEB_SOURCE_SESSION_ID: session.id, CC_WEB_CROSS_HOP_COUNT: String(hopCount), }; } function appendCcwebMcpConfig(args, mcpEnv) { if (!mcpEnv) return; const envVars = Object.keys(mcpEnv); const serverArgs = Array.isArray(ccwebMcpServerArgs) && ccwebMcpServerArgs.length > 0 ? ccwebMcpServerArgs : [ccwebMcpServerArg]; args.push( '-c', 'mcp_servers.ccweb.type="stdio"', '-c', `mcp_servers.ccweb.command=${tomlString(nodePath || 'node')}`, '-c', `mcp_servers.ccweb.args=${tomlStringArray(serverArgs)}`, '-c', `mcp_servers.ccweb.env_vars=${tomlStringArray(envVars)}`, '-c', 'mcp_servers.ccweb.startup_timeout_sec=10', '-c', 'mcp_servers.ccweb.tool_timeout_sec=60' ); } function appendProjectMcpConfig(args, mcpServer) { const server = String(mcpServer?.server || mcpServer?.name || '').trim(); const config = mcpServer?.config && typeof mcpServer.config === 'object' ? mcpServer.config : null; if (!server || !config) return; const prefix = `mcp_servers.${tomlKeySegment(server)}`; for (const key of ['type', 'command', 'args', 'env', 'env_vars', 'url', 'bearer_token_env_var', 'startup_timeout_sec', 'tool_timeout_sec']) { if (!Object.prototype.hasOwnProperty.call(config, key)) continue; args.push('-c', `${prefix}.${key}=${tomlValue(config[key])}`); } } function appendProjectMcpConfigs(args, mcpServers) { if (!Array.isArray(mcpServers)) return; for (const mcpServer of mcpServers) appendProjectMcpConfig(args, mcpServer); } function buildClaudeSpawnSpec(session, options = {}) { const hasAttachments = Array.isArray(options.attachments) && options.attachments.length > 0; const args = ['-p', '--output-format', 'stream-json', '--verbose']; if (hasAttachments) args.push('--input-format', 'stream-json'); const permMode = session.permissionMode || 'yolo'; switch (permMode) { case 'yolo': args.push('--dangerously-skip-permissions'); break; case 'plan': args.push('--permission-mode', 'plan'); break; case 'default': args.push('--permission-mode', 'default'); break; } if (session.claudeSessionId) { args.push('--resume', session.claudeSessionId); } if (session.model) { args.push('--model', session.model); } const env = { ...processEnv }; delete env.CLAUDECODE; delete env.CLAUDE_CODE; delete env.CC_WEB_PASSWORD; for (const k of Object.keys(env)) { if (k.startsWith('ANTHROPIC_')) delete env[k]; } const modelCfg = loadModelConfig(); if (modelCfg.mode === 'custom' && modelCfg.activeTemplate) { const tpl = (modelCfg.templates || []).find((t) => t.name === modelCfg.activeTemplate); if (tpl) applyCustomTemplateToSettings(tpl); } return { command: CLAUDE_PATH, args, env, cwd: session.cwd || processEnv.HOME || processEnv.USERPROFILE || process.cwd(), parser: 'claude', mode: permMode, resume: !!session.claudeSessionId, }; } function buildCodexSpawnSpec(session, options = {}) { const codexConfig = loadCodexConfig(); const runtimeConfig = prepareCodexCustomRuntime(codexConfig); if (runtimeConfig?.error) { return { error: runtimeConfig.error }; } const runtimeId = getRuntimeSessionId(session); const args = ['exec']; args.push('--json', '--skip-git-repo-check'); const ccwebMcpEnv = createCcwebMcpEnv(session, options); appendCcwebMcpConfig(args, ccwebMcpEnv); appendProjectMcpConfigs(args, options.projectMcpConfigs); const permMode = session.permissionMode || 'yolo'; // `-s/--sandbox` is an option for `codex exec`, but not for `codex exec resume`. // When resuming, it must appear before the `resume` subcommand, otherwise Codex CLI errors // with: "unexpected argument '-s' found". if (runtimeId && permMode === 'plan') { args.push('-s', 'read-only'); } if (runtimeId) args.push('resume'); switch (permMode) { case 'yolo': args.push('--dangerously-bypass-approvals-and-sandbox'); break; case 'plan': if (!runtimeId) args.push('-s', 'read-only'); break; case 'default': default: args.push('--full-auto'); break; } const effectiveModel = session.model || getDefaultCodexModel(); if (effectiveModel) { const raw = String(effectiveModel).trim(); // cc-web UI supports "gpt-5.4(high)" style selection, but Codex CLI expects: // - model: "gpt-5.4" // - reasoning effort: config key `model_reasoning_effort = "high"` const m = raw.match(/^(.*)\((low|medium|high|xhigh)\)\s*$/i); if (m) { const base = String(m[1] || '').trim(); const lvl = String(m[2] || '').trim().toLowerCase(); if (base) args.push('--model', base); // Use TOML string literal to avoid parsing ambiguity. args.push('-c', `model_reasoning_effort="${lvl}"`); } else { args.push('--model', raw); } } if (Array.isArray(options.attachments)) { for (const attachment of options.attachments) { if (attachment?.path) args.push('--image', attachment.path); } } if (runtimeId) { args.push(runtimeId, '-'); } else { if (session.cwd) args.push('-C', session.cwd); args.push('-'); } const env = { ...processEnv, ...(ccwebMcpEnv || {}) }; delete env.CC_WEB_PASSWORD; delete env.CLAUDECODE; delete env.CLAUDE_CODE; if (runtimeConfig?.mode === 'custom') { env.CODEX_HOME = runtimeConfig.homeDir; env.OPENAI_API_KEY = runtimeConfig.apiKey; delete env.OPENAI_BASE_URL; } return { command: CODEX_PATH, args, env, cwd: session.cwd || processEnv.HOME || processEnv.USERPROFILE || process.cwd(), parser: 'codex', mode: permMode, resume: !!runtimeId, }; } function codexToolName(item) { switch (item?.type) { case 'command_execution': return 'CommandExecution'; case 'mcp_tool_call': return 'McpToolCall'; case 'file_change': return 'FileChange'; case 'reasoning': return 'Reasoning'; default: return item?.type || 'CodexItem'; } } function codexToolInput(item) { if (!item) return null; if (item.type === 'command_execution') return { command: item.command || '' }; return truncateObj(item, 500); } function codexToolMeta(item) { if (!item) return null; switch (item.type) { case 'command_execution': return { kind: 'command_execution', title: 'Shell Command', subtitle: item.command || '', exitCode: typeof item.exit_code === 'number' ? item.exit_code : null, status: item.status || null, }; case 'mcp_tool_call': return { kind: 'mcp_tool_call', title: 'MCP Tool', subtitle: item.tool_name || item.name || item.server_name || '', status: item.status || null, }; case 'file_change': return { kind: 'file_change', title: 'File Change', subtitle: item.path || item.file_path || '', status: item.status || null, }; case 'reasoning': return { kind: 'reasoning', title: 'Reasoning', subtitle: typeof item.text === 'string' ? item.text.slice(0, 120) : '', status: item.status || null, }; default: return { kind: item.type || 'codex_item', title: codexToolName(item), subtitle: '', status: item.status || null, }; } } function codexToolResult(item) { if (!item) return ''; if (typeof item.aggregated_output === 'string' && item.aggregated_output) return item.aggregated_output; if (typeof item.text === 'string' && item.text) return item.text; return JSON.stringify(truncateObj(item, 1200)); } function formatAgentMessageDividerTime(date = new Date()) { const pad = (value) => String(value).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; } function createAgentMessageDivider() { const time = formatAgentMessageDividerTime(); return `
`; } function hasAgentMessageBoundaryAtEnd(text) { return /\n\s*(?:---|\*\*\*|___)\s*$/.test(text) || /(?:^|\n)\s*