mod audit;
mod backfill;
mod connect;
mod devices;
mod doctor;
mod eval;
mod hook;
mod init;
mod project;
mod promote;
mod status;
mod ui;
use std::path::PathBuf;
use clap::{CommandFactory, Parser, Subcommand};
use recall_hooks::exit;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const COMMIT: Option<&str> = option_env!("RECALL_GIT_COMMIT");
#[derive(Parser)]
#[command(
name = "recall",
about = "Sync Claude Code's auto memory across machines and cloud sessions",
// clap's own `--version` is replaced rather than merely enabled. Its
// default prints `recall <version>` and nothing else, while `recall
// version` prints the commit too — two ways of asking the same question
// giving different answers is the failure this project keeps finding in
// itself. `version_line` below is the single source both go through.
disable_version_flag = true,
// `recall` alone still prints help and exits 2, which it did before this
// flag existed. Without this, an optional subcommand would make a bare
// `recall` a silent success.
arg_required_else_help = true
)]
struct Cli {
#[arg(short = 'V', long = "version", action = clap::ArgAction::SetTrue)]
version: bool,
#[command(subcommand)]
command: Option<Cmd>,
}
fn version_line() -> String {
let commit = COMMIT.unwrap_or("unknown");
match recall_wire::discovery::channel() {
recall_wire::discovery::CHANNEL_RELEASE => format!("recall {VERSION} ({commit})"),
_ => format!(
"recall {VERSION} ({commit}, dev build {})",
recall_wire::discovery::version()
),
}
}
#[derive(Subcommand)]
enum Cmd {
Init {
#[arg(long)]
path: Option<PathBuf>,
},
Backfill,
Promote {
file: PathBuf,
#[arg(long, value_enum, default_value_t = promote::Target::Global)]
to: promote::Target,
},
Connect {
url: Option<String>,
#[arg(long)]
name: Option<String>,
#[arg(long, short)]
yes: bool,
},
#[command(subcommand)]
Devices(devices::Cmd),
#[command(subcommand)]
Authkey(devices::KeyCmd),
#[command(subcommand)]
Audit(audit::Cmd),
#[command(subcommand)]
Eval(eval::Cmd),
Disconnect {
url: Option<String>,
},
Status {
#[arg(long)]
json: bool,
},
Doctor {
#[arg(long)]
json: bool,
},
#[command(hide = true)]
Serve,
Push,
Pull,
Version,
}
fn main() {
let cli = Cli::parse();
if cli.version {
println!("{}", version_line());
std::process::exit(exit::OK);
}
let Some(command) = cli.command else {
Cli::command().print_help().ok();
std::process::exit(exit::CONFIG);
};
let result = match command {
Cmd::Version => {
println!("{}", version_line());
Ok(exit::OK)
}
Cmd::Init { path } => init::run(path.as_deref()),
Cmd::Backfill => block_on_current(backfill::run()),
Cmd::Serve => {
eprintln!(
"recall: the server is its own binary since 0.4.0. Run recall-server \
instead, with the same environment. See \
https://github.com/pimlabs/recall/blob/main/docs/reference/install.md"
);
Ok(exit::CONFIG)
}
Cmd::Promote { file, to } => block_on_current(promote::run(&file, to)),
Cmd::Connect { url, name, yes } => {
block_on_current(connect::connect(connect::Args { url, name, yes }))
}
Cmd::Disconnect { url } => connect::disconnect(url.as_deref()),
Cmd::Devices(cmd) => block_on_current(devices::run(cmd)),
Cmd::Authkey(cmd) => block_on_current(devices::run_authkey(cmd)),
Cmd::Audit(cmd) => block_on_current(audit::run(cmd)),
Cmd::Eval(cmd) => block_on_current(eval::run(cmd)),
Cmd::Status { json } => block_on_current(status::run(json)),
Cmd::Doctor { json } => block_on_current(doctor::run(json)),
Cmd::Push => block_on_current(hook::push()),
Cmd::Pull => block_on_current(hook::pull()),
};
match result {
Ok(code) => std::process::exit(code),
Err(err) => {
eprintln!("recall: {err:#}");
std::process::exit(exit::CONFIG)
}
}
}
fn block_on_current<F: std::future::Future<Output = anyhow::Result<i32>>>(
fut: F,
) -> anyhow::Result<i32> {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(fut)
}