factorio-sensei 0.1.1

AI coaching copilot for Factorio 2.x — connects to your live game via RCON and gives real-time advice powered by Claude
Documentation
mod cli;
mod mod_install;
mod repl;

use std::sync::Arc;

use clap::Parser;
use factorio_rcon::RconClient;
use factorio_sensei::agent;
use tokio::sync::Mutex;

const DIM: &str = "\x1b[2m";
const RESET: &str = "\x1b[0m";
const GREEN_BOLD: &str = "\x1b[1;32m";

fn main() -> anyhow::Result<()> {
    let _ = dotenvy::dotenv();
    let cli = cli::Cli::parse();

    if cli.command == Some(cli::Command::InstallMod) {
        let path = mod_install::install()?;
        println!("{GREEN_BOLD}Mod installed to:{RESET} {}", path.display());
        println!("{DIM}Restart Factorio and enable the mod in your save.{RESET}");
        return Ok(());
    }

    if std::env::var("ANTHROPIC_API_KEY").is_err() {
        eprintln!("\x1b[1;31mError:{RESET} ANTHROPIC_API_KEY not set.\n");
        eprintln!("Get your API key at: https://console.anthropic.com/settings/keys\n");
        eprintln!("Then either:");
        eprintln!("  export ANTHROPIC_API_KEY=sk-ant-...");
        eprintln!("  or create a .env file with: ANTHROPIC_API_KEY=sk-ant-...\n");
        std::process::exit(1);
    }

    let rt = tokio::runtime::Runtime::new()?;

    eprintln!("{DIM}Connecting to Factorio RCON at {}...{RESET}", cli.addr);
    let rcon = rt.block_on(async {
        let client = RconClient::connect(&cli.addr, &cli.password).await?;
        Ok::<_, anyhow::Error>(Arc::new(Mutex::new(client)))
    })?;

    let mut wiki_articles = factorio_sensei::knowledge::builtin_articles();
    let wiki_dir = std::path::Path::new("data/wiki");
    if wiki_dir.exists() {
        match factorio_sensei::knowledge::load_wiki_articles(wiki_dir) {
            Ok(extra) => wiki_articles.extend(extra),
            Err(e) => {
                eprintln!("{DIM}Warning: could not load extra wiki ({e}){RESET}");
            }
        }
    }
    eprintln!(
        "{DIM}Loaded {} knowledge article(s).{RESET}",
        wiki_articles.len()
    );

    let model_name = cli.model.as_deref().unwrap_or(agent::DEFAULT_MODEL);
    eprintln!("{DIM}Connected! Model: {model_name}. Type /help for commands.{RESET}\n");

    let _rt_guard = rt.enter();
    let sensei = agent::build_sensei(&rcon, cli.model.as_deref(), &wiki_articles);

    if cli.bridge {
        let bridge_rcon = rcon.clone();
        let bridge_sensei = sensei.clone();
        rt.spawn(async move {
            factorio_sensei::bridge::run(
                bridge_rcon,
                bridge_sensei,
                std::time::Duration::from_secs(2),
            )
            .await;
        });
        eprintln!("{DIM}In-game /sensei bridge enabled.{RESET}");
    }

    repl::run(&rt, &sensei)
}