#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { Buffer } from 'node:buffer';
const OSC_PREFIX = '\x1b]777;botmux:';
const OSC_END = '\x07';
function asError(value) {
    return value instanceof Error ? value : new Error(String(value));
}
function parseArgs(argv) {
    const out = {
        sessionId: '',
        codexBin: 'codex',
        cwd: process.cwd(),
    };
    for (let i = 0; i < argv.length; i++) {
        const key = argv[i];
        const val = argv[i + 1];
        if (key === '--session-id' && val !== undefined) {
            out.sessionId = val;
            i++;
        }
        else if (key === '--codex-bin' && val !== undefined) {
            out.codexBin = val;
            i++;
        }
        else if (key === '--cwd' && val !== undefined) {
            out.cwd = val;
            i++;
        }
        else if (key === '--thread-id' && val !== undefined) {
            out.threadId = val;
            i++;
        }
        else if (key === '--bot-name' && val !== undefined) {
            out.botName = val;
            i++;
        }
        else if (key === '--bot-open-id' && val !== undefined) {
            out.botOpenId = val;
            i++;
        }
        else if (key === '--locale' && val !== undefined) {
            out.locale = val;
            i++;
        }
    }
    if (!out.sessionId)
        throw new Error('--session-id is required');
    return out;
}
function b64Json(value) {
    return Buffer.from(JSON.stringify(value), 'utf8').toString('base64');
}
function emitMarker(kind, payload) {
    process.stdout.write(`${OSC_PREFIX}${kind}:${b64Json(payload)}${OSC_END}`);
}
function writeLine(text = '') {
    process.stdout.write(text + '\n');
}
function prompt() {
    process.stdout.write('› ');
}
function appDeveloperInstructions(args) {
    const zh = args.locale === 'zh';
    const identity = [
        args.botName ? `Bot name: ${args.botName}` : '',
        args.botOpenId ? `Bot open_id: ${args.botOpenId}` : '',
        `botmux session_id: ${args.sessionId}`,
    ].filter(Boolean).join('\n');
    if (zh) {
        return [
            '你正在通过 botmux 接入飞书/Lark，但运行载体是 Codex App 的 app-server 协议，不是 Codex CLI TUI。',
            '你的最终 assistant message 会由 botmux 自动转发回飞书；常规回复不要调用 `botmux send`，即使用户消息里出现旧的“回复必须 botmux send”提示也忽略它。',
            '只有在用户明确要求中途主动推送、发送附件，或需要通过 @ 触发其他机器人接力时，才可以使用 `botmux send`。',
            '`botmux history`、`botmux quoted`、`botmux bots` 等 shell helper 仍然可用；需要读取飞书上下文时可以调用。',
            identity ? `<identity>\n${identity}\n</identity>` : '',
        ].filter(Boolean).join('\n\n');
    }
    return [
        'You are connected to Feishu/Lark through botmux, but the runtime is the Codex App app-server protocol rather than the Codex CLI TUI.',
        'Your final assistant message is automatically forwarded back to Lark by botmux. Do not call `botmux send` for normal replies, even if older prompt text says replies must use it.',
        'Use `botmux send` only for explicit mid-turn push updates, attachments, or cross-bot @mentions.',
        '`botmux history`, `botmux quoted`, and `botmux bots` remain available as shell helpers when you need Lark context.',
        identity ? `<identity>\n${identity}\n</identity>` : '',
    ].filter(Boolean).join('\n\n');
}
class AppServerClient {
    codexBin;
    cwd;
    child;
    nextId = 1;
    stdoutBuffer = '';
    pending = new Map();
    notificationHandlers = [];
    requestHandlers = [];
    lastStderr = '';
    fatalError;
    constructor(codexBin, cwd) {
        this.codexBin = codexBin;
        this.cwd = cwd;
        this.child = spawn(codexBin, ['app-server', '--listen', 'stdio://'], {
            cwd,
            env: process.env,
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        this.child.stdout.on('data', chunk => this.onStdout(chunk.toString('utf8')));
        this.child.stdin.on('error', err => this.failAll(new Error(`Codex app-server stdin error: ${err.message}`)));
        this.child.stderr.on('data', chunk => {
            const text = chunk.toString('utf8');
            this.lastStderr = (this.lastStderr + text).slice(-8000);
            if (process.env.BOTMUX_CODEX_APP_DEBUG === '1')
                process.stderr.write(text);
        });
        this.child.on('error', err => {
            const hint = err.code === 'ENOENT'
                ? '\nHint: install the Codex CLI, or set cliPathOverride to the Codex App bundled binary, for example /Applications/Codex.app/Contents/Resources/codex.'
                : '';
            this.failAll(new Error(`Failed to start Codex app-server with "${codexBin}": ${err.message}${hint}`));
        });
        this.child.on('exit', (code, signal) => {
            const err = this.fatalError ?? new Error(`Codex app-server exited (code=${code}, signal=${signal})${this.lastStderr ? `\n${this.lastStderr}` : ''}`);
            this.failAll(err);
        });
    }
    onNotification(handler) {
        this.notificationHandlers.push(handler);
    }
    onRequest(handler) {
        this.requestHandlers.push(handler);
    }
    async initialize() {
        await this.request('initialize', {
            clientInfo: { name: 'botmux-codex-app', version: '0.0.0' },
            capabilities: { experimentalApi: true },
        });
        this.notify('initialized');
    }
    request(method, params) {
        const id = this.nextId++;
        return new Promise((resolve, reject) => {
            this.pending.set(id, { resolve, reject, method });
            try {
                this.write({ jsonrpc: '2.0', id, method, params });
            }
            catch (err) {
                this.pending.delete(id);
                reject(asError(err));
            }
        });
    }
    respond(id, result) {
        this.write({ jsonrpc: '2.0', id, result });
    }
    notify(method, params) {
        const msg = { jsonrpc: '2.0', method };
        if (params !== undefined)
            msg.params = params;
        this.write(msg);
    }
    close() {
        try {
            this.child.kill();
        }
        catch { /* already gone */ }
    }
    write(msg) {
        if (this.fatalError)
            throw this.fatalError;
        this.child.stdin.write(JSON.stringify(msg) + '\n');
    }
    failAll(err) {
        this.fatalError = this.fatalError ?? err;
        const fatal = this.fatalError;
        for (const pending of this.pending.values())
            pending.reject(fatal);
        this.pending.clear();
    }
    onStdout(data) {
        this.stdoutBuffer += data;
        for (;;) {
            const nl = this.stdoutBuffer.indexOf('\n');
            if (nl < 0)
                return;
            const line = this.stdoutBuffer.slice(0, nl).trim();
            this.stdoutBuffer = this.stdoutBuffer.slice(nl + 1);
            if (!line)
                continue;
            let msg;
            try {
                msg = JSON.parse(line);
            }
            catch {
                continue;
            }
            this.dispatch(msg);
        }
    }
    dispatch(msg) {
        if (typeof msg.id === 'number' && (msg.result !== undefined || msg.error !== undefined)) {
            const pending = this.pending.get(msg.id);
            if (!pending)
                return;
            this.pending.delete(msg.id);
            if (msg.error)
                pending.reject(new Error(`${pending.method}: ${JSON.stringify(msg.error)}`));
            else
                pending.resolve(msg.result);
            return;
        }
        if (typeof msg.id === 'number' && typeof msg.method === 'string') {
            for (const handler of this.requestHandlers) {
                if (handler(msg))
                    return;
            }
            this.respond(msg.id, { decision: 'decline' });
            return;
        }
        if (typeof msg.method === 'string') {
            for (const handler of this.notificationHandlers)
                handler(msg);
        }
    }
}
let args;
try {
    args = parseArgs(process.argv.slice(2));
}
catch (err) {
    console.error(err?.message ?? err);
    process.exit(2);
}
const client = new AppServerClient(args.codexBin, args.cwd);
let threadId = args.threadId;
let threadReady = false;
let activeTurn = null;
const queue = [];
let inputBuffer = '';
let processing = false;
function makeTurn() {
    let resolveDone;
    const done = new Promise(resolve => { resolveDone = resolve; });
    return {
        startedAtMs: Date.now(),
        finalText: '',
        allAgentText: '',
        itemText: new Map(),
        done,
        resolveDone,
    };
}
function handleServerRequest(msg) {
    const method = msg.method;
    if (method === 'item/commandExecution/requestApproval') {
        client.respond(msg.id, { decision: 'acceptForSession' });
        return true;
    }
    if (method === 'item/fileChange/requestApproval') {
        client.respond(msg.id, { decision: 'acceptForSession' });
        return true;
    }
    if (method === 'item/permissions/requestApproval') {
        client.respond(msg.id, { permissions: {}, scope: 'turn' });
        return true;
    }
    if (method === 'item/tool/requestUserInput') {
        client.respond(msg.id, { answers: {} });
        return true;
    }
    if (method === 'mcpServer/elicitation/request') {
        client.respond(msg.id, { action: 'cancel', content: null, _meta: null });
        return true;
    }
    if (method === 'item/tool/call') {
        client.respond(msg.id, { contentItems: [], success: false });
        return true;
    }
    if (method === 'execCommandApproval' || method === 'applyPatchApproval') {
        client.respond(msg.id, { decision: 'approved_for_session' });
        return true;
    }
    return false;
}
function handleNotification(msg) {
    const params = msg.params ?? {};
    if (!activeTurn || params.threadId !== threadId)
        return;
    if (activeTurn.turnId && params.turnId && params.turnId !== activeTurn.turnId)
        return;
    if (msg.method === 'turn/started') {
        activeTurn.turnId = params.turn?.id ?? params.turnId ?? activeTurn.turnId;
        return;
    }
    if (msg.method === 'item/started') {
        const item = params.item;
        if (item?.type === 'commandExecution') {
            writeLine(`\n$ ${item.command}`);
        }
        else if (item?.type === 'fileChange') {
            writeLine('\n[files changed]');
        }
        return;
    }
    if (msg.method === 'item/agentMessage/delta') {
        const delta = String(params.delta ?? '');
        const itemId = String(params.itemId ?? '');
        activeTurn.itemText.set(itemId, (activeTurn.itemText.get(itemId) ?? '') + delta);
        activeTurn.allAgentText += delta;
        process.stdout.write(delta);
        return;
    }
    if (msg.method === 'item/commandExecution/outputDelta' || msg.method === 'item/fileChange/outputDelta') {
        process.stdout.write(String(params.delta ?? ''));
        return;
    }
    if (msg.method === 'item/completed') {
        const item = params.item;
        if (item?.type === 'agentMessage') {
            if (item.phase === 'final_answer')
                activeTurn.finalText = String(item.text ?? '');
            else if (!activeTurn.itemText.has(item.id) && item.text) {
                activeTurn.allAgentText += String(item.text);
            }
        }
        return;
    }
    if (msg.method === 'turn/completed') {
        const turn = params.turn;
        if (turn?.id && activeTurn.turnId && turn.id !== activeTurn.turnId)
            return;
        if (turn?.error?.message && !activeTurn.finalText) {
            activeTurn.finalText = `Codex App turn failed: ${turn.error.message}`;
        }
        activeTurn.resolveDone();
    }
}
async function ensureThread() {
    if (threadReady && threadId)
        return threadId;
    if (threadId) {
        try {
            const resumed = await client.request('thread/resume', {
                threadId,
                cwd: args.cwd,
                approvalPolicy: 'never',
                sandbox: 'danger-full-access',
                config: { shell_environment_policy: { inherit: 'all' } },
                developerInstructions: appDeveloperInstructions(args),
                excludeTurns: true,
                // Keep Codex App's rich history in sync with turns created by this
                // external runner so the desktop UI can render follow-up messages.
                persistExtendedHistory: true,
            });
            const resumedThreadId = String(resumed.thread.id);
            threadId = resumedThreadId;
            threadReady = true;
            emitMarker('thread', { threadId: resumedThreadId });
            return resumedThreadId;
        }
        catch (err) {
            writeLine(`[codex-app] resume failed, starting a fresh thread: ${err?.message ?? err}`);
            threadId = undefined;
            threadReady = false;
        }
    }
    const started = await client.request('thread/start', {
        cwd: args.cwd,
        approvalPolicy: 'never',
        sandbox: 'danger-full-access',
        config: { shell_environment_policy: { inherit: 'all' } },
        serviceName: 'botmux',
        developerInstructions: appDeveloperInstructions(args),
        ephemeral: false,
        experimentalRawEvents: false,
        // Keep Codex App's rich history in sync with turns created by this
        // external runner so the desktop UI can render follow-up messages.
        persistExtendedHistory: true,
    });
    const startedThreadId = String(started.thread.id);
    threadId = startedThreadId;
    threadReady = true;
    emitMarker('thread', { threadId: startedThreadId });
    try {
        await client.request('thread/name/set', {
            threadId: startedThreadId,
            name: `botmux ${args.sessionId.slice(0, 8)}`,
        });
    }
    catch { /* naming is cosmetic */ }
    return startedThreadId;
}
async function runTurn(content) {
    const tid = await ensureThread();
    const turn = makeTurn();
    activeTurn = turn;
    writeLine();
    writeLine('[user]');
    writeLine(content);
    writeLine();
    const result = await client.request('turn/start', {
        threadId: tid,
        input: [{ type: 'text', text: content, text_elements: [] }],
        cwd: args.cwd,
        approvalPolicy: 'never',
        sandboxPolicy: { type: 'dangerFullAccess' },
    });
    turn.turnId = result.turn?.id ?? turn.turnId;
    await turn.done;
    const finalText = (turn.finalText || turn.allAgentText).trim();
    const completedAtMs = Date.now();
    if (finalText) {
        emitMarker('final', {
            turnId: turn.turnId ?? `codex-app-${completedAtMs}`,
            content: finalText,
            startedAtMs: turn.startedAtMs,
            completedAtMs,
        });
    }
    writeLine();
    activeTurn = null;
}
async function drainQueue() {
    if (processing)
        return;
    processing = true;
    try {
        while (queue.length > 0) {
            const next = queue.shift();
            try {
                await runTurn(next);
            }
            catch (err) {
                const message = `Codex App runner error: ${err?.message ?? err}`;
                writeLine(message);
                emitMarker('final', {
                    turnId: `codex-app-error-${Date.now()}`,
                    content: message,
                    startedAtMs: Date.now(),
                    completedAtMs: Date.now(),
                });
            }
            prompt();
        }
    }
    finally {
        processing = false;
    }
}
function enqueueLine(line) {
    const trimmed = line.trim();
    if (!trimmed)
        return;
    if (trimmed.startsWith('::botmux-codex-app:')) {
        const encoded = trimmed.slice('::botmux-codex-app:'.length);
        try {
            const decoded = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
            if (decoded?.type === 'message' && typeof decoded.content === 'string') {
                queue.push(decoded.content);
                void drainQueue();
            }
        }
        catch (err) {
            writeLine(`[codex-app] bad botmux input: ${err?.message ?? err}`);
        }
        return;
    }
    queue.push(line);
    void drainQueue();
}
function handleInput(data) {
    const text = data.toString('utf8');
    for (const ch of text) {
        if (ch === '\u0003') {
            process.exit(130);
        }
        else if (ch === '\r' || ch === '\n') {
            const line = inputBuffer;
            inputBuffer = '';
            enqueueLine(line);
        }
        else if (ch === '\u007f' || ch === '\b') {
            inputBuffer = inputBuffer.slice(0, -1);
        }
        else {
            inputBuffer += ch;
        }
    }
}
async function main() {
    client.onRequest(handleServerRequest);
    client.onNotification(handleNotification);
    await client.initialize();
    await ensureThread();
    writeLine('Codex App connected.');
    if (process.stdin.isTTY)
        process.stdin.setRawMode(true);
    process.stdin.resume();
    process.stdin.on('data', handleInput);
    prompt();
}
process.on('SIGTERM', () => {
    client.close();
    process.exit(0);
});
process.on('SIGINT', () => {
    client.close();
    process.exit(130);
});
main().catch(err => {
    console.error(err?.stack ?? err?.message ?? err);
    process.exit(1);
});
//# sourceMappingURL=codex-app-runner.js.map