mod completions;
mod deck;
mod findings;
mod mem;
mod paths;
mod publish;
mod review;
mod site;
mod skills;
mod update;
use clap::{Parser, Subcommand};
use std::process::ExitCode;
#[derive(Parser)]
#[command(
name = "indra",
version,
about = "One CLI for AI agent harnesses: graph memory and a web console to view, monitor and run agents",
long_about = "One CLI for AI agent harnesses.\n\n\
indra mem graph memory: Markdown notes with typed [[links]], ranked recall, Obsidian-compatible\n\
indra serve web console: agents, skills, prompts, graph maps, memory, runs, launch and watch agents\n\
indra ls the console's picture in the terminal\n\
indra nodes the machines agents run on, local and over SSH\n\
indra run run a multi-step, multi-node workflow and follow it\n\n\
The security harness is the default profile; any Claude Code project or custom harness works too.",
after_help = "Start here:\n \
indra serve --open open the console in your browser\n \
indra claude -m qwen3-coder Claude Code on a local model (Ollama)\n \
indra update get the newest version (--auto on: by itself)\n \
indra completions --install Tab completion in your shell"
)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
#[command(
alias = "m",
long_about = "Graph memory for people and AI agents.\n\n\
Notes are plain Markdown with [[wikilinks]] and typed relations (`mitigated-by:: [[note]]`). \
The vault is an ordinary folder that also opens in Obsidian.\n\n\
Vault location: --vault, else $INDRA_VAULT, else the nearest parent folder holding `.indra/`, \
else a per-user default (see `indra mem where`)."
)]
Mem(mem::cli::MemArgs),
Serve(deck::cli::ServeArgs),
Ls(deck::cli::LsArgs),
Check(deck::cli::CheckArgs),
Init(deck::cli::InitArgs),
Nodes(deck::cli::NodesArgs),
Run(deck::cli::RunArgs),
Node(deck::cli::NodeArgs),
Secret(deck::cli::SecretArgs),
Agents(deck::cli::AgentsArgs),
Attach(deck::cli::AttachArgs),
Claude(deck::claude::ClaudeArgs),
Publish(publish::PublishArgs),
Review(review::ReviewArgs),
Skills(skills::SkillsArgs),
Update(update::UpdateArgs),
Completions(completions::CompletionsArgs),
Mcp(deck::cli::McpArgs),
Accounts(deck::cli::AccountsArgs),
User(deck::users::UserArgs),
#[command(hide = true)]
Dev {
#[command(subcommand)]
cmd: DevCmd,
},
}
#[derive(Subcommand)]
enum DevCmd {
Site(site::SiteArgs),
Dashes {
#[arg(long)]
repo: Option<std::path::PathBuf>,
#[arg(long)]
fix: bool,
},
}
fn main() -> ExitCode {
let cli = Cli::parse();
if !matches!(
cli.cmd,
Cmd::Update(_) | Cmd::Completions(_) | Cmd::Node(_) | Cmd::Mcp(_) | Cmd::Dev { .. }
) {
update::on_start();
}
let res = match cli.cmd {
Cmd::Mem(a) => mem::cli::run(a),
Cmd::Serve(a) => deck::cli::run_serve(a),
Cmd::Ls(a) => deck::cli::run_ls(a),
Cmd::Check(a) => deck::cli::run_check(a),
Cmd::Init(a) => deck::cli::init(a),
Cmd::Nodes(a) => deck::cli::run_nodes(a),
Cmd::Run(a) => deck::cli::run_workflow(a),
Cmd::Node(a) => deck::cli::run_node(a),
Cmd::Secret(a) => deck::cli::run_secret(a),
Cmd::Agents(a) => deck::cli::run_agents(a),
Cmd::Attach(a) => deck::cli::run_attach(a),
Cmd::Claude(a) => deck::claude::run(a),
Cmd::Update(a) => update::run(a),
Cmd::Completions(a) => completions::run(a),
Cmd::Mcp(a) => deck::cli::run_mcp(a),
Cmd::Accounts(a) => deck::cli::run_accounts(a),
Cmd::User(a) => deck::users::run(a),
Cmd::Publish(a) => publish::run(a),
Cmd::Review(a) => review::run(a),
Cmd::Skills(a) => skills::run(a),
Cmd::Dev {
cmd: DevCmd::Site(a),
} => site::run(a),
Cmd::Dev {
cmd: DevCmd::Dashes { repo, fix },
} => site::dashes(repo, fix),
};
match res {
Ok(code) => code,
Err(e) => {
eprintln!("indra: {}", e);
ExitCode::FAILURE
}
}
}