// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Arguments for executing a Codex command.
struct CodexExecArgs {
/// The input prompt/message to send to Codex
input : String
/// Base URL for the API
base_url : String?
/// API key for authentication
api_key : String?
/// Thread ID to resume, or None for a new thread
thread_id : String?
/// Model to use (e.g., "gpt-4")
model : String?
/// Sandbox mode that controls access level
sandbox_mode : SandboxMode?
/// Working directory to run commands in
working_directory : String?
/// Whether to skip git repository check
skip_git_repo_check : Bool?
/// Output schema file path
output_schema_file : String?
}
///|
pub fn CodexExecArgs::new(
input : String,
base_url? : String,
api_key? : String,
thread_id? : String,
model? : String,
sandbox_mode? : SandboxMode,
working_directory? : String,
skip_git_repo_check? : Bool,
output_schema_file? : String,
) -> CodexExecArgs {
{
input,
base_url,
api_key,
thread_id,
model,
sandbox_mode,
working_directory,
skip_git_repo_check,
output_schema_file,
}
}
// Constants for environment variable names
// TODO: Use these when implementing process spawning with env vars
///|
const INTERNAL_ORIGINATOR_ENV : String = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
///|
const MOONBIT_SDK_ORIGINATOR : String = "codex_sdk_moonbit"
///|
/// The CodexExec class handles spawning and communicating with the codex CLI.
priv struct CodexExec {
executable_path : String
}
///|
/// Create a new CodexExec instance.
///
/// # Arguments
/// * `executable_path` - Optional path to the codex executable. Default to "codex".
fn CodexExec::new(executable_path? : String = "codex") -> CodexExec {
// For simplicity, we assume "codex" is in PATH if not provided.
{ executable_path, }
}
///|
/// Run a Codex command and yield output lines as they arrive.
///
/// This spawns the codex CLI process, sends the input, and yields each line
/// of JSON output as it's received.
///
/// # Arguments
/// * `args` - The arguments for the Codex execution
///
/// # Returns
/// An iterator that yields String lines from the Codex output
fn CodexExec::run(
self : Self,
args : CodexExecArgs,
) -> @generator.AsyncGenerator[String] raise {
// Build command arguments
let command_args : Array[String] = ["exec", "--experimental-json"]
// Add optional arguments
if args.model is Some(model) {
command_args.push("--model")
command_args.push(model)
}
if args.sandbox_mode is Some(sandbox_mode) {
command_args.push("--sandbox")
command_args.push(sandbox_mode.to_string())
}
if args.working_directory is Some(working_directory) {
command_args.push("--cd")
command_args.push(working_directory)
}
if args.skip_git_repo_check is Some(true) {
command_args.push("--skip-git-repo-check")
}
if args.output_schema_file is Some(output_schema_file) {
command_args.push("--output-schema")
command_args.push(output_schema_file)
}
if args.thread_id is Some(thread_id) {
command_args.push("resume")
command_args.push(thread_id)
}
// Add env
let env = @sys.get_env_vars()
let extra_env = {}
if env.get(INTERNAL_ORIGINATOR_ENV) is None {
extra_env.set(INTERNAL_ORIGINATOR_ENV, MOONBIT_SDK_ORIGINATOR)
}
if args.base_url is Some(base_url) {
extra_env.set("OPENAI_BASE_URL", base_url)
}
if args.api_key is Some(api_key) {
extra_env.set("CODEX_API_KEY", api_key)
}
let stdin = @process.write_to_process() catch { e => raise @error.reraise(e) }
let stdout = @process.read_from_process() catch {
e => raise @error.reraise(e)
}
// CodexExec
@generator.AsyncGenerator::new(fn(yield_) {
@async.with_task_group(taskgroup => {
taskgroup.spawn_bg(() => {
let (exit_code, stderr) = @process.collect_stderr(
self.executable_path,
command_args,
stdin=stdin.0,
stdout=stdout.1,
extra_env~,
)
if exit_code != 0 {
@error.fail("Codex CLI failed: \{stderr.text()}")
}
})
taskgroup.spawn_bg(() => {
defer stdin.1.close()
stdin.1.write(args.input)
})
taskgroup.spawn_bg(() => {
let reader = stdout.0
defer reader.close()
while reader.read_until("\n") is Some(text) {
yield_(text)
}
})
}) catch {
e => if !(e is @generator.Return) { raise @error.reraise(e) }
}
})
}
///|
/// Convert SandboxMode to its string representation for CLI arguments.
fn SandboxMode::to_string(self : SandboxMode) -> String {
match self {
ReadOnly => "read-only"
WorkspaceWrite => "workspace-write"
DangerFullAccess => "danger-full-access"
}
}