This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

stigmergic / container / server.ts
8.7 kB 239 lines
1// ─── Strata Analysis Container Server ──────────────────────────────── 2// HTTP server running inside the Cloudflare Container. 3// Endpoints: 4// POST /analyze — run strata analysis on an island 5// POST /deriveIsland — derive island (connected component) from a lexmin vertex 6// POST /sync — sync vault + carry data 7// GET /health — health check 8 9import { analyze } from "./analysis.ts"; 10import { syncAll, syncVault, syncCarry } from "./sync.ts"; 11import { createServer } from "http"; 12import { 13 execSync, 14} from "child_process"; 15 16const PORT = 8080; 17const CARRY_PATH = process.env.CARRY_PATH || "/data/carry"; 18 19createServer(async (req, res) => { 20 const url = new URL(req.url || "/", `http://localhost:${PORT}`); 21 22 // ── Health ── 23 if (url.pathname === "/health" && req.method === "GET") { 24 // Include data status in health check 25 const { readdirSync, statSync, existsSync } = await import("fs"); 26 const { join } = await import("path"); 27 let vaultFiles = 0, carryFiles = 0, carryDomains: string[] = []; 28 try { 29 const walk = (dir: string): number => { 30 let count = 0; 31 for (const e of readdirSync(dir, { withFileTypes: true })) { 32 if (e.name.startsWith(".")) continue; 33 if (e.isDirectory()) count += walk(join(dir, e.name)); 34 else if (e.name.endsWith(".md")) count++; 35 } 36 return count; 37 }; 38 vaultFiles = walk("/data/vault"); 39 const carryBase = "/data/carry"; 40 try { 41 for (const d of readdirSync(carryBase)) { 42 if (d.startsWith(".")) continue; 43 const p = join(carryBase, d); 44 if (statSync(p).isDirectory()) { 45 carryDomains.push(d); 46 carryFiles += walk(p); 47 } 48 } 49 } catch {} 50 } catch {} 51 res.writeHead(200, { "Content-Type": "application/json" }); 52 res.end(JSON.stringify({ 53 status: "ok", 54 vaultFiles, 55 carryFiles, 56 carryDomains, 57 carryTokenSet: !!process.env.CARRY_GITHUB_TOKEN, 58 obsidianTokenSet: !!process.env.OBSIDIAN_AUTH_TOKEN, 59 obsidianConfigDir: existsSync("/root/.config/obsidian-headless/auth_token"), 60 })); 61 return; 62 } 63 64 // ── Sync ── 65 if (url.pathname === "/sync" && req.method === "POST") { 66 try { 67 const { execSync } = await import("child_process"); 68 const { existsSync, readFileSync } = await import("fs"); 69 70 // Sync vault 71 let vaultLog = ""; 72 try { 73 const out = execSync("ob sync --path /data/vault 2>&1", { timeout: 120000, encoding: "utf-8" }); 74 vaultLog = out.slice(-500); 75 } catch (e: any) { 76 vaultLog = e.stdout?.slice(-500) || "" + e.stderr?.slice(-500) || "" || e.message?.slice(-500) || ""; 77 } 78 79 // Sync carry 80 let carryLog = "already synced"; 81 try { 82 const out = execSync("git -C /data/carry pull origin main 2>&1", { timeout: 30000, encoding: "utf-8" }); 83 carryLog = out.slice(-200); 84 } catch (e: any) { 85 carryLog = e.message?.slice(-200) || "pull failed"; 86 } 87 88 res.writeHead(200, { "Content-Type": "application/json" }); 89 res.end(JSON.stringify({ ok: true, vaultLog, carryLog })); 90 } catch (e: any) { 91 res.writeHead(500, { "Content-Type": "application/json" }); 92 res.end(JSON.stringify({ error: e.message })); 93 } 94 return; 95 } 96 97 // ── Derive Islands ── 98 // Input: { connections: Array<{source, target, connectionType, note, did, rkey}>, seed?: string } 99 // - With seed: derive single island containing that vertex 100 // - Without seed: detect all islands (all connected components) 101 // Output: { islands: Array<{id, lexmin, vertices, edges}> } 102 if (url.pathname === "/deriveIsland" && req.method === "POST") { 103 const chunks: Buffer[] = []; 104 for await (const chunk of req) chunks.push(chunk); 105 const body = Buffer.concat(chunks).toString(); 106 107 try { 108 const input = JSON.parse(body); 109 const { connections, seed } = input; 110 111 if (!Array.isArray(connections)) { 112 res.writeHead(400, { "Content-Type": "application/json" }); 113 res.end(JSON.stringify({ error: "connections[] required" })); 114 return; 115 } 116 117 // Build adjacency list 118 const adj = new Map<string, Array<typeof connections[0]>>(); 119 const allVertices = new Set<string>(); 120 for (const conn of connections) { 121 const s = conn.source as string; 122 const t = conn.target as string; 123 if (!adj.has(s)) adj.set(s, []); 124 if (!adj.has(t)) adj.set(t, []); 125 adj.get(s)!.push(conn); 126 adj.get(t)!.push(conn); 127 allVertices.add(s); 128 allVertices.add(t); 129 } 130 131 // BFS from a seed vertex to find one connected component 132 function bfsComponent(start: string): { 133 vertices: string[]; 134 edges: any[]; 135 } { 136 const visited = new Set<string>(); 137 const queue = [start]; 138 const componentEdges: any[] = []; 139 140 while (queue.length > 0) { 141 const current = queue.shift()!; 142 if (visited.has(current)) continue; 143 visited.add(current); 144 145 for (const conn of adj.get(current) || []) { 146 const s = conn.source as string; 147 const t = conn.target as string; 148 componentEdges.push({ 149 uri: `at://${conn.did}/network.cosmik.connection/${conn.rkey}`, 150 did: conn.did, 151 rkey: conn.rkey, 152 source: s, 153 target: t, 154 connectionType: conn.connectionType || "relates", 155 note: conn.note || "", 156 }); 157 if (!visited.has(s)) queue.push(s); 158 if (!visited.has(t)) queue.push(t); 159 } 160 } 161 162 // Dedupe edges 163 const vertexSet = new Set(visited); 164 const edges = componentEdges.filter(e => vertexSet.has(e.source) && vertexSet.has(e.target)); 165 const seenEdges = new Set<string>(); 166 const dedupedEdges = edges.filter(e => { 167 const key = `${e.source}|${e.target}|${e.rkey}`; 168 if (seenEdges.has(key)) return false; 169 seenEdges.add(key); 170 return true; 171 }); 172 173 const sortedVertices = [...visited].sort(); 174 return { vertices: sortedVertices, edges: dedupedEdges }; 175 } 176 177 const { createHash } = await import("crypto"); 178 179 if (seed) { 180 // Single island from seed vertex 181 const { vertices, edges } = bfsComponent(seed); 182 const lexmin = vertices[0]; 183 const id = createHash("sha256").update(lexmin).digest("hex").slice(0, 16); 184 res.writeHead(200, { "Content-Type": "application/json" }); 185 res.end(JSON.stringify({ islands: [{ id, lexmin, vertices, edges }] })); 186 } else { 187 // All islands — find all connected components 188 const globalVisited = new Set<string>(); 189 const islands: any[] = []; 190 191 for (const vertex of allVertices) { 192 if (globalVisited.has(vertex)) continue; 193 const { vertices, edges } = bfsComponent(vertex); 194 for (const v of vertices) globalVisited.add(v); 195 const lexmin = vertices[0]; 196 const id = createHash("sha256").update(lexmin).digest("hex").slice(0, 16); 197 islands.push({ id, lexmin, vertices, edges }); 198 } 199 200 // Sort by vertex count descending 201 islands.sort((a, b) => b.vertices.length - a.vertices.length); 202 203 res.writeHead(200, { "Content-Type": "application/json" }); 204 res.end(JSON.stringify({ islands })); 205 } 206 } catch (e: any) { 207 console.error("deriveIsland error:", e); 208 res.writeHead(500, { "Content-Type": "application/json" }); 209 res.end(JSON.stringify({ error: e.message })); 210 } 211 return; 212 } 213 214 // ── Analyze ── 215 if (url.pathname === "/analyze" && req.method === "POST") { 216 const chunks: Buffer[] = []; 217 for await (const chunk of req) chunks.push(chunk); 218 const body = Buffer.concat(chunks).toString(); 219 220 try { 221 const input = JSON.parse(body); 222 const result = await analyze(input); 223 res.writeHead(200, { "Content-Type": "application/json" }); 224 res.end(JSON.stringify(result)); 225 } catch (e: any) { 226 console.error("Analysis error:", e); 227 res.writeHead(500, { "Content-Type": "application/json" }); 228 res.end(JSON.stringify({ error: e.message })); 229 } 230 return; 231 } 232 233 res.writeHead(404); 234 res.end("Not found"); 235}).listen(PORT, () => { 236 console.log(`Strata analysis container listening on port ${PORT}`); 237 // Sync on startup 238 syncAll().catch(e => console.error("Initial sync failed:", e)); 239});