use std::collections::HashSet;
use std::path::{Path, PathBuf};
use futures::StreamExt;
use objectiveai_sdk::cli::command::command_executor::sse;
use objectiveai_sdk::cli::command::{
AgentArguments, CommandExecutor, ParseError, Request, SseCommandExecutor, parse_request,
};
use tokio::io::AsyncWriteExt;
const DAEMON_LOCK_KEY: &str = "plugins-daemon";
#[tokio::main]
async fn main() {
let _ = dotenv::dotenv();
let dir = objectiveai_dir();
let _ = dotenv::from_path(dir.join(".env"));
let args: Vec<String> = std::env::args().collect();
let code = run(args).await;
std::process::exit(code);
}
fn objectiveai_dir() -> PathBuf {
std::env::var_os("OBJECTIVEAI_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".objectiveai")
})
}
async fn run(args: Vec<String>) -> i32 {
let mut stdout = tokio::io::stdout();
let request = match parse_request(args.get(1..).unwrap_or_default()) {
Ok(request) => request,
Err(ParseError::Clap(clap_err)) => {
if is_informational(&clap_err) {
write_help_line(&mut stdout, &clap_err.to_string()).await;
return 0;
}
write_error_line(&mut stdout, clap_err.to_string(), Some(true)).await;
return 1;
}
Err(ParseError::FromArgs(e)) => {
write_error_line(&mut stdout, e.to_string(), Some(true)).await;
return 1;
}
};
enum Special {
DaemonKill,
KillAll,
None,
}
let special = match &request {
Request::Daemon(objectiveai_sdk::cli::command::daemon::Request::Kill(_)) => {
Special::DaemonKill
}
Request::KillAll(_) => Special::KillAll,
_ => Special::None,
};
match special {
Special::DaemonKill => return handle_daemon_kill(&mut stdout).await,
Special::KillAll => return handle_kill_all(&mut stdout, request).await,
Special::None => {}
}
let (executor, agent_arguments) = match connect().await {
Ok(pair) => pair,
Err(message) => {
write_error_line(&mut stdout, message, Some(true)).await;
return 1;
}
};
match executor
.execute::<_, serde_json::Value>(request, Some(&agent_arguments))
.await
{
Ok(mut stream) => {
let mut saw_error = false;
while let Some(item) = stream.next().await {
match item {
Ok(value) => write_json_line(&mut stdout, &value).await,
Err(e) => {
saw_error = true;
write_execute_error(&mut stdout, e).await;
}
}
}
if saw_error { 1 } else { 0 }
}
Err(e) => {
write_execute_error(&mut stdout, e).await;
1
}
}
}
struct Layout {
dir: PathBuf,
state: String,
lock_dir: PathBuf,
daemon_exe: PathBuf,
}
fn resolve_layout() -> Layout {
let dir = objectiveai_dir();
let state = std::env::var("OBJECTIVEAI_STATE")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "default".to_string());
let lock_dir = dir.join("state").join(&state).join("locks");
let daemon_bin = if cfg!(windows) {
"objectiveai-daemon.exe"
} else {
"objectiveai-daemon"
};
let daemon_exe = dir.join("bin").join(daemon_bin);
Layout { dir, state, lock_dir, daemon_exe }
}
fn daemon_signature() -> Option<String> {
std::env::var("DAEMON_SIGNATURE").ok().filter(|s| !s.is_empty())
}
fn executor_for(url: &str) -> SseCommandExecutor {
let executor = SseCommandExecutor::new(format!("{url}/execute"));
match daemon_signature() {
Some(signature) => executor.signature(signature),
None => executor,
}
}
async fn connect() -> Result<(SseCommandExecutor, AgentArguments), String> {
if let Ok(addr) = std::env::var("DAEMON_ADDRESS")
&& !addr.is_empty()
{
return Ok((executor_for(&addr), agent_arguments_from_env()));
}
let layout = resolve_layout();
let url = objectiveai_sdk::lockfile::spawn_until_published(
&layout.daemon_exe,
&layout.lock_dir,
DAEMON_LOCK_KEY,
|cmd| {
cmd.arg("daemon")
.arg("spawn")
.arg("--dangerous-advanced")
.arg("{\"foreground\":true}");
cmd.env("OBJECTIVEAI_DIR", &layout.dir);
cmd.env("OBJECTIVEAI_STATE", &layout.state);
for var in [
"OBJECTIVEAI_AGENT_INSTANCE_HIERARCHY",
"OBJECTIVEAI_AGENT_ID",
"OBJECTIVEAI_AGENT_FULL_ID",
"OBJECTIVEAI_AGENT_REMOTE",
"OBJECTIVEAI_RESPONSE_ID",
"OBJECTIVEAI_RESPONSE_IDS",
"OBJECTIVEAI_PLUGIN_OWNER",
"OBJECTIVEAI_PLUGIN_REPOSITORY",
"OBJECTIVEAI_PLUGIN_VERSION",
] {
cmd.env_remove(var);
}
cmd.env_remove(objectiveai_sdk::mcp::MCP_SESSION_ID_ENV);
match std::env::var("DAEMON_SECRET") {
Ok(s) if !s.is_empty() => {
cmd.env("SECRET", s);
}
_ => {
cmd.env_remove("SECRET");
}
}
cmd.env_remove("ADDRESS");
cmd.env_remove("PORT");
},
)
.await
.map_err(|e| format!("ensure objectiveai-daemon: {e}"))?;
Ok((executor_for(&url), agent_arguments_from_env()))
}
async fn handle_daemon_kill(stdout: &mut tokio::io::Stdout) -> i32 {
let layout = resolve_layout();
let killed: usize = match objectiveai_sdk::lockfile::owners(&layout.lock_dir, DAEMON_LOCK_KEY)
.await
{
Ok(pids) => pids.into_iter().map(objectiveai_sdk::process::kill_pid).sum(),
Err(e) => {
write_error_line(stdout, format!("read daemon lock owners: {e}"), Some(true)).await;
return 1;
}
};
write_json_line(
stdout,
&objectiveai_sdk::cli::command::daemon::kill::Response { killed },
)
.await;
0
}
async fn handle_kill_all(stdout: &mut tokio::io::Stdout, request: Request) -> i32 {
let layout = resolve_layout();
let daemon_up = match objectiveai_sdk::lockfile::try_read(&layout.lock_dir, DAEMON_LOCK_KEY)
.await
{
Ok(url) => url,
Err(e) => {
write_error_line(stdout, format!("read daemon lock: {e}"), Some(true)).await;
return 1;
}
};
let killed = match daemon_up {
Some(url) => {
let others = match kill_all_via_daemon(&url, request).await {
Ok(n) => n,
Err(message) => {
write_error_line(
stdout,
format!("{message}; finishing with a local sweep"),
Some(false),
)
.await;
kill_tree_locally(&layout.dir).await
}
};
let daemon_killed: usize =
match objectiveai_sdk::lockfile::owners(&layout.lock_dir, DAEMON_LOCK_KEY).await {
Ok(pids) => pids.into_iter().map(objectiveai_sdk::process::kill_pid).sum(),
Err(_) => 0,
};
others + daemon_killed
}
None => kill_tree_locally(&layout.dir).await,
};
write_json_line(
stdout,
&objectiveai_sdk::cli::command::kill_all::Response { killed },
)
.await;
0
}
async fn kill_all_via_daemon(url: &str, mut request: Request) -> Result<usize, String> {
if let Request::KillAll(kr) = &mut request {
kr.base.jq = None;
kr.base.python = None;
}
let executor = executor_for(url);
let mut stream = executor
.execute::<_, serde_json::Value>(request, Some(&agent_arguments_from_env()))
.await
.map_err(|e| format!("kill-all over daemon: {e}"))?;
match stream.next().await {
Some(Ok(value)) => Ok(value
.get("killed")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0) as usize),
Some(Err(e)) => Err(format!("kill-all over daemon: {e}")),
None => Ok(0),
}
}
async fn kill_tree_locally(dir: &Path) -> usize {
let me = std::process::id();
let mut killed: HashSet<u32> = HashSet::new();
for _ in 0..2 {
let Ok(pids) = objectiveai_sdk::lockfile::owners_in_tree(dir).await else {
break;
};
for pid in pids {
if pid == me || pid == 0 {
continue;
}
if objectiveai_sdk::process::kill_pid(pid) == 1 {
killed.insert(pid);
}
}
}
killed.len()
}
fn agent_arguments_from_env() -> AgentArguments {
let var = |key: &str| std::env::var(key).ok().filter(|s| !s.is_empty());
AgentArguments {
agent_instance_hierarchy: var("OBJECTIVEAI_AGENT_INSTANCE_HIERARCHY")
.or_else(|| Some("cli".to_string())),
agent_id: var("OBJECTIVEAI_AGENT_ID"),
agent_full_id: var("OBJECTIVEAI_AGENT_FULL_ID"),
agent_remote: var("OBJECTIVEAI_AGENT_REMOTE"),
response_id: var("OBJECTIVEAI_RESPONSE_ID"),
response_ids: var("OBJECTIVEAI_RESPONSE_IDS"),
mcp_session_id: None,
}
}
fn is_informational(e: &clap::Error) -> bool {
use clap::error::ErrorKind;
matches!(
e.kind(),
ErrorKind::DisplayHelp
| ErrorKind::DisplayVersion
| ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
)
}
async fn write_execute_error(stdout: &mut tokio::io::Stdout, e: sse::Error) {
match e {
sse::Error::Cli(cli) => write_json_line(stdout, &cli).await,
other => write_error_line(stdout, other.to_string(), Some(true)).await,
}
}
async fn write_json_line<T: serde::Serialize>(stdout: &mut tokio::io::Stdout, value: &T) {
let line = match serde_json::to_string(value) {
Ok(s) => s,
Err(e) => format!(r#"{{"type":"error","fatal":false,"message":"serialize error: {e}"}}"#),
};
let _ = stdout.write_all(line.as_bytes()).await;
let _ = stdout.write_all(b"\n").await;
let _ = stdout.flush().await;
}
async fn write_error_line(
stdout: &mut tokio::io::Stdout,
message: impl Into<String>,
fatal: Option<bool>,
) {
let payload = objectiveai_sdk::cli::Error {
r#type: objectiveai_sdk::cli::ErrorType::Error,
level: Some(objectiveai_sdk::cli::Level::Error),
fatal,
message: serde_json::Value::String(message.into()),
};
write_json_line(stdout, &payload).await;
}
async fn write_help_line(stdout: &mut tokio::io::Stdout, help: &str) {
let payload = serde_json::json!({ "type": "help", "help": help });
write_json_line(stdout, &payload).await;
}