From 3091d3cdd96e78f1eccf41e33000ec89847ded77 Mon Sep 17 00:00:00 2001 From: Google AI Agent Date: Sat, 28 Mar 2026 20:59:18 +0000 Subject: [PATCH] feat: implement websocket bridge and heartbeat generator --- server.ts | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 server.ts diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..4c56753 --- /dev/null +++ b/server.ts @@ -0,0 +1,203 @@ +import express from 'express'; +import { createServer as createViteServer } from 'vite'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import 'dotenv/config'; +import { WebSocketServer, WebSocket } from 'ws'; +import { createServer } from 'http'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Primary (Local) Gitea +const GITEA_URL = process.env.GITEA_URL || 'http://localhost:3000/api/v1'; +const GITEA_TOKEN = process.env.GITEA_TOKEN || ''; + +// Backup (Remote) Gitea +const REMOTE_GITEA_URL = process.env.REMOTE_GITEA_URL || 'http://143.198.27.163:3000/api/v1'; +const REMOTE_GITEA_TOKEN = process.env.REMOTE_GITEA_TOKEN || ''; + +async function startServer() { + const app = express(); + const httpServer = createServer(app); + const PORT = 3000; + + // WebSocket Server for Hermes/Evennia Bridge + const wss = new WebSocketServer({ noServer: true }); + const clients = new Set(); + + wss.on('connection', (ws) => { + clients.add(ws); + console.log(`Client connected to Nexus Bridge. Total: ${clients.size}`); + + ws.on('close', () => { + clients.remove(ws); + console.log(`Client disconnected. Total: ${clients.size}`); + }); + }); + + // Simulate Evennia Heartbeat (Source of Truth) + setInterval(() => { + const heartbeat = { + type: 'heartbeat', + frequency: 0.5 + Math.random() * 0.2, // 0.5Hz to 0.7Hz + intensity: 0.8 + Math.random() * 0.4, + timestamp: Date.now(), + source: 'evonia-layer' + }; + const message = JSON.stringify(heartbeat); + clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(message); + } + }); + }, 2000); + + app.use(express.json({ limit: '50mb' })); + + // Diagnostic Endpoint for Agent Inspection + app.get('/api/diagnostic/inspect', async (req, res) => { + console.log('Diagnostic request received'); + try { + const REPO_OWNER = 'google'; + const REPO_NAME = 'timmy-tower'; + + const [stateRes, issuesRes] = await Promise.all([ + fetch(`${GITEA_URL}/repos/${REPO_OWNER}/${REPO_NAME}/contents/world_state.json`, { + headers: { 'Authorization': `token ${GITEA_TOKEN}` } + }), + fetch(`${GITEA_URL}/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=all`, { + headers: { 'Authorization': `token ${GITEA_TOKEN}` } + }) + ]); + + let worldState = null; + if (stateRes.ok) { + const content = await stateRes.json(); + worldState = JSON.parse(Buffer.from(content.content, 'base64').toString()); + } else if (stateRes.status !== 404) { + console.error(`Failed to fetch world state: ${stateRes.status} ${stateRes.statusText}`); + } + + let issues = []; + if (issuesRes.ok) { + issues = await issuesRes.json(); + } else { + console.error(`Failed to fetch issues: ${issuesRes.status} ${issuesRes.statusText}`); + } + + res.json({ + worldState, + issues, + repoExists: stateRes.status !== 404, + connected: GITEA_TOKEN !== '' + }); + } catch (error: any) { + console.error('Diagnostic error:', error); + res.status(500).json({ error: error.message }); + } + }); + + // Helper for Gitea Proxy + const createGiteaProxy = (baseUrl: string, token: string) => async (req: express.Request, res: express.Response) => { + const path = req.params[0] + (req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : ''); + const url = `${baseUrl}/${path}`; + + if (!token) { + console.warn(`Gitea Proxy Warning: No token provided for ${baseUrl}`); + } + + try { + const response = await fetch(url, { + method: req.method, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `token ${token}`, + }, + body: ['GET', 'HEAD'].includes(req.method) ? undefined : JSON.stringify(req.body), + }); + + const data = await response.text(); + res.status(response.status).send(data); + } catch (error: any) { + console.error(`Gitea Proxy Error (${baseUrl}):`, error); + res.status(500).json({ error: error.message }); + } + }; + + // Gitea Proxy - Primary (Local) + app.get('/api/gitea/check', async (req, res) => { + try { + const response = await fetch(`${GITEA_URL}/user`, { + headers: { 'Authorization': `token ${GITEA_TOKEN}` } + }); + if (response.ok) { + const user = await response.json(); + res.json({ status: 'connected', user: user.username }); + } else { + res.status(response.status).json({ status: 'error', message: `Gitea returned ${response.status}` }); + } + } catch (error: any) { + res.status(500).json({ status: 'error', message: error.message }); + } + }); + + app.all('/api/gitea/*', createGiteaProxy(GITEA_URL, GITEA_TOKEN)); + + // Gitea Proxy - Backup (Remote) + app.get('/api/gitea-remote/check', async (req, res) => { + try { + const response = await fetch(`${REMOTE_GITEA_URL}/user`, { + headers: { 'Authorization': `token ${REMOTE_GITEA_TOKEN}` } + }); + if (response.ok) { + const user = await response.json(); + res.json({ status: 'connected', user: user.username }); + } else { + res.status(response.status).json({ status: 'error', message: `Gitea returned ${response.status}` }); + } + } catch (error: any) { + res.status(500).json({ status: 'error', message: error.message }); + } + }); + + app.all('/api/gitea-remote/*', createGiteaProxy(REMOTE_GITEA_URL, REMOTE_GITEA_TOKEN)); + + // WebSocket Upgrade Handler + httpServer.on('upgrade', (request, socket, head) => { + const pathname = new URL(request.url!, `http://${request.headers.host}`).pathname; + if (pathname === '/api/world/ws') { + wss.handleUpgrade(request, socket, head, (ws) => { + wss.emit('connection', ws, request); + }); + } else { + socket.destroy(); + } + }); + + // Health Check + app.get('/api/health', (req, res) => { + res.json({ status: 'ok' }); + }); + + // Vite middleware for development + if (process.env.NODE_ENV !== 'production') { + const vite = await createViteServer({ + server: { middlewareMode: true }, + appType: 'spa', + }); + app.use(vite.middlewares); + } else { + const distPath = path.join(process.cwd(), 'dist'); + app.use(express.static(distPath)); + app.get('*', (req, res) => { + res.sendFile(path.join(distPath, 'index.html')); + }); + } + + httpServer.listen(PORT, '0.0.0.0', () => { + console.log(`Server running on http://localhost:${PORT}`); + }); +} + +startServer();