mod channel;
mod chat;
mod decisions;
mod detect;
mod evolution;
mod gateway;
mod login;
mod model;
mod profile;
mod run;
mod sidecar_support;
#[cfg(feature = "tape-viewer")]
mod tape;
mod task;
use std::path::PathBuf;
use std::sync::Arc;
use clap::{Subcommand, ValueEnum};
use crate::builtin::BuiltinImpl;
use crate::framework::EliFramework;
#[derive(Debug, Subcommand)]
pub enum CliCommand {
Run {
message: String,
#[arg(long, default_value = "cli")]
channel: String,
#[arg(long, default_value = "local")]
chat_id: String,
#[arg(long, default_value = "human")]
sender_id: String,
#[arg(long)]
session_id: Option<String>,
},
Chat {
#[arg(long, default_value = "local")]
chat_id: String,
#[arg(long)]
session_id: Option<String>,
},
Login {
provider: String,
#[arg(long)]
codex_home: Option<PathBuf>,
#[arg(long, default_value_t = true)]
browser: bool,
#[arg(long)]
manual: bool,
#[arg(long, default_value_t = 300.0)]
timeout: f64,
#[arg(long)]
api_key: bool,
},
Use {
profile: Option<String>,
},
Status,
Channel {
#[command(subcommand)]
action: ChannelAction,
},
#[command(hide = true)]
Hooks,
Model {
name: Option<String>,
},
Gateway,
#[cfg(feature = "tape-viewer")]
Tape {
#[arg(long, default_value_t = 7700)]
port: u16,
#[arg(long)]
dir: Option<std::path::PathBuf>,
},
Decisions {
#[command(subcommand)]
action: DecisionAction,
},
Evolution {
#[command(subcommand)]
action: EvolutionAction,
},
Task {
#[command(subcommand)]
action: TaskAction,
},
}
#[derive(Debug, Subcommand)]
pub enum DecisionAction {
List,
Remove {
index: usize,
},
Export,
}
#[derive(Debug, Subcommand)]
pub enum ChannelAction {
Login {
channel: String,
},
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum EvolutionStatusArg {
Pending,
Promoted,
Rejected,
RolledBack,
}
#[derive(Debug, Subcommand)]
pub enum EvolutionAction {
List {
#[arg(long)]
status: Option<EvolutionStatusArg>,
},
History {
#[arg(long, default_value_t = 20)]
limit: usize,
},
Show {
id: String,
},
Distill {
tape: String,
#[arg(long)]
persist: bool,
},
AutoRun {
tape: String,
},
Evaluate {
id: String,
},
CaptureRule {
title: String,
#[arg(long)]
summary: String,
#[arg(long)]
content: String,
},
CaptureSkill {
skill_name: String,
#[arg(long)]
title: Option<String>,
#[arg(long)]
description: String,
#[arg(long)]
content: String,
},
CaptureKnowledge {
artifact_name: String,
#[arg(long)]
title: Option<String>,
#[arg(long)]
summary: String,
#[arg(long)]
content: String,
},
CaptureRuntimePolicy {
artifact_name: String,
#[arg(long)]
title: Option<String>,
#[arg(long)]
summary: String,
#[arg(long)]
content: String,
},
Promote {
id: String,
#[arg(long)]
force: bool,
},
Reject {
id: String,
},
Rollback {
id: String,
},
}
#[derive(Debug, Subcommand)]
pub enum TaskAction {
Add {
description: String,
#[arg(long, short)]
kind: Option<String>,
#[arg(long, short, default_value_t = 1)]
priority: u8,
#[arg(long)]
parent: Option<String>,
},
List {
#[arg(long, short)]
status: Option<String>,
#[arg(long, short)]
kind: Option<String>,
#[arg(long, short, default_value_t = 20)]
limit: usize,
},
Show {
task_id: String,
},
Cancel {
task_id: String,
#[arg(long, short)]
reason: Option<String>,
},
Board,
Stats,
}
pub async fn execute(cmd: CliCommand) -> anyhow::Result<()> {
match cmd {
CliCommand::Run {
message,
channel,
chat_id,
sender_id,
session_id,
} => run::run_command(message, channel, chat_id, sender_id, session_id).await,
CliCommand::Chat {
chat_id,
session_id,
} => chat::chat_command(chat_id, session_id).await,
CliCommand::Login {
provider,
codex_home,
browser,
manual,
timeout,
api_key,
} => login::login_command(provider, codex_home, browser, manual, timeout, api_key).await,
CliCommand::Use { profile } => profile::use_command(profile),
CliCommand::Model { name } => model::model_command(name).await,
CliCommand::Status => profile::status_command(),
CliCommand::Channel { action } => channel::channel_command(action).await,
CliCommand::Hooks => {
hooks_command().await;
Ok(())
}
CliCommand::Gateway => gateway::gateway_command().await,
#[cfg(feature = "tape-viewer")]
CliCommand::Tape { port, dir } => tape::tape_command(port, dir).await,
CliCommand::Decisions { action } => match action {
DecisionAction::List => decisions::list_command().await,
DecisionAction::Remove { index } => decisions::remove_command(index).await,
DecisionAction::Export => decisions::export_command().await,
},
CliCommand::Evolution { action } => match action {
EvolutionAction::List { status } => {
evolution::list_command(status.map(map_evolution_status)).await
}
EvolutionAction::History { limit } => evolution::history_command(limit).await,
EvolutionAction::Show { id } => evolution::show_command(id).await,
EvolutionAction::Distill { tape, persist } => {
evolution::distill_command(tape, persist).await
}
EvolutionAction::AutoRun { tape } => evolution::auto_run_command(tape).await,
EvolutionAction::Evaluate { id } => evolution::evaluate_command(id).await,
EvolutionAction::CaptureRule {
title,
summary,
content,
} => evolution::capture_rule_command(title, summary, content).await,
EvolutionAction::CaptureSkill {
skill_name,
title,
description,
content,
} => evolution::capture_skill_command(skill_name, title, description, content).await,
EvolutionAction::CaptureKnowledge {
artifact_name,
title,
summary,
content,
} => evolution::capture_knowledge_command(artifact_name, title, summary, content).await,
EvolutionAction::CaptureRuntimePolicy {
artifact_name,
title,
summary,
content,
} => {
evolution::capture_runtime_policy_command(artifact_name, title, summary, content)
.await
}
EvolutionAction::Promote { id, force } => evolution::promote_command(id, force).await,
EvolutionAction::Reject { id } => evolution::reject_command(id).await,
EvolutionAction::Rollback { id } => evolution::rollback_command(id).await,
},
CliCommand::Task { action } => {
crate::taskboard::init_task_store(&crate::builtin::config::eli_home());
match action {
TaskAction::Add {
description,
kind,
priority,
parent,
} => task::add_command(description, kind, priority, parent).await,
TaskAction::List {
status,
kind,
limit,
} => task::list_command(status, kind, limit).await,
TaskAction::Show { task_id } => task::show_command(task_id).await,
TaskAction::Cancel { task_id, reason } => {
task::cancel_command(task_id, reason).await
}
TaskAction::Board => task::board_command().await,
TaskAction::Stats => task::stats_command().await,
}
}
}
}
async fn hooks_command() {
let (framework, _builtin) = builtin_framework().await;
let mut report: Vec<_> = framework.hook_report().await.into_iter().collect();
report.sort_by(|a, b| a.0.cmp(&b.0));
println!("Hook implementations:");
for (name, mut plugins) in report {
plugins.sort();
println!(" {name}:");
if plugins.is_empty() {
println!(" - (none)");
continue;
}
for plugin in plugins {
println!(" - {plugin}");
}
}
}
pub(crate) fn strip_fake_tool_calls(text: &str) -> String {
static RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(r"(?s)<function_calls>.*?</function_calls>")
.expect("SAFETY: regex is a static literal")
});
RE.replace_all(text, "").trim().to_owned()
}
async fn builtin_framework() -> (Arc<EliFramework>, Arc<BuiltinImpl>) {
let builtin = Arc::new(BuiltinImpl::new());
let framework = Arc::new(EliFramework::new());
framework.register_plugin("builtin", builtin.clone()).await;
(framework, builtin)
}
fn print_usage(usage: &crate::types::TurnUsageInfo) {
if usage.total_tokens > 0 {
eprintln!(
"\x1b[2m[tokens: {} in + {} out = {}]\x1b[0m",
usage.input_tokens, usage.output_tokens, usage.total_tokens,
);
}
}
fn map_evolution_status(status: EvolutionStatusArg) -> crate::evolution::CandidateStatus {
match status {
EvolutionStatusArg::Pending => crate::evolution::CandidateStatus::Pending,
EvolutionStatusArg::Promoted => crate::evolution::CandidateStatus::Promoted,
EvolutionStatusArg::Rejected => crate::evolution::CandidateStatus::Rejected,
EvolutionStatusArg::RolledBack => crate::evolution::CandidateStatus::RolledBack,
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Debug, Parser)]
struct TestCli {
#[command(subcommand)]
command: CliCommand,
}
#[test]
fn test_parse_evolution_auto_run() {
let cmd = TestCli::try_parse_from(["eli", "evolution", "auto-run", "tape-1"]).unwrap();
match cmd.command {
CliCommand::Evolution {
action: EvolutionAction::AutoRun { tape },
} => assert_eq!(tape, "tape-1"),
other => panic!("unexpected command: {other:?}"),
}
}
#[test]
fn test_parse_evolution_history_limit() {
let cmd = TestCli::try_parse_from(["eli", "evolution", "history", "--limit", "7"]).unwrap();
match cmd.command {
CliCommand::Evolution {
action: EvolutionAction::History { limit },
} => assert_eq!(limit, 7),
other => panic!("unexpected command: {other:?}"),
}
}
#[test]
fn test_parse_evolution_capture_knowledge() {
let cmd = TestCli::try_parse_from([
"eli",
"evolution",
"capture-knowledge",
"incident-handbook",
"--summary",
"Escalation notes",
"--content",
"body",
])
.unwrap();
match cmd.command {
CliCommand::Evolution {
action: EvolutionAction::CaptureKnowledge { artifact_name, .. },
} => assert_eq!(artifact_name, "incident-handbook"),
other => panic!("unexpected command: {other:?}"),
}
}
#[test]
fn test_parse_evolution_capture_runtime_policy() {
let cmd = TestCli::try_parse_from([
"eli",
"evolution",
"capture-runtime-policy",
"auto-evolution",
"--summary",
"Tune thresholds",
"--content",
"{\"auto_evolution\":{\"min_score\":95}}",
])
.unwrap();
match cmd.command {
CliCommand::Evolution {
action: EvolutionAction::CaptureRuntimePolicy { artifact_name, .. },
} => assert_eq!(artifact_name, "auto-evolution"),
other => panic!("unexpected command: {other:?}"),
}
}
#[test]
fn test_parse_channel_login_weixin() {
let cmd = TestCli::try_parse_from(["eli", "channel", "login", "weixin"]).unwrap();
match cmd.command {
CliCommand::Channel {
action: ChannelAction::Login { channel },
} => assert_eq!(channel, "weixin"),
other => panic!("unexpected command: {other:?}"),
}
}
#[test]
fn test_parse_login_coding_plan() {
let cmd = TestCli::try_parse_from(["eli", "login", "coding-plan"]).unwrap();
match cmd.command {
CliCommand::Login { provider, .. } => assert_eq!(provider, "coding-plan"),
other => panic!("unexpected command: {other:?}"),
}
}
}