use std::path::Path;
use anyhow::Result;
use clap::Args;
use crate::cli::CliOutput;
use crate::recover::{HostKind, RecoverOpts, recover_from_transcript};
#[derive(Args, Debug, Clone)]
pub struct RecoverPreviousSessionArgs {
#[arg(long, value_name = "HOST", default_value = "auto")]
pub host: String,
#[arg(long, value_name = "PATH")]
pub transcript: Option<std::path::PathBuf>,
#[arg(long, value_name = "RFC3339")]
pub since: Option<String>,
#[arg(long, value_name = "NS")]
pub namespace: Option<String>,
#[arg(long, default_value_t = 100, value_name = "N")]
pub limit: usize,
#[arg(long, default_value_t = false)]
pub dry_run: bool,
#[arg(long, default_value_t = false)]
pub quiet: bool,
#[arg(long, default_value_t = false)]
pub json: bool,
}
pub fn run(
db_path: &Path,
args: &RecoverPreviousSessionArgs,
out: &mut CliOutput<'_>,
) -> Result<i32> {
let host = parse_host_kind(&args.host).unwrap_or(HostKind::Auto);
let agent_id = std::env::var("AI_MEMORY_AGENT_ID")
.unwrap_or_else(|_| "ai:recover-cli:placeholder".to_string());
let opts = RecoverOpts {
host,
transcript_override: args.transcript.clone(),
since_iso: args.since.clone(),
namespace: args.namespace.clone(),
limit: args.limit,
dry_run: args.dry_run,
quiet: args.quiet,
agent_id,
};
let report = match recover_from_transcript(db_path, &opts) {
Ok(r) => r,
Err(e) => {
let _ = writeln!(out.stderr, "ai-memory recover-previous-session: {e}");
return Ok(if args.quiet { 0 } else { 2 });
}
};
if args.json {
let json = serde_json::to_string_pretty(&report)?;
writeln!(out.stdout, "{json}")?;
} else {
emit_human(&report, args.quiet, out)?;
}
Ok(0)
}
fn parse_host_kind(s: &str) -> Option<HostKind> {
match s {
"auto" => Some(HostKind::Auto),
"claude-code" => Some(HostKind::ClaudeCode),
"codex" => Some(HostKind::Codex),
"gemini" => Some(HostKind::Gemini),
_ => None,
}
}
fn emit_human(
report: &crate::recover::RecoverReport,
quiet: bool,
out: &mut CliOutput<'_>,
) -> Result<()> {
if quiet {
writeln!(
out.stdout,
"recover-previous-session: host={} transcript={} atomised={} skipped_dedup={} skipped_limit={} fast_path={} elapsed_ms={}",
report.host_kind.as_str(),
report
.transcript_path
.as_deref()
.map_or("none".to_string(), |p| p.display().to_string()),
report.lines_atomised,
report.lines_skipped_dedup,
report.lines_skipped_limit,
report.fast_path_hit,
report.elapsed_ms,
)?;
return Ok(());
}
writeln!(
out.stdout,
"recover-previous-session report (host={}, elapsed={} ms):",
report.host_kind.as_str(),
report.elapsed_ms
)?;
if let Some(p) = &report.transcript_path {
writeln!(out.stdout, " transcript : {}", p.display())?;
} else {
writeln!(out.stdout, " transcript : (none located)")?;
}
writeln!(out.stdout, " fast_path_hit : {}", report.fast_path_hit)?;
writeln!(
out.stdout,
" lines : total={} atomised={} skipped_dedup={} skipped_limit={}",
report.lines_total,
report.lines_atomised,
report.lines_skipped_dedup,
report.lines_skipped_limit
)?;
writeln!(
out.stdout,
" elapsed (ms) : resolve_path={} dedup_query={} parse={} writes={}",
report.elapsed_ms_resolve_path,
report.elapsed_ms_dedup_query,
report.elapsed_ms_parse,
report.elapsed_ms_writes
)?;
if !report.memories_created.is_empty() {
writeln!(
out.stdout,
" memories_created ({}):",
report.memories_created.len()
)?;
for id in &report.memories_created {
writeln!(out.stdout, " - {id}")?;
}
}
if !report.errors.is_empty() {
writeln!(out.stdout, " errors ({}):", report.errors.len())?;
for e in &report.errors {
writeln!(out.stdout, " - {e}")?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_host_kind_known_values() {
assert_eq!(parse_host_kind("auto"), Some(HostKind::Auto));
assert_eq!(parse_host_kind("claude-code"), Some(HostKind::ClaudeCode));
assert_eq!(parse_host_kind("codex"), Some(HostKind::Codex));
assert_eq!(parse_host_kind("gemini"), Some(HostKind::Gemini));
}
#[test]
fn parse_host_kind_unknown_returns_none() {
assert_eq!(parse_host_kind("cursor"), None);
assert_eq!(parse_host_kind(""), None);
}
}