talon-cli 0.4.2

Talon CLI: hybrid retrieval over Obsidian vaults and markdown corpora, with grounded answers, MCP server, and agent-native output.
Documentation
use std::backtrace::Backtrace;
use std::fmt::Write as _;
use std::io::Write as _;
use std::panic::PanicHookInfo;
use std::path::PathBuf;
use std::process;
use std::sync::Once;
use std::time::{SystemTime, UNIX_EPOCH};

use fs_err as fs;

static INSTALL_PANIC_HOOK: Once = Once::new();

const READY_FILE: &str = "mcp-ready";
const CRASH_FILE: &str = "mcp-last-crash";

#[derive(Debug)]
pub struct McpReadyGuard;

impl Drop for McpReadyGuard {
    fn drop(&mut self) {
        clear_ready();
    }
}

#[must_use]
pub const fn ready_guard() -> McpReadyGuard {
    McpReadyGuard
}

pub fn install_panic_hook() {
    INSTALL_PANIC_HOOK.call_once(|| {
        std::panic::set_hook(Box::new(|info| {
            record_panic("panic hook", info);
            clear_ready();
        }));
    });
}

pub fn mark_ready() {
    let _ = write_marker(
        READY_FILE,
        &format!("pid={}\nstarted_ms={}\n", process::id(), now_ms()),
    );
}

pub fn clear_ready() {
    let _ = fs::remove_file(state_file(READY_FILE));
}

pub fn record_caught_panic(context: &str, payload: &(dyn std::any::Any + Send)) {
    let mut message = String::new();
    let _ = writeln!(message, "context={context}");
    let _ = writeln!(message, "pid={}", process::id());
    let _ = writeln!(message, "timestamp_ms={}", now_ms());
    let _ = writeln!(message, "payload={}", panic_payload(payload));
    let _ = writeln!(message, "backtrace={}", Backtrace::force_capture());
    let _ = write_crash_report(&message);
}

#[must_use]
pub fn crash_status_warning() -> Option<String> {
    let path = state_file(CRASH_FILE);
    let report = fs::read_to_string(&path).ok()?;
    let first_lines = report.lines().take(3).collect::<Vec<_>>().join("; ");
    Some(format!(
        "last MCP panic recorded at {}: {first_lines}",
        path.display()
    ))
}

fn record_panic(context: &str, info: &PanicHookInfo<'_>) {
    let mut message = String::new();
    let _ = writeln!(message, "context={context}");
    let _ = writeln!(message, "pid={}", process::id());
    let _ = writeln!(message, "timestamp_ms={}", now_ms());
    let _ = writeln!(message, "panic={info}");
    let _ = writeln!(message, "backtrace={}", Backtrace::force_capture());
    let _ = write_crash_report(&message);
}

fn write_crash_report(report: &str) -> std::io::Result<()> {
    write_marker(CRASH_FILE, report)?;
    let path = state_file(&format!(
        "talon-mcp-panic-{}-{}.log",
        now_ms(),
        process::id()
    ));
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let mut file = fs::File::create(path)?;
    file.write_all(report.as_bytes())
}

fn write_marker(file_name: &str, contents: &str) -> std::io::Result<()> {
    let path = state_file(file_name);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(path, contents)
}

fn state_file(file_name: &str) -> PathBuf {
    state_dir().join(file_name)
}

fn state_dir() -> PathBuf {
    dirs::state_dir()
        .or_else(dirs::home_dir)
        .unwrap_or_else(std::env::temp_dir)
        .join("talon")
}

fn panic_payload(payload: &(dyn std::any::Any + Send)) -> &str {
    if let Some(message) = payload.downcast_ref::<&'static str>() {
        message
    } else if let Some(message) = payload.downcast_ref::<String>() {
        message.as_str()
    } else {
        "<non-string panic payload>"
    }
}

fn now_ms() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |duration| duration.as_millis())
}