#!/usr/bin/env node const crypto = require('crypto'); const path = require('path'); const readline = require('readline'); const { spawn } = require('child_process'); const args = process.argv.slice(2); if (args[0] !== 'app-server') { const child = spawn(process.execPath, [path.join(__dirname, 'mock-codex.js'), ...args], { stdio: 'inherit', }); child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); process.exit(code || 0); }); child.on('error', (err) => { console.error(err.stack || err.message); process.exit(1); }); return; } const threads = new Map(); const pendingServerRequests = new Map(); const resumeMismatchThreads = new Set(); let nextServerRequestId = 1; let mcpReloadCount = 0; function send(message) { process.stdout.write(`${JSON.stringify(message)}\n`); } function textFromInput(input) { return (Array.isArray(input) ? input : []) .map((item) => { if (item?.type === 'text') return item.text || ''; if (item?.type === 'localImage') return `[image:${path.basename(item.path || 'image')}]`; return ''; }) .filter(Boolean) .join('\n') .trim(); } function tokenUsage(text) { const inputTokens = Math.max(1, Math.ceil(String(text || '').length / 4)); const outputTokens = 7; return { last: { inputTokens, cachedInputTokens: 1, outputTokens, reasoningOutputTokens: 0, totalTokens: inputTokens + outputTokens + 1, }, total: { inputTokens, cachedInputTokens: 1, outputTokens, reasoningOutputTokens: 0, totalTokens: inputTokens + outputTokens + 1, }, }; } function retryScenarioKey(text, marker) { return new RegExp(marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i').test(String(text || '')) ? marker : String(text || ''); } function collaborationSummary(params = {}) { const collaborationMode = params.collaborationMode; const settings = collaborationMode?.settings || {}; return JSON.stringify({ mode: collaborationMode?.mode || null, hasModel: Boolean(settings.model), hasDeveloperInstructions: /Codex sub-agent spawning rules/.test(String(settings.developer_instructions || '')), hasWaitAgentRetryGuidance: /wait_agent[\s\S]*timeout_ms[\s\S]*additional wait_agent rounds/.test(String(settings.developer_instructions || '')), hasReasoningEffort: Object.prototype.hasOwnProperty.call(settings, 'reasoning_effort'), hasTopLevelModel: Object.prototype.hasOwnProperty.call(params, 'model'), hasTopLevelEffort: Object.prototype.hasOwnProperty.call(params, 'effort'), }); } function ensureThread(threadId, params = {}) { const id = threadId || `app-thread-${crypto.randomUUID()}`; if (!threads.has(id)) { threads.set(id, { id, cwd: params.cwd || process.cwd(), dynamicTools: Array.isArray(params.dynamicTools) ? params.dynamicTools : [], config: params.config && typeof params.config === 'object' ? params.config : {}, activeTurnId: null, timer: null, steers: [], capacityRetryAttempts: new Map(), reconnectRetryAttempts: new Map(), goal: null, }); } const thread = threads.get(id); if (Array.isArray(params.dynamicTools)) thread.dynamicTools = params.dynamicTools; if (params.config && typeof params.config === 'object') thread.config = params.config; return thread; } function threadPayload(thread) { return { id: thread.id, cwd: thread.cwd, status: thread.activeTurnId ? 'running' : 'idle', turns: [], }; } function emitChildCollabTurn(threadId, turnId, finalMessage) { const childThread = ensureThread(threadId); childThread.activeTurnId = turnId; send({ method: 'turn/started', params: { threadId, turn: { id: turnId, status: 'running', items: [], }, }, }); send({ method: 'item/agentMessage/delta', params: { threadId, turnId, itemId: 'agent-msg', delta: finalMessage, }, }); send({ method: 'item/completed', params: { threadId, turnId, completedAtMs: Date.now(), item: { id: 'agent-msg', type: 'agentMessage', content: [{ type: 'text', text: finalMessage }], status: 'completed', }, }, }); send({ method: 'turn/completed', params: { threadId, turn: { id: turnId, status: 'completed', items: [], }, }, }); childThread.activeTurnId = null; } function completeTurn(thread, turnId, text, status = 'completed') { if (thread.activeTurnId !== turnId) return; const suffix = thread.steers.length > 0 ? ` | steer: ${thread.steers.join(' | ')}` : ''; const responseText = `Codex App mock handled: ${text}${suffix}`; send({ method: 'item/agentMessage/delta', params: { threadId: thread.id, turnId, itemId: 'agent-msg', delta: responseText, }, }); if (/tool/i.test(text)) { send({ method: 'item/started', params: { threadId: thread.id, turnId, startedAtMs: Date.now(), item: { id: 'tool-cmd', type: 'commandExecution', command: '/bin/bash -lc echo codexapp', status: 'inProgress', }, }, }); send({ method: 'item/commandExecution/outputDelta', params: { threadId: thread.id, turnId, itemId: 'tool-cmd', delta: 'codexapp\n', }, }); send({ method: 'item/completed', params: { threadId: thread.id, turnId, completedAtMs: Date.now(), item: { id: 'tool-cmd', type: 'commandExecution', command: '/bin/bash -lc echo codexapp', aggregatedOutput: 'codexapp\n', exitCode: 0, status: 'completed', }, }, }); } if (/huge output/i.test(text)) { const hugeOutput = `huge-output-start\n${'0123456789abcdef'.repeat(30000)}\nhuge-output-end\n`; send({ method: 'item/started', params: { threadId: thread.id, turnId, startedAtMs: Date.now(), item: { id: 'huge-tool', type: 'commandExecution', command: '/bin/bash -lc huge-output', status: 'inProgress', }, }, }); send({ method: 'item/commandExecution/outputDelta', params: { threadId: thread.id, turnId, itemId: 'huge-tool', delta: hugeOutput, }, }); send({ method: 'item/completed', params: { threadId: thread.id, turnId, completedAtMs: Date.now(), item: { id: 'huge-tool', type: 'commandExecution', command: '/bin/bash -lc huge-output', aggregatedOutput: hugeOutput, exitCode: 0, status: 'completed', }, }, }); } if (/subagent/i.test(text)) { send({ method: 'item/started', params: { threadId: thread.id, turnId, startedAtMs: Date.now(), item: { id: 'tool-collab', type: 'collabAgentToolCall', tool: 'spawn_agent', prompt: '整理前端改动并回报状态', receiverThreadIds: ['child-thread-a', 'child-thread-b'], agentsStates: { 'child-thread-a': { name: '实现代理', role: 'frontend', status: 'in_progress', summary: '正在补充结构化渲染与样式', }, 'child-thread-b': { name: '验证代理', role: 'qa', status: 'completed', summary: '已完成基础事件链检查', }, }, status: 'inProgress', }, }, }); send({ method: 'item/completed', params: { threadId: thread.id, turnId, completedAtMs: Date.now(), item: { id: 'tool-collab', type: 'collabAgentToolCall', tool: 'spawn_agent', prompt: '整理前端改动并回报状态', receiverThreadIds: ['child-thread-a', 'child-thread-b'], agentsStates: { 'child-thread-a': { name: '实现代理', role: 'frontend', status: 'completed', summary: '结构化渲染已完成', }, 'child-thread-b': { name: '验证代理', role: 'qa', status: 'completed', summary: '事件链与持久化检查通过', }, }, status: 'completed', }, }, }); emitChildCollabTurn( 'child-thread-a', 'child-turn-a', '子代理最终消息:结构化渲染和关闭按钮链路已完成。' ); emitChildCollabTurn( 'child-thread-b', 'child-turn-b', '子代理最终消息:事件路由、持久化和状态推送检查通过。' ); } send({ method: 'thread/tokenUsage/updated', params: { threadId: thread.id, turnId, tokenUsage: tokenUsage(`${text}${suffix}`), }, }); send({ method: 'turn/completed', params: { threadId: thread.id, turn: { id: turnId, status, items: [], }, }, }); thread.activeTurnId = null; thread.timer = null; thread.steers = []; } function requestClient(method, params, callback) { const id = `mock-server-request-${nextServerRequestId++}`; pendingServerRequests.set(id, callback); send({ id, method, params }); } function completeDynamicToolTurn(thread, turnId, text) { const callId = 'dynamic-ccweb-list'; send({ method: 'item/started', params: { threadId: thread.id, turnId, startedAtMs: Date.now(), item: { id: callId, type: 'dynamicToolCall', namespace: 'ccweb', tool: 'ccweb_list_conversations', arguments: { limit: 5 }, status: 'inProgress', }, }, }); requestClient('item/tool/call', { threadId: thread.id, turnId, callId, namespace: 'ccweb', tool: 'ccweb_list_conversations', arguments: { limit: 5 }, }, (message) => { const result = message.result || {}; send({ method: 'item/completed', params: { threadId: thread.id, turnId, completedAtMs: Date.now(), item: { id: callId, type: 'dynamicToolCall', namespace: 'ccweb', tool: 'ccweb_list_conversations', arguments: { limit: 5 }, status: result.success === false ? 'failed' : 'completed', contentItems: result.contentItems || [], success: result.success !== false, durationMs: 1, }, }, }); completeTurn(thread, turnId, `dynamic result: ${JSON.stringify(result)}`); }); } function completeMcpToolTurn(thread, turnId) { const itemId = 'mcp-ccweb-list'; const ccwebConfig = thread.config?.['mcp_servers.ccweb'] || null; const projectConfig = thread.config?.['mcp_servers.reg-app-project'] || null; const env = ccwebConfig?.env || {}; const payload = { ok: true, currentConversationId: env.CC_WEB_SOURCE_SESSION_ID || null, sourceHopCount: env.CC_WEB_CROSS_HOP_COUNT || null, hasCcwebMcpConfig: Boolean(ccwebConfig), hasProjectMcpConfig: Boolean(projectConfig), ccwebCommand: ccwebConfig?.command || null, ccwebArgs: ccwebConfig?.args || null, }; const itemBase = { id: itemId, type: 'mcpToolCall', server: 'ccweb', tool: 'ccweb_list_conversations', arguments: { limit: 5 }, }; send({ method: 'item/started', params: { threadId: thread.id, turnId, startedAtMs: Date.now(), item: { ...itemBase, status: 'inProgress', }, }, }); send({ method: 'item/completed', params: { threadId: thread.id, turnId, completedAtMs: Date.now(), item: { ...itemBase, status: 'completed', result: { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], structuredContent: payload, isError: false, }, }, }, }); completeTurn(thread, turnId, `mcp result: ${JSON.stringify(payload)}`); } function emitCapacityError(thread, turnId) { send({ method: 'error', params: { threadId: thread.id, turnId, type: 'error', error: { type: 'service_unavailable_error', code: 'server_is_overloaded', message: 'Our servers are currently overloaded. Please try again later.', param: null, }, sequence_number: 2, }, }); thread.activeTurnId = null; } function emitPartialCapacityOutput(thread, turnId) { send({ method: 'item/agentMessage/delta', params: { threadId: thread.id, turnId, itemId: 'agent-msg', delta: 'partial capacity output before retry', }, }); send({ method: 'item/started', params: { threadId: thread.id, turnId, startedAtMs: Date.now(), item: { id: 'capacity-tool', type: 'commandExecution', command: '/bin/bash -lc echo capacity', status: 'inProgress', }, }, }); send({ method: 'item/commandExecution/outputDelta', params: { threadId: thread.id, turnId, itemId: 'capacity-tool', delta: 'capacity tool output\n', }, }); } function completeGuidedInputTurn(thread, turnId) { requestClient('item/tool/requestUserInput', { threadId: thread.id, turnId, itemId: 'guided-input-call', questions: [{ id: 'choice', header: '选择', question: '选择一个测试答案', isOther: true, isSecret: false, options: [ { label: 'A', description: '测试选项 A' }, { label: 'B', description: '测试选项 B' }, ], }], }, (message) => { const answer = message.result?.answers?.choice?.answers?.[0] || 'empty'; completeTurn(thread, turnId, `guided answer: ${answer}`); }); } function completeApprovalTurn(thread, turnId) { requestClient('item/commandExecution/requestApproval', { threadId: thread.id, turnId, itemId: 'approval-command-call', reason: 'Need to run an approval-gated command', command: 'echo approved', cwd: thread.cwd, }, (message) => { const decision = message.result?.decision || 'missing'; completeTurn(thread, turnId, `approval decision: ${decision}`); }); } function completeEmptyReasoningTurn(thread, turnId, text) { send({ method: 'item/started', params: { threadId: thread.id, turnId, startedAtMs: Date.now(), item: { id: 'reasoning-empty', type: 'reasoning', content: [], summary: [], status: 'inProgress', }, }, }); send({ method: 'item/completed', params: { threadId: thread.id, turnId, completedAtMs: Date.now(), item: { id: 'reasoning-empty', type: 'reasoning', content: [], summary: [], status: 'completed', }, }, }); completeTurn(thread, turnId, text); } function startTurn(params) { const thread = ensureThread(params.threadId, params); const turnId = `app-turn-${crypto.randomUUID()}`; const text = textFromInput(params.input); thread.activeTurnId = turnId; thread.steers = []; send({ method: 'turn/started', params: { threadId: thread.id, turn: { id: turnId, status: 'running', items: [], }, }, }); if (/runtime warning/i.test(text)) { const message = 'Heads up: Long threads and multiple compactions can cause the model to be less accurate. Start a new thread when possible to keep threads small and targeted.'; for (let i = 0; i < 2; i += 1) { send({ method: 'warning', params: { threadId: thread.id, turnId, message, }, }); } } if (/codexapp capacity retry/i.test(text)) { const retryKey = retryScenarioKey(text, 'codexapp capacity retry'); const attempts = (thread.capacityRetryAttempts.get(retryKey) || 0) + 1; thread.capacityRetryAttempts.set(retryKey, attempts); if (attempts <= 2) { if (attempts === 2) emitPartialCapacityOutput(thread, turnId); emitCapacityError(thread, turnId); return { turn: { id: turnId, status: 'running', items: [] } }; } } if (/codexapp reconnect retry/i.test(text)) { const retryKey = retryScenarioKey(text, 'codexapp reconnect retry'); const attempts = (thread.reconnectRetryAttempts.get(retryKey) || 0) + 1; thread.reconnectRetryAttempts.set(retryKey, attempts); if (attempts === 1) { emitPartialCapacityOutput(thread, turnId); send({ method: 'error', params: { threadId: thread.id, turnId, message: 'Reconnecting... 1/5', }, }); thread.activeTurnId = null; return { turn: { id: turnId, status: 'running', items: [] } }; } } if (/codexapp retry thread mismatch/i.test(text)) { const retryKey = retryScenarioKey(text, 'codexapp retry thread mismatch'); const attempts = (thread.capacityRetryAttempts.get(retryKey) || 0) + 1; thread.capacityRetryAttempts.set(retryKey, attempts); if (attempts === 1) { resumeMismatchThreads.add(thread.id); emitCapacityError(thread, turnId); return { turn: { id: turnId, status: 'running', items: [] } }; } } if (/collaboration/i.test(text)) { completeTurn(thread, turnId, `collaboration mode: ${collaborationSummary(params)}`); return { turn: { id: turnId, status: 'running', items: [] } }; } if (/empty reasoning/i.test(text)) { completeEmptyReasoningTurn(thread, turnId, text); return { turn: { id: turnId, status: 'running', items: [] } }; } if (/dynamic/i.test(text)) { completeMcpToolTurn(thread, turnId); return { turn: { id: turnId, status: 'running', items: [] } }; } if (/guided/i.test(text)) { if (!params.collaborationMode || params.collaborationMode.mode !== 'plan') { completeTurn(thread, turnId, 'guided input unavailable outside plan'); return { turn: { id: turnId, status: 'running', items: [] } }; } completeGuidedInputTurn(thread, turnId); return { turn: { id: turnId, status: 'running', items: [] } }; } if (/approval/i.test(text)) { completeApprovalTurn(thread, turnId); return { turn: { id: turnId, status: 'running', items: [] } }; } const delay = /recover/i.test(text) ? 5000 : /slow/i.test(text) ? 900 : 80; if (/recover/i.test(text)) { send({ method: 'item/agentMessage/delta', params: { threadId: thread.id, turnId, itemId: 'agent-msg', delta: `partial before restart: ${text}`, }, }); send({ method: 'item/started', params: { threadId: thread.id, turnId, startedAtMs: Date.now(), item: { id: 'recover-tool', type: 'commandExecution', command: '/bin/bash -lc echo recover', status: 'inProgress', }, }, }); send({ method: 'item/commandExecution/outputDelta', params: { threadId: thread.id, turnId, itemId: 'recover-tool', delta: 'recover tool output\n', }, }); } thread.timer = setTimeout(() => completeTurn(thread, turnId, text), delay); return { turn: { id: turnId, status: 'running', items: [] } }; } function interruptTurn(params) { const thread = ensureThread(params.threadId, params); if (thread.timer) clearTimeout(thread.timer); if (thread.activeTurnId === params.turnId) { completeTurn(thread, params.turnId, 'interrupted', 'interrupted'); } return {}; } function steerTurn(params) { const thread = ensureThread(params.threadId, params); if (!thread.activeTurnId || thread.activeTurnId !== params.expectedTurnId) { return { error: { code: -32001, message: 'expectedTurnId does not match active turn', }, }; } const text = textFromInput(params.input); if (text) thread.steers.push(text); send({ method: 'item/agentMessage/delta', params: { threadId: thread.id, turnId: thread.activeTurnId, itemId: 'agent-msg', delta: `\n[steer accepted: ${text}]`, }, }); return { result: {} }; } function handleRequest(message) { const id = message.id; const method = message.method; const params = message.params || {}; if (id && !method && pendingServerRequests.has(id)) { const callback = pendingServerRequests.get(id); pendingServerRequests.delete(id); callback(message); return; } try { if (method === 'initialize') { send({ id, result: { serverInfo: { name: 'mock-codex-app-server', version: '0.0.0' } } }); return; } if (method === 'experimentalFeature/enablement/set') { send({ id, result: { enablement: params.enablement || {} } }); return; } if (method === 'collaborationMode/list') { send({ id, result: { data: [{ mode: 'default' }, { mode: 'plan' }] } }); return; } if (method === 'config/mcpServer/reload') { mcpReloadCount += 1; send({ method: 'mcpServer/startupStatus/updated', params: { server: 'ccweb', state: 'starting', message: 'ccweb MCP starting', threadId: null, }, }); send({ method: 'mcpServer/startupStatus/updated', params: { name: 'ccweb', status: 'ready', message: 'ccweb MCP ready CC_WEB_MCP_TOKEN=mock-secret-token', threadId: null, }, }); send({ id, result: { reloaded: true, reloadCount: mcpReloadCount } }); return; } if (method === 'thread/start') { const thread = ensureThread(null, params); send({ id, result: { thread: threadPayload(thread), model: params.model || 'gpt-5.5', cwd: thread.cwd, modelProvider: 'mock', approvalPolicy: params.approvalPolicy || 'never', approvalsReviewer: 'user', sandbox: params.sandbox || 'danger-full-access' } }); return; } if (method === 'thread/resume') { if (params.threadId && resumeMismatchThreads.delete(params.threadId)) { const thread = ensureThread(null, params); send({ id, result: { thread: threadPayload(thread), model: params.model || 'gpt-5.5', cwd: thread.cwd, modelProvider: 'mock', approvalPolicy: params.approvalPolicy || 'never', approvalsReviewer: 'user', sandbox: params.sandbox || 'danger-full-access' } }); return; } const thread = ensureThread(params.threadId, params); send({ id, result: { thread: threadPayload(thread), model: params.model || 'gpt-5.5', cwd: thread.cwd, modelProvider: 'mock', approvalPolicy: params.approvalPolicy || 'never', approvalsReviewer: 'user', sandbox: params.sandbox || 'danger-full-access' } }); return; } if (method === 'thread/goal/get') { const thread = ensureThread(params.threadId, params); send({ id, result: { goal: thread.goal } }); return; } if (method === 'thread/goal/set') { const thread = ensureThread(params.threadId, params); const now = Date.now(); const previous = thread.goal || {}; const objective = String(params.objective || previous.objective || 'mock goal').trim(); thread.goal = { threadId: thread.id, objective, status: String(params.status || previous.status || 'active'), tokenBudget: previous.tokenBudget ?? null, tokensUsed: previous.tokensUsed ?? 3, timeUsedSeconds: previous.timeUsedSeconds ?? 0, createdAt: previous.createdAt || now, updatedAt: now, }; send({ method: 'thread/goal/updated', params: { threadId: thread.id, goal: thread.goal }, }); send({ id, result: { goal: thread.goal } }); return; } if (method === 'thread/goal/clear') { const thread = ensureThread(params.threadId, params); const cleared = !!thread.goal; thread.goal = null; send({ method: 'thread/goal/cleared', params: { threadId: thread.id }, }); send({ id, result: { cleared } }); return; } if (method === 'turn/start') { send({ id, result: startTurn(params) }); return; } if (method === 'turn/steer') { const result = steerTurn(params); send({ id, ...result }); return; } if (method === 'turn/interrupt') { send({ id, result: interruptTurn(params) }); return; } if (method === 'initialized') return; send({ id, error: { code: -32601, message: `Unknown mock method: ${method}` } }); } catch (err) { send({ id, error: { code: -32603, message: err.message || String(err) } }); } } const rl = readline.createInterface({ input: process.stdin }); rl.on('line', (line) => { if (!line.trim()) return; let message; try { message = JSON.parse(line); } catch { return; } if (Object.prototype.hasOwnProperty.call(message, 'id')) handleRequest(message); });