use anyhow::Result;
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use octomind::config::Config;
mod commands;
#[derive(Parser)]
#[command(name = "octomind")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "Octomind is a smart AI developer assistant with configurable MCP support")]
struct CliArgs {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Config(commands::ConfigArgs),
Run(commands::RunArgs),
Server(commands::ServerArgs),
Acp(commands::AcpArgs),
Tap(commands::TapArgs),
Untap(commands::UntapArgs),
Vars(commands::VarsArgs),
Send(commands::SendArgs),
Completion {
#[arg(value_enum)]
shell: Shell,
},
#[command(hide = true)]
Complete(commands::CompleteArgs),
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let _tracker = octomind::config::get_env_tracker();
if let Err(e) = octomind::config::get_env_tracker()
.lock()
.unwrap()
.load_dotenv_override()
{
octomind::log_debug!("Failed to load .env file: {}", e);
}
let launch_cwd = std::env::current_dir().unwrap_or_default();
octomind::mcp::set_session_working_directory(launch_cwd);
let args = CliArgs::parse();
let config = Config::load()?;
let result = run_with_cleanup(args, config).await;
if let Err(e) = octomind::mcp::server::cleanup_servers() {
octomind::log_error!("Warning: Error cleaning up MCP servers: {}", e);
}
result
}
async fn run_with_cleanup(args: CliArgs, config: Config) -> Result<(), anyhow::Error> {
let log_level = config.log_level.as_str();
if let Commands::Run(_) = &args.command {
if let Err(e) = octomind::logging::tracing_setup::init_tracing(
octomind::logging::tracing_setup::LoggingMode::Cli,
log_level,
) {
eprintln!("Warning: Failed to initialize tracing: {e}");
}
}
let sandbox_enabled = match &args.command {
Commands::Run(a) => config.sandbox || a.sandbox,
Commands::Server(a) => config.sandbox || a.sandbox,
Commands::Acp(a) => config.sandbox || a.sandbox,
_ => false,
};
if sandbox_enabled {
let cwd = std::env::current_dir()?;
octomind::sandbox::apply(&cwd)?;
}
match args.command {
Commands::Config(config_args) => commands::config::execute(&config_args, config)?,
Commands::Run(run_args) => commands::run::execute(&run_args, &config).await?,
Commands::Server(server_args) => commands::server::execute(&server_args, &config).await?,
Commands::Acp(acp_args) => commands::acp::execute(&acp_args, &config).await?,
Commands::Tap(tap_args) => commands::tap::execute(&tap_args)?,
Commands::Untap(untap_args) => commands::untap::execute(&untap_args)?,
Commands::Vars(vars_args) => commands::vars::execute(&vars_args, &config).await?,
Commands::Send(send_args) => commands::send::execute(&send_args).await?,
Commands::Completion { shell } => {
let mut app = CliArgs::command();
let name = app.get_name().to_string();
let mut buf = Vec::new();
generate(shell, &mut app, &name, &mut buf);
let script = String::from_utf8_lossy(&buf);
let patched = patch_completion_script(&script, shell);
print!("{patched}");
}
Commands::Complete(complete_args) => commands::complete::execute(&complete_args, &config)?,
}
Ok(())
}
fn patch_completion_script(script: &str, shell: Shell) -> String {
match shell {
Shell::Bash => patch_bash(script),
Shell::Zsh => patch_zsh(script),
Shell::Fish => patch_fish(script),
_ => script.to_string(),
}
}
fn patch_bash(script: &str) -> String {
let marker = " octomind__run)\n";
let Some(run_pos) = script.find(marker) else {
return script.to_string();
};
let block_start = run_pos + marker.len();
let end_marker = "\n octomind__";
let block_len = script[block_start..]
.find(end_marker)
.unwrap_or(script.len() - block_start);
let block_end = block_start + block_len;
let block = &script[block_start..block_end];
let block = block.replace(" || ${COMP_CWORD} -eq 2", "");
let block = block.replace(" [TAG]", "");
let block = block.replace(
" COMPREPLY=()\n ;;\n",
" COMPREPLY=($(compgen -W \"$(octomind complete run 2>/dev/null)\" -- \"${cur}\"))\n return 0\n ;;\n",
);
format!(
"{}{}{}{}",
&script[..run_pos],
marker,
block,
&script[block_end..]
)
}
fn patch_zsh(script: &str) -> String {
let helper = "\n_octomind_complete_run() {\n local -a tags\n tags=(${(f)\"$(octomind complete run 2>/dev/null)\"})\n compadd -a tags\n}\n";
let after_first_line = script.find('\n').map(|i| i + 1).unwrap_or(script.len());
let first_line = &script[..after_first_line];
let rest = &script[after_first_line..];
let run_marker = "\n(run)\n";
let patched_rest = if let Some(run_start) = rest.find(run_marker) {
let block_body_start = run_start + run_marker.len();
let block_body = &rest[block_body_start..];
let block_len = block_body.find("\n(").unwrap_or(block_body.len());
let run_block = &block_body[..block_len];
let tag_prefix = "'::tag -- ";
let tag_suffix = ":_default' \\";
if let (Some(tag_pos), Some(suffix_rel)) = (
run_block.find(tag_prefix),
run_block
.find(tag_prefix)
.and_then(|p| run_block[p..].find(tag_suffix)),
) {
let abs = block_body_start + tag_pos + suffix_rel;
format!(
"{}{}{}",
&rest[..abs],
":_octomind_complete_run' \\",
&rest[abs + tag_suffix.len()..]
)
} else {
rest.to_string()
}
} else {
rest.to_string()
};
format!("{first_line}{helper}\n{patched_rest}")
}
fn patch_fish(script: &str) -> String {
let dynamic_line = concat!(
"\n# Dynamic TAG completions for `octomind run`\n",
"complete -c octomind -n '__fish_octomind_using_subcommand run' ",
"-f -a '(octomind complete run 2>/dev/null)' ",
"-d 'Agent tag or role name'\n"
);
format!("{script}{dynamic_line}")
}