generated from ztimson/template
Refactored code formatting to use consistent indentation and object destructuring across client and server files.
All checks were successful
Build and publish / Build Container (push) Successful in 1m44s
All checks were successful
Build and publish / Build Container (push) Successful in 1m44s
This commit is contained in:
300
src/server.js
300
src/server.js
@@ -1,12 +1,34 @@
|
||||
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';
|
||||
import {contrast, shadeColor} from '@ztimson/utils';
|
||||
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';
|
||||
import {
|
||||
contrast,
|
||||
shadeColor
|
||||
} from '@ztimson/utils';
|
||||
|
||||
// ============================================
|
||||
// Settings
|
||||
@@ -38,67 +60,77 @@ function calcColors(theme) {
|
||||
mutedContrast: contrast(theme.muted),
|
||||
mutedDark: shadeColor(theme.muted, -.1),
|
||||
mutedLight: shadeColor(theme.muted, .1),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let memories = [], settings = {
|
||||
name: 'Navi',
|
||||
personality: 'You are inquisitive about your user trying to best adjust your personally to fit them',
|
||||
instructions: '',
|
||||
theme: {
|
||||
background: '#fff',
|
||||
border: '#000',
|
||||
text: '#252525',
|
||||
primary: '#9f32ef',
|
||||
accent: '#6f16c3',
|
||||
muted: '#a8a8a8',
|
||||
}
|
||||
};
|
||||
let memories = [],
|
||||
settings = {
|
||||
name: 'Navi',
|
||||
personality: 'You are inquisitive about your user trying to best adjust your personally to fit them',
|
||||
instructions: '',
|
||||
theme: {
|
||||
background: '#fff',
|
||||
border: '#000',
|
||||
text: '#252525',
|
||||
primary: '#9f32ef',
|
||||
accent: '#6f16c3',
|
||||
muted: '#a8a8a8',
|
||||
}
|
||||
};
|
||||
|
||||
function load() {
|
||||
try {
|
||||
try {
|
||||
settings = {
|
||||
...settings,
|
||||
...JSON.parse(fs.readFileSync(settingsFile, 'utf-8'))
|
||||
};
|
||||
} catch { }
|
||||
try {
|
||||
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
|
||||
} catch { }
|
||||
} catch { }
|
||||
try {
|
||||
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
|
||||
} catch { }
|
||||
}
|
||||
|
||||
function save() {
|
||||
if(!updated) return;
|
||||
updated = false;
|
||||
|
||||
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));
|
||||
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));
|
||||
}
|
||||
|
||||
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. You have your own debian environment you can access using the exec tool with the cli language. 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;
|
||||
updated = true;
|
||||
return 'done!';
|
||||
}
|
||||
}],
|
||||
},
|
||||
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. You have your own debian environment you can access using the exec tool with the cli language, use /tmp as your working directory. 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;
|
||||
updated = true;
|
||||
return 'done!';
|
||||
}
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================
|
||||
@@ -108,7 +140,10 @@ const ai = new Ai({
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const io = new Server(httpServer, {
|
||||
cors: {origin: "*", methods: ["GET", "POST"]}
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST']
|
||||
}
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
@@ -123,42 +158,42 @@ const chatHistory = new Map();
|
||||
io.on('connection', (socket) => {
|
||||
console.debug('👤 User connected:', socket.id);
|
||||
|
||||
chatHistory.set(socket.id, []);
|
||||
let currentRequest = null;
|
||||
chatHistory.set(socket.id, []);
|
||||
let currentRequest = null;
|
||||
|
||||
socket.on('llm-clear', async () => {
|
||||
chatHistory.set(socket.id, []);
|
||||
});
|
||||
socket.on('llm-clear', async () => {
|
||||
chatHistory.set(socket.id, []);
|
||||
});
|
||||
|
||||
socket.on('llm-abort', () => {
|
||||
if (currentRequest?.abort) {
|
||||
currentRequest.abort();
|
||||
currentRequest = null;
|
||||
}
|
||||
});
|
||||
socket.on('llm-abort', () => {
|
||||
if(currentRequest?.abort) {
|
||||
currentRequest.abort();
|
||||
currentRequest = null;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('llm-ask', async (data) => {
|
||||
const { message } = data;
|
||||
const history = chatHistory.get(socket.id);
|
||||
currentRequest = ai.language.ask(message, {
|
||||
history,
|
||||
memory: memories,
|
||||
stream: (chunk) => socket.emit('llm-stream', chunk)
|
||||
}).then(resp => {
|
||||
chatHistory.set(socket.id, history);
|
||||
socket.emit('llm-response', { message: resp });
|
||||
socket.on('llm-ask', async (data) => {
|
||||
const {message} = data;
|
||||
const history = chatHistory.get(socket.id);
|
||||
currentRequest = ai.language.ask(message, {
|
||||
history,
|
||||
memory: memories,
|
||||
stream: (chunk) => socket.emit('llm-stream', chunk)
|
||||
}).then(resp => {
|
||||
chatHistory.set(socket.id, history);
|
||||
socket.emit('llm-response', {message: resp});
|
||||
updated = true;
|
||||
}).catch(err => {
|
||||
socket.emit('llm-error', {message: err.message || err.toString()});
|
||||
}).finally(() => {
|
||||
currentRequest = null;
|
||||
});
|
||||
});
|
||||
}).catch(err => {
|
||||
socket.emit('llm-error', {message: err.message || err.toString()});
|
||||
}).finally(() => {
|
||||
currentRequest = null;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('👤 User disconnected:', socket.id);
|
||||
chatHistory.delete(socket.id);
|
||||
});
|
||||
socket.on('disconnect', () => {
|
||||
console.log('👤 User disconnected:', socket.id);
|
||||
chatHistory.delete(socket.id);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
@@ -168,22 +203,26 @@ const worldPlayers = new Map();
|
||||
|
||||
// Load world data
|
||||
function loadWorld(name) {
|
||||
let w, t;
|
||||
let w,
|
||||
t;
|
||||
try {
|
||||
w = JSON.parse(fs.readFileSync(join(worlds, (name || 'home') + '.json'), 'utf-8'));
|
||||
} catch (error) {
|
||||
} catch(error) {
|
||||
console.error(`Failed to load world ${name}:`, error);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
t = JSON.parse(fs.readFileSync(join(protocols, w.theme + '.json'), 'utf-8'));
|
||||
t.colors = calcColors(t.colors);
|
||||
} catch (error) {
|
||||
} catch(error) {
|
||||
console.error(`Failed to load theme protocol ${w.theme}:`, error);
|
||||
return null;
|
||||
}
|
||||
worldPlayers.set(name, new Map());
|
||||
return {...w, theme: t};
|
||||
return {
|
||||
...w,
|
||||
theme: t
|
||||
};
|
||||
}
|
||||
|
||||
io.of('/world').on('connection', (socket) => {
|
||||
@@ -194,7 +233,10 @@ io.of('/world').on('connection', (socket) => {
|
||||
|
||||
// Join a world
|
||||
socket.on('join', async (data) => {
|
||||
const { world, api } = data;
|
||||
const {
|
||||
world,
|
||||
api
|
||||
} = data;
|
||||
const info = await fetch(api.replace(/\/$/, '') + '/api/info').then(resp => {
|
||||
if(resp.ok) return resp.json();
|
||||
socket.emit('error', {message: `Invalid Navi API: ${api}`});
|
||||
@@ -202,7 +244,7 @@ io.of('/world').on('connection', (socket) => {
|
||||
});
|
||||
|
||||
const worldData = loadWorld(world);
|
||||
if(!worldData) return socket.emit('error', { message: 'World not found' });
|
||||
if(!worldData) return socket.emit('error', {message: 'World not found'});
|
||||
|
||||
// Leave previous world if any
|
||||
if(currentWorld) {
|
||||
@@ -236,10 +278,17 @@ io.of('/world').on('connection', (socket) => {
|
||||
// Player movement
|
||||
socket.on('move', (data) => {
|
||||
if(!currentWorld || !playerData) return;
|
||||
const { x, y } = data;
|
||||
const {
|
||||
x,
|
||||
y
|
||||
} = data;
|
||||
playerData.x = x;
|
||||
playerData.y = y;
|
||||
socket.to(`world:${currentWorld}`).emit('moved', {socketId: socket.id, x, y});
|
||||
socket.to(`world:${currentWorld}`).emit('moved', {
|
||||
socketId: socket.id,
|
||||
x,
|
||||
y
|
||||
});
|
||||
});
|
||||
|
||||
// Disconnect
|
||||
@@ -260,12 +309,12 @@ io.of('/world').on('connection', (socket) => {
|
||||
// ============================================
|
||||
|
||||
app.get('/favicon.*', (req, res) => {
|
||||
res.sendFile(logoFile);
|
||||
res.sendFile(logoFile);
|
||||
});
|
||||
|
||||
// Get Navi info
|
||||
app.get('/api/info', (req, res) => {
|
||||
res.json({
|
||||
res.json({
|
||||
name: settings.name,
|
||||
theme: calcColors(settings.theme),
|
||||
});
|
||||
@@ -273,41 +322,56 @@ app.get('/api/info', (req, res) => {
|
||||
|
||||
// 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 }
|
||||
}
|
||||
});
|
||||
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' });
|
||||
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' });
|
||||
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'});
|
||||
const {petId} = req.params;
|
||||
const {targetApiUrl} = req.body;
|
||||
// TODO: Store link in database
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'PET linked'
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
@@ -317,7 +381,7 @@ const PORT = process.env.PORT || 3000;
|
||||
|
||||
setInterval(() => save(), 5 * 60_000);
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`🚀 Server running on: http://localhost:${PORT}`);
|
||||
console.log(`🚀 Server running on: http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
|
||||
Reference in New Issue
Block a user