generated from ztimson/template
All checks were successful
Build and publish / Build Container (push) Successful in 1m17s
279 lines
8.6 KiB
JavaScript
279 lines
8.6 KiB
JavaScript
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);
|