use anyhow::{Context, Result};
use crate::ui::{self, Tone};
#[derive(Debug, PartialEq, Eq)]
pub struct WrapInvocation {
pub agent: String,
pub args: Vec<String>,
}
const KNOWN_AGENTS: &[&str] = &["claude", "codex", "cursor", "aider", "copilot", "gemini"];
fn parse_invocation(raw: &[String]) -> Result<WrapInvocation> {
let mut it = raw.iter();
let agent = it
.next()
.context("`wrap` needs an agent to run, e.g. `llmtrim wrap claude`")?
.clone();
let mut args: Vec<String> = it.cloned().collect();
if args.first().map(String::as_str) == Some("--") {
args.remove(0);
}
Ok(WrapInvocation { agent, args })
}
#[derive(Debug, PartialEq, Eq)]
enum Readiness {
Ready,
DaemonDown,
EnvUnwired,
}
fn readiness(daemon_running: bool, env_wired: bool) -> Readiness {
match (env_wired, daemon_running) {
(true, true) => Readiness::Ready,
(true, false) => Readiness::DaemonDown,
(false, _) => Readiness::EnvUnwired,
}
}
pub fn https_proxy_is_local() -> bool {
std::env::var("HTTPS_PROXY")
.or_else(|_| std::env::var("https_proxy"))
.map(|v| v.contains("127.0.0.1") || v.contains("localhost"))
.unwrap_or(false)
}
pub fn run(raw: Vec<String>) -> Result<()> {
let inv = parse_invocation(&raw)?;
let color = ui::color_stdout();
let daemon_running = crate::daemon::running().is_some();
let env_wired = https_proxy_is_local();
match readiness(daemon_running, env_wired) {
Readiness::Ready => {}
Readiness::DaemonDown => {
let port = crate::setup::resolve_port(None, None)?;
let pid = crate::daemon::spawn_detached(port)
.context("interceptor is down and could not be started")?;
eprintln!(
"{}",
ui::note(
ui::color_stderr(),
&format!("Started the interceptor (pid {pid} · port {port}).")
)
);
}
Readiness::EnvUnwired => {
let hint = if crate::setup::profile_has_block() {
"You've run `llmtrim setup`, but this shell started before it. Open a new \
shell (or re-source your profile) and try again."
} else {
"Run `llmtrim setup` once (then open a new shell), and try again."
};
anyhow::bail!(
"HTTPS_PROXY isn't pointing at llmtrim in this shell, so `{}` wouldn't route \
through it.\n{hint}",
inv.agent
);
}
}
eprintln!(
"{}",
ui::paint(color, Tone::Dim, &format!("llmtrim wrap → {}", inv.agent))
);
exec_agent(&inv)
}
fn agent_is_claude(agent: &str) -> bool {
let base = agent
.rsplit(['/', '\\'])
.next()
.unwrap_or(agent)
.to_ascii_lowercase();
base == "claude" || base.starts_with("claude-") || base == "claude.exe"
}
fn exec_agent(inv: &WrapInvocation) -> Result<()> {
let mut cmd = std::process::Command::new(&inv.agent);
cmd.args(&inv.args);
if llmtrim_core::config::sub_skip_anthropic_login()
&& agent_is_claude(&inv.agent)
&& std::env::var_os("ANTHROPIC_AUTH_TOKEN").is_none()
{
cmd.env(
"ANTHROPIC_AUTH_TOKEN",
crate::statusline::SUB_AUTH_TOKEN_VALUE,
);
}
let status = cmd.status().with_context(|| {
if KNOWN_AGENTS.contains(&inv.agent.as_str()) {
format!(
"failed to launch `{}`: is it installed and on your PATH?",
inv.agent
)
} else {
format!(
"failed to launch `{}`: not found on PATH (pass an installed binary, \
e.g. one of: {})",
inv.agent,
KNOWN_AGENTS.join(", ")
)
}
})?;
std::process::exit(status.code().unwrap_or(1));
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|x| x.to_string()).collect()
}
#[test]
fn parses_agent_with_no_args() {
let inv = parse_invocation(&s(&["claude"])).expect("agent only");
assert_eq!(inv.agent, "claude");
assert!(inv.args.is_empty());
}
#[test]
fn forwards_trailing_args_verbatim() {
let inv = parse_invocation(&s(&["claude", "chat", "--model", "x"])).expect("with args");
assert_eq!(inv.agent, "claude");
assert_eq!(inv.args, s(&["chat", "--model", "x"]));
}
#[test]
fn drops_single_leading_double_dash() {
let inv = parse_invocation(&s(&["aider", "--", "--foo", "bar"])).expect("dash sep");
assert_eq!(inv.agent, "aider");
assert_eq!(inv.args, s(&["--foo", "bar"]));
}
#[test]
fn only_first_double_dash_is_dropped() {
let inv = parse_invocation(&s(&["x", "--", "--", "y"])).expect("two dashes");
assert_eq!(inv.args, s(&["--", "y"]));
}
#[test]
fn empty_invocation_is_an_error() {
assert!(parse_invocation(&[]).is_err());
}
#[test]
fn accepts_any_binary_name_not_just_known_ones() {
let inv = parse_invocation(&s(&["some-random-tool"])).expect("free-form");
assert_eq!(inv.agent, "some-random-tool");
}
#[test]
fn readiness_ready_when_both_present() {
assert_eq!(readiness(true, true), Readiness::Ready);
}
#[test]
fn readiness_daemon_down_when_env_wired_only() {
assert_eq!(readiness(false, true), Readiness::DaemonDown);
}
#[test]
fn readiness_env_unwired_takes_precedence() {
assert_eq!(readiness(false, false), Readiness::EnvUnwired);
assert_eq!(readiness(true, false), Readiness::EnvUnwired);
}
#[test]
fn agent_is_claude_matches_claude_binaries_only() {
assert!(agent_is_claude("claude"));
assert!(agent_is_claude("/usr/bin/claude"));
assert!(agent_is_claude("claude-2"));
assert!(agent_is_claude(r"C:\Tools\claude.exe"));
assert!(!agent_is_claude("codex"));
assert!(!agent_is_claude("gemini"));
assert!(!agent_is_claude("/usr/bin/aider"));
}
}