Menu
Public repository / master
AkurAI-Notes / crates/notes-cli/src/main.rs
Latest change 1a9e82dde55323be1dbcafc350f7d90a24175917 - Wire akurai-log Monitor shipper into the server command by Ólafur Búi Ólafsson
#![forbid(unsafe_code)]
mod cmd_mcp;
mod cmd_repair;
mod cmd_seed;
mod cmd_serve;
mod cmd_tool;
use std::path::PathBuf;
use std::process::ExitCode;
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match run(&args) {
Ok(()) => ExitCode::SUCCESS,
Err(msg) => {
eprintln!("error: {msg}");
ExitCode::FAILURE
}
}
}
fn run(args: &[String]) -> Result<(), String> {
match args.first().map(String::as_str) {
Some("serve") => {
akurai_log::init("akurai-notes");
let cfg = parse_serve(&args[1..])?;
cmd_serve::run(cfg).map_err(|e| e.to_string())
}
Some("mcp") => {
let cfg = parse_mcp(&args[1..])?;
cmd_mcp::run(cfg).map_err(|e| e.to_string())
}
Some("tools") => cmd_tool::list(),
Some("tool") => {
let name = args.get(1).ok_or("tool needs an MCP tool name")?;
cmd_tool::run(name, args.iter().any(|arg| arg == "--confirm"))
}
Some("seed") => {
let db_path = args.get(1).map(|s| s.as_str()).unwrap_or("notes.db");
cmd_seed::run(db_path)
}
Some("repair") => {
let source = args.get(1).ok_or("repair needs a source database")?;
let output = args.get(2).ok_or("repair needs an output database")?;
cmd_repair::run(source, output)
}
Some("version") | Some("--version") | Some("-V") => {
println!("akurai-notes {VERSION}");
Ok(())
}
Some("help") | Some("--help") | Some("-h") | None => {
print_help();
Ok(())
}
Some(other) => Err(format!(
"unknown command '{other}' (try: akurai-notes help)"
)),
}
}
fn parse_serve(args: &[String]) -> Result<cmd_serve::Config, String> {
let mut host = "127.0.0.1".to_string();
let mut port: u16 = 8099;
let mut dir = PathBuf::from("site/frontend");
let mut db = "notes.db".to_string();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--host" => {
host = next(args, &mut i, "--host")?;
}
"--port" | "-p" => {
port = next(args, &mut i, "--port")?
.parse()
.map_err(|_| "invalid --port".to_string())?;
}
"--dir" | "--frontend-dir" => {
dir = PathBuf::from(next(args, &mut i, "--dir")?);
}
"--db" => {
db = next(args, &mut i, "--db")?;
}
other => return Err(format!("unknown flag '{other}'")),
}
i += 1;
}
Ok(cmd_serve::Config {
host,
port,
dir,
db,
})
}
fn parse_mcp(args: &[String]) -> Result<cmd_mcp::McpRunConfig, String> {
let mut host = "127.0.0.1".to_string();
let mut port: u16 = 8101;
let mut db = "/var/lib/akurai-notes/notes.db".to_string();
let mut token = String::new();
let mut embed_url = String::new();
let mut embed_model = String::new();
let mut embed_api_key = String::new();
let mut owner_sub = String::new();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--host" => host = next(args, &mut i, "--host")?,
"--port" | "-p" => {
port = next(args, &mut i, "--port")?
.parse()
.map_err(|_| "invalid --port".to_string())?;
}
"--db" => db = next(args, &mut i, "--db")?,
"--token" => token = next(args, &mut i, "--token")?,
"--embed-url" => embed_url = next(args, &mut i, "--embed-url")?,
"--embed-model" => embed_model = next(args, &mut i, "--embed-model")?,
"--embed-api-key" => embed_api_key = next(args, &mut i, "--embed-api-key")?,
"--owner-sub" => owner_sub = next(args, &mut i, "--owner-sub")?,
other => return Err(format!("unknown flag '{other}'")),
}
i += 1;
}
if token.is_empty() {
token = std::env::var("AKURAI_NOTES_MCP_TOKEN").unwrap_or_default();
}
if owner_sub.is_empty() {
owner_sub = std::env::var("AKURAI_NOTES_MCP_OWNER_SUB").unwrap_or_default();
}
if embed_url.is_empty() {
embed_url = std::env::var("AKURAI_NOTES_EMBED_URL").unwrap_or_default();
}
if embed_model.is_empty() {
embed_model = std::env::var("AKURAI_NOTES_EMBED_MODEL").unwrap_or_default();
}
if embed_api_key.is_empty() {
embed_api_key = std::env::var("AKURAI_NOTES_EMBED_API_KEY").unwrap_or_default();
}
Ok(cmd_mcp::McpRunConfig {
host,
port,
db,
token,
embed_url,
embed_model,
embed_api_key,
owner_sub,
})
}
fn next(args: &[String], i: &mut usize, flag: &str) -> Result<String, String> {
*i += 1;
args.get(*i)
.cloned()
.ok_or_else(|| format!("{flag} needs a value"))
}
fn print_help() {
println!("akurai-notes {VERSION} — Private notes with semantic search\n");
println!("USAGE:");
println!(" akurai-notes serve [opts] Start the Notes web server");
println!(" akurai-notes mcp [opts] Start the MCP server");
println!(" akurai-notes seed [db] Seed the database with demo data");
println!(" akurai-notes tools List every Notes MCP tool");
println!(" akurai-notes tool NAME [--confirm] < arguments.json");
println!(" JSON arguments are read from stdin");
println!(" Deletes require --confirm");
println!(" Includes system_status health/count check");
println!(" akurai-notes repair <src> <out> Rebuild a database without range scans");
println!(" akurai-notes version Print version\n");
println!("SERVE OPTIONS:");
println!(" --host <addr> Bind host (default: 127.0.0.1)");
println!(" --port, -p <n> Bind port (default: 8099)");
println!(" --dir <path> Frontend directory (default: site/frontend)");
println!(" --db <path> Database file (default: notes.db)\n");
println!("MCP OPTIONS:");
println!(" --host <addr> Bind host (default: 127.0.0.1)");
println!(" --port, -p <n> Bind port (default: 8101)");
println!(" --db <path> Database file (default: /var/lib/akurai-notes/notes.db)");
println!(" --token <tok> Bearer token (env: AKURAI_NOTES_MCP_TOKEN)");
println!(" --embed-url <url> Embedding API URL");
println!(" --embed-model <m> Embedding model name");
println!(" --embed-api-key <k> Embedding bearer token");
println!(" --owner-sub <sub> Owner subject (env: AKURAI_NOTES_MCP_OWNER_SUB)");
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, OnceLock};
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
}
struct EnvGuard(Vec<(&'static str, Option<String>)>);
impl EnvGuard {
fn capture(keys: &[&'static str]) -> Self {
Self(
keys.iter()
.map(|key| (*key, std::env::var(key).ok()))
.collect(),
)
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (key, value) in &self.0 {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
}
#[test]
fn parse_mcp_reads_embed_env_when_flags_are_absent() {
let _lock = env_lock();
let _env = EnvGuard::capture(&[
"AKURAI_NOTES_EMBED_URL",
"AKURAI_NOTES_EMBED_MODEL",
"AKURAI_NOTES_EMBED_API_KEY",
]);
std::env::set_var(
"AKURAI_NOTES_EMBED_URL",
"http://127.0.0.1:4219/v1/embeddings",
);
std::env::set_var("AKURAI_NOTES_EMBED_MODEL", "intfloat/multilingual-e5-small");
std::env::set_var("AKURAI_NOTES_EMBED_API_KEY", "router-secret");
let args: Vec<String> = Vec::new();
let cfg = parse_mcp(&args).unwrap();
assert_eq!(cfg.embed_url, "http://127.0.0.1:4219/v1/embeddings");
assert_eq!(cfg.embed_model, "intfloat/multilingual-e5-small");
assert_eq!(cfg.embed_api_key, "router-secret");
}
#[test]
fn parse_mcp_prefers_embed_flags_over_env() {
let _lock = env_lock();
let _env = EnvGuard::capture(&[
"AKURAI_NOTES_EMBED_URL",
"AKURAI_NOTES_EMBED_MODEL",
"AKURAI_NOTES_EMBED_API_KEY",
]);
std::env::set_var(
"AKURAI_NOTES_EMBED_URL",
"http://127.0.0.1:4219/v1/embeddings",
);
std::env::set_var("AKURAI_NOTES_EMBED_MODEL", "env-model");
std::env::set_var("AKURAI_NOTES_EMBED_API_KEY", "env-secret");
let args = vec![
"--embed-url".to_string(),
"http://127.0.0.1:7777/v1/embeddings".to_string(),
"--embed-model".to_string(),
"flag-model".to_string(),
"--embed-api-key".to_string(),
"flag-secret".to_string(),
];
let cfg = parse_mcp(&args).unwrap();
assert_eq!(cfg.embed_url, "http://127.0.0.1:7777/v1/embeddings");
assert_eq!(cfg.embed_model, "flag-model");
assert_eq!(cfg.embed_api_key, "flag-secret");
}
}