pub mod auth;
pub mod middleware;
pub mod routes;
use std::sync::Arc;
use axocoatl_daemon::AxocoatlDaemon;
use axum::{
routing::{get, post},
Router,
};
use tokio::sync::RwLock;
pub type AppState = Arc<RwLock<AxocoatlDaemon>>;
pub fn build_router(state: AppState) -> Router {
Router::new()
.route("/", get(routes::dashboard))
.route("/lattice/{file}", get(routes::lattice_asset))
.route("/vendor/{*file}", get(routes::vendor_asset))
.route("/health", get(routes::health))
.route("/health/ready", get(routes::health_ready))
.route("/health/live", get(routes::health_live))
.route("/api/llm-health", get(routes::llm_health))
.route("/api/agents", get(routes::list_agents))
.route(
"/api/agents/{agent_id}/execute",
post(routes::execute_agent),
)
.route("/api/agents/{agent_id}/status", get(routes::agent_status))
.route(
"/api/agents/{agent_id}/restart",
post(routes::restart_agent),
)
.route(
"/api/agents/{agent_id}",
axum::routing::patch(routes::patch_agent),
)
.route("/api/mcp/catalog", get(routes::mcp_catalog))
.route("/api/mcp/install", post(routes::install_mcp))
.route(
"/api/mcp/servers/{name}",
post(routes::reconnect_mcp).delete(routes::remove_mcp),
)
.route("/api/mcp/servers", get(routes::list_mcp_servers))
.route(
"/api/mcp/permissions",
get(routes::list_mcp_permissions).delete(routes::revoke_mcp_permission),
)
.route("/api/mcp/tools", get(routes::list_mcp_tools))
.route("/api/schedules", get(routes::list_schedules))
.route("/api/proactive", get(routes::list_proactive))
.route(
"/api/schedules/{id}",
axum::routing::patch(routes::patch_schedule),
)
.route("/api/schedules/{id}/run", post(routes::run_schedule))
.route("/api/skills", get(routes::list_skills))
.route("/api/skills/{id}/fire", post(routes::fire_skill))
.route("/api/events/recent", get(routes::recent_events))
.route("/api/workflows", get(routes::list_workflows))
.route(
"/api/workflows/{workflow_id}/execute",
post(routes::execute_workflow),
)
.route("/api/tokens/report", get(routes::token_report))
.route(
"/api/sessions",
get(routes::list_sessions).post(routes::create_session),
)
.route("/api/sessions/{id}/execute", post(routes::execute_session))
.route("/api/sessions/{id}/tree", get(routes::session_tree))
.route(
"/api/sessions/{id}/file",
get(routes::session_file).post(routes::session_file_write),
)
.route(
"/api/sessions/{id}/tasks",
get(routes::session_tasks).post(routes::session_task_spawn),
)
.route(
"/api/sessions/{id}/terminals/{tid}/ws",
get(routes::session_terminal_ws),
)
.route(
"/api/sessions/{id}/proxy/{port}",
get(routes::session_browser_proxy_root),
)
.route(
"/api/sessions/{id}/proxy/{port}/{*tail}",
get(routes::session_browser_proxy),
)
.route("/axo-tap.js", get(routes::axo_tap_script))
.route("/brand/{file}", get(routes::brand_asset))
.route(
"/api/automations",
get(routes::list_automations).post(routes::create_automation),
)
.route(
"/api/automations/{id}",
get(routes::get_automation)
.put(routes::update_automation)
.patch(routes::update_automation)
.delete(routes::delete_automation),
)
.route("/api/automations/{id}/run", post(routes::run_automation))
.route("/api/automations/{id}/move", post(routes::move_automation))
.route(
"/api/automation-folders",
get(routes::list_automation_folders)
.post(routes::create_automation_folder)
.patch(routes::rename_automation_folder)
.delete(routes::delete_automation_folder),
)
.route("/api/automations/{id}/runs", get(routes::list_runs))
.route("/api/automations/{id}/runs/{run_id}", get(routes::get_run))
.route(
"/api/automations/{id}/runs/{run_id}/fork",
post(routes::fork_run),
)
.route("/api/tools", get(routes::list_tools))
.route("/api/interrupts", get(routes::list_interrupts))
.route(
"/api/automations/{id}/runs/{run_id}/nodes/{node_id}/resume",
post(routes::resume_interrupt),
)
.route(
"/api/automations/{id}/runs/{run_id}/nodes/{node_id}/cancel",
post(routes::cancel_interrupt),
)
.route(
"/api/sessions/{id}",
axum::routing::delete(routes::close_session).patch(routes::rename_session),
)
.route(
"/api/chat",
get(routes::list_chats).post(routes::create_chat),
)
.route(
"/api/chat/{id}",
get(routes::get_chat)
.patch(routes::patch_chat)
.delete(routes::delete_chat),
)
.route("/api/chat/{id}/fork", post(routes::fork_chat))
.route("/api/chat/{id}/export", get(routes::export_chat))
.route(
"/api/chat/{id}/attach",
post(routes::upload_chat_attachment).put(routes::attach_file_to_chat),
)
.route(
"/api/chat/{id}/attach/{file_id}",
get(routes::get_chat_attachment)
.delete(routes::delete_chat_attachment)
.patch(routes::pin_chat_attachment),
)
.route(
"/api/files",
get(routes::list_files).post(routes::upload_file),
)
.route(
"/api/files/{id}",
get(routes::get_file_meta)
.patch(routes::patch_file)
.delete(routes::delete_file),
)
.route("/api/files/{id}/bytes", get(routes::get_file_bytes))
.route("/api/llm/models", get(routes::list_models))
.route("/api/fs/list", get(routes::fs_list_dirs))
.route("/api/fs/project", get(routes::fs_project_probe))
.route("/ws", get(routes::ws))
.layer(axum::middleware::from_fn(middleware::request_logging))
.layer(middleware::cors_headers())
.with_state(state)
}
pub async fn serve(daemon: AxocoatlDaemon, host: &str, port: u16) -> std::io::Result<()> {
let state: AppState = Arc::new(RwLock::new(daemon));
serve_shared(state, host, port).await
}
pub async fn serve_shared(state: AppState, host: &str, port: u16) -> std::io::Result<()> {
let app = build_router(state);
let addr = format!("{host}:{port}");
tracing::info!(addr = %addr, "Starting Axocoatl API server");
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app).await?;
Ok(())
}