const http = require('http'); const WebSocket = require('ws'); const fs = require('fs'); const path = require('path'); const PORT = 8160; let pendingRequests = []; let wsClient = null; let requestId = 0; const callbacks = new Map(); const ENTITY_DIR = path.join(__dirname, '..', 'entity'); async function extractKeywords(text) { try { const resp = await fetch('http://127.0.0.1:8093/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'xlam', messages: [{ role: 'user', content: `Extract 3-5 keyword phrases from: "${text}". Return JSON: {"keywords":["k1","k2"]}` }], temperature: 0.1, max_tokens: 60, }), }); if (resp.ok) { const data = await resp.json(); const match = data.choices?.[0]?.message?.content?.match(/\{[\s\S]*"keywords"[\s\S]*\}/); if (match) return JSON.parse(match[0]).keywords.map(k => k.toLowerCase()); } } catch (e) {} const stop = new Set(['what','that','this','with','from','have','your','been','does','were','they','about','would','could','should','there','where','which']); return (text.toLowerCase().match(/[a-z]{4,}/g) || []).filter(w => !stop.has(w)).slice(0, 5); } function grepEntity(keywords) { const results = []; function scan(dir) { try { for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const fp = path.join(dir, e.name); if (e.isDirectory()) { scan(fp); continue; } if (!/\.(md|json|txt)$/.test(e.name)) continue; try { const lines = fs.readFileSync(fp, 'utf8').split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.length < 10 || line.length > 300) continue; const lower = line.toLowerCase(); let score = 0; for (const kw of keywords) { if (lower.includes(kw)) score++; } if (score > 0) results.push({ line, score, file: path.relative(ENTITY_DIR, fp) }); } } catch (e) {} } } catch (e) {} } scan(ENTITY_DIR); results.sort((a, b) => b.score - a.score); return results.slice(0, 5); } const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } if (req.url === '/v1/chat/completions' && req.method === 'POST') { let body = ''; req.on('data', c => body += c); req.on('end', async () => { try { const data = JSON.parse(body); const userMsg = data.messages?.find(m => m.role === 'user')?.content || ''; let entity = data.entity || ''; if (!entity) { const sysMsg = data.messages?.find(m => m.role === 'system')?.content || ''; const match = sysMsg.match(/^You are (\w[\w-]*)/i); if (match) entity = match[1].toLowerCase(); } if (!entity) entity = 'grandma'; const sysContent = data.messages?.find(m => m.role === 'system')?.content || ''; console.log(`[bridge] entity=${entity} | user="${userMsg.slice(0,60)}" | sys=${sysContent.length} chars | from-body=${!!data.entity}`); const id = ++requestId; const promise = new Promise((resolve, reject) => { callbacks.set(id, { resolve, reject, timer: setTimeout(() => { callbacks.delete(id); reject(new Error('timeout')); }, 120000) }); }); if (wsClient && wsClient.readyState === 1) { wsClient.send(JSON.stringify({ id, entity, message: userMsg, systemContext: sysContent, timestamp: Date.now() })); } else { pendingRequests.push({ id, entity, message: userMsg, keywords, grepResults }); } const result = await promise; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: result.text } }], entity, model: 'gemma-26b-thinking-layer', })); } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); } }); return; } if (req.url === '/api/keywords' && req.method === 'POST') { let body = ''; req.on('data', c => body += c); req.on('end', async () => { const { text } = JSON.parse(body); const keywords = await extractKeywords(text); const grepResults = grepEntity(keywords); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ keywords, grepResults })); }); return; } if (req.url === '/api/save-memory' && req.method === 'POST') { let body = ''; req.on('data', c => body += c); req.on('end', () => { const { entity, turn } = JSON.parse(body); const memPath = path.join(ENTITY_DIR, 'memory', 'running.md'); try { const ts = new Date().toISOString().slice(0, 19); fs.appendFileSync(memPath, `\n[${ts}] [${entity}] ${turn}\n`); res.writeHead(200); res.end('ok'); } catch (e) { res.writeHead(500); res.end(e.message); } }); return; } if (req.url === '/status') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ connected: wsClient?.readyState === 1, pending: pendingRequests.length })); return; } // GET /api/recent?entity=anima&lines=20 if (req.url.startsWith('/api/recent') && req.method === 'GET') { const params = new URL(req.url, 'http://localhost').searchParams; const entity = params.get('entity') || 'all'; const lines = parseInt(params.get('lines') || '30'); const memPath = path.join(ENTITY_DIR, 'memory', 'running.md'); try { const content = fs.readFileSync(memPath, 'utf8'); const allLines = content.split('\n').filter(l => l.trim()); const filtered = entity === 'all' ? allLines : allLines.filter(l => l.toLowerCase().includes(`[${entity}]`)); const recent = filtered.slice(-lines); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ entity, lines: recent.length, memory: recent })); } catch (e) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ entity, lines: 0, memory: [] })); } return; } res.writeHead(404); res.end('not found'); }); const wss = new WebSocket.Server({ server }); wss.on('connection', (ws) => { console.log('[bridge] WebGPU webapp connected'); wsClient = ws; for (const req of pendingRequests) ws.send(JSON.stringify(req)); pendingRequests = []; ws.on('message', (data) => { const msg = JSON.parse(data); if (msg.id && callbacks.has(msg.id)) { const cb = callbacks.get(msg.id); clearTimeout(cb.timer); callbacks.delete(msg.id); cb.resolve({ text: msg.text }); } }); ws.on('close', () => { console.log('[bridge] webapp disconnected'); wsClient = null; }); }); server.listen(PORT, () => { console.log(`[garden-bridge] HTTP + WebSocket on :${PORT}`); console.log(`[garden-bridge] OpenAI-compatible: POST http://localhost:${PORT}/v1/chat/completions`); console.log(`[garden-bridge] Pass { "entity": "esh" } in body to select entity`); });