UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

1,392 lines • 51.7 kB
'use strict';

var ink = require('ink');
var React = require('react');
var persistence = require('./types-BYXHizYk.cjs');
var index = require('./index-BXjnng_s.cjs');
var resolveModelFromMeta = require('./resolveModelFromMeta-CyeSH3Aa.cjs');
var index_js = require('@modelcontextprotocol/sdk/client/index.js');
var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
var z = require('zod');
var types_js = require('@modelcontextprotocol/sdk/types.js');
var child_process = require('child_process');
var node_util = require('node:util');
var BasePermissionHandler = require('./BasePermissionHandler-DG8EAV5r.cjs');
var BaseReasoningProcessor = require('./BaseReasoningProcessor-C2oXu3bi.cjs');
var node_crypto = require('node:crypto');
var os = require('node:os');
var path = require('node:path');
var createSessionMetadata = require('./createSessionMetadata-DbNQwTyC.cjs');
var fs = require('node:fs');
var setupOfflineReconnection = require('./setupOfflineReconnection-DhiemrMG.cjs');
require('axios');
require('chalk');
require('fs');
require('node:events');
require('socket.io-client');
require('tweetnacl');
require('node:module');
require('util');
require('fs/promises');
require('os');
require('crypto');
require('path');
require('url');
require('node:child_process');
require('node:fs/promises');
require('http');
require('net');
require('expo-server-sdk');
require('node:net');
require('node:readline');
require('node:url');
require('node:http');
require('node:https');
require('node:stream/promises');
require('tar');
require('ps-list');
require('cross-spawn');
require('tmp');
require('qrcode-terminal');
require('open');
require('fastify');
require('fastify-type-provider-zod');
require('ws');
require('playwright-core');
require('imapflow');
require('mailparser');
require('yaml');
require('@modelcontextprotocol/sdk/client/streamableHttp.js');
require('readline');
require('@modelcontextprotocol/sdk/server/mcp.js');
require('@modelcontextprotocol/sdk/server/streamableHttp.js');
require('libsodium-wrappers');
require('pixelmatch');
require('pngjs');

const ENV_ALLOWLIST = /* @__PURE__ */ new Set([
  // System essentials
  "PATH",
  "HOME",
  "USER",
  "SHELL",
  "TERM",
  "LANG",
  "LC_ALL",
  "NODE_ENV",
  // OpenAI — explicit vars only (no broad prefix to avoid leaking internal secrets)
  "OPENAI_API_KEY",
  "OPENAI_BASE_URL",
  "OPENAI_MODEL",
  "OPENAI_ORG_ID",
  // Consortium
  "CONSORTIUM_HTTP_MCP_URL"
]);
const ENV_PREFIX_ALLOWLIST = [
  "CODEX_"
];
function filterEnv(env) {
  const filtered = {};
  for (const key of Object.keys(env)) {
    const value = env[key];
    if (typeof value !== "string") continue;
    if (ENV_ALLOWLIST.has(key) || ENV_PREFIX_ALLOWLIST.some((prefix) => key.startsWith(prefix))) {
      filtered[key] = value;
    }
  }
  return filtered;
}

const execFileAsync = node_util.promisify(child_process.execFile);
const DEFAULT_TIMEOUT = 14 * 24 * 60 * 60 * 1e3;
let codexMcpCommandProbe = null;
function getCodexMcpCommand() {
  if (!codexMcpCommandProbe) {
    codexMcpCommandProbe = probeCodexMcpCommand();
  }
  return codexMcpCommandProbe;
}
async function probeCodexMcpCommand() {
  try {
    const { stdout } = await execFileAsync("codex", ["--version"], { encoding: "utf8", timeout: 1e4 });
    const version = stdout.trim();
    const match = version.match(/codex-cli\s+(\d+\.\d+\.\d+(?:-alpha\.\d+)?)/);
    if (!match) {
      persistence.logger.debug("[CodexMCP] Could not parse codex version:", version);
      return null;
    }
    const versionStr = match[1];
    const [major, minor, patch] = versionStr.split(/[-.]/).map(Number);
    if (major > 0 || minor > 43) return "mcp-server";
    if (minor === 43 && patch === 0) {
      if (versionStr.includes("-alpha.")) {
        const alphaNum = parseInt(versionStr.split("-alpha.")[1]);
        return alphaNum >= 5 ? "mcp-server" : "mcp";
      }
      return "mcp-server";
    }
    return "mcp";
  } catch (error) {
    persistence.logger.debug("[CodexMCP] Codex CLI not found or not executable:", error);
    return null;
  }
}
class CodexMcpClient {
  client;
  transport = null;
  connected = false;
  sessionId = null;
  conversationId = null;
  handler = null;
  permissionHandler = null;
  constructor() {
    this.client = new index_js.Client(
      { name: "consortium-codex-client", version: "1.0.0" },
      { capabilities: { elicitation: {} } }
    );
    this.client.setNotificationHandler(z.z.object({
      method: z.z.literal("codex/event"),
      params: z.z.object({
        msg: z.z.any()
      })
    }).passthrough(), (data) => {
      const msg = data.params.msg;
      this.updateIdentifiersFromEvent(msg);
      this.handler?.(msg);
    });
  }
  setHandler(handler) {
    this.handler = handler;
  }
  /**
   * Set the permission handler for tool approval
   */
  setPermissionHandler(handler) {
    this.permissionHandler = handler;
  }
  async connect() {
    if (this.connected) return;
    const mcpCommand = await getCodexMcpCommand();
    if (mcpCommand === null) {
      throw new Error(
        "Codex CLI not found or not executable.\n\nTo install codex:\n  npm install -g @openai/codex\n\nAlternatively, use Claude:\n  consortium claude"
      );
    }
    persistence.logger.debug(`[CodexMCP] Connecting to Codex MCP server using command: codex ${mcpCommand}`);
    this.transport = new stdio_js.StdioClientTransport({
      command: "codex",
      args: [mcpCommand],
      env: filterEnv(process.env)
    });
    this.registerPermissionHandlers();
    await this.client.connect(this.transport);
    this.connected = true;
    persistence.logger.debug("[CodexMCP] Connected to Codex");
  }
  registerPermissionHandlers() {
    this.client.setRequestHandler(
      types_js.ElicitRequestSchema,
      async (request) => {
        console.log("[CodexMCP] Received elicitation request:", request.params);
        const params = request.params;
        const toolName = "CodexBash";
        const toElicitResult = (decision) => {
          const action = decision === "approved" || decision === "approved_for_session" ? "accept" : decision === "abort" ? "cancel" : "decline";
          return { action, content: { decision } };
        };
        if (!this.permissionHandler) {
          persistence.logger.debug("[CodexMCP] No permission handler set, denying by default");
          return toElicitResult("denied");
        }
        try {
          const result = await this.permissionHandler.handleToolCall(
            params.codex_call_id,
            toolName,
            {
              command: params.codex_command,
              cwd: params.codex_cwd
            }
          );
          persistence.logger.debug("[CodexMCP] Permission result:", result);
          return toElicitResult(result.decision);
        } catch (error) {
          persistence.logger.debug("[CodexMCP] Error handling permission request:", error);
          return toElicitResult("denied");
        }
      }
    );
    persistence.logger.debug("[CodexMCP] Permission handlers registered");
  }
  async startSession(config, options) {
    if (!this.connected) await this.connect();
    persistence.logger.debug("[CodexMCP] Starting Codex session:", config);
    const response = await this.client.callTool({
      name: "codex",
      arguments: config
    }, void 0, {
      signal: options?.signal,
      timeout: DEFAULT_TIMEOUT
      // maxTotalTimeout: 10000000000 
    });
    persistence.logger.debug("[CodexMCP] startSession response:", response);
    this.extractIdentifiers(response);
    return response;
  }
  async continueSession(prompt, options) {
    if (!this.connected) await this.connect();
    if (!this.sessionId) {
      throw new Error("No active session. Call startSession first.");
    }
    if (!this.conversationId) {
      this.conversationId = this.sessionId;
      persistence.logger.debug("[CodexMCP] conversationId missing, defaulting to sessionId:", this.conversationId);
    }
    const args = { sessionId: this.sessionId, conversationId: this.conversationId, prompt };
    persistence.logger.debug("[CodexMCP] Continuing Codex session:", args);
    const response = await this.client.callTool({
      name: "codex-reply",
      arguments: args
    }, void 0, {
      signal: options?.signal,
      timeout: DEFAULT_TIMEOUT
    });
    persistence.logger.debug("[CodexMCP] continueSession response:", response);
    this.extractIdentifiers(response);
    return response;
  }
  updateIdentifiersFromEvent(event) {
    if (!event || typeof event !== "object") {
      return;
    }
    const candidates = [event];
    if (event.data && typeof event.data === "object") {
      candidates.push(event.data);
    }
    for (const candidate of candidates) {
      const sessionId = candidate.session_id ?? candidate.sessionId;
      if (sessionId) {
        this.sessionId = sessionId;
        persistence.logger.debug("[CodexMCP] Session ID extracted from event:", this.sessionId);
      }
      const conversationId = candidate.conversation_id ?? candidate.conversationId;
      if (conversationId) {
        this.conversationId = conversationId;
        persistence.logger.debug("[CodexMCP] Conversation ID extracted from event:", this.conversationId);
      }
    }
  }
  extractIdentifiers(response) {
    const meta = response?.meta || {};
    if (meta.sessionId) {
      this.sessionId = meta.sessionId;
      persistence.logger.debug("[CodexMCP] Session ID extracted:", this.sessionId);
    } else if (response?.sessionId) {
      this.sessionId = response.sessionId;
      persistence.logger.debug("[CodexMCP] Session ID extracted:", this.sessionId);
    }
    if (meta.conversationId) {
      this.conversationId = meta.conversationId;
      persistence.logger.debug("[CodexMCP] Conversation ID extracted:", this.conversationId);
    } else if (response?.conversationId) {
      this.conversationId = response.conversationId;
      persistence.logger.debug("[CodexMCP] Conversation ID extracted:", this.conversationId);
    }
    const content = response?.content;
    if (Array.isArray(content)) {
      for (const item of content) {
        if (!this.sessionId && item?.sessionId) {
          this.sessionId = item.sessionId;
          persistence.logger.debug("[CodexMCP] Session ID extracted from content:", this.sessionId);
        }
        if (!this.conversationId && item && typeof item === "object" && "conversationId" in item && item.conversationId) {
          this.conversationId = item.conversationId;
          persistence.logger.debug("[CodexMCP] Conversation ID extracted from content:", this.conversationId);
        }
      }
    }
  }
  getSessionId() {
    return this.sessionId;
  }
  hasActiveSession() {
    return this.sessionId !== null;
  }
  clearSession() {
    const previousSessionId = this.sessionId;
    this.sessionId = null;
    this.conversationId = null;
    persistence.logger.debug("[CodexMCP] Session cleared, previous sessionId:", previousSessionId);
  }
  /**
   * Store the current session ID without clearing it, useful for abort handling
   */
  storeSessionForResume() {
    persistence.logger.debug("[CodexMCP] Storing session for potential resume:", this.sessionId);
    return this.sessionId;
  }
  /**
   * Force close the Codex MCP transport and clear all session identifiers.
   * Use this for permanent shutdown (e.g. kill/exit). Prefer `disconnect()` for
   * transient connection resets where you may want to keep the session id.
   */
  async forceCloseSession() {
    persistence.logger.debug("[CodexMCP] Force closing session");
    try {
      await this.disconnect();
    } finally {
      this.clearSession();
    }
    persistence.logger.debug("[CodexMCP] Session force-closed");
  }
  async disconnect() {
    if (!this.connected) return;
    const pid = this.transport?.pid ?? null;
    persistence.logger.debug(`[CodexMCP] Disconnecting; child pid=${pid ?? "none"}`);
    try {
      persistence.logger.debug("[CodexMCP] client.close begin");
      await this.client.close();
      persistence.logger.debug("[CodexMCP] client.close done");
    } catch (e) {
      persistence.logger.debug("[CodexMCP] Error closing client, attempting transport close directly", e);
      try {
        persistence.logger.debug("[CodexMCP] transport.close begin");
        await this.transport?.close?.();
        persistence.logger.debug("[CodexMCP] transport.close done");
      } catch {
      }
    }
    if (pid) {
      try {
        process.kill(pid, 0);
        persistence.logger.debug("[CodexMCP] Child still alive, sending SIGKILL");
        try {
          process.kill(pid, "SIGKILL");
        } catch {
        }
      } catch {
      }
    }
    this.transport = null;
    this.connected = false;
    persistence.logger.debug(`[CodexMCP] Disconnected; session ${this.sessionId ?? "none"} preserved`);
  }
}

class CodexPermissionHandler extends BasePermissionHandler.BasePermissionHandler {
  constructor(session) {
    super(session);
  }
  getLogPrefix() {
    return "[Codex]";
  }
  /**
   * Handle a tool permission request
   * @param toolCallId - The unique ID of the tool call
   * @param toolName - The name of the tool being called
   * @param input - The input parameters for the tool
   * @returns Promise resolving to permission result
   */
  async handleToolCall(toolCallId, toolName, input) {
    return this.createPendingRequest(toolCallId, toolName, input);
  }
}

class ReasoningProcessor extends BaseReasoningProcessor.BaseReasoningProcessor {
  getToolName() {
    return "CodexReasoning";
  }
  getLogPrefix() {
    return "[ReasoningProcessor]";
  }
  /**
   * Process a reasoning delta and accumulate content.
   */
  processDelta(delta) {
    this.processInput(delta);
  }
  /**
   * Complete the reasoning section with final text.
   */
  complete(fullText) {
    this.completeReasoning(fullText);
  }
}

class DiffProcessor {
  previousDiff = null;
  onMessage = null;
  constructor(onMessage) {
    this.onMessage = onMessage || null;
  }
  /**
   * Process a turn_diff message and check if the unified_diff has changed
   */
  processDiff(unifiedDiff) {
    if (this.previousDiff !== unifiedDiff) {
      persistence.logger.debug("[DiffProcessor] Unified diff changed, sending CodexDiff tool call");
      const callId = node_crypto.randomUUID();
      const toolCall = {
        type: "tool-call",
        name: "CodexDiff",
        callId,
        input: {
          unified_diff: unifiedDiff
        },
        id: node_crypto.randomUUID()
      };
      this.onMessage?.(toolCall);
      const toolResult = {
        type: "tool-call-result",
        callId,
        output: {
          status: "completed"
        },
        id: node_crypto.randomUUID()
      };
      this.onMessage?.(toolResult);
    }
    this.previousDiff = unifiedDiff;
    persistence.logger.debug("[DiffProcessor] Updated stored diff");
  }
  /**
   * Reset the processor state (called on task_complete or turn_aborted)
   */
  reset() {
    persistence.logger.debug("[DiffProcessor] Resetting diff state");
    this.previousDiff = null;
  }
  /**
   * Set the message callback for sending messages directly
   */
  setMessageCallback(callback) {
    this.onMessage = callback;
  }
  /**
   * Get the current diff value
   */
  getCurrentDiff() {
    return this.previousDiff;
  }
}

const CodexDisplay = ({ messageBuffer, logPath, onExit }) => {
  const [messages, setMessages] = React.useState([]);
  const [confirmationMode, setConfirmationMode] = React.useState(false);
  const [actionInProgress, setActionInProgress] = React.useState(false);
  const confirmationTimeoutRef = React.useRef(null);
  const { stdout } = ink.useStdout();
  const terminalWidth = stdout.columns || 80;
  const terminalHeight = stdout.rows || 24;
  React.useEffect(() => {
    setMessages(messageBuffer.getMessages());
    const unsubscribe = messageBuffer.onUpdate((newMessages) => {
      setMessages(newMessages);
    });
    return () => {
      unsubscribe();
      if (confirmationTimeoutRef.current) {
        clearTimeout(confirmationTimeoutRef.current);
      }
    };
  }, [messageBuffer]);
  const resetConfirmation = React.useCallback(() => {
    setConfirmationMode(false);
    if (confirmationTimeoutRef.current) {
      clearTimeout(confirmationTimeoutRef.current);
      confirmationTimeoutRef.current = null;
    }
  }, []);
  const setConfirmationWithTimeout = React.useCallback(() => {
    setConfirmationMode(true);
    if (confirmationTimeoutRef.current) {
      clearTimeout(confirmationTimeoutRef.current);
    }
    confirmationTimeoutRef.current = setTimeout(() => {
      resetConfirmation();
    }, 15e3);
  }, [resetConfirmation]);
  ink.useInput(React.useCallback(async (input, key) => {
    if (actionInProgress) return;
    if (key.ctrl && input === "c") {
      if (confirmationMode) {
        resetConfirmation();
        setActionInProgress(true);
        await new Promise((resolve) => setTimeout(resolve, 100));
        onExit?.();
      } else {
        setConfirmationWithTimeout();
      }
      return;
    }
    if (confirmationMode) {
      resetConfirmation();
    }
  }, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation]));
  const getMessageColor = (type) => {
    switch (type) {
      case "user":
        return "magenta";
      case "assistant":
        return "cyan";
      case "system":
        return "blue";
      case "tool":
        return "yellow";
      case "result":
        return "green";
      case "status":
        return "gray";
      default:
        return "white";
    }
  };
  const formatMessage = (msg) => {
    const lines = msg.content.split("\n");
    const maxLineLength = terminalWidth - 10;
    return lines.map((line) => {
      if (line.length <= maxLineLength) return line;
      const chunks = [];
      for (let i = 0; i < line.length; i += maxLineLength) {
        chunks.push(line.slice(i, i + maxLineLength));
      }
      return chunks.join("\n");
    }).join("\n");
  };
  return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
    ink.Box,
    {
      flexDirection: "column",
      width: terminalWidth,
      height: terminalHeight - 4,
      borderStyle: "round",
      borderColor: "gray",
      paddingX: 1,
      overflow: "hidden"
    },
    /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "\u{1F916} Codex Agent Messages"), /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
    /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
      // Show only the last messages that fit in the available space
      messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(ink.Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
    ))
  ), /* @__PURE__ */ React.createElement(
    ink.Box,
    {
      width: terminalWidth,
      borderStyle: "round",
      borderColor: actionInProgress ? "gray" : confirmationMode ? "red" : "green",
      paddingX: 2,
      justifyContent: "center",
      alignItems: "center",
      flexDirection: "column"
    },
    /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", alignItems: "center" }, actionInProgress ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Exiting agent...") : confirmationMode ? /* @__PURE__ */ React.createElement(ink.Text, { color: "red", bold: true }, "\u26A0\uFE0F  Press Ctrl-C again to exit the agent") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ink.Text, { color: "green", bold: true }, "\u{1F916} Codex Agent Running \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
  ));
};

function emitReadyIfIdle({ pending, queueSize, shouldExit, sendReady, notify }) {
  if (shouldExit) {
    return false;
  }
  if (pending) {
    return false;
  }
  if (queueSize() > 0) {
    return false;
  }
  sendReady();
  notify?.();
  return true;
}
async function runCodex(opts) {
  const sessionTag = node_crypto.randomUUID();
  persistence.connectionState.setBackend("Codex");
  const api = await persistence.ApiClient.create(opts.credentials);
  persistence.logger.debug(`[codex] Starting with options: startedBy=${opts.startedBy || "terminal"}`);
  const settings = await persistence.readSettings();
  let machineId = settings?.machineId;
  if (!machineId) {
    console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/ConsortiumAI/consortium-cli/issues`);
    process.exit(1);
  }
  persistence.logger.debug(`Using machineId: ${machineId}`);
  const activeOrgId = await index.ensureActiveOrg(opts.credentials);
  await api.getOrCreateMachine({
    machineId,
    metadata: index.initialMachineMetadata,
    organizationId: activeOrgId ?? void 0
  });
  const { state, metadata } = createSessionMetadata.createSessionMetadata({
    flavor: "codex",
    machineId,
    startedBy: opts.startedBy
  });
  const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state, organizationId: activeOrgId ?? void 0, projectId: process.env.CONSORTIUM_PROJECT_ID || void 0 });
  let session;
  let permissionHandler;
  const { session: initialSession, reconnectionHandle } = setupOfflineReconnection.setupOfflineReconnection({
    organizationId: activeOrgId ?? void 0,
    api,
    sessionTag,
    metadata,
    state,
    response,
    onSessionSwap: (newSession) => {
      session = newSession;
      if (permissionHandler) {
        permissionHandler.updateSession(newSession);
      }
    }
  });
  session = initialSession;
  if (response) {
    try {
      persistence.logger.debug(`[START] Reporting session ${response.id} to daemon`);
      const result = await index.notifyDaemonSessionStarted(response.id, metadata);
      if (result.error) {
        persistence.logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
      } else {
        persistence.logger.debug(`[START] Reported session ${response.id} to daemon`);
      }
    } catch (error) {
      persistence.logger.debug("[START] Failed to report to daemon (may not be running):", error);
    }
  }
  const messageQueue = new index.MessageQueue2((mode) => index.hashObject({
    permissionMode: mode.permissionMode,
    model: mode.model
  }));
  session.setQueueStatsProvider(() => messageQueue.stats());
  let currentPermissionMode = void 0;
  let currentModel = void 0;
  let hasStoredFirstMessage = false;
  const attachments = index.createPromptFileAttachments({
    credentials: opts.credentials,
    sessionId: session.sessionId,
    logTag: "codex"
  });
  session.onUserMessage((message) => {
    attachments.observeUserMessage(message);
    if (!hasStoredFirstMessage && message.content.text) {
      hasStoredFirstMessage = true;
      session.updateMetadata((m) => ({
        ...m,
        firstMessage: message.content.text.substring(0, 100)
      }));
    }
    let messagePermissionMode = currentPermissionMode;
    if (message.meta?.permissionMode) {
      messagePermissionMode = message.meta.permissionMode;
      currentPermissionMode = messagePermissionMode;
      persistence.logger.debug(`[Codex] Permission mode updated from user message to: ${currentPermissionMode}`);
    } else {
      persistence.logger.debug(`[Codex] User message received with no permission mode override, using current: ${currentPermissionMode ?? "default (effective)"}`);
    }
    const resolvedModel = resolveModelFromMeta.resolveModelFromMeta(message.meta, currentModel);
    const messageModel = resolvedModel.model;
    currentModel = resolvedModel.model;
    if (resolvedModel.changed) {
      persistence.logger.debug(`[Codex] Model updated from user message: ${messageModel || "reset to default"}`);
    } else {
      persistence.logger.debug(`[Codex] User message received with no model override, using current: ${currentModel || "default"}`);
    }
    const enhancedMode = {
      permissionMode: messagePermissionMode || "default",
      model: messageModel
    };
    messageQueue.push(message.content.text, enhancedMode);
  });
  let thinking = false;
  let streamId = null;
  let answerSeq = 0;
  let reasoningSeq = 0;
  session.keepAlive(thinking, "remote");
  const keepAliveInterval = setInterval(() => {
    session.keepAlive(thinking, "remote");
  }, 2e3);
  session.updateAgentState((state2) => ({ ...state2, controlledByUser: false }));
  session.updateMetadata((m) => ({ ...m, startupTimings: { readyTotal: Math.round(process.uptime() * 1e3) } }));
  const sendReady = () => {
    session.sendSessionEvent({ type: "ready" });
    try {
      api.push().sendToAllDevices(
        "It's ready!",
        "Codex is waiting for your command",
        { sessionId: session.sessionId }
      );
    } catch (pushError) {
      persistence.logger.debug("[Codex] Failed to send ready push", pushError);
    }
  };
  function logActiveHandles(tag) {
    if (!process.env.DEBUG) return;
    const anyProc = process;
    const handles = typeof anyProc._getActiveHandles === "function" ? anyProc._getActiveHandles() : [];
    const requests = typeof anyProc._getActiveRequests === "function" ? anyProc._getActiveRequests() : [];
    persistence.logger.debug(`[codex][handles] ${tag}: handles=${handles.length} requests=${requests.length}`);
    try {
      const kinds = handles.map((h) => h && h.constructor ? h.constructor.name : typeof h);
      persistence.logger.debug(`[codex][handles] kinds=${JSON.stringify(kinds)}`);
    } catch {
    }
  }
  let abortController = new AbortController();
  let shouldExit = false;
  let storedSessionIdForResume = null;
  async function handleAbort() {
    persistence.logger.debug("[Codex] Abort requested - stopping current task");
    try {
      if (client.hasActiveSession()) {
        storedSessionIdForResume = client.storeSessionForResume();
        persistence.logger.debug("[Codex] Stored session for resume:", storedSessionIdForResume);
      }
      abortController.abort();
      reasoningProcessor.abort();
      persistence.logger.debug("[Codex] Abort completed - session remains active");
    } catch (error) {
      persistence.logger.debug("[Codex] Error during abort:", error);
    } finally {
      abortController = new AbortController();
    }
  }
  const handleKillSession = async () => {
    persistence.logger.debug("[Codex] Kill session requested - terminating process");
    await handleAbort();
    persistence.logger.debug("[Codex] Abort completed, proceeding with termination");
    try {
      if (session) {
        session.updateMetadata((currentMetadata) => ({
          ...currentMetadata,
          lifecycleState: "archived",
          lifecycleStateSince: Date.now(),
          archivedBy: "cli",
          archiveReason: "User terminated"
        }));
        session.sendSessionDeath();
        await session.flush();
        await session.close();
      }
      try {
        await client.forceCloseSession();
      } catch (e) {
        persistence.logger.debug("[Codex] Error while force closing Codex session during termination", e);
      }
      index.stopCaffeinate();
      consortiumServer.stop();
      persistence.logger.debug("[Codex] Session termination complete, exiting");
      process.exit(0);
    } catch (error) {
      persistence.logger.debug("[Codex] Error during session termination:", error);
      process.exit(1);
    }
  };
  session.rpcHandlerManager.registerHandler("abort", handleAbort);
  index.registerKillSessionHandler(session.rpcHandlerManager, handleKillSession);
  const messageBuffer = new index.MessageBuffer();
  const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
  let inkInstance = null;
  if (hasTTY) {
    console.clear();
    inkInstance = ink.render(React.createElement(CodexDisplay, {
      messageBuffer,
      logPath: process.env.DEBUG ? persistence.logger.getLogPath() : void 0,
      onExit: async () => {
        persistence.logger.debug("[codex]: Exiting agent via Ctrl-C");
        shouldExit = true;
        await handleAbort();
      }
    }), {
      exitOnCtrlC: false,
      patchConsole: false
    });
  }
  if (hasTTY) {
    process.stdin.resume();
    if (process.stdin.isTTY) {
      process.stdin.setRawMode(true);
    }
    process.stdin.setEncoding("utf8");
  }
  const client = new CodexMcpClient();
  function findCodexResumeFile(sessionId) {
    if (!sessionId) return null;
    try {
      let collectFilesRecursive2 = function(dir, acc = []) {
        let entries;
        try {
          entries = fs.readdirSync(dir, { withFileTypes: true });
        } catch {
          return acc;
        }
        for (const entry of entries) {
          const full = path.join(dir, entry.name);
          if (entry.isDirectory()) {
            collectFilesRecursive2(full, acc);
          } else if (entry.isFile()) {
            acc.push(full);
          }
        }
        return acc;
      };
      var collectFilesRecursive = collectFilesRecursive2;
      const codexHomeDir = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
      const rootDir = path.join(codexHomeDir, "sessions");
      const candidates = collectFilesRecursive2(rootDir).filter((full) => full.endsWith(`-${sessionId}.jsonl`)).filter((full) => {
        try {
          return fs.statSync(full).isFile();
        } catch {
          return false;
        }
      }).sort((a, b) => {
        const sa = fs.statSync(a).mtimeMs;
        const sb = fs.statSync(b).mtimeMs;
        return sb - sa;
      });
      return candidates[0] || null;
    } catch {
      return null;
    }
  }
  permissionHandler = new CodexPermissionHandler(session);
  const reasoningProcessor = new ReasoningProcessor((message) => {
    session.sendCodexMessage(message);
  });
  const diffProcessor = new DiffProcessor((message) => {
    session.sendCodexMessage(message);
  });
  client.setPermissionHandler(permissionHandler);
  const codexPatchChanges = /* @__PURE__ */ new Map();
  client.setHandler((msg) => {
    persistence.logger.debug(`[Codex] MCP message: ${JSON.stringify(msg)}`);
    if (msg.type === "agent_message") {
      messageBuffer.addMessage(msg.message, "assistant");
    } else if (msg.type === "agent_reasoning_delta") ; else if (msg.type === "agent_reasoning") {
      messageBuffer.addMessage(`[Thinking] ${msg.text.substring(0, 100)}...`, "system");
    } else if (msg.type === "exec_command_begin") {
      messageBuffer.addMessage(`Executing: ${msg.command}`, "tool");
    } else if (msg.type === "exec_command_end") {
      const output = msg.output || msg.error || "Command completed";
      const truncatedOutput = output.substring(0, 200);
      messageBuffer.addMessage(
        `Result: ${truncatedOutput}${output.length > 200 ? "..." : ""}`,
        "result"
      );
    } else if (msg.type === "task_started") {
      messageBuffer.addMessage("Starting task...", "status");
    } else if (msg.type === "task_complete") {
      messageBuffer.addMessage("Task completed", "status");
      sendReady();
    } else if (msg.type === "turn_aborted") {
      messageBuffer.addMessage("Turn aborted", "status");
      sendReady();
    }
    if (msg.type === "task_started") {
      streamId = node_crypto.randomUUID();
      answerSeq = 0;
      reasoningSeq = 0;
      if (!thinking) {
        persistence.logger.debug("thinking started");
        thinking = true;
        session.keepAlive(thinking, "remote");
      }
    }
    if (msg.type === "task_complete" || msg.type === "turn_aborted") {
      if (streamId) {
        session.sendStreamDelta("codex", streamId, "answer", answerSeq++, "", true);
        streamId = null;
      }
      if (thinking) {
        persistence.logger.debug("thinking completed");
        thinking = false;
        session.keepAlive(thinking, "remote");
      }
      diffProcessor.reset();
    }
    if (msg.type === "agent_reasoning_section_break") {
      reasoningProcessor.handleSectionBreak();
    }
    if (msg.type === "agent_reasoning_delta") {
      reasoningProcessor.processDelta(msg.delta);
      if (streamId && msg.delta) session.sendStreamDelta("codex", streamId, "reasoning", reasoningSeq++, msg.delta);
    }
    if (msg.type === "agent_reasoning") {
      reasoningProcessor.complete(msg.text);
    }
    if (msg.type === "agent_message_delta") {
      if (streamId && msg.delta) session.sendStreamDelta("codex", streamId, "answer", answerSeq++, msg.delta);
    }
    if (msg.type === "agent_message") {
      session.sendCodexMessage({
        type: "message",
        message: msg.message,
        id: node_crypto.randomUUID()
      });
      if (typeof msg.message === "string") {
        void session.autoDetectArtifactsFromText("codex", msg.message);
      }
    }
    if (msg.type === "exec_command_begin" || msg.type === "exec_approval_request") {
      let { call_id, type, ...inputs } = msg;
      session.sendCodexMessage({
        type: "tool-call",
        name: "CodexBash",
        callId: call_id,
        input: inputs,
        id: node_crypto.randomUUID()
      });
    }
    if (msg.type === "exec_command_end") {
      let { call_id, type, ...output } = msg;
      session.sendCodexMessage({
        type: "tool-call-result",
        callId: call_id,
        output,
        id: node_crypto.randomUUID()
      });
    }
    if (msg.type === "token_count") {
      session.sendCodexMessage({
        ...msg,
        id: node_crypto.randomUUID()
      });
      try {
        const usage = persistence.usageFromCodexLastTokenUsage(msg.info?.last_token_usage);
        if (usage) {
          session.sendUsageData(usage, currentModel, void 0, {
            keyPrefix: "codex",
            providerId: "openai",
            zeroCost: true
          });
        }
      } catch (err) {
        persistence.logger.debug("[Codex] failed to emit token_count usage", err);
      }
      try {
        const accountKeyId = process.env[index.LMP_ACCOUNT_KEY_ID_ENV];
        const rateLimits = msg.rate_limits;
        if (accountKeyId && rateLimits) {
          persistence.recordAccountUsageBatch(persistence.snapshotsFromCodexRateLimits(rateLimits, {
            accountKeyId,
            providerId: "openai"
          }));
        }
      } catch (err) {
        persistence.logger.debug("[Codex] failed to publish rate_limits snapshot", err);
      }
    }
    if (msg.type === "patch_apply_begin") {
      let { call_id, auto_approved, changes } = msg;
      try {
        codexPatchChanges.set(call_id, changes);
      } catch {
      }
      const changeCount = Object.keys(changes).length;
      const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
      messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
      session.sendCodexMessage({
        type: "tool-call",
        name: "CodexPatch",
        callId: call_id,
        input: {
          auto_approved,
          changes
        },
        id: node_crypto.randomUUID()
      });
    }
    if (msg.type === "patch_apply_end") {
      let { call_id, stdout, stderr, success } = msg;
      if (success) {
        const message = stdout || "Files modified successfully";
        messageBuffer.addMessage(message.substring(0, 200), "result");
      } else {
        const errorMsg = stderr || "Failed to modify files";
        messageBuffer.addMessage(`Error: ${errorMsg.substring(0, 200)}`, "result");
      }
      session.sendCodexMessage({
        type: "tool-call-result",
        callId: call_id,
        output: {
          stdout,
          stderr,
          success
        },
        id: node_crypto.randomUUID()
      });
      const changes = codexPatchChanges.get(call_id);
      codexPatchChanges.delete(call_id);
      if (success && changes) {
        for (const [path, change] of Object.entries(changes)) {
          const addContent = change?.add?.content;
          if (change?.delete) continue;
          void session.promoteFileWriteArtifact("codex", path, typeof addContent === "string" ? addContent : void 0);
        }
      }
    }
    if (msg.type === "turn_diff") {
      if (msg.unified_diff) {
        diffProcessor.processDiff(msg.unified_diff);
      }
    }
    if ((msg.type === "mcp_tool_call_end" || msg.type === "exec_command_end") && msg.result !== void 0) {
      void resolveModelFromMeta.scanToolResultForMedia(msg.result, async (m) => {
        try {
          const uploaded = await session.uploadArtifact({ bytes: m.bytes, mime: m.mime });
          session.sendCodexMessage({
            type: "artifact",
            artifactId: uploaded.id,
            mime: uploaded.mime,
            name: m.name,
            sizeBytes: uploaded.sizeBytes,
            id: node_crypto.randomUUID()
          });
        } catch (err) {
          const errMsg = err instanceof Error ? err.message : String(err);
          persistence.logger.debug("[codex] scanned-media upload failed:", errMsg);
          session.sendCodexMessage({
            type: "message",
            message: `\u26A0\uFE0F Could not display ${m.name ?? "media"}: ${errMsg}`,
            id: node_crypto.randomUUID()
          });
        }
      }).catch((err) => persistence.logger.debug("[codex] media scan failed:", err));
    }
    if (msg.type === "exec_command_end" && (msg.stdout || msg.stderr)) {
      void resolveModelFromMeta.scanToolResultForMedia({ stdout: msg.stdout, stderr: msg.stderr }, async (m) => {
        try {
          const uploaded = await session.uploadArtifact({ bytes: m.bytes, mime: m.mime });
          session.sendCodexMessage({
            type: "artifact",
            artifactId: uploaded.id,
            mime: uploaded.mime,
            name: m.name,
            sizeBytes: uploaded.sizeBytes,
            id: node_crypto.randomUUID()
          });
        } catch (err) {
          persistence.logger.debug("[codex] exec-scanned-media upload failed:", err);
        }
      }).catch((err) => persistence.logger.debug("[codex] exec media scan failed:", err));
    }
    if (msg.type === "raw_response_item" && msg.item?.type === "image_generation_call") {
      const item = msg.item;
      const base64 = typeof item.result === "string" ? item.result : void 0;
      const mime = typeof item.output_format === "string" ? `image/${item.output_format}` : "image/png";
      const name = typeof item.revised_prompt === "string" ? item.revised_prompt.slice(0, 60) : "Generated image";
      if (base64 && base64.length > 0) {
        (async () => {
          try {
            const bytes = Uint8Array.from(Buffer.from(base64, "base64"));
            const uploaded = await session.uploadArtifact({ bytes, mime });
            session.sendCodexMessage({
              type: "artifact",
              artifactId: uploaded.id,
              mime: uploaded.mime,
              name,
              sizeBytes: uploaded.sizeBytes,
              id: node_crypto.randomUUID()
            });
          } catch (err) {
            const msg2 = err instanceof Error ? err.message : String(err);
            persistence.logger.debug("[codex] image_generation_call upload failed:", msg2);
            session.sendCodexMessage({
              type: "message",
              message: `\u26A0\uFE0F Could not display generated image: ${msg2}`,
              id: node_crypto.randomUUID()
            });
          }
        })();
      }
    }
  });
  const consortiumServer = await index.startConsortiumServer(session, {
    sendArtifact: (msg) => session.sendCodexMessage(msg)
  });
  const bridgeCommand = path.join(persistence.projectPath(), "bin", "consortium-mcp.mjs");
  const mcpServers = {
    consortium: {
      command: bridgeCommand,
      args: [],
      env: {
        CONSORTIUM_HTTP_MCP_URL: consortiumServer.url
      }
    }
  };
  await index.provisionAcpSession(process.cwd(), mcpServers, session.sessionId);
  try {
    persistence.logger.debug("[codex]: client.connect begin");
    await client.connect();
    persistence.logger.debug("[codex]: client.connect done");
    let wasCreated = false;
    let currentModeHash = null;
    let pending = null;
    let nextExperimentalResume = process.env.CONSORTIUM_CODEX_RESUME_FILE ?? null;
    if (nextExperimentalResume) {
      persistence.logger.debug(`[Codex] Using daemon-provided resume file: ${nextExperimentalResume}`);
    }
    while (!shouldExit) {
      logActiveHandles("loop-top");
      let message = pending;
      pending = null;
      if (!message) {
        const waitSignal = abortController.signal;
        const batch = await messageQueue.waitForMessagesAndGetAsString(waitSignal);
        if (!batch) {
          if (waitSignal.aborted && !shouldExit) {
            persistence.logger.debug("[codex]: Wait aborted while idle; ignoring and continuing");
            continue;
          }
          persistence.logger.debug(`[codex]: batch=${!!batch}, shouldExit=${shouldExit}`);
          break;
        }
        message = batch;
      }
      if (!message) {
        break;
      }
      if (wasCreated && currentModeHash && message.hash !== currentModeHash) {
        persistence.logger.debug("[Codex] Mode changed \u2013 restarting Codex session");
        messageBuffer.addMessage("\u2550".repeat(40), "status");
        messageBuffer.addMessage("Starting new Codex session (mode changed)...", "status");
        try {
          const prevSessionId = client.getSessionId();
          nextExperimentalResume = findCodexResumeFile(prevSessionId);
          if (nextExperimentalResume) {
            persistence.logger.debug(`[Codex] Found resume file for session ${prevSessionId}: ${nextExperimentalResume}`);
            messageBuffer.addMessage("Resuming previous context\u2026", "status");
          } else {
            persistence.logger.debug("[Codex] No resume file found for previous session");
            messageBuffer.addMessage("Model changed, but previous context could not be preserved \u2014 starting fresh.", "status");
            session.sendCodexMessage({
              type: "message",
              message: "_(Model changed, but previous context could not be preserved \u2014 starting a fresh Codex session.)_",
              id: node_crypto.randomUUID()
            });
          }
        } catch (e) {
          persistence.logger.debug("[Codex] Error while searching resume file", e);
        }
        client.clearSession();
        wasCreated = false;
        currentModeHash = null;
        pending = message;
        permissionHandler.reset();
        reasoningProcessor.abort();
        diffProcessor.reset();
        thinking = false;
        session.keepAlive(thinking, "remote");
        continue;
      }
      messageBuffer.addMessage(message.message, "user");
      currentModeHash = message.hash;
      try {
        const approvalPolicy = (() => {
          switch (message.mode.permissionMode) {
            // Codex native modes
            case "default":
              return "untrusted";
            // Ask for non-trusted commands
            case "read-only":
              return "never";
            // Never ask, read-only enforced by sandbox
            case "safe-yolo":
              return "on-failure";
            // Auto-run, ask only on failure
            case "yolo":
              return "on-failure";
            // Auto-run, ask only on failure
            // Defensive fallback for Claude-specific modes (backward compatibility)
            case "bypassPermissions":
              return "on-failure";
            // Full access: map to yolo behavior
            case "acceptEdits":
              return "on-request";
            // Let model decide (closest to auto-approve edits)
            case "plan":
              return "untrusted";
            // Conservative: ask for non-trusted
            default:
              return "untrusted";
          }
        })();
        const sandbox = (() => {
          switch (message.mode.permissionMode) {
            // Codex native modes
            case "default":
              return "workspace-write";
            // Can write in workspace
            case "read-only":
              return "read-only";
            // Read-only filesystem
            case "safe-yolo":
              return "workspace-write";
            // Can write in workspace
            case "yolo":
              return "danger-full-access";
            // Full system access
            // Defensive fallback for Claude-specific modes
            case "bypassPermissions":
              return "danger-full-access";
            // Full access: map to yolo
            case "acceptEdits":
              return "workspace-write";
            // Can edit files in workspace
            case "plan":
              return "workspace-write";
            // Can write for planning
            default:
              return "workspace-write";
          }
        })();
        const promptToSend = await attachments.preparePrompt(message.message);
        if (!wasCreated) {
          const startConfig = {
            prompt: promptToSend,
            sandbox,
            "approval-policy": approvalPolicy,
            config: { mcp_servers: mcpServers }
          };
          if (message.mode.model) {
            const [model, effort] = message.mode.model.split(":", 2);
            startConfig.model = model;
            if (effort) {
              startConfig.config.model_reasoning_effort = effort;
            }
          }
          let resumeFile = null;
          if (nextExperimentalResume) {
            resumeFile = nextExperimentalResume;
            nextExperimentalResume = null;
            persistence.logger.debug("[Codex] Using resume file from mode change:", resumeFile);
          } else if (storedSessionIdForResume) {
            const abortResumeFile = findCodexResumeFile(storedSessionIdForResume);
            if (abortResumeFile) {
              resumeFile = abortResumeFile;
              persistence.logger.debug("[Codex] Using resume file from aborted session:", resumeFile);
              messageBuffer.addMessage("Resuming from aborted session...", "status");
            }
            storedSessionIdForResume = null;
          }
          if (resumeFile) {
            if (fs.existsSync(resumeFile)) {
              startConfig.config.experimental_resume = resumeFile;
            } else {
              persistence.logger.debug(`[Codex] Resume file missing at apply time: ${resumeFile} \u2014 starting fresh`);
              messageBuffer.addMessage("Previous context could not be preserved \u2014 starting fresh.", "status");
              session.sendCodexMessage({
                type: "message",
                message: "_(Previous context could not be preserved \u2014 starting a fresh Codex session.)_",
                id: node_crypto.randomUUID()
              });
            }
          }
          await client.startSession(
            startConfig,
            { signal: abortController.signal }
          );
          wasCreated = true;
          try {
            const codexSessionId = client.getSessionId();
            if (codexSessionId) {
              const rolloutPath = findCodexResumeFile(codexSessionId);
              session.updateMetadata((m) => ({
                ...m,
                agentSessionId: codexSessionId,
                ...rolloutPath ? { codexRolloutPath: rolloutPath } : {}
              }));
            }
          } catch (e) {
            persistence.logger.debug("[Codex] Failed to persist resume metadata", e);
          }
        } else {
          const response2 = await client.continueSession(
            promptToSend,
            { signal: abortController.signal }
          );
          persistence.logger.debug("[Codex] continueSession response:", response2);
        }
      } catch (error) {
        persistence.logger.warn("Error in codex session:", error);
        const isAbortError = error instanceof Error && error.name === "AbortError";
        if (isAbortError) {
          messageBuffer.addMessage("Aborted by user", "status");
          session.sendSessionEvent({ type: "message", message: "Aborted by user" });
        } else {
          messageBuffer.addMessage("Process exited unexpectedly", "status");
          session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
          if (client.hasActiveSession()) {
            storedSessionIdForResume = client.storeSessionForResume();
            persistence.logger.debug("[Codex] Stored session after unexpected error:", storedSessionIdForResume);
          }
        }
      } finally {
        reasoningProcessor.abort();
        diffProcessor.reset();
        thinking = false;
        session.keepAlive(thinking, "remote");
        emitReadyIfIdle({
          pending,
          queueSize: () => messageQueue.size(),
          shouldExit,
          sendReady
        });
        logActiveHandles("after-turn");
      }
    }
  } finally {
    persistence.logger.debug("[codex]: Final cleanup start");
    logActiveHandles("cleanup-start");
    if (reconnectionHandle) {
      persistence.logger.debug("[codex]: Cancelling offline reconnection");
      reconnectionHandle.cancel();
    }
    try {
      persistence.logger.debug("[codex]: sendSessionDeath");
      session.sendSessionDeath();
      persistence.logger.debug("[codex]: flush begin");
      await session.flush();
      persistence.logger.debug("[codex]: flush done");
      persistence.logger.debug("[codex]: session.close begin");
      await session.close();
      persistence.logger.debug("[codex]: session.close done");
    } catch (e) {
      persistence.logger.debug("[codex]: Error while closing session", e);
    }
    persistence.logger.debug("[codex]: client.forceCloseSession begin");
    await client.forceCloseSession();
    persistence.logger.debug("[codex]: client.forceCloseSession done");
    persistence.logger.debug("[codex]: consortiumServer.stop");
    consortiumServer.stop();
    try {
      await attachments.dispose();
    } catch (e) {
      persistence.logger.debug("[codex]: Error disposing attachments scratch dir", e);
    }
    if (process.stdin.isTTY) {
      persistence.logger.debug("[codex]: setRawMode(false)");
      try {
        process.stdin.setRawMode(false);
      } catch {
      }
    }
    if (hasTTY) {
      persistence.logger.debug("[codex]: stdin.pause()");
      try {
        process.stdin.pause();
      } catch {
      }
    }
    persistence.logger.debug("[codex]: clearInterval(keepAlive)");
    clearInterval(keepAliveInterval);
    if (inkInstance) {
      persistence.logger.debug("[codex]: inkInstance.unmount()");
      inkInstance.unmount();
    }
    messageBuffer.clear();
    logActiveHandles("cleanup-end");
    persistence.logger.debug("[codex]: Final cleanup completed");
  }
}

exports.emitReadyIfIdle = emitReadyIfIdle;
exports.runCodex = runCodex;