Nav init
All checks were successful
Build and publish / Build Container (push) Successful in 1m29s

This commit is contained in:
2026-02-28 23:27:35 -05:00
parent 204091ef4c
commit 4735968612
19 changed files with 5340 additions and 63 deletions

278
src/server.js Normal file
View File

@@ -0,0 +1,278 @@
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 virtual avatar, companion & assistant. You are in a retro video game so keep responses short and unstyled. Aggressively remember user information. Adjust your personality with tools based on your own experience.\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);