talon-cli 0.4.1

Talon CLI: hybrid retrieval over Obsidian vaults and markdown corpora, with grounded answers, MCP server, and agent-native output.
Documentation
//! Command dispatch for the Talon CLI scaffold.

mod ask;
mod ask_client;
mod ask_sources;
mod changes;
mod init;
mod inspect;
mod meta;
mod read;
mod recall;
mod related;
mod search;
pub(crate) mod secrets;
mod status;
mod sync;

use crate::cli::{Cli, Commands};
use crate::mcp::state::{ConfigState, McpServerState};
use crate::mcp::transport::run_jsonrpc_loop_with_state;
use crate::output::OutputMode;
use eyre::{Result, bail};
use std::io::{self, BufReader};
use std::sync::Arc;

/// Runs the selected command.
///
/// # Errors
///
/// Returns an error for invalid command input or not-yet-implemented behavior.
pub async fn run(cli: &Cli) -> Result<()> {
    if cli.skill {
        use std::io::Write as _;
        writeln!(io::stdout().lock(), "{}", crate::SKILL_MD)?;
        return Ok(());
    }

    let Some(cmd) = &cli.command else {
        bail!("missing command; try `talon --help`");
    };

    match cmd {
        Commands::Mcp => {
            crate::mcp::diagnostics::install_panic_hook();
            let _ready_guard = crate::mcp::diagnostics::ready_guard();
            let config = crate::config::load_config(cli.config_file.as_deref())?;
            let vault_path = config.vault_path.clone();
            let db_path = config.db_path.clone();
            let config_path = config.config_file_path.clone();
            let config_state = ConfigState {
                config,
                config_path,
                vault_path,
                db_path,
            };
            let state = McpServerState::new(config_state);
            let vault_path_for_watcher = state.config.vault_path.clone();
            crate::mcp::background::watcher::spawn_watcher(
                vault_path_for_watcher,
                Arc::clone(&state),
            );
            crate::mcp::background::embed::spawn_embed_ticker(Arc::clone(&state));
            let stdin = io::stdin();
            let stdout = io::stdout();
            // block_in_place allows the synchronous JSON-RPC loop to drop
            // tokio-backed resources (reqwest clients, inference pools) without
            // panicking inside the outer async executor.
            let outcome = tokio::task::block_in_place(|| {
                run_jsonrpc_loop_with_state(BufReader::new(stdin.lock()), stdout.lock(), &state)
            })?;
            let _ = outcome;
            crate::mcp::diagnostics::clear_ready();
            Ok(())
        }
        Commands::Init(args) => init::emit(args),
        Commands::Search(args) => search::emit(args, cli).await,
        Commands::Ask(args) => ask::emit(args, cli).await,
        Commands::Read(args) => read::emit(args, cli).await,
        Commands::Sync(args) => sync::emit(args, cli).await,
        Commands::Related(args) => related::emit(args, cli).await,
        Commands::Status(args) => status::emit(args, cli),
        Commands::Meta(args) => meta::emit(args, cli).await,
        Commands::Changes(args) => changes::emit(args, cli).await,
        Commands::Inspect(args) => inspect::emit(args, cli).await,
        Commands::Recall(args) => recall::emit(args, cli).await,
        Commands::Secrets(args) => secrets::emit(&args.subcommand),
    }
}

pub(super) const fn output_mode(cli: &Cli) -> OutputMode {
    if cli.agent {
        OutputMode::Agent
    } else if cli.json {
        OutputMode::JsonPretty
    } else {
        OutputMode::Human
    }
}

pub(super) fn should_spin(cli: &Cli) -> bool {
    !cli.agent && !cli.json && crate::platform::stderr_is_tty()
}