use clap::{Parser, Subcommand};
use std::path::PathBuf;
use tracing::info;
#[derive(Parser, Debug)]
#[command(name = "takobull")]
#[command(about = "Ultra-lightweight personal AI Assistant for embedded systems", long_about = None)]
#[command(version)]
#[command(author)]
struct Args {
#[arg(short, long, value_name = "FILE", global = true)]
config: Option<PathBuf>,
#[arg(short, long, default_value = "info", global = true)]
log_level: String,
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
enum Commands {
Agent {
#[arg(short, long)]
message: Option<String>,
},
Gateway,
Status,
Cron {
#[command(subcommand)]
action: CronAction,
},
Onboard {
#[arg(long)]
force: bool,
},
}
#[derive(Subcommand, Debug)]
enum CronAction {
List,
Add {
#[arg(short, long)]
expression: String,
#[arg(short, long)]
description: String,
},
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
takobull::logging::setup::init_logging(&args.log_level)?;
info!("Starting TakoBull v{}", env!("CARGO_PKG_VERSION"));
if let Some(config_path) = &args.config {
info!("Configuration file: {:?}", config_path);
}
match args.command {
Some(Commands::Agent { message }) => {
handle_agent(message).await?;
}
Some(Commands::Gateway) => {
handle_gateway().await?;
}
Some(Commands::Status) => {
handle_status().await?;
}
Some(Commands::Cron { action }) => {
handle_cron(action).await?;
}
Some(Commands::Onboard { force }) => {
handle_onboard(force).await?;
}
None => {
println!("TakoBull v{}", env!("CARGO_PKG_VERSION"));
println!("Ultra-lightweight personal AI Assistant for embedded systems");
println!("\nUsage: takobull [OPTIONS] <COMMAND>");
println!("\nCommands:");
println!(" agent Chat with the agent");
println!(" gateway Start the gateway for channel integrations");
println!(" status Show system status");
println!(" cron Manage scheduled cron jobs");
println!(" onboard Initialize configuration and workspace");
println!("\nOptions:");
println!(" -c, --config <FILE> Path to configuration file");
println!(" -l, --log-level <LOG_LEVEL> Log level (debug, info, warn, error)");
println!(" -v, --verbose Enable verbose output");
println!(" -h, --help Print help");
println!(" -V, --version Print version");
}
}
info!("TakoBull completed successfully");
Ok(())
}
async fn handle_agent(message: Option<String>) -> Result<(), Box<dyn std::error::Error>> {
info!("Starting agent");
let home = std::env::var("HOME")?;
let config_path = format!("{}/.takobull/config.yaml", home);
let workspace_path = format!("{}/.takobull/workspace", home);
if !std::path::Path::new(&config_path).exists() {
eprintln!("❌ Config not found: {}", config_path);
eprintln!("Run 'takobull onboard' first to initialize");
return Err("Config file not found".into());
}
let config_content = std::fs::read_to_string(&config_path)?;
info!("Loaded config from: {}", config_path);
if let Some(msg) = message {
info!("Processing message: {}", msg);
let config: serde_yaml::Value = serde_yaml::from_str(&config_content)?;
let provider = config["agents"]["defaults"]["provider"]
.as_str()
.unwrap_or("openrouter")
.to_string();
let model = config["agents"]["defaults"]["model"]
.as_str()
.unwrap_or("meta-llama/llama-2-70b-chat")
.to_string();
let provider_config = &config["providers"][&provider];
let api_key = provider_config["api_key"]
.as_str()
.unwrap_or("")
.to_string();
let default_api_base = match provider.as_str() {
"openai" | "gpt" => "https://api.openai.com/v1",
"anthropic" | "claude" => "https://api.anthropic.com/v1",
"openrouter" => "https://openrouter.ai/api/v1",
"groq" => "https://api.groq.com/openai/v1",
"zhipu" | "glm" => "https://open.bigmodel.cn/api/paas/v4",
"gemini" | "google" => "https://generativelanguage.googleapis.com/v1beta",
"deepseek" => "https://api.deepseek.com/v1",
"ollama" => "http://localhost:11434", "vllm" => "", _ => "https://openrouter.ai/api/v1",
};
let api_base = provider_config["api_base"]
.as_str()
.unwrap_or(default_api_base)
.to_string();
info!("Using provider: {}, model: {}", provider, model);
if provider == "vllm" && api_base.is_empty() {
eprintln!("❌ vLLM requires api_base to be configured");
eprintln!("Set the api_base in ~/.takobull/config.yaml under providers.vllm.api_base");
eprintln!("Example: providers.vllm.api_base: http://localhost:8000");
return Err("vLLM api_base not configured".into());
}
let requires_api_key = !matches!(provider.as_str(), "ollama" | "vllm");
if api_key.is_empty() && requires_api_key {
eprintln!("❌ API key not configured for provider: {}", provider);
eprintln!("Set the API key in ~/.takobull/config.yaml under providers.{}.api_key", provider);
return Err("API key not configured".into());
}
let llm_client = takobull::llm::LlmClient::new(&provider, &model, &api_key, &api_base);
let tool_registry = takobull::tools::ToolRegistry::new();
let write_file_tool = std::sync::Arc::new(
takobull::tools::WriteFileTool::new(workspace_path)
);
tool_registry.register(write_file_tool).await;
let executor = takobull::agent::AgentExecutor::new(llm_client, tool_registry);
println!("🤖 Processing: {}", msg);
match executor.execute(&msg).await {
Ok(response) => {
println!("{}", response);
info!("Response: {}", response);
}
Err(e) => {
eprintln!("❌ Error: {}", e);
return Err(e);
}
}
} else {
info!("Starting interactive agent mode");
println!("🤖 TakoBull Interactive Mode");
println!("Type 'exit' to quit\n");
println!("(Interactive mode not yet implemented)");
}
Ok(())
}
async fn handle_gateway() -> Result<(), Box<dyn std::error::Error>> {
info!("Starting gateway");
println!("Gateway mode (not yet implemented)");
Ok(())
}
async fn handle_status() -> Result<(), Box<dyn std::error::Error>> {
info!("Showing status");
let home = std::env::var("HOME")?;
let config_path = format!("{}/.takobull/config.yaml", home);
let workspace = format!("{}/.takobull/workspace", home);
println!("🤖 takobull Status");
println!("Version: v{}", env!("CARGO_PKG_VERSION"));
let config_exists = std::path::Path::new(&config_path).exists();
let config_status = if config_exists { "✓" } else { "✗" };
println!("Config: {} {}", config_path, config_status);
let workspace_exists = std::path::Path::new(&workspace).exists();
let workspace_status = if workspace_exists { "✓" } else { "✗" };
println!("Workspace: {} {}", workspace, workspace_status);
if config_exists {
let config_content = std::fs::read_to_string(&config_path)?;
let config: serde_yaml::Value = serde_yaml::from_str(&config_content)?;
let model = config["agents"]["defaults"]["model"]
.as_str()
.unwrap_or("unknown");
println!("Model: {}", model);
let selected_provider = config["agents"]["defaults"]["provider"]
.as_str()
.unwrap_or("openai");
let api_key = config["providers"][selected_provider]["api_key"].as_str();
let status = if api_key.is_some() { "✓" } else { "not set" };
let provider_name = match selected_provider {
"openrouter" => "OpenRouter API",
"anthropic" => "Anthropic API",
"openai" => "OpenAI API",
"gemini" => "Gemini API",
"zhipu" => "Zhipu API",
"groq" => "Groq API",
_ => selected_provider,
};
println!("Provider: {} ({})", provider_name, status);
}
println!("------");
Ok(())
}
async fn handle_cron(action: CronAction) -> Result<(), Box<dyn std::error::Error>> {
match action {
CronAction::List => {
info!("Listing cron jobs");
println!("Cron jobs (not yet implemented)");
}
CronAction::Add {
expression,
description,
} => {
info!("Adding cron job: {} - {}", expression, description);
println!("Added cron job: {} - {}", expression, description);
}
}
Ok(())
}
async fn handle_onboard(force: bool) -> Result<(), Box<dyn std::error::Error>> {
info!("Starting onboard process");
let home = std::env::var("HOME")?;
let workspace_dir = format!("{}/.takobull/workspace", home);
let config_path = format!("{}/.takobull/config.yaml", home);
std::fs::create_dir_all(&workspace_dir)?;
println!("✓ Created workspace directory: {}", workspace_dir);
let subdirs = vec!["sessions", "memory", "state", "cron", "skills"];
for subdir in subdirs {
std::fs::create_dir_all(format!("{}/{}", workspace_dir, subdir))?;
}
println!("✓ Created workspace subdirectories");
if !std::path::Path::new(&config_path).exists() || force {
let default_config = r#"# TakoBull Configuration
# Ultra-lightweight personal AI Assistant for embedded systems
agents:
defaults:
workspace: "~/.takobull/workspace"
restrict_to_workspace: true
provider: "openrouter"
model: "meta-llama/llama-2-70b-chat"
max_tokens: 8192
temperature: 0.7
max_tool_iterations: 20
channels:
telegram:
enabled: false
token: ""
allow_from: []
discord:
enabled: false
token: ""
allow_from: []
providers:
openrouter:
api_key: ""
api_base: "https://openrouter.ai/api/v1"
anthropic:
api_key: ""
api_base: "https://api.anthropic.com"
openai:
api_key: ""
api_base: "https://api.openai.com/v1"
gemini:
api_key: ""
api_base: "https://generativelanguage.googleapis.com/v1beta/openai/"
zhipu:
api_key: ""
api_base: "https://open.bigmodel.cn/api/paas/v4"
groq:
api_key: ""
api_base: "https://api.groq.com/openai/v1"
deepseek:
api_key: ""
api_base: "https://api.deepseek.com"
vllm:
api_key: ""
api_base: ""
tools:
web:
brave:
enabled: true
api_key: ""
max_results: 5
duckduckgo:
enabled: true
max_results: 5
heartbeat:
enabled: true
interval: 30
logging:
level: "info"
format: "json"
"#;
std::fs::write(&config_path, default_config)?;
println!("✓ Created default config: {}", config_path);
} else {
println!("✓ Config already exists: {}", config_path);
}
let workspace_files = vec![
("AGENTS.md", "# Agent Configuration\n\nConfigure agent behavior here.\n"),
("IDENTITY.md", "# Agent Identity\n\nDefine your agent's identity and personality.\n"),
("SOUL.md", "# Agent Soul\n\nDefine your agent's core values and principles.\n"),
("TOOLS.md", "# Available Tools\n\nList of tools available to the agent.\n"),
("USER.md", "# User Preferences\n\nDefine user preferences and settings.\n"),
("HEARTBEAT.md", "# Periodic Tasks\n\nDefine tasks to run periodically.\n"),
("MEMORY.md", "# Long-term Memory\n\nAgent's long-term memory storage.\n"),
];
for (filename, content) in workspace_files {
let filepath = format!("{}/{}", workspace_dir, filename);
if !std::path::Path::new(&filepath).exists() {
std::fs::write(&filepath, content)?;
}
}
println!("✓ Created workspace files");
println!("\n✅ Onboarding complete!");
println!("\nNext steps:");
println!("1. Edit config: {}", config_path);
println!("2. Set your API keys (OPENROUTER_API_KEY, etc.)");
println!("3. Run: takobull agent -m \"Hello\"");
info!("Onboarding completed successfully");
Ok(())
}