tail-fin-cli 0.2.1

Multi-site browser automation CLI — Twitter, Reddit, Bloomberg, Coupang, PCC, Instagram, YouTube, Grok, SeekingAlpha, Xiaohongshu, 591, Nansen
use clap::Subcommand;
use tail_fin_common::TailFinError;

use crate::session::{print_json, Ctx};

#[derive(Subcommand)]
pub enum GrokAction {
    /// Ask Grok a question
    Ask {
        prompt: String,
        #[arg(long, default_value_t = 30)]
        timeout: u64,
    },
    /// List recent conversations
    Conversations,
}

pub async fn run(action: GrokAction, ctx: &Ctx) -> Result<(), TailFinError> {
    if ctx.cookies.is_some() {
        eprintln!("Error: Grok requires browser mode (Cloudflare protection).");
        eprintln!("  (Connects to Chrome at 127.0.0.1:9222 by default)");
        std::process::exit(1);
    }

    let chrome_host = ctx.connect.as_deref().unwrap_or("127.0.0.1:9222");
    let session = crate::session::browser_session(chrome_host, ctx.headed).await?;
    let client = tail_fin_grok::GrokClient::new(session);

    match action {
        GrokAction::Ask { prompt, timeout } => {
            let resp = client.ask(&prompt, timeout).await?;
            print_json(&serde_json::json!({
                "response": resp.response,
                "conversation_id": resp.conversation_id,
            }))?;
        }
        GrokAction::Conversations => {
            let convs = client.list_conversations().await?;
            print_json(&serde_json::json!({
                "conversations": convs,
            }))?;
        }
    }
    Ok(())
}

pub struct Adapter;

impl crate::adapter::CliAdapter for Adapter {
    fn name(&self) -> &'static str {
        "grok"
    }

    fn about(&self) -> &'static str {
        "Grok AI operations"
    }

    fn command(&self) -> clap::Command {
        <GrokAction as clap::Subcommand>::augment_subcommands(
            clap::Command::new("grok").about("Grok AI operations"),
        )
    }

    fn dispatch<'a>(
        &'a self,
        matches: &'a clap::ArgMatches,
        ctx: &'a crate::session::Ctx,
    ) -> std::pin::Pin<
        Box<
            dyn std::future::Future<Output = Result<(), tail_fin_common::TailFinError>> + Send + 'a,
        >,
    > {
        Box::pin(async move {
            let action = <GrokAction as clap::FromArgMatches>::from_arg_matches(matches)
                .map_err(|e| tail_fin_common::TailFinError::Api(e.to_string()))?;
            run(action, ctx).await
        })
    }
}