// server.el — Engram HTTP server. // // Engram is the in-process graph store. The runtime owns the data; this // file is the thin HTTP face. Every route maps to one or two engram_* // builtins. There is no SQL, no db layer, no SQLite — the runtime IS the // database. // // Built and linked with: // elc src/server.el > server.c // cc -std=c11 -O2 -lcurl -lpthread -o engram server.c el_runtime.c // ./engram // // Configuration via environment: // ENGRAM_BIND — host:port (default :8742) // ENGRAM_API_KEY — bearer auth (optional) // ENGRAM_DATA_DIR — snapshot location (default ~/.neuron/engram) // ── Helpers ─────────────────────────────────────────────────────────────────── fn parse_port(bind: String) -> Int { // ":8742" → 8742; "0.0.0.0:8742" → 8742; bare "8742" → 8742 let colon: Int = str_index_of(bind, ":") if colon < 0 { return str_to_int(bind) } let after: String = str_slice(bind, colon + 1, str_len(bind)) return str_to_int(after) } fn ok_json() -> String { "{\"ok\":true}" } fn err_json(msg: String) -> String { "{\"error\":\"" + msg + "\"}" } fn strip_query(path: String) -> String { let q: Int = str_index_of(path, "?") if q < 0 { return path } str_slice(path, 0, q) } fn query_param(path: String, key: String) -> String { let q: Int = str_index_of(path, "?") if q < 0 { return "" } let qs: String = str_slice(path, q + 1, str_len(path)) let needle: String = key + "=" let pos: Int = str_index_of(qs, needle) if pos < 0 { return "" } let after: String = str_slice(qs, pos + str_len(needle), str_len(qs)) let amp: Int = str_index_of(after, "&") if amp < 0 { return after } str_slice(after, 0, amp) } fn query_int(path: String, key: String, default_val: Int) -> Int { let v: String = query_param(path, key) if str_eq(v, "") { return default_val } str_to_int(v) } // Extract last path segment after a known prefix: extract_id("/api/nodes/abc-123", "/api/nodes/") → "abc-123" fn extract_id(path: String, prefix: String) -> String { let clean: String = strip_query(path) if !str_starts_with(clean, prefix) { return "" } let after: String = str_slice(clean, str_len(prefix), str_len(clean)) let slash: Int = str_index_of(after, "/") if slash < 0 { return after } str_slice(after, 0, slash) } // ── Routes ──────────────────────────────────────────────────────────────────── fn route_stats(method: String, path: String, body: String) -> String { engram_stats_json() } fn route_create_node(method: String, path: String, body: String) -> String { let content: String = json_get_string(body, "content") let node_type: String = json_get_string(body, "node_type") if str_eq(node_type, "") { let node_type = "Memory" } let salience: Float = json_get_float(body, "salience") if salience == 0.0 { let salience = 0.5 } let id: String = engram_node(content, node_type, salience) "{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}" } fn route_get_node(method: String, path: String, body: String) -> String { let id: String = extract_id(path, "/api/nodes/") if str_eq(id, "") { return err_json("missing id") } return engram_get_node_json(id) } fn route_scan_nodes(method: String, path: String, body: String) -> String { let limit: Int = query_int(path, "limit", 50) let offset: Int = query_int(path, "offset", 0) let nt: String = query_param(path, "node_type") if str_eq(nt, "") { return engram_scan_nodes_json(limit, offset) } return engram_scan_nodes_by_type_json(nt, limit, offset) } // route_scan_edges — bulk export of all edges as a JSON array. Implemented // via engram_save → fs_read of the canonical on-disk snapshot, which the // runtime keeps in lockstep with the in-memory graph. Live against the // running graph, not a stale export. fn route_scan_edges(method: String, path: String, body: String) -> String { let dir: String = env("ENGRAM_DATA_DIR") if str_eq(dir, "") { let dir = "/tmp/engram" } let snap_path: String = dir + "/snapshot.json" engram_save(snap_path) let snap: String = fs_read(snap_path) if str_eq(snap, "") { return "[]" } // json_get truncates at the first delimiter (no bracket depth tracking), // so for the edges ARRAY value we need json_get_raw, which honors // brackets and returns the full sub-JSON. let edges: String = json_get_raw(snap, "edges") if str_eq(edges, "") { return "[]" } return edges } fn route_search(method: String, path: String, body: String) -> String { let q: String = "" if str_eq(method, "GET") { let q = query_param(path, "q") } else { let q = json_get_string(body, "query") } let limit: Int = query_int(path, "limit", 20) if limit == 0 { let limit = json_get_int(body, "limit") } if limit == 0 { let limit = 20 } return engram_search_json(q, limit) } fn route_activate(method: String, path: String, body: String) -> String { let q: String = "" let depth: Int = 3 if str_eq(method, "GET") { let q = query_param(path, "q") let depth = query_int(path, "depth", 3) } else { let q = json_get_string(body, "query") let bd: Int = json_get_int(body, "depth") if bd > 0 { let depth = bd } } return "{\"results\":" + engram_activate_json(q, depth) + "}" } fn route_create_edge(method: String, path: String, body: String) -> String { let from_id: String = json_get_string(body, "from_id") let to_id: String = json_get_string(body, "to_id") let relation: String = json_get_string(body, "relation") if str_eq(relation, "") { let relation = "associates" } let weight: Float = json_get_float(body, "weight") if weight == 0.0 { let weight = 0.5 } engram_connect(from_id, to_id, weight, relation) "{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}" } fn route_neighbors(method: String, path: String, body: String) -> String { let id: String = extract_id(path, "/api/neighbors/") if str_eq(id, "") { return err_json("missing id") } let depth: Int = query_int(path, "depth", 1) return engram_neighbors_json(id, depth, "both") } fn route_strengthen(method: String, path: String, body: String) -> String { let id: String = json_get_string(body, "node_id") if str_eq(id, "") { return err_json("missing node_id") } engram_strengthen(id) ok_json() } fn route_forget(method: String, path: String, body: String) -> String { let id: String = extract_id(path, "/api/nodes/") if str_eq(id, "") { return err_json("missing id") } engram_forget(id) ok_json() } fn route_save(method: String, path: String, body: String) -> String { let p: String = json_get_string(body, "path") if str_eq(p, "") { let dir: String = env("ENGRAM_DATA_DIR") if str_eq(dir, "") { let dir = "/tmp/engram" } let p = dir + "/snapshot.json" } engram_save(p) "{\"ok\":true,\"path\":\"" + p + "\"}" } fn route_load(method: String, path: String, body: String) -> String { let p: String = json_get_string(body, "path") if str_eq(p, "") { let dir: String = env("ENGRAM_DATA_DIR") if str_eq(dir, "") { let dir = "/tmp/engram" } let p = dir + "/snapshot.json" } engram_load(p) ok_json() } fn route_health(method: String, path: String, body: String) -> String { "{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}" } // ── Auth ────────────────────────────────────────────────────────────────────── fn check_auth_ok(method: String, body: String) -> Bool { let key: String = env("ENGRAM_API_KEY") if str_eq(key, "") { return true } // Read-only methods don't require auth. Until http_serve surfaces // request headers we can't accept a Bearer token cleanly; mutating // requests must include "_auth": "" in the JSON body. if str_eq(method, "GET") { return true } let provided: String = json_get_string(body, "_auth") if str_eq(provided, key) { return true } return false } // ── Dispatcher ──────────────────────────────────────────────────────────────── fn handle_request(method: String, path: String, body: String) -> String { let clean: String = strip_query(path) // Health is always reachable if str_eq(method, "GET") { if str_eq(clean, "/health") || str_eq(clean, "/") { return route_health(method, path, body) } } // Auth (when ENGRAM_API_KEY is set) if !check_auth_ok(method, body) { return err_json("unauthorized") } // Stats if str_eq(method, "GET") && (str_eq(clean, "/api/stats") || str_eq(clean, "/stats")) { return route_stats(method, path, body) } // Nodes if str_eq(method, "POST") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) { return route_create_node(method, path, body) } if str_eq(method, "GET") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes") || str_eq(clean, "/nodes/list") || str_eq(clean, "/api/nodes/list")) { return route_scan_nodes(method, path, body) } if str_eq(method, "GET") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) { return route_scan_edges(method, path, body) } if str_eq(method, "GET") && str_starts_with(clean, "/api/nodes/") { return route_get_node(method, path, body) } if str_eq(method, "DELETE") && str_starts_with(clean, "/api/nodes/") { return route_forget(method, path, body) } // Edges if str_eq(method, "POST") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) { return route_create_edge(method, path, body) } if str_eq(method, "GET") && str_starts_with(clean, "/api/neighbors/") { return route_neighbors(method, path, body) } // Activation + Search if str_eq(method, "POST") && (str_eq(clean, "/api/activate") || str_eq(clean, "/activate")) { return route_activate(method, path, body) } if str_eq(method, "GET") && str_starts_with(clean, "/api/activate") { return route_activate(method, path, body) } if str_eq(method, "POST") && (str_eq(clean, "/api/search") || str_eq(clean, "/search")) { return route_search(method, path, body) } if str_eq(method, "GET") && str_starts_with(clean, "/api/search") { return route_search(method, path, body) } // Strengthen if str_eq(method, "POST") && (str_eq(clean, "/api/strengthen") || str_eq(clean, "/strengthen")) { return route_strengthen(method, path, body) } // Persistence if str_eq(method, "POST") && (str_eq(clean, "/api/save") || str_eq(clean, "/save")) { return route_save(method, path, body) } if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) { return route_load(method, path, body) } "{\"error\":\"not found\",\"path\":\"" + clean + "\"}" } // ── Entry ───────────────────────────────────────────────────────────────────── let bind_str: String = env("ENGRAM_BIND") if str_eq(bind_str, "") { let bind_str = ":8742" } let port: Int = parse_port(bind_str) // On startup, try to load any existing snapshot (best effort). let data_dir: String = env("ENGRAM_DATA_DIR") if str_eq(data_dir, "") { let data_dir = "/tmp/engram" } let snapshot_path: String = data_dir + "/snapshot.json" engram_load(snapshot_path) println("[engram] runtime-native graph engine") println("[engram] data_dir=" + data_dir) println("[engram] node_count=" + int_to_str(engram_node_count())) println("[engram] edge_count=" + int_to_str(engram_edge_count())) println("[engram] listening on " + int_to_str(port)) http_set_handler("handle_request") http_serve(port, "handle_request")