second-brain-api 0.2.0

HTTP API server for second-brain: REST endpoints for recall, remember, and ingest
mod ingest;
mod routes;
mod watcher;

use std::path::PathBuf;
use std::sync::Arc;

use axum::extract::DefaultBodyLimit;
use axum::Router;
use tokio::net::TcpListener;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;

use second_brain_core::embedding::Embedder;
use second_brain_core::kuzu_store::KuzuStore;

pub struct AppState {
    pub store: Arc<KuzuStore>,
    pub embedder: Option<Arc<Embedder>>,
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("second_brain=info".parse().unwrap())
                .add_directive("tower_http=debug".parse().unwrap()),
        )
        .init();

    let db_path = std::env::var("SECOND_BRAIN_DB")
        .map(PathBuf::from)
        .unwrap_or_else(|_| {
            let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
            PathBuf::from(home).join(".second-brain").join("graph.kuzu")
        });

    if let Some(parent) = db_path.parent() {
        std::fs::create_dir_all(parent).ok();
    }

    let machine_id = hostname::get()
        .map(|h| h.to_string_lossy().to_string())
        .unwrap_or_else(|_| "unknown".to_string());

    let store = match KuzuStore::open(&db_path, machine_id) {
        Ok(s) => Arc::new(s),
        Err(e) => {
            tracing::error!("failed to open database at {}: {e}", db_path.display());
            std::process::exit(1);
        }
    };

    tracing::info!("database opened at {}", db_path.display());

    let embedder = match Embedder::new() {
        Ok(e) => {
            tracing::info!("embedding model loaded");
            Some(Arc::new(e))
        }
        Err(e) => {
            tracing::warn!("embedding model unavailable: {e}");
            None
        }
    };

    watcher::spawn(store.clone(), embedder.clone());

    let state = Arc::new(AppState { store, embedder });

    let app = Router::new()
        .merge(routes::router())
        .layer(DefaultBodyLimit::max(64 * 1024 * 1024))
        .layer(CorsLayer::permissive())
        .layer(TraceLayer::new_for_http())
        .with_state(state);

    let bind = std::env::var("SECOND_BRAIN_BIND").unwrap_or_else(|_| "127.0.0.1:7200".to_string());
    let listener = TcpListener::bind(&bind).await.unwrap();
    tracing::info!("listening on {bind}");

    axum::serve(listener, app).await.unwrap();
}