use agentdb::AgentDB;
use clap::{CommandFactory, Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "agentdb",
version = env!("CARGO_PKG_VERSION"),
about = "Inspect and manage AgentDB database files",
long_about = None,
)]
struct Cli {
#[arg(short = 'i', long = "interactive", value_name = "PATH")]
interactive: Option<String>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Version,
Stats {
path: String,
},
Collections {
path: String,
},
Sql {
path: String,
query: String,
},
Search {
path: String,
collection: String,
#[arg(num_args = 1.., value_name = "f32")]
vector: Vec<f32>,
#[arg(short, long, default_value_t = 5)]
top_k: usize,
},
Reindex {
path: String,
},
Inspect {
path: String,
},
Migrate {
path: String,
},
Export {
path: String,
#[arg(short, long, value_name = "TABLE")]
table: Option<String>,
},
Completions {
#[arg(long, value_name = "SHELL")]
shell: clap_complete::Shell,
},
Shell {
path: String,
},
Mcp {
path: String,
},
}
fn main() {
let cli = Cli::parse();
if let Err(e) = run(cli) {
eprintln!("error: {e}");
std::process::exit(1);
}
}
fn run(cli: Cli) -> agentdb::Result<()> {
if let Some(path) = cli.interactive {
return cmd_shell(&path);
}
match cli.command {
Some(Commands::Version) => cmd_version(),
Some(Commands::Stats { path }) => cmd_stats(&path),
Some(Commands::Collections { path }) => cmd_collections(&path),
Some(Commands::Sql { path, query }) => cmd_sql(&path, &query),
Some(Commands::Search {
path,
collection,
vector,
top_k,
}) => cmd_search(&path, &collection, &vector, top_k),
Some(Commands::Reindex { path }) => cmd_reindex(&path),
Some(Commands::Inspect { path }) => cmd_inspect(&path),
Some(Commands::Migrate { path }) => cmd_migrate(&path),
Some(Commands::Export { path, table }) => cmd_export(&path, table.as_deref()),
Some(Commands::Completions { shell }) => cmd_completions(shell),
Some(Commands::Shell { path }) => cmd_shell(&path),
Some(Commands::Mcp { path }) => cmd_mcp(&path),
None => {
use std::io::Write as _;
let _ = writeln!(
std::io::stderr(),
"No command specified. Run `agentdb --help` for usage."
);
std::process::exit(2);
}
}
}
fn cmd_version() -> agentdb::Result<()> {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
let family = std::env::consts::FAMILY;
println!("agentdb {}", env!("CARGO_PKG_VERSION"));
println!("target: {arch}-{family}-{os}");
Ok(())
}
fn cmd_stats(path: &str) -> agentdb::Result<()> {
let db = AgentDB::open(path)?;
let s = db.stats()?;
println!("path: {path}");
println!("collections: {}", s.collections);
println!("vectors: {}", s.vectors);
println!("nodes: {}", s.nodes);
println!("edges: {}", s.edges);
println!("conversations: {}", s.conversations);
println!("messages: {}", s.messages);
println!("workflows: {}", s.workflows);
println!("workflow_steps: {}", s.workflow_steps);
println!("traces: {}", s.traces);
Ok(())
}
fn cmd_collections(path: &str) -> agentdb::Result<()> {
let db = AgentDB::open(path)?;
let cols = db.vectors().list_collections()?;
if cols.is_empty() {
println!("No collections found.");
return Ok(());
}
println!("{:<30} {:>6} {:>10}", "name", "dim", "vectors");
println!("{}", "-".repeat(50));
for (name, dim, count) in &cols {
println!("{:<30} {:>6} {:>10}", name, dim, count);
}
Ok(())
}
fn cmd_sql(path: &str, query: &str) -> agentdb::Result<()> {
let db = AgentDB::open(path)?;
let rows = db.query_json(query)?;
println!(
"{}",
serde_json::to_string_pretty(&rows).unwrap_or_default()
);
Ok(())
}
fn cmd_search(path: &str, collection: &str, vector: &[f32], top_k: usize) -> agentdb::Result<()> {
use agentdb::{DistanceMetric, SearchOptions};
let db = AgentDB::open(path)?;
let dim = vector.len();
let col = db.vectors().collection(collection, dim)?;
let results = col.search(
vector,
SearchOptions {
top_k,
metric: DistanceMetric::Cosine,
filter: None,
},
)?;
if results.is_empty() {
println!("No results.");
return Ok(());
}
println!("{:<30} {:>8} metadata", "id", "score");
println!("{}", "-".repeat(60));
for r in &results {
let meta = r
.metadata
.as_ref()
.map(|m| m.to_string())
.unwrap_or_default();
println!("{:<30} {:>8.4} {}", r.id, r.score, meta);
}
Ok(())
}
fn cmd_reindex(path: &str) -> agentdb::Result<()> {
let db = AgentDB::open(path)?;
let cols = db.vectors().list_collections()?;
let mut rebuilt = 0usize;
for (name, dim, _) in &cols {
let col = db.vectors().collection(name, *dim)?;
col.reindex()?;
rebuilt += 1;
println!("reindexed: {name}");
}
println!("done — {rebuilt} collection(s) reindexed.");
Ok(())
}
fn cmd_inspect(path: &str) -> agentdb::Result<()> {
let db = AgentDB::open(path)?;
println!("=== AgentDB Inspect ===");
println!("path: {path}");
println!();
let s = db.stats()?;
println!("Statistics");
println!(" collections : {}", s.collections);
println!(" vectors : {}", s.vectors);
println!(" nodes : {}", s.nodes);
println!(" edges : {}", s.edges);
println!(" conversations : {}", s.conversations);
println!(" messages : {}", s.messages);
println!(" workflows : {}", s.workflows);
println!(" workflow_steps : {}", s.workflow_steps);
println!(" traces : {}", s.traces);
println!();
let cols = db.vectors().list_collections()?;
if !cols.is_empty() {
println!("Collections");
println!(" {:<28} {:>6} {:>10}", "name", "dim", "vectors");
println!(" {}", "-".repeat(48));
for (name, dim, count) in &cols {
println!(" {:<28} {:>6} {:>10}", name, dim, count);
}
println!();
}
let rows =
db.query_json("SELECT id, kind FROM _adb_nodes ORDER BY created_at DESC LIMIT 10")?;
if !rows.is_empty() {
println!("Recent nodes (up to 10)");
for row in &rows {
println!(
" {} — {}",
row.get("id").and_then(|v| v.as_str()).unwrap_or("-"),
row.get("kind").and_then(|v| v.as_str()).unwrap_or("-"),
);
}
}
Ok(())
}
fn cmd_migrate(path: &str) -> agentdb::Result<()> {
use rusqlite::Connection;
println!("Migrating: {path}");
let conn = Connection::open(path).map_err(agentdb::AgentDbError::Sqlite)?;
let old_version: String = conn
.query_row(
"SELECT COALESCE(
(SELECT value FROM _adb_meta WHERE key = 'schema_version'),
'0'
)",
[],
|r| r.get(0),
)
.unwrap_or_else(|_| "0".to_string());
println!(" current schema version: {old_version}");
agentdb::schema::migrate(&conn)?;
println!(
" migrated to schema version: {}",
agentdb::schema::SCHEMA_VERSION
);
println!("done.");
Ok(())
}
fn cmd_export(path: &str, only_table: Option<&str>) -> agentdb::Result<()> {
use rusqlite::{Connection, params};
let conn = Connection::open(path).map_err(agentdb::AgentDbError::Sqlite)?;
let mut stmt = conn
.prepare(
"SELECT name, sql FROM sqlite_master \
WHERE type = 'table' \
AND name NOT LIKE 'sqlite_%' \
ORDER BY name",
)
.map_err(agentdb::AgentDbError::Sqlite)?;
let tables: Vec<(String, String)> = stmt
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.map_err(agentdb::AgentDbError::Sqlite)?
.filter_map(|r| r.ok())
.filter(|(name, _)| {
only_table.map_or(true, |t| name == t)
})
.collect();
if tables.is_empty() {
if let Some(t) = only_table {
eprintln!("warning: table '{t}' not found in {path}");
} else {
eprintln!("warning: no user tables found in {path}");
}
return Ok(());
}
println!("-- AgentDB SQL dump");
println!("-- Source: {path}");
println!("-- Generated by agentdb {}", env!("CARGO_PKG_VERSION"));
println!("PRAGMA foreign_keys = OFF;");
println!("BEGIN TRANSACTION;");
println!();
for (table_name, ddl) in &tables {
println!("-- Table: {table_name}");
println!("{ddl};");
println!();
let select_sql = format!("SELECT * FROM \"{table_name}\"");
let mut row_stmt = match conn.prepare(&select_sql) {
Ok(s) => s,
Err(e) => {
eprintln!("warning: could not query '{table_name}': {e}");
continue;
}
};
let col_count = row_stmt.column_count();
let col_names: Vec<String> = (0..col_count)
.map(|i| row_stmt.column_name(i).unwrap_or("?").to_string())
.collect();
let col_list = col_names
.iter()
.map(|c| format!("\"{c}\""))
.collect::<Vec<_>>()
.join(", ");
let mut rows = row_stmt
.query(params![])
.map_err(agentdb::AgentDbError::Sqlite)?;
let mut row_count = 0u64;
loop {
let row = match rows.next().map_err(agentdb::AgentDbError::Sqlite)? {
Some(r) => r,
None => break,
};
let values: Vec<String> = (0..col_count)
.map(|i| {
use rusqlite::types::ValueRef;
match row.get_ref_unwrap(i) {
ValueRef::Null => "NULL".to_string(),
ValueRef::Integer(n) => n.to_string(),
ValueRef::Real(f) => {
format!("{f:?}")
}
ValueRef::Text(s) => {
let escaped = String::from_utf8_lossy(s).replace('\'', "''");
format!("'{escaped}'")
}
ValueRef::Blob(b) => {
let hex: String = b.iter().map(|byte| format!("{byte:02X}")).collect();
format!("X'{hex}'")
}
}
})
.collect();
println!(
"INSERT INTO \"{table_name}\" ({col_list}) VALUES ({});",
values.join(", ")
);
row_count += 1;
}
if row_count == 0 {
println!("-- (no rows)");
}
println!();
}
println!("COMMIT;");
Ok(())
}
fn cmd_completions(shell: clap_complete::Shell) -> agentdb::Result<()> {
use clap_complete::generate;
let mut cmd = Cli::command();
let bin_name = cmd.get_name().to_string();
generate(shell, &mut cmd, bin_name, &mut std::io::stdout());
Ok(())
}
fn cmd_mcp(path: &str) -> agentdb::Result<()> {
use agentdb::mcp::McpServer;
use std::io::{self, BufRead, Write};
let db = AgentDB::open(path)?;
let server = McpServer::new(db);
let stdin = io::stdin();
let stdout = io::stdout();
for line in stdin.lock().lines() {
let line = line.map_err(|e| {
agentdb::AgentDbError::InvalidArgument(format!("stdin read error: {e}"))
})?;
if line.trim().is_empty() {
continue;
}
if let Some(response) = server.handle_message(&line) {
let mut out = stdout.lock();
let _ = writeln!(out, "{response}");
let _ = out.flush();
}
}
Ok(())
}
fn cmd_shell(path: &str) -> agentdb::Result<()> {
use std::io::{self, BufRead, Write};
let db = AgentDB::open(path)?;
println!("AgentDB shell v{}", env!("CARGO_PKG_VERSION"));
println!("Connected to: {path}");
println!("Type .help for help, .quit to exit.");
println!();
let stdin = io::stdin();
let stdout = io::stdout();
let mut sql_buf = String::new();
loop {
let prompt = if sql_buf.trim().is_empty() {
"agentdb> "
} else {
" -> "
};
{
let mut out = stdout.lock();
let _ = write!(out, "{prompt}");
let _ = out.flush();
}
let mut line = String::new();
match stdin.lock().read_line(&mut line) {
Ok(0) => {
println!();
println!("Bye.");
break;
}
Ok(_) => {}
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {
if !sql_buf.is_empty() {
println!(" (input cleared)");
sql_buf.clear();
} else {
println!();
}
continue;
}
Err(e) => {
eprintln!("read error: {e}");
break;
}
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with('.') {
if !sql_buf.trim().is_empty() {
eprintln!(
"error: complete the current statement first (end with `;`), \
or press Ctrl-C to discard it."
);
continue;
}
match trimmed {
".quit" | ".exit" => {
println!("Bye.");
break;
}
".help" => shell_help(),
".stats" => {
if let Err(e) = cmd_stats(path) {
eprintln!("error: {e}");
}
}
".collections" => {
if let Err(e) = cmd_collections(path) {
eprintln!("error: {e}");
}
}
".inspect" => {
if let Err(e) = cmd_inspect(path) {
eprintln!("error: {e}");
}
}
other => {
eprintln!("Unknown dot-command: {other}");
eprintln!("Type .help for a list of commands.");
}
}
continue;
}
sql_buf.push_str(&line);
if sql_buf.trim_end().ends_with(';') {
let query = sql_buf.trim().to_string();
sql_buf.clear();
let upper = query.trim_start().to_ascii_uppercase();
let is_select = upper.starts_with("SELECT")
|| upper.starts_with("WITH")
|| upper.starts_with("PRAGMA")
|| upper.starts_with("EXPLAIN");
if is_select {
match db.query_json(&query) {
Ok(rows) => {
println!(
"{}",
serde_json::to_string_pretty(&rows).unwrap_or_default()
);
println!(
"({} row{})",
rows.len(),
if rows.len() == 1 { "" } else { "s" }
);
}
Err(e) => eprintln!("error: {e}"),
}
} else {
match db.execute(&query) {
Ok(n) => println!("({n} row{} affected)", if n == 1 { "" } else { "s" }),
Err(e) => eprintln!("error: {e}"),
}
}
}
}
Ok(())
}
fn shell_help() {
println!("AgentDB interactive shell");
println!();
println!("SQL queries");
println!(" Enter any SQL statement. Statements may span multiple lines.");
println!(" Terminate with a semicolon (;) to execute.");
println!();
println!("Dot-commands");
println!(" .help show this help text");
println!(" .stats print database statistics");
println!(" .collections list vector collections");
println!(" .inspect full database summary");
println!(" .quit exit the shell");
println!(" .exit exit the shell (alias for .quit)");
println!();
println!("Keyboard shortcuts");
println!(" Ctrl-C discard current input line and start fresh");
println!(" Ctrl-D exit the shell (EOF)");
}