use anyhow::{Context, Result};
use tokio::process::Command;
use super::common;
pub async fn run() -> Result<()> {
let turn = common::read_turn_or_error();
let (message, session_id) = common::message_and_session(&turn);
let (response, new_session_id) =
common::with_session_resume_fallback("agy", &message, &session_id, |m, s| async move {
call_agy(&m, &s).await
})
.await?;
let mut emitter = common::SessionEmitter::new(&session_id);
emitter.emit_result_opt(
(!response.trim().is_empty()).then_some(response.as_str()),
new_session_id.as_deref(),
)?;
Ok(())
}
async fn call_agy(message: &str, session_id: &str) -> Result<(String, Option<String>)> {
let log_path = format!("/tmp/agy-ilink-{}.log", std::process::id());
let mut args: Vec<String> = vec![
"--dangerously-skip-permissions".into(),
"--log-file".into(),
log_path.clone(),
];
if let Ok(model) = std::env::var("AGY_MODEL") {
if !model.trim().is_empty() {
args.push("--model".into());
args.push(model.trim().to_string());
}
}
if !session_id.is_empty() {
args.push("--conversation".into());
args.push(session_id.to_string());
}
args.push("-p".into());
args.push(message.to_string());
let mut cmd = Command::new("agy");
cmd.args(&args);
cmd.stdin(std::process::Stdio::piped());
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
cmd.kill_on_drop(true);
let mut child = cmd
.spawn()
.context("failed to spawn `agy`; ensure Antigravity CLI is installed and in PATH")?;
drop(child.stdin.take());
let child_stdout = child.stdout.take().context("stdout pipe missing")?;
let child_stderr = child.stderr.take().context("stderr pipe missing")?;
let stderr_task = common::spawn_capped_drain(child_stderr);
let stdout_task = common::spawn_capped_drain(child_stdout);
let status = child.wait().await.context("wait for agy")?;
let stderr = stderr_task.await.unwrap_or_default();
let stdout = stdout_task.await.unwrap_or_default();
let new_conv_id = if session_id.is_empty() {
extract_conversation_id_from_log(&log_path, "Created conversation").await
} else {
Some(session_id.to_string())
};
let _ = tokio::fs::remove_file(&log_path).await;
common::ensure_success("agy", status, &stderr, !stdout.trim().is_empty())?;
Ok((stdout, new_conv_id))
}
async fn extract_conversation_id_from_log(log_path: &str, prefix: &str) -> Option<String> {
let content = tokio::fs::read_to_string(log_path).await.ok()?;
for line in content.lines() {
let Some(pos) = line.find(prefix) else {
continue;
};
let after = line[pos + prefix.len()..].trim_start();
if after.len() >= 36 {
let candidate = &after[..36];
if is_uuid_like(candidate) {
return Some(candidate.to_string());
}
}
}
None
}
fn is_uuid_like(s: &str) -> bool {
let parts: Vec<&str> = s.splitn(5, '-').collect();
if parts.len() != 5 {
return false;
}
let expected_lens = [8, 4, 4, 4, 12];
parts
.iter()
.zip(expected_lens.iter())
.all(|(p, &len)| p.len() == len && p.chars().all(|c| c.is_ascii_hexdigit()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uuid_validation() {
assert!(is_uuid_like("83b95686-35cf-4940-9857-f0ad892a346c"));
assert!(is_uuid_like("00000000-0000-0000-0000-000000000000"));
assert!(!is_uuid_like("not-a-uuid"));
assert!(!is_uuid_like("83b95686-35cf-4940-9857"));
}
#[test]
fn extract_from_log_line() {
let line = "I0615 19:29:54.053019 92471 server.go:755] Created conversation 83b95686-35cf-4940-9857-f0ad892a346c";
let prefix = "Created conversation";
let pos = line.find(prefix).unwrap();
let after = line[pos + prefix.len()..].trim_start();
let candidate = &after[..36];
assert!(is_uuid_like(candidate));
assert_eq!(candidate, "83b95686-35cf-4940-9857-f0ad892a346c");
}
}