import express from 'express'; import { createServer } from 'http'; import { Server } from 'socket.io'; import cors from 'cors'; import fs from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import {Ai, DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool} from '@ztimson/ai-utils'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const httpServer = createServer(app); let memories = [], settings = {}; const settingsFile = join(__dirname, '../navi', 'settings.json'); const memoriesFile = join(__dirname, '../navi', 'memories.json'); const logoFile = join(__dirname, '../navi', 'logo.png'); function load() { try { settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8')); } catch { } try { memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8')); } catch { } } function save() { const dir = dirname(settingsFile); if (!fs.existsSync(dir)) { fs.mkdir(dir, { recursive: true }, (err) => { if (err) throw err; // Fail loudly if dirs can’t be made πŸ’€ }); } fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2)); fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2)); } function shutdown() { save(); process.exit(0); } load(); const ai = new Ai({ llm: { models: { 'Ministral-3': {proto: 'openai', host: 'http://10.69.0.55:11728', token: 'ignore'}, }, system: `You are a NetNavi, personal assistant & companion. Keep responses short and unstyled. Use your remember tool liberally to store all facts. Adjust your personality with tools based on your interactions with the user.\n\nPersonality:\n${settings.personality || ''}\n\nUser Requests:\n${settings.instructions || ''}`, tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, { name: 'adjust_personality', description: 'Replace your current personality instructions', args: {instructions: {type: 'string', description: 'Bullet point list of how to behave'}}, fn: (args) => { settings.personality = args.instructions; save(); return 'done!'; } }], }, }); const io = new Server(httpServer, { cors: {origin: "*", methods: ["GET", "POST"]} }); app.use(cors()); app.use(express.json()); app.use(express.static('public')); // ============================================ // WORLD MANAGEMENT // ============================================ const worldPlayers = new Map(); const chatHistory = new Map(); // Load world data function loadWorld(worldId) { try { const worldPath = join(__dirname, '../worlds', worldId || '', 'world.json'); const world = JSON.parse(fs.readFileSync(worldPath, 'utf-8')); const themePath = join(__dirname, '../worlds', worldId || '', world.theme); const theme = JSON.parse(fs.readFileSync(themePath, 'utf-8')); worldPlayers.set(worldId, new Map()); return {world, theme}; } catch (error) { console.error(`Failed to load world ${worldId}:`, error); return null; } } // ============================================ // SOCKET.IO - WORLD CHANNELS // ============================================ io.on('connection', (socket) => { console.debug('πŸ”Œ Client connected:', socket.id); let currentWorld = null; let playerData = null; // Join a world socket.on('join-world', (data) => { const { worldId, playerInfo } = data; const worldData = loadWorld(worldId); if(!worldData) return socket.emit('error', { message: 'World not found' }); // Leave previous world if any if(currentWorld) { socket.leave(`world:${currentWorld}`); const players = worldPlayers.get(currentWorld); if(players) { players.delete(socket.id); socket.to(`world:${currentWorld}`).emit('player-left', {socketId: socket.id}); } } // Join new world currentWorld = worldId; playerData = { socketId: socket.id, name: playerInfo.name, apiUrl: playerInfo.apiUrl, x: worldData.world.pet.startX, y: worldData.world.pet.startY }; socket.join(`world:${worldId}`); const players = worldPlayers.get(worldId); players.set(socket.id, playerData); socket.emit('world-data', worldData); const currentPlayers = Array.from(players.values()); socket.emit('current-players', currentPlayers); socket.to(`world:${worldId}`).emit('player-joined', playerData); }); // Player movement socket.on('player-move', (data) => { if(!currentWorld || !playerData) return; const { x, y } = data; playerData.x = x; playerData.y = y; socket.to(`world:${currentWorld}`).emit('player-moved', {socketId: socket.id, x, y}); }); // Disconnect socket.on('disconnect', () => { console.debug('πŸ”Œ Client disconnected:', socket.id); if(currentWorld) { const players = worldPlayers.get(currentWorld); if(players) { players.delete(socket.id); socket.to(`world:${currentWorld}`).emit('player-left', {socketId: socket.id}); } } }); }); // ============================================ // LLM CHANNEL // ============================================ const petNamespace = io.of('/llm'); petNamespace.on('connection', (socket) => { chatHistory.set(socket.id, []); let currentRequest = null; socket.on('clear', async () => { chatHistory.set(socket.id, []); }); socket.on('abort', () => { if (currentRequest?.abort) { currentRequest.abort(); currentRequest = null; } }); socket.on('message', async (data) => { const { message, apiUrl } = data; const history = chatHistory.get(socket.id); currentRequest = ai.language.ask(message, { history, memory: memories, stream: (chunk) => socket.emit('stream', chunk) }).then(resp => { chatHistory.set(socket.id, history); socket.emit('response', { message: resp }); }).catch(err => { socket.emit('error', {message: err.message || err.toString()}); }).finally(() => { currentRequest = null; }); }); socket.on('disconnect', () => { console.log('πŸ”Œ LLM Client disconnected:', socket.id); chatHistory.delete(socket.id); }); }); // ============================================ // REST API ENDPOINTS // ============================================ app.get('/favicon.*', (req, res) => { res.sendFile(logoFile); }); // Get PET info app.get('/api/info', (req, res) => { const { petId } = req.params; // TODO: Fetch from database res.json({ id: petId, name: 'MyCoolPET', owner: 'player1', bandwidth: 75, shards: [], stats: { level: 5, health: 100 } }); }); // Get sprite sheet app.get('/api/sprite', (req, res) => { const { petId } = req.params; // TODO: Return actual sprite sheet URL res.json({ spriteUrl: '/sprites/default-pet.png', frameWidth: 32, frameHeight: 32, animations: { idle: { frames: [0, 1, 2, 3], speed: 200 }, walk: { frames: [4, 5, 6, 7], speed: 100 } } }); }); // Send message to user (push notification / email / etc) app.post('/api/message', (req, res) => { const { userId } = req.params; const { message } = req.body; // TODO: Implement notification system res.json({ success: true, message: 'Message sent' }); }); // Send message to PET LLM app.post('/api/message', (req, res) => { const { petId } = req.params; const { message } = req.body; // TODO: Queue message for LLM processing res.json({ success: true, message: 'Message queued' }); }); // Link another PET app.post('/api/link', (req, res) => { const { petId } = req.params; const { targetApiUrl } = req.body; // TODO: Store link in database res.json({success: true, message: 'PET linked'}); }); // ============================================ // START SERVER // ============================================ const PORT = process.env.PORT || 3000; httpServer.listen(PORT, () => { loadWorld(); console.log('βœ… Home world loaded'); console.log(`πŸš€ Server running on: http://localhost:${PORT}`); }); process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown);