diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 737eb25b..4264ab7f 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -475,7 +475,6 @@ dependencies = [ "clap", "codex-core", "codex-exec", - "codex-repl", "codex-tui", "serde_json", "tokio", @@ -557,20 +556,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "codex-repl" -version = "0.0.2504292236" -dependencies = [ - "anyhow", - "clap", - "codex-core", - "owo-colors 4.2.0", - "rand", - "tokio", - "tracing", - "tracing-subscriber", -] - [[package]] name = "codex-tui" version = "0.1.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 65614be5..953f21bd 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -7,7 +7,6 @@ members = [ "core", "exec", "execpolicy", - "repl", "tui", ] diff --git a/codex-rs/README.md b/codex-rs/README.md index c01323e5..a6ccc851 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -19,5 +19,4 @@ This folder is the root of a Cargo workspace. It contains quite a bit of experim - [`core/`](./core) contains the business logic for Codex. Ultimately, we hope this to be a library crate that is generally useful for building other Rust/native applications that use Codex. - [`exec/`](./exec) "headless" CLI for use in automation. - [`tui/`](./tui) CLI that launches a fullscreen TUI built with [Ratatui](https://ratatui.rs/). -- [`repl/`](./repl) CLI that launches a lightweight REPL similar to the Python or Node.js REPL. - [`cli/`](./cli) CLI multitool that provides the aforementioned CLIs via subcommands. diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 6a3a3593..7035bf2d 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -20,7 +20,6 @@ anyhow = "1" clap = { version = "4", features = ["derive"] } codex-core = { path = "../core" } codex-exec = { path = "../exec" } -codex-repl = { path = "../repl" } codex-tui = { path = "../tui" } serde_json = "1" tokio = { version = "1", features = [ diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 6866714e..af217425 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -5,7 +5,6 @@ use codex_cli::seatbelt; use codex_cli::LandlockCommand; use codex_cli::SeatbeltCommand; use codex_exec::Cli as ExecCli; -use codex_repl::Cli as ReplCli; use codex_tui::Cli as TuiCli; use crate::proto::ProtoCli; @@ -34,10 +33,6 @@ enum Subcommand { #[clap(visible_alias = "e")] Exec(ExecCli), - /// Run the REPL. - #[clap(visible_alias = "r")] - Repl(ReplCli), - /// Run the Protocol stream via stdin/stdout #[clap(visible_alias = "p")] Proto(ProtoCli), @@ -75,9 +70,6 @@ async fn main() -> anyhow::Result<()> { Some(Subcommand::Exec(exec_cli)) => { codex_exec::run_main(exec_cli).await?; } - Some(Subcommand::Repl(repl_cli)) => { - codex_repl::run_main(repl_cli).await?; - } Some(Subcommand::Proto(proto_cli)) => { proto::run_main(proto_cli).await?; } diff --git a/codex-rs/justfile b/codex-rs/justfile index f2ef5029..61339a23 100644 --- a/codex-rs/justfile +++ b/codex-rs/justfile @@ -10,10 +10,6 @@ install: tui *args: cargo run --bin codex -- tui {{args}} -# Run the REPL app -repl *args: - cargo run --bin codex -- repl {{args}} - # Run the Proto app proto *args: cargo run --bin codex -- proto {{args}} diff --git a/codex-rs/repl/Cargo.toml b/codex-rs/repl/Cargo.toml deleted file mode 100644 index 81f8c64c..00000000 --- a/codex-rs/repl/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "codex-repl" -version = { workspace = true } -edition = "2021" - -[[bin]] -name = "codex-repl" -path = "src/main.rs" - -[lib] -name = "codex_repl" -path = "src/lib.rs" - -[dependencies] -anyhow = "1" -clap = { version = "4", features = ["derive"] } -codex-core = { path = "../core", features = ["cli"] } -owo-colors = "4.2.0" -rand = "0.9" -tokio = { version = "1", features = [ - "io-std", - "macros", - "process", - "rt-multi-thread", - "signal", -] } -tracing = { version = "0.1.41", features = ["log"] } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/codex-rs/repl/src/cli.rs b/codex-rs/repl/src/cli.rs deleted file mode 100644 index c9fa1ee9..00000000 --- a/codex-rs/repl/src/cli.rs +++ /dev/null @@ -1,65 +0,0 @@ -use clap::ArgAction; -use clap::Parser; -use codex_core::ApprovalModeCliArg; -use codex_core::SandboxPermissionOption; -use std::path::PathBuf; - -/// Command‑line arguments. -#[derive(Debug, Parser)] -#[command( - author, - version, - about = "Interactive Codex CLI that streams all agent actions." -)] -pub struct Cli { - /// User prompt to start the session. - pub prompt: Option, - - /// Override the default model from ~/.codex/config.toml. - #[arg(short, long)] - pub model: Option, - - /// Optional images to attach to the prompt. - #[arg(long, value_name = "FILE")] - pub images: Vec, - - /// Increase verbosity (-v info, -vv debug, -vvv trace). - /// - /// The flag may be passed up to three times. Without any -v the CLI only prints warnings and errors. - #[arg(short, long, action = ArgAction::Count)] - pub verbose: u8, - - /// Don't use colored ansi output for verbose logging - #[arg(long)] - pub no_ansi: bool, - - /// Configure when the model requires human approval before executing a command. - #[arg(long = "ask-for-approval", short = 'a')] - pub approval_policy: Option, - - /// Convenience alias for low-friction sandboxed automatic execution (-a on-failure, network-disabled sandbox that can write to cwd and TMPDIR) - #[arg(long = "full-auto", default_value_t = false)] - pub full_auto: bool, - - #[clap(flatten)] - pub sandbox: SandboxPermissionOption, - - /// Allow running Codex outside a Git repository. By default the CLI - /// aborts early when the current working directory is **not** inside a - /// Git repo because most agents rely on `git` for interacting with the - /// code‑base. Pass this flag if you really know what you are doing. - #[arg(long, action = ArgAction::SetTrue, default_value_t = false)] - pub allow_no_git_exec: bool, - - /// Disable server‑side response storage (sends the full conversation context with every request) - #[arg(long = "disable-response-storage", default_value_t = false)] - pub disable_response_storage: bool, - - /// Record submissions into file as JSON - #[arg(short = 'S', long)] - pub record_submissions: Option, - - /// Record events into file as JSON - #[arg(short = 'E', long)] - pub record_events: Option, -} diff --git a/codex-rs/repl/src/lib.rs b/codex-rs/repl/src/lib.rs deleted file mode 100644 index fea756b7..00000000 --- a/codex-rs/repl/src/lib.rs +++ /dev/null @@ -1,448 +0,0 @@ -use std::io::stdin; -use std::io::stdout; -use std::io::Write; -use std::sync::Arc; - -use codex_core::config::Config; -use codex_core::config::ConfigOverrides; -use codex_core::protocol; -use codex_core::protocol::AskForApproval; -use codex_core::protocol::FileChange; -use codex_core::protocol::SandboxPolicy; -use codex_core::util::is_inside_git_repo; -use codex_core::util::notify_on_sigint; -use codex_core::Codex; -use owo_colors::OwoColorize; -use owo_colors::Style; -use tokio::io::AsyncBufReadExt; -use tokio::io::BufReader; -use tokio::io::Lines; -use tokio::io::Stdin; -use tokio::sync::Notify; -use tracing::debug; -use tracing_subscriber::EnvFilter; - -mod cli; -pub use cli::Cli; - -/// Initialize the global logger once at startup based on the `--verbose` flag. -fn init_logger(verbose: u8, allow_ansi: bool) { - // Map -v occurrences to explicit log levels: - // 0 → warn (default) - // 1 → info - // 2 → debug - // ≥3 → trace - - let default_level = match verbose { - 0 => "warn", - 1 => "info", - 2 => "codex=debug", - _ => "codex=trace", - }; - - // Only initialize the logger once – repeated calls are ignored. `try_init` will return an - // error if another crate (like tests) initialized it first, which we can safely ignore. - // By default `tracing_subscriber::fmt()` writes formatted logs to stderr. That is fine when - // running the CLI manually but in our smoke tests we capture **stdout** (via `assert_cmd`) and - // ignore stderr. As a result none of the `tracing::info!` banners or warnings show up in the - // recorded output making it much harder to debug live runs. - - // Switch the logger's writer to stdout so both human runs and the integration tests see the - // same stream. Disable ANSI colors because the binary already prints plain text and color - // escape codes make predicate matching brittle. - let _ = tracing_subscriber::fmt() - .with_env_filter( - EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::try_new(default_level)) - .unwrap(), - ) - .with_ansi(allow_ansi) - .with_writer(std::io::stdout) - .try_init(); -} - -pub async fn run_main(cli: Cli) -> anyhow::Result<()> { - let ctrl_c = notify_on_sigint(); - - // Abort early when the user runs Codex outside a Git repository unless - // they explicitly acknowledged the risks with `--allow-no-git-exec`. - if !cli.allow_no_git_exec && !is_inside_git_repo() { - eprintln!( - "We recommend running codex inside a git repository. \ - If you understand the risks, you can proceed with \ - `--allow-no-git-exec`." - ); - std::process::exit(1); - } - - // Initialize logging before any other work so early errors are captured. - init_logger(cli.verbose, !cli.no_ansi); - - let (sandbox_policy, approval_policy) = if cli.full_auto { - ( - Some(SandboxPolicy::new_full_auto_policy()), - Some(AskForApproval::OnFailure), - ) - } else { - let sandbox_policy = cli.sandbox.permissions.clone().map(Into::into); - (sandbox_policy, cli.approval_policy.map(Into::into)) - }; - - // Load config file and apply CLI overrides (model & approval policy) - let overrides = ConfigOverrides { - model: cli.model.clone(), - approval_policy, - sandbox_policy, - disable_response_storage: if cli.disable_response_storage { - Some(true) - } else { - None - }, - }; - let config = Config::load_with_overrides(overrides)?; - - codex_main(cli, config, ctrl_c).await -} - -async fn codex_main(cli: Cli, cfg: Config, ctrl_c: Arc) -> anyhow::Result<()> { - let mut builder = Codex::builder(); - if let Some(path) = cli.record_submissions { - builder = builder.record_submissions(path); - } - if let Some(path) = cli.record_events { - builder = builder.record_events(path); - } - let codex = builder.spawn(Arc::clone(&ctrl_c))?; - let init_id = random_id(); - let init = protocol::Submission { - id: init_id.clone(), - op: protocol::Op::ConfigureSession { - model: cfg.model, - instructions: cfg.instructions, - approval_policy: cfg.approval_policy, - sandbox_policy: cfg.sandbox_policy, - disable_response_storage: cfg.disable_response_storage, - }, - }; - - out( - "initializing session", - MessagePriority::BackgroundEvent, - MessageActor::User, - ); - codex.submit(init).await?; - - // init - loop { - out( - "waiting for session initialization", - MessagePriority::BackgroundEvent, - MessageActor::User, - ); - let event = codex.next_event().await?; - if event.id == init_id { - if let protocol::EventMsg::Error { message } = event.msg { - anyhow::bail!("Error during initialization: {message}"); - } else { - out( - "session initialized", - MessagePriority::BackgroundEvent, - MessageActor::User, - ); - break; - } - } - } - - // run loop - let mut reader = InputReader::new(ctrl_c.clone()); - loop { - let text = match &cli.prompt { - Some(input) => input.clone(), - None => match reader.request_input().await? { - Some(input) => input, - None => { - // ctrl + d - println!(); - return Ok(()); - } - }, - }; - if text.trim().is_empty() { - continue; - } - // Interpret certain single‑word commands as immediate termination requests. - let trimmed = text.trim(); - if trimmed == "q" { - // Exit gracefully. - println!("Exiting…"); - return Ok(()); - } - - let sub = protocol::Submission { - id: random_id(), - op: protocol::Op::UserInput { - items: vec![protocol::InputItem::Text { text }], - }, - }; - - out( - "sending request to model", - MessagePriority::TaskProgress, - MessageActor::User, - ); - codex.submit(sub).await?; - - // Wait for agent events **or** user interrupts (Ctrl+C). - 'inner: loop { - // Listen for either the next agent event **or** a SIGINT notification. Using - // `tokio::select!` allows the user to cancel a long‑running request that would - // otherwise leave the CLI stuck waiting for a server response. - let event = { - let interrupted = ctrl_c.notified(); - tokio::select! { - _ = interrupted => { - // Forward an interrupt to the agent so it can abort any in‑flight task. - let _ = codex - .submit(protocol::Submission { - id: random_id(), - op: protocol::Op::Interrupt, - }) - .await; - - // Exit the inner loop and return to the main input prompt. The agent - // will emit a `TurnInterrupted` (Error) event which is drained later. - break 'inner; - } - res = codex.next_event() => res? - } - }; - - debug!(?event, "Got event"); - let id = event.id; - match event.msg { - protocol::EventMsg::Error { message } => { - println!("Error: {message}"); - break 'inner; - } - protocol::EventMsg::TaskComplete => break 'inner, - protocol::EventMsg::AgentMessage { message } => { - out(&message, MessagePriority::UserMessage, MessageActor::Agent) - } - protocol::EventMsg::SessionConfigured { model } => { - debug!(model, "Session initialized"); - } - protocol::EventMsg::ExecApprovalRequest { - command, - cwd, - reason, - } => { - let reason_str = reason - .as_deref() - .map(|r| format!(" [{r}]")) - .unwrap_or_default(); - - let prompt = format!( - "approve command in {} {}{} (y/N): ", - cwd.display(), - command.join(" "), - reason_str - ); - let decision = request_user_approval2(prompt)?; - let sub = protocol::Submission { - id: random_id(), - op: protocol::Op::ExecApproval { id, decision }, - }; - out( - "submitting command approval", - MessagePriority::TaskProgress, - MessageActor::User, - ); - codex.submit(sub).await?; - } - protocol::EventMsg::ApplyPatchApprovalRequest { - changes, - reason: _, - grant_root: _, - } => { - let file_list = changes - .keys() - .map(|path| path.to_string_lossy().to_string()) - .collect::>() - .join(", "); - let request = - format!("approve apply_patch that will touch? {file_list} (y/N): "); - let decision = request_user_approval2(request)?; - let sub = protocol::Submission { - id: random_id(), - op: protocol::Op::PatchApproval { id, decision }, - }; - out( - "submitting patch approval", - MessagePriority::UserMessage, - MessageActor::Agent, - ); - codex.submit(sub).await?; - } - protocol::EventMsg::ExecCommandBegin { - command, - cwd, - call_id: _, - } => { - out( - &format!("running command: '{}' in '{}'", command.join(" "), cwd), - MessagePriority::BackgroundEvent, - MessageActor::Agent, - ); - } - protocol::EventMsg::ExecCommandEnd { - stdout, - stderr, - exit_code, - call_id: _, - } => { - let msg = if exit_code == 0 { - "command completed (exit 0)".to_string() - } else { - // Prefer stderr but fall back to stdout if empty. - let err_snippet = if !stderr.trim().is_empty() { - stderr.trim() - } else { - stdout.trim() - }; - format!("command failed (exit {exit_code}): {err_snippet}") - }; - out(&msg, MessagePriority::BackgroundEvent, MessageActor::Agent); - out( - "sending results to model", - MessagePriority::TaskProgress, - MessageActor::Agent, - ); - } - protocol::EventMsg::PatchApplyBegin { changes, .. } => { - // Emit PatchApplyBegin so the front‑end can show progress. - let summary = changes - .iter() - .map(|(path, change)| match change { - FileChange::Add { .. } => format!("A {}", path.display()), - FileChange::Delete => format!("D {}", path.display()), - FileChange::Update { .. } => format!("M {}", path.display()), - }) - .collect::>() - .join(", "); - - out( - &format!("applying patch: {summary}"), - MessagePriority::BackgroundEvent, - MessageActor::Agent, - ); - } - protocol::EventMsg::PatchApplyEnd { success, .. } => { - let status = if success { "success" } else { "failed" }; - out( - &format!("patch application {status}"), - MessagePriority::BackgroundEvent, - MessageActor::Agent, - ); - out( - "sending results to model", - MessagePriority::TaskProgress, - MessageActor::Agent, - ); - } - // Broad fallback; if the CLI is unaware of an event type, it will just - // print it as a generic BackgroundEvent. - e => { - out( - &format!("event: {e:?}"), - MessagePriority::BackgroundEvent, - MessageActor::Agent, - ); - } - } - } - } -} - -fn random_id() -> String { - let id: u64 = rand::random(); - id.to_string() -} - -fn request_user_approval2(request: String) -> anyhow::Result { - println!("{}", request); - - let mut line = String::new(); - stdin().read_line(&mut line)?; - let answer = line.trim().to_ascii_lowercase(); - let is_accepted = answer == "y" || answer == "yes"; - let decision = if is_accepted { - protocol::ReviewDecision::Approved - } else { - protocol::ReviewDecision::Denied - }; - Ok(decision) -} - -#[derive(Debug, Clone, Copy)] -enum MessagePriority { - BackgroundEvent, - TaskProgress, - UserMessage, -} -enum MessageActor { - Agent, - User, -} - -impl From for String { - fn from(actor: MessageActor) -> Self { - match actor { - MessageActor::Agent => "codex".to_string(), - MessageActor::User => "user".to_string(), - } - } -} - -fn out(msg: &str, priority: MessagePriority, actor: MessageActor) { - let actor: String = actor.into(); - let style = match priority { - MessagePriority::BackgroundEvent => Style::new().fg_rgb::<127, 127, 127>(), - MessagePriority::TaskProgress => Style::new().fg_rgb::<200, 200, 200>(), - MessagePriority::UserMessage => Style::new().white(), - }; - - println!("{}> {}", actor.bold(), msg.style(style)); -} - -struct InputReader { - reader: Lines>, - ctrl_c: Arc, -} - -impl InputReader { - pub fn new(ctrl_c: Arc) -> Self { - Self { - reader: BufReader::new(tokio::io::stdin()).lines(), - ctrl_c, - } - } - - pub async fn request_input(&mut self) -> std::io::Result> { - print!("user> "); - stdout().flush()?; - let interrupted = self.ctrl_c.notified(); - tokio::select! { - line = self.reader.next_line() => { - match line? { - Some(input) => Ok(Some(input.trim().to_string())), - None => Ok(None), - } - } - _ = interrupted => { - println!(); - Ok(Some(String::new())) - } - } - } -} diff --git a/codex-rs/repl/src/main.rs b/codex-rs/repl/src/main.rs deleted file mode 100644 index f6920794..00000000 --- a/codex-rs/repl/src/main.rs +++ /dev/null @@ -1,11 +0,0 @@ -use clap::Parser; -use codex_repl::run_main; -use codex_repl::Cli; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let cli = Cli::parse(); - run_main(cli).await?; - - Ok(()) -}