gdelt 0.1.0

CLI for GDELT Project - optimized for agentic usage with local data caching
//! GDELT CLI - Command-line interface for the GDELT Project
//!
//! Optimized for agentic usage with local data caching, analytics,
//! and comprehensive API access.

use clap::Parser;
use std::process::ExitCode;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

mod analytics;
mod api;
mod cli;
mod config;
mod daemon;
mod data;
mod db;
mod enrich;
mod error;
mod mcp;
mod models;
mod skill;

use cli::args::{Cli, Command, OutputFormat};
use error::{ExitStatus, Result};

#[tokio::main]
async fn main() -> ExitCode {
    let cli = Cli::parse();

    // Initialize logging based on verbosity
    init_logging(&cli);

    // Run the command and handle errors
    match run(cli).await {
        Ok(status) => status.into(),
        Err(e) => {
            let status = e.exit_status();
            eprintln!("error: {e}");
            if let Some(source) = std::error::Error::source(&e) {
                eprintln!("caused by: {source}");
            }
            status.into()
        }
    }
}

fn init_logging(cli: &Cli) {
    let filter = match cli.global.verbose {
        0 if cli.global.quiet => EnvFilter::new("error"),
        0 => EnvFilter::new("warn"),
        1 => EnvFilter::new("info"),
        2 => EnvFilter::new("debug"),
        _ => EnvFilter::new("trace"),
    };

    let filter = if cli.global.debug {
        EnvFilter::new("debug")
    } else if cli.global.trace {
        EnvFilter::new("trace")
    } else {
        filter
    };

    let subscriber = tracing_subscriber::registry().with(filter);

    if cli.global.output == OutputFormat::Json {
        subscriber.with(fmt::layer().json()).init();
    } else {
        subscriber
            .with(fmt::layer().with_target(false).without_time())
            .init();
    }
}

async fn run(cli: Cli) -> Result<ExitStatus> {
    // Handle special info commands first
    if cli.global.help_json {
        return print_help_json(&cli);
    }

    match cli.command {
        // DOC 2.0 API commands
        Command::Doc(cmd) => handle_doc(cmd, &cli.global).await,

        // GEO 2.0 API commands
        Command::Geo(cmd) => handle_geo(cmd, &cli.global).await,

        // TV 2.0 API commands
        Command::Tv(cmd) => handle_tv(cmd, &cli.global).await,

        // TV AI API commands
        Command::Tvai(cmd) => handle_tvai(cmd, &cli.global).await,

        // Data download/sync commands
        Command::Data(cmd) => handle_data(cmd, &cli.global).await,

        // Local events query commands
        Command::Events(cmd) => handle_events(cmd, &cli.global).await,

        // Local GKG query commands
        Command::Gkg(cmd) => handle_gkg(cmd, &cli.global).await,

        // Database management commands
        Command::Db(cmd) => handle_db(cmd, &cli.global).await,

        // MCP server (foreground)
        Command::Serve(cmd) => handle_serve(cmd, &cli.global).await,

        // Daemon management commands
        Command::Daemon(cmd) => handle_daemon(cmd, &cli.global).await,

        // Skill installation commands
        Command::Skill(cmd) => handle_skill(cmd, &cli.global).await,

        // Analytics commands
        Command::Analytics(cmd) => handle_analytics(cmd, &cli.global).await,

        // Configuration commands
        Command::Config(cmd) => handle_config(cmd, &cli.global).await,

        // Schema introspection commands
        Command::Schema(cmd) => handle_schema(cmd, &cli.global).await,

        // Article enrichment command
        Command::Enrich(cmd) => handle_enrich(cmd, &cli.global).await,

        // Shell completions
        Command::Completions(cmd) => handle_completions(cmd),
    }
}

fn print_help_json(_cli: &Cli) -> Result<ExitStatus> {
    let help = serde_json::json!({
        "name": "gdelt",
        "version": env!("CARGO_PKG_VERSION"),
        "description": env!("CARGO_PKG_DESCRIPTION"),
        "commands": get_command_help(),
        "global_options": get_global_options_help(),
        "exit_codes": error::all_exit_codes(),
    });
    println!("{}", serde_json::to_string_pretty(&help)?);
    Ok(ExitStatus::Success)
}

fn get_command_help() -> serde_json::Value {
    serde_json::json!([
        {
            "name": "doc",
            "description": "DOC 2.0 API - Search news articles and documents",
            "subcommands": ["search", "timeline", "wordcloud", "themes"]
        },
        {
            "name": "geo",
            "description": "GEO 2.0 API - Geographic search and mapping",
            "subcommands": ["search", "points", "heatmap", "aggregate"]
        },
        {
            "name": "tv",
            "description": "TV 2.0 API - Television news search",
            "subcommands": ["search", "clips", "timeline", "stations"]
        },
        {
            "name": "tvai",
            "description": "TV AI API - AI-enhanced TV analysis",
            "subcommands": ["search", "concepts", "visual"]
        },
        {
            "name": "data",
            "description": "Download and manage historical GDELT data",
            "subcommands": ["download", "sync", "status", "list", "delete"]
        },
        {
            "name": "events",
            "description": "Query local events database",
            "subcommands": ["query", "lookup", "actors", "countries", "codes"]
        },
        {
            "name": "gkg",
            "description": "Query local Global Knowledge Graph",
            "subcommands": ["query", "themes", "persons", "organizations", "locations"]
        },
        {
            "name": "db",
            "description": "Database management",
            "subcommands": ["stats", "vacuum", "export", "import", "query"]
        },
        {
            "name": "serve",
            "description": "Start MCP server (foreground)"
        },
        {
            "name": "daemon",
            "description": "Daemon management",
            "subcommands": ["start", "stop", "restart", "status", "logs", "install", "uninstall"]
        },
        {
            "name": "skill",
            "description": "Claude Code skill management",
            "subcommands": ["install", "uninstall", "update", "status"]
        },
        {
            "name": "analytics",
            "description": "Built-in analytics",
            "subcommands": ["trends", "entities", "sentiment", "compare", "report"]
        },
        {
            "name": "config",
            "description": "Configuration management",
            "subcommands": ["show", "get", "set", "reset", "validate", "path"]
        },
        {
            "name": "schema",
            "description": "Schema introspection for agents",
            "subcommands": ["commands", "command", "output", "codes", "tools"]
        },
        {
            "name": "enrich",
            "description": "Enrich articles with GKG metadata and optionally fetch full text"
        },
        {
            "name": "completions",
            "description": "Generate shell completions"
        }
    ])
}

fn get_global_options_help() -> serde_json::Value {
    serde_json::json!([
        {
            "name": "output",
            "short": "o",
            "long": "output",
            "type": "enum",
            "values": ["auto", "json", "jsonl", "csv", "table", "markdown"],
            "default": "auto",
            "description": "Output format"
        },
        {
            "name": "quiet",
            "short": "q",
            "long": "quiet",
            "type": "bool",
            "description": "Suppress non-essential output"
        },
        {
            "name": "verbose",
            "short": "v",
            "long": "verbose",
            "type": "count",
            "description": "Increase verbosity (-v, -vv, -vvv)"
        },
        {
            "name": "dry-run",
            "long": "dry-run",
            "type": "bool",
            "description": "Preview without executing"
        },
        {
            "name": "yes",
            "short": "y",
            "long": "yes",
            "type": "bool",
            "description": "Auto-confirm prompts"
        },
        {
            "name": "no-cache",
            "long": "no-cache",
            "type": "bool",
            "description": "Bypass cache"
        },
        {
            "name": "cache-only",
            "long": "cache-only",
            "type": "bool",
            "description": "Only use cached data"
        },
        {
            "name": "timeout",
            "long": "timeout",
            "type": "integer",
            "default": 30,
            "description": "Request timeout in seconds"
        },
        {
            "name": "help-json",
            "long": "help-json",
            "type": "bool",
            "description": "Machine-readable help (JSON)"
        }
    ])
}

// Command handlers - implemented in cli::handlers module

use cli::args::{
    AnalyticsCommand, ConfigCommand, DataCommand, DaemonCommand, DbCommand,
    DocCommand, EnrichArgs, EventsCommand, GeoCommand, GkgCommand, GlobalArgs, SchemaCommand,
    ServeArgs, SkillCommand, TvAiCommand, TvCommand, CompletionsArgs, Shell,
};
use cli::handlers;

async fn handle_doc(cmd: DocCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_doc(cmd, global).await
}

async fn handle_geo(cmd: GeoCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_geo(cmd, global).await
}

async fn handle_tv(cmd: TvCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_tv(cmd, global).await
}

async fn handle_tvai(cmd: TvAiCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_tvai(cmd, global).await
}

async fn handle_data(cmd: DataCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_data(cmd, global).await
}

async fn handle_events(cmd: EventsCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_events(cmd, global).await
}

async fn handle_gkg(cmd: GkgCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_gkg(cmd, global).await
}

async fn handle_db(cmd: DbCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_db(cmd, global).await
}

async fn handle_serve(cmd: ServeArgs, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_serve(cmd, global).await
}

async fn handle_daemon(cmd: DaemonCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_daemon(cmd, global).await
}

async fn handle_skill(cmd: SkillCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_skill(cmd, global).await
}

async fn handle_analytics(cmd: AnalyticsCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_analytics(cmd, global).await
}

async fn handle_config(cmd: ConfigCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_config(cmd, global).await
}

async fn handle_schema(cmd: SchemaCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_schema(cmd, global).await
}

async fn handle_enrich(cmd: EnrichArgs, global: &GlobalArgs) -> Result<ExitStatus> {
    handlers::handle_enrich(cmd, global).await
}

fn handle_completions(cmd: CompletionsArgs) -> Result<ExitStatus> {
    use clap::CommandFactory;
    use clap_complete::generate;

    let shell = match cmd.shell {
        Shell::Bash => clap_complete::Shell::Bash,
        Shell::Zsh => clap_complete::Shell::Zsh,
        Shell::Fish => clap_complete::Shell::Fish,
        Shell::PowerShell => clap_complete::Shell::PowerShell,
    };

    let mut cli = Cli::command();
    generate(shell, &mut cli, "gdelt", &mut std::io::stdout());
    Ok(ExitStatus::Success)
}