mod ingest;
mod routes;
mod watcher;
use std::path::PathBuf;
use std::sync::Arc;
use axum::Router;
use axum::extract::DefaultBodyLimit;
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();
}