tail-fin-daemon 0.6.4

Long-running browser-session daemon for tail-fin (tfd binary). Keeps Chrome tabs warm across invocations via a Unix-socket protocol; registers Site implementations through a runtime Arc<dyn Site> registry.
Documentation
use clap::Subcommand;
use night_fury_daemon_core::cli::make_req;
use night_fury_daemon_core::{client, spawn};
use serde_json::json;

use super::{acquire_then_exec, ExecArgs};

#[derive(Subcommand)]
pub enum GrokCmd {
    /// Continue an existing Grok conversation (new-chat unsupported).
    Ask {
        prompt: String,
        /// Conversation id from `/c/{cid}` — required.
        #[arg(long)]
        cid: String,
        /// Parent response id (optional; empty → read from tab URL).
        #[arg(long, default_value = "")]
        rid: String,
        /// Attach image file(s). Repeat flag for multiple files.
        #[arg(long = "image")]
        images: Vec<String>,
        /// Per-image size limit in MB before request is rejected.
        #[arg(long)]
        max_image_mb: Option<usize>,
        /// Total image payload limit in MB.
        #[arg(long)]
        max_total_image_mb: Option<usize>,
        /// Hide image debug metadata in response.
        #[arg(long, default_value_t = false)]
        no_image_debug: bool,
    },
    /// List recent conversations
    Conversations,
}

fn build_cmd_and_params(cmd: GrokCmd) -> (&'static str, serde_json::Value) {
    match cmd {
        GrokCmd::Ask {
            prompt,
            cid,
            rid,
            images,
            max_image_mb,
            max_total_image_mb,
            no_image_debug,
        } => (
            "grok.ask",
            json!({
                "prompt": prompt,
                "cid": cid,
                "rid": rid,
                "images": images,
                "image_debug": !no_image_debug,
                "max_image_mb": max_image_mb,
                "max_total_image_mb": max_total_image_mb,
            }),
        ),
        GrokCmd::Conversations => ("grok.conversations", json!({})),
    }
}

pub async fn run(
    cmd: GrokCmd,
    socket: &str,
    host: &str,
    session: Option<String>,
) -> anyhow::Result<()> {
    let (cmd_name, params) = build_cmd_and_params(cmd);

    if let Some(sid) = session {
        spawn::ensure_daemon(socket).await?;
        let req = make_req(cmd_name, Some(&sid), params);
        let resp = client::send(socket, &req).await?;
        super::print_and_check(&resp);
        return Ok(());
    }

    let args = ExecArgs {
        site: "grok",
        host,
        cmd: cmd_name,
        params,
    };
    acquire_then_exec(socket, args).await
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ask_envelope_contains_image_limits_and_debug_toggle() {
        let (cmd, params) = build_cmd_and_params(GrokCmd::Ask {
            prompt: "p".into(),
            cid: "c".into(),
            rid: "r".into(),
            images: vec!["/tmp/a.png".into()],
            max_image_mb: Some(2),
            max_total_image_mb: Some(5),
            no_image_debug: true,
        });
        assert_eq!(cmd, "grok.ask");
        assert_eq!(params["image_debug"].as_bool(), Some(false));
        assert_eq!(params["max_image_mb"].as_u64(), Some(2));
        assert_eq!(params["max_total_image_mb"].as_u64(), Some(5));
    }

    #[test]
    fn conversations_envelope_is_empty_object() {
        let (cmd, params) = build_cmd_and_params(GrokCmd::Conversations);
        assert_eq!(cmd, "grok.conversations");
        assert_eq!(params, json!({}));
    }
}