use clap::Subcommand;
use objectiveai_sdk::cli::output::{Handle, Notification, Output, Tool, ToolLine, Tools};
use tokio::io::AsyncBufReadExt;
mod install;
#[derive(Subcommand)]
pub enum Commands {
Get {
name: String,
},
Install,
List {
#[arg(long, default_value_t = 0)]
offset: usize,
#[arg(long, default_value_t = 100)]
limit: usize,
},
#[command(external_subcommand)]
Run(Vec<String>),
}
impl Commands {
pub async fn handle(
self,
cli_config: &crate::Config,
handle: &Handle,
) -> Result<(), crate::error::Error> {
match self {
Commands::Get { name } => get(cli_config, handle, &name).await,
Commands::Install => install::emit_instructions(handle).await,
Commands::List { offset, limit } => list(cli_config, handle, offset, limit).await,
Commands::Run(args) => dispatch_tool(args, cli_config, handle).await,
}
}
}
async fn get(
cli_config: &crate::Config,
handle: &Handle,
name: &str,
) -> Result<(), crate::error::Error> {
let fs_client = objectiveai_sdk::filesystem::Client::new(
cli_config.config_base_dir.as_deref(),
cli_config.commit_author_name.as_deref(),
cli_config.commit_author_email.as_deref(),
);
let tool = fs_client.get_tool(name).await;
Output::<Tool>::Notification(Notification { agent_id: None, value: Tool { tool } })
.emit(handle)
.await;
Ok(())
}
async fn list(
cli_config: &crate::Config,
handle: &Handle,
offset: usize,
limit: usize,
) -> Result<(), crate::error::Error> {
let fs_client = objectiveai_sdk::filesystem::Client::new(
cli_config.config_base_dir.as_deref(),
cli_config.commit_author_name.as_deref(),
cli_config.commit_author_email.as_deref(),
);
let tools = fs_client.list_tools(offset, limit).await;
Output::<Tools>::Notification(Notification { agent_id: None, value: Tools { tools } })
.emit(handle)
.await;
Ok(())
}
pub async fn dispatch_tool(
args: Vec<String>,
cli_config: &crate::Config,
handle: &Handle,
) -> Result<(), crate::error::Error> {
let mut iter = args.into_iter();
let name = iter
.next()
.ok_or(crate::error::Error::MissingArgs("tool name"))?;
let rest: Vec<String> = iter.collect();
let fs_client = objectiveai_sdk::filesystem::Client::new(
cli_config.config_base_dir.as_deref(),
cli_config.commit_author_name.as_deref(),
cli_config.commit_author_email.as_deref(),
);
let exe = match fs_client.resolve_tool(&name).await {
Some(p) => p,
None => return Err(crate::error::Error::ToolNotFound(name)),
};
let mut child = tokio::process::Command::new(&exe)
.args(&rest)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(crate::error::Error::ToolSpawn)?;
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
tokio::join!(
forward_stream(stdout, handle, Stream::Stdout),
forward_stream(stderr, handle, Stream::Stderr),
);
let status = child
.wait()
.await
.map_err(crate::error::Error::ToolRead)?;
if status.success() {
Ok(())
} else {
Err(crate::error::Error::ToolExit(status.code().unwrap_or(1)))
}
}
enum Stream {
Stdout,
Stderr,
}
async fn forward_stream<R>(stream: R, handle: &Handle, which: Stream)
where
R: tokio::io::AsyncRead + Unpin,
{
let mut reader = tokio::io::BufReader::new(stream);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) | Err(_) => return,
Ok(_) => {
let trimmed = line.trim_end_matches(['\r', '\n']).to_string();
let value = match which {
Stream::Stdout => ToolLine {
line: trimmed,
stdout: Some(true),
stderr: None,
},
Stream::Stderr => ToolLine {
line: trimmed,
stdout: None,
stderr: Some(true),
},
};
Output::<ToolLine>::Notification(Notification {
agent_id: None,
value,
})
.emit(handle)
.await;
}
}
}
}