use anyhow::Context as _;
use clap::Parser;
use std::path::PathBuf;
use std::time::Duration;
use agent_block_core::host::{PromptSource, ScriptSource, SecretKeySource};
use agent_block_core::{run, BlockConfig};
use agent_block_mcp::DEFAULT_RPC_TIMEOUT;
#[derive(Parser, Debug)]
#[command(
name = "agent-block",
about = "Single-purpose agent building block with built-in mesh communication"
)]
struct Cli {
#[arg(short = 's', long)]
script: PathBuf,
#[arg(short = 'r', long)]
relay: Option<String>,
#[arg(long, env = "AGENT_BLOCK_MESH_SECRET_KEY")]
secret_key: Option<String>,
#[arg(short = 'p', long, default_value = ".")]
project: PathBuf,
#[arg(long, value_name = "SECS", value_parser = clap::value_parser!(u64).range(1..))]
mcp_timeout_secs: Option<u64>,
#[arg(long, env = "AGENT_BLOCK_PROMPT")]
prompt: Option<String>,
#[arg(short = 'c', long, env = "AGENT_BLOCK_CONTEXT")]
context: Option<String>,
#[arg(long, value_name = "FILE", conflicts_with = "prompt")]
prompt_file: Option<PathBuf>,
#[arg(long, value_name = "FILE", conflicts_with = "context")]
context_file: Option<PathBuf>,
}
#[tokio::main]
async fn main() {
if let Err(err) = run_cli().await {
eprintln!("error: {err}");
for cause in err.chain().skip(1) {
eprintln!("caused by: {cause}");
}
std::process::exit(1);
}
}
async fn run_cli() -> anyhow::Result<()> {
let _ = rustls::crypto::ring::default_provider().install_default();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let cli = Cli::parse();
let mcp_rpc_timeout = cli
.mcp_timeout_secs
.map(Duration::from_secs)
.unwrap_or(DEFAULT_RPC_TIMEOUT);
let prompt = match (cli.prompt, cli.prompt_file) {
(None, None) => None,
(Some(s), None) => Some(PromptSource::Inline(s)),
(None, Some(p)) => {
let content = std::fs::read_to_string(&p)
.with_context(|| format!("failed to read --prompt-file '{}'", p.display()))?;
Some(PromptSource::Inline(content))
}
(Some(_), Some(_)) => {
anyhow::bail!("--prompt and --prompt-file are mutually exclusive");
}
};
let context = match (cli.context, cli.context_file) {
(None, None) => None,
(Some(s), None) => Some(PromptSource::Inline(s)),
(None, Some(p)) => {
let content = std::fs::read_to_string(&p)
.with_context(|| format!("failed to read --context-file '{}'", p.display()))?;
Some(PromptSource::Inline(content))
}
(Some(_), Some(_)) => {
anyhow::bail!("--context and --context-file are mutually exclusive");
}
};
let mut builder = BlockConfig::builder(ScriptSource::Path(cli.script), cli.project)
.mcp_rpc_timeout(mcp_rpc_timeout);
if let Some(relay) = cli.relay {
builder = builder.relay_url(relay);
}
if let Some(secret_key) = cli.secret_key {
builder = builder.secret_key(SecretKeySource::Inline(secret_key));
}
if let Some(prompt) = prompt {
builder = builder.prompt(prompt);
}
if let Some(context) = context {
builder = builder.context(context);
}
let config = builder.build();
Ok(run(config).await?)
}