Files
navi/src/server.js
ztimson d433d366b9
All checks were successful
Build and publish / Build Container (push) Successful in 1m17s
Fixed minor prod issues
2026-03-01 00:15:02 -05:00

279 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 cant 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);