5b28002001
Major changes from this session: Training: - 0.6.0 training running: 9B on steel141 3090 Ti, 27B on rented H100 NVL - 7,256 merged training examples (up from 3,183) - New training data: failure modes (85), midloop messaging (27), prompt injection defense (29), personality (32), gold from quarantine bank (232), new tool examples (30), claude's own experience (10) - All training data RCON-validated at 100% pass rate - Bake-off: gemma3:27b 66%, qwen3.5:27b 61%, translategemma:27b 56% Oracle Bot (Mind's Eye): - Invisible spectator bot (mineflayer) streams world state via WebSocket - HTML5 Canvas frontend at mind.mortdec.ai - Real-time tool trace visualization with expandable entries - Streaming model tokens during inference - Gateway integration: fire-and-forget POST /trace on every tool call Reinforcement Learning: - Gymnasium environment wrapping mineflayer bot (minecraft_env.py) - PPO training via Stable Baselines3 (10K param policy network) - Behavioral cloning pretraining (97.5% accuracy on expert policy) - Infinite training loop with auto-restart and checkpoint resume - Bot learns combat, survival, navigation from raw experience Bot Army: - 8-soldier marching formation with autonomous combat - Combat bots using mineflayer-pvp, pathfinder, armor-manager - Multilingual prayer bots via translategemma:27b (18 languages) - Frame-based AI architecture: LLM planner + reactive micro-scripts Infrastructure: - Fixed mattpc.sethpc.xyz billing gateway (API key + player list parser) - Billing gateway now tracks all LAN traffic (LAN auto-auth) - Gateway fallback for empty god-mode responses - Updated mortdec.ai landing page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
866 lines
25 KiB
Markdown
866 lines
25 KiB
Markdown
# Oracle Bot Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Build a live HTML5 viewport ("Mind's Eye") that renders what the Mortdecai AI sees, powered by an invisible spectator bot streaming world state and tool traces to browsers.
|
|
|
|
**Architecture:** Single Node.js process (mineflayer + Express + ws) connects to Paper server in spectator mode, receives tool traces from the gateway via POST, and streams world data + traces to browsers via WebSocket. Frontend is a single HTML5 Canvas page.
|
|
|
|
**Tech Stack:** Node.js, mineflayer, express, ws, HTML5 Canvas, vanilla JS
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-03-22-oracle-bot-design.md`
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
```
|
|
oracle-bot/ # NEW directory at repo root
|
|
├── package.json # deps: mineflayer, express, ws
|
|
├── server.js # ENTRYPOINT — Express + ws + requires bot
|
|
├── bot.js # mineflayer spectator connection + chunk tracking
|
|
├── world-state.js # Abstracted world state: blocks, entities, players
|
|
├── public/
|
|
│ └── index.html # Single-file frontend (Canvas + JS + CSS)
|
|
└── oracle-bot.service # systemd unit file for deployment
|
|
```
|
|
|
|
**Modified files:**
|
|
- `langgraph_gateway.py` (in PaperFork repo) — add fire-and-forget POST to `/trace` after tool calls
|
|
|
|
---
|
|
|
|
### Task 1: Project Scaffold + Bot Connection
|
|
|
|
**Files:**
|
|
- Create: `oracle-bot/package.json`
|
|
- Create: `oracle-bot/bot.js`
|
|
|
|
- [ ] **Step 1: Create package.json**
|
|
|
|
```json
|
|
{
|
|
"name": "oracle-bot",
|
|
"version": "0.1.0",
|
|
"description": "Mortdecai Mind's Eye — live AI vision viewport",
|
|
"main": "server.js",
|
|
"scripts": {
|
|
"start": "node server.js",
|
|
"dev": "node server.js --dev"
|
|
},
|
|
"dependencies": {
|
|
"mineflayer": "^4.23.0",
|
|
"express": "^4.21.0",
|
|
"ws": "^8.18.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Install dependencies**
|
|
|
|
Run: `cd oracle-bot && npm install`
|
|
Expected: `node_modules/` created, no errors
|
|
|
|
- [ ] **Step 3: Write bot.js — mineflayer spectator connection**
|
|
|
|
```javascript
|
|
/**
|
|
* bot.js — Mineflayer spectator bot for Oracle vision system.
|
|
*
|
|
* Connects to Paper server in offline mode. Expects server to set
|
|
* spectator gamemode on join. Tracks chunk data, entities, players.
|
|
* Emits events for world-state.js to consume.
|
|
*/
|
|
|
|
const mineflayer = require('mineflayer');
|
|
const EventEmitter = require('events');
|
|
|
|
const DEFAULT_CONFIG = {
|
|
host: process.env.MC_HOST || '192.168.0.244',
|
|
port: parseInt(process.env.MC_PORT || '25568', 10),
|
|
username: process.env.BOT_USERNAME || 'OracleBot',
|
|
version: '1.21.11',
|
|
};
|
|
|
|
class OracleBot extends EventEmitter {
|
|
constructor(config = {}) {
|
|
super();
|
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
this.bot = null;
|
|
this.connected = false;
|
|
this.following = null; // player name we're following
|
|
this._reconnectDelay = 1000;
|
|
this._maxReconnectDelay = 30000;
|
|
}
|
|
|
|
connect() {
|
|
console.log(`[bot] Connecting to ${this.config.host}:${this.config.port} as ${this.config.username}...`);
|
|
|
|
this.bot = mineflayer.createBot({
|
|
host: this.config.host,
|
|
port: this.config.port,
|
|
username: this.config.username,
|
|
auth: 'offline',
|
|
version: this.config.version,
|
|
});
|
|
|
|
this.bot.on('login', () => {
|
|
console.log(`[bot] Logged in as ${this.bot.username}`);
|
|
this.connected = true;
|
|
this._reconnectDelay = 1000; // reset backoff
|
|
this.emit('connected');
|
|
});
|
|
|
|
this.bot.on('spawn', () => {
|
|
const pos = this.bot.entity.position;
|
|
console.log(`[bot] Spawned at (${pos.x.toFixed(0)}, ${pos.y.toFixed(0)}, ${pos.z.toFixed(0)}) mode=${this.bot.game.gameMode}`);
|
|
|
|
// Request spectator if not already
|
|
if (this.bot.game.gameMode !== 'spectator') {
|
|
console.log('[bot] Not in spectator mode, requesting...');
|
|
this.bot.chat('/gamemode spectator');
|
|
}
|
|
|
|
this.emit('spawned');
|
|
});
|
|
|
|
this.bot.on('kicked', (reason) => {
|
|
console.log(`[bot] Kicked: ${reason}`);
|
|
this.connected = false;
|
|
this.emit('disconnected', reason);
|
|
this._scheduleReconnect();
|
|
});
|
|
|
|
this.bot.on('end', (reason) => {
|
|
console.log(`[bot] Disconnected: ${reason}`);
|
|
this.connected = false;
|
|
this.emit('disconnected', reason);
|
|
this._scheduleReconnect();
|
|
});
|
|
|
|
this.bot.on('error', (err) => {
|
|
console.error(`[bot] Error: ${err.message}`);
|
|
});
|
|
|
|
// Track player joins/leaves
|
|
this.bot.on('playerJoined', (player) => {
|
|
if (player.username !== this.config.username) {
|
|
this.emit('playerJoined', player.username);
|
|
}
|
|
});
|
|
|
|
this.bot.on('playerLeft', (player) => {
|
|
if (player.username !== this.config.username) {
|
|
this.emit('playerLeft', player.username);
|
|
}
|
|
});
|
|
}
|
|
|
|
_scheduleReconnect() {
|
|
console.log(`[bot] Reconnecting in ${this._reconnectDelay}ms...`);
|
|
setTimeout(() => {
|
|
this._reconnectDelay = Math.min(this._reconnectDelay * 2, this._maxReconnectDelay);
|
|
this.connect();
|
|
}, this._reconnectDelay);
|
|
}
|
|
|
|
/** Teleport to a player and follow them. */
|
|
async followPlayer(playerName) {
|
|
if (!this.connected || !this.bot) return;
|
|
const player = this.bot.players[playerName];
|
|
if (!player || !player.entity) {
|
|
console.log(`[bot] Cannot find player entity for ${playerName}`);
|
|
return;
|
|
}
|
|
this.following = playerName;
|
|
const pos = player.entity.position;
|
|
this.bot.entity.position.set(pos.x, pos.y + 2, pos.z);
|
|
console.log(`[bot] Following ${playerName} at (${pos.x.toFixed(0)}, ${pos.y.toFixed(0)}, ${pos.z.toFixed(0)})`);
|
|
}
|
|
|
|
/** Get online players (excluding self). */
|
|
getPlayers() {
|
|
if (!this.bot) return [];
|
|
return Object.values(this.bot.players)
|
|
.filter(p => p.username !== this.config.username)
|
|
.map(p => ({
|
|
name: p.username,
|
|
x: p.entity ? Math.floor(p.entity.position.x) : null,
|
|
y: p.entity ? Math.floor(p.entity.position.y) : null,
|
|
z: p.entity ? Math.floor(p.entity.position.z) : null,
|
|
}));
|
|
}
|
|
|
|
/** Scan blocks in a top-down slice around center at given Y level. */
|
|
scanArea(centerX, centerY, centerZ, radius = 16) {
|
|
if (!this.bot) return [];
|
|
const blocks = [];
|
|
for (let dx = -radius; dx <= radius; dx++) {
|
|
for (let dz = -radius; dz <= radius; dz++) {
|
|
const block = this.bot.blockAt(this.bot.vec3(centerX + dx, centerY, centerZ + dz));
|
|
if (block && block.name !== 'air') {
|
|
blocks.push({
|
|
x: centerX + dx,
|
|
y: centerY,
|
|
z: centerZ + dz,
|
|
type: block.name,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return blocks;
|
|
}
|
|
|
|
/** Get nearby entities around a position. */
|
|
getNearbyEntities(centerX, centerY, centerZ, radius = 30) {
|
|
if (!this.bot) return [];
|
|
const entities = [];
|
|
for (const entity of Object.values(this.bot.entities)) {
|
|
if (entity === this.bot.entity) continue;
|
|
const dist = entity.position.distanceTo(this.bot.vec3(centerX, centerY, centerZ));
|
|
if (dist <= radius) {
|
|
entities.push({
|
|
type: entity.type === 'player' ? 'player' : (entity.name || entity.type),
|
|
name: entity.username || null,
|
|
x: Math.floor(entity.position.x),
|
|
y: Math.floor(entity.position.y),
|
|
z: Math.floor(entity.position.z),
|
|
distance: Math.round(dist),
|
|
});
|
|
}
|
|
}
|
|
return entities;
|
|
}
|
|
|
|
/** Get server time and weather info. */
|
|
getWorldInfo() {
|
|
if (!this.bot) return {};
|
|
return {
|
|
time: this.bot.time?.timeOfDay || 0,
|
|
isRaining: this.bot.isRaining || false,
|
|
gameMode: this.bot.game?.gameMode || 'unknown',
|
|
};
|
|
}
|
|
|
|
destroy() {
|
|
if (this.bot) {
|
|
this.bot.removeAllListeners();
|
|
this.bot.end();
|
|
this.bot = null;
|
|
}
|
|
this.connected = false;
|
|
}
|
|
}
|
|
|
|
module.exports = OracleBot;
|
|
```
|
|
|
|
- [ ] **Step 4: Test bot connection manually**
|
|
|
|
Run: `cd oracle-bot && node -e "const B = require('./bot'); const b = new B(); b.connect(); b.on('spawned', () => { console.log('Players:', b.getPlayers()); setTimeout(() => { b.destroy(); process.exit(0); }, 3000); });"`
|
|
|
|
Expected: Bot connects, logs spawned position, lists online players, exits.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add oracle-bot/package.json oracle-bot/bot.js oracle-bot/package-lock.json
|
|
git commit -m "feat(oracle): scaffold project + mineflayer spectator bot"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: World State Abstraction
|
|
|
|
**Files:**
|
|
- Create: `oracle-bot/world-state.js`
|
|
|
|
- [ ] **Step 1: Write world-state.js**
|
|
|
|
```javascript
|
|
/**
|
|
* world-state.js — Unified world state for the Oracle vision system.
|
|
*
|
|
* Aggregates bot perception (chunks, entities) and gateway traces
|
|
* into a single state object that can be serialized and streamed.
|
|
*/
|
|
|
|
class WorldState {
|
|
constructor(bot) {
|
|
this.bot = bot;
|
|
this.mode = 'idle'; // 'idle' | 'god' | 'sudo'
|
|
this.activePlayer = null; // player the AI is currently interacting with
|
|
this.activeSessionId = null;
|
|
this.traces = []; // recent tool traces (ring buffer, max 50)
|
|
this.lastActivity = 0; // timestamp of last trace
|
|
this.ACTIVE_TIMEOUT = 10000; // ms before reverting to idle
|
|
this.MAX_TRACES = 50;
|
|
}
|
|
|
|
/** Called when gateway sends a trace event. */
|
|
onTrace(traceData) {
|
|
this.lastActivity = Date.now();
|
|
|
|
// Switch to active mode
|
|
if (traceData.mode && traceData.mode !== this.mode) {
|
|
this.mode = traceData.mode;
|
|
}
|
|
if (traceData.player) {
|
|
this.activePlayer = traceData.player;
|
|
}
|
|
if (traceData.session_id) {
|
|
this.activeSessionId = traceData.session_id;
|
|
}
|
|
|
|
// Store trace
|
|
this.traces.push({
|
|
...traceData,
|
|
ts: Date.now(),
|
|
});
|
|
if (this.traces.length > this.MAX_TRACES) {
|
|
this.traces.shift();
|
|
}
|
|
}
|
|
|
|
/** Check if we should revert to idle. */
|
|
checkIdle() {
|
|
if (this.mode !== 'idle' && Date.now() - this.lastActivity > this.ACTIVE_TIMEOUT) {
|
|
this.mode = 'idle';
|
|
this.activeSessionId = null;
|
|
return true; // changed to idle
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Build a heartbeat message (idle mode, low frequency). */
|
|
buildHeartbeat() {
|
|
return {
|
|
v: 1,
|
|
type: 'heartbeat',
|
|
mode: this.mode,
|
|
activePlayer: this.activePlayer,
|
|
players: this.bot.getPlayers(),
|
|
world: this.bot.getWorldInfo(),
|
|
ts: Date.now(),
|
|
};
|
|
}
|
|
|
|
/** Build a world snapshot message (active mode, high frequency). */
|
|
buildWorldSnapshot() {
|
|
if (!this.activePlayer) return null;
|
|
|
|
const players = this.bot.getPlayers();
|
|
const target = players.find(p => p.name === this.activePlayer);
|
|
if (!target || target.x === null) return null;
|
|
|
|
const blocks = this.bot.scanArea(target.x, target.y, target.z, 16);
|
|
const entities = this.bot.getNearbyEntities(target.x, target.y, target.z, 30);
|
|
|
|
return {
|
|
v: 1,
|
|
type: 'world',
|
|
mode: this.mode,
|
|
activePlayer: this.activePlayer,
|
|
center: { x: target.x, y: target.y, z: target.z },
|
|
blocks,
|
|
entities,
|
|
players,
|
|
ts: Date.now(),
|
|
};
|
|
}
|
|
|
|
/** Build a trace event message for the browser. */
|
|
buildTraceEvent(traceData) {
|
|
return {
|
|
v: 1,
|
|
type: 'trace',
|
|
...traceData,
|
|
ts: Date.now(),
|
|
};
|
|
}
|
|
|
|
/** Build a mode change message. */
|
|
buildModeChange() {
|
|
return {
|
|
v: 1,
|
|
type: 'mode',
|
|
mode: this.mode,
|
|
player: this.activePlayer,
|
|
ts: Date.now(),
|
|
};
|
|
}
|
|
|
|
/** Build a status message (connected/disconnected). */
|
|
buildStatus(connected) {
|
|
return {
|
|
v: 1,
|
|
type: 'status',
|
|
connected,
|
|
ts: Date.now(),
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = WorldState;
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add oracle-bot/world-state.js
|
|
git commit -m "feat(oracle): world state abstraction layer"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Express + WebSocket Server
|
|
|
|
**Files:**
|
|
- Create: `oracle-bot/server.js`
|
|
|
|
- [ ] **Step 1: Write server.js**
|
|
|
|
```javascript
|
|
/**
|
|
* server.js — Oracle Bot entrypoint.
|
|
*
|
|
* Express HTTP server + WebSocket for browser streaming.
|
|
* Receives tool traces from gateway, merges with bot world data,
|
|
* streams to connected browsers.
|
|
*/
|
|
|
|
const express = require('express');
|
|
const http = require('http');
|
|
const WebSocket = require('ws');
|
|
const path = require('path');
|
|
const OracleBot = require('./bot');
|
|
const WorldState = require('./world-state');
|
|
|
|
const PORT = parseInt(process.env.PORT || '3333', 10);
|
|
const MAX_WS_CONNECTIONS = 100;
|
|
const MAX_PER_IP = 5;
|
|
const HEARTBEAT_INTERVAL = 5000; // 5s idle heartbeat
|
|
const ACTIVE_INTERVAL = 1000; // 1s active world snapshots
|
|
|
|
// ── Setup ────────────────────────────────────────────────────
|
|
|
|
const app = express();
|
|
app.use(express.json({ limit: '64kb' }));
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
const server = http.createServer(app);
|
|
const wss = new WebSocket.Server({ server, path: '/ws' });
|
|
|
|
const bot = new OracleBot();
|
|
const state = new WorldState(bot);
|
|
|
|
// Track connections per IP
|
|
const ipCounts = new Map();
|
|
|
|
// ── WebSocket ────────────────────────────────────────────────
|
|
|
|
wss.on('connection', (ws, req) => {
|
|
const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress;
|
|
|
|
// Rate limit
|
|
const count = ipCounts.get(ip) || 0;
|
|
if (wss.clients.size >= MAX_WS_CONNECTIONS || count >= MAX_PER_IP) {
|
|
ws.close(4029, 'Too many connections');
|
|
return;
|
|
}
|
|
ipCounts.set(ip, count + 1);
|
|
console.log(`[ws] Client connected from ${ip} (${wss.clients.size} total)`);
|
|
|
|
// Send current state on connect
|
|
ws.send(JSON.stringify(state.buildHeartbeat()));
|
|
if (state.mode !== 'idle') {
|
|
ws.send(JSON.stringify(state.buildModeChange()));
|
|
}
|
|
|
|
ws.on('close', () => {
|
|
const c = ipCounts.get(ip) || 1;
|
|
if (c <= 1) ipCounts.delete(ip);
|
|
else ipCounts.set(ip, c - 1);
|
|
console.log(`[ws] Client disconnected (${wss.clients.size} total)`);
|
|
});
|
|
});
|
|
|
|
function broadcast(msg) {
|
|
const data = JSON.stringify(msg);
|
|
for (const client of wss.clients) {
|
|
if (client.readyState === WebSocket.OPEN) {
|
|
client.send(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── HTTP Endpoints (internal only) ───────────────────────────
|
|
|
|
app.post('/trace', (req, res) => {
|
|
const trace = req.body;
|
|
if (!trace || !trace.tool) {
|
|
return res.status(400).json({ error: 'Missing tool field' });
|
|
}
|
|
|
|
// Update world state
|
|
const previousMode = state.mode;
|
|
state.onTrace(trace);
|
|
|
|
// If mode changed, broadcast mode change
|
|
if (state.mode !== previousMode) {
|
|
broadcast(state.buildModeChange());
|
|
// Follow the active player
|
|
if (trace.player) {
|
|
bot.followPlayer(trace.player);
|
|
}
|
|
}
|
|
|
|
// Broadcast trace event to browsers
|
|
broadcast(state.buildTraceEvent(trace));
|
|
|
|
// Send a world snapshot (active mode burst)
|
|
const snapshot = state.buildWorldSnapshot();
|
|
if (snapshot) {
|
|
broadcast(snapshot);
|
|
}
|
|
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
app.post('/command', (req, res) => {
|
|
const { action, target, center, radius } = req.body || {};
|
|
|
|
if (action === 'follow' && target) {
|
|
bot.followPlayer(target);
|
|
return res.json({ ok: true, action: 'follow', target });
|
|
}
|
|
|
|
if (action === 'scan' && center) {
|
|
const blocks = bot.scanArea(center.x, center.y, center.z, radius || 16);
|
|
return res.json({ ok: true, blocks: blocks.length });
|
|
}
|
|
|
|
res.status(400).json({ error: 'Unknown action' });
|
|
});
|
|
|
|
app.get('/health', (req, res) => {
|
|
res.json({
|
|
ok: true,
|
|
botConnected: bot.connected,
|
|
wsClients: wss.clients.size,
|
|
mode: state.mode,
|
|
activePlayer: state.activePlayer,
|
|
});
|
|
});
|
|
|
|
// ── Periodic Updates ─────────────────────────────────────────
|
|
|
|
setInterval(() => {
|
|
if (wss.clients.size === 0) return;
|
|
|
|
// Check idle transition
|
|
const wentIdle = state.checkIdle();
|
|
if (wentIdle) {
|
|
broadcast(state.buildModeChange());
|
|
}
|
|
|
|
if (state.mode === 'idle') {
|
|
// Low-frequency heartbeat
|
|
broadcast(state.buildHeartbeat());
|
|
}
|
|
}, HEARTBEAT_INTERVAL);
|
|
|
|
// Active mode: higher frequency world snapshots
|
|
setInterval(() => {
|
|
if (wss.clients.size === 0 || state.mode === 'idle') return;
|
|
|
|
const snapshot = state.buildWorldSnapshot();
|
|
if (snapshot) {
|
|
broadcast(snapshot);
|
|
}
|
|
}, ACTIVE_INTERVAL);
|
|
|
|
// ── Bot Events ───────────────────────────────────────────────
|
|
|
|
bot.on('connected', () => {
|
|
console.log('[server] Bot connected to MC server');
|
|
broadcast(state.buildStatus(true));
|
|
});
|
|
|
|
bot.on('disconnected', (reason) => {
|
|
console.log(`[server] Bot disconnected: ${reason}`);
|
|
broadcast(state.buildStatus(false));
|
|
});
|
|
|
|
bot.on('playerJoined', (name) => {
|
|
console.log(`[server] Player joined: ${name}`);
|
|
broadcast(state.buildHeartbeat());
|
|
});
|
|
|
|
bot.on('playerLeft', (name) => {
|
|
console.log(`[server] Player left: ${name}`);
|
|
broadcast(state.buildHeartbeat());
|
|
});
|
|
|
|
// ── Start ────────────────────────────────────────────────────
|
|
|
|
bot.connect();
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`[server] Oracle Bot listening on port ${PORT}`);
|
|
console.log(`[server] Frontend: http://localhost:${PORT}`);
|
|
console.log(`[server] WebSocket: ws://localhost:${PORT}/ws`);
|
|
console.log(`[server] Health: http://localhost:${PORT}/health`);
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Test server starts and health endpoint works**
|
|
|
|
Run: `cd oracle-bot && timeout 15 node server.js &; sleep 5; curl -s http://localhost:3333/health; kill %1`
|
|
Expected: `{"ok":true,"botConnected":true,"wsClients":0,"mode":"idle","activePlayer":null}`
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add oracle-bot/server.js
|
|
git commit -m "feat(oracle): express + websocket server with trace/command endpoints"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: HTML5 Canvas Frontend
|
|
|
|
**Files:**
|
|
- Create: `oracle-bot/public/index.html`
|
|
|
|
- [ ] **Step 1: Write index.html — complete single-file frontend**
|
|
|
|
This is the largest file. Single HTML file with embedded CSS and JS. Key sections:
|
|
- CSS: dark theme, Rajdhani Bold font, Sethian orange accents, three-panel layout
|
|
- Canvas: 2D top-down tile map, block color mapping, entity rendering
|
|
- Tool trace panel: scrolling timeline with tool call entries
|
|
- Status bar: mode indicator, player info, connection status
|
|
- WebSocket: auto-connect, reconnect on close, handle all message types
|
|
- Visual modes: idle (muted pulse), god (orange/gold), sudo (blue/green)
|
|
|
|
The file will be ~400-500 lines. Key implementation details:
|
|
|
|
**Block color map:**
|
|
```javascript
|
|
const BLOCK_COLORS = {
|
|
stone: '#808080', cobblestone: '#6B6B6B', dirt: '#8B6914',
|
|
grass_block: '#4CAF50', sand: '#F4E4A0', gravel: '#A0A0A0',
|
|
oak_planks: '#BC8F4F', oak_log: '#6B4226', spruce_planks: '#5C3A1E',
|
|
water: '#2196F3', lava: '#FF5722', redstone_wire: '#FF0000',
|
|
diamond_ore: '#4FC3F7', iron_ore: '#D4A574', gold_ore: '#FFD54F',
|
|
bedrock: '#1A1A1A', obsidian: '#1A0A2E', glass: '#E0F7FA',
|
|
torch: '#FFEB3B',
|
|
// default for unknown blocks
|
|
_default: '#9E9E9E',
|
|
};
|
|
```
|
|
|
|
**Canvas rendering loop:**
|
|
- `requestAnimationFrame` for smooth animations
|
|
- World tiles drawn from `blocks` array, centered on active player
|
|
- Entities drawn as colored dots with labels
|
|
- Active scan areas pulse with translucent overlay
|
|
- Tool traces appear as floating text annotations
|
|
|
|
**WebSocket handler:**
|
|
```javascript
|
|
const ws = new WebSocket(`ws://${location.host}/ws`);
|
|
ws.onmessage = (e) => {
|
|
const msg = JSON.parse(e.data);
|
|
switch (msg.type) {
|
|
case 'heartbeat': updateHeartbeat(msg); break;
|
|
case 'world': updateWorld(msg); break;
|
|
case 'trace': addTrace(msg); break;
|
|
case 'mode': updateMode(msg); break;
|
|
case 'status': updateStatus(msg); break;
|
|
}
|
|
};
|
|
```
|
|
|
|
- [ ] **Step 2: Test frontend loads in browser**
|
|
|
|
Run: `cd oracle-bot && node server.js &; sleep 3; echo "Open http://localhost:3333 in browser"; curl -s http://localhost:3333/ | head -5`
|
|
Expected: HTML page served, title contains "MORTDECAI"
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add oracle-bot/public/index.html
|
|
git commit -m "feat(oracle): HTML5 Canvas frontend — Mind's Eye viewport"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Gateway Integration
|
|
|
|
**Files:**
|
|
- Modify: `/root/bin/Sethpc-Minecraft-PaperFork/langgraph_gateway.py` (tool loop section, ~line 1318)
|
|
|
|
- [ ] **Step 1: Add oracle trace posting to gateway**
|
|
|
|
Add a helper function near the top of the file (after imports):
|
|
|
|
```python
|
|
# ── Oracle Bot integration ─────────────────────────────────────────
|
|
ORACLE_URL = 'http://localhost:3333/trace'
|
|
|
|
def _oracle_trace(tool_name, tool_args, result, step, session):
|
|
"""Fire-and-forget trace event to Oracle Bot."""
|
|
try:
|
|
requests.post(ORACLE_URL, json={
|
|
'tool': tool_name,
|
|
'input': {k: str(v)[:200] for k, v in (tool_args or {}).items()},
|
|
'ok': result.get('ok', result.get('success', False)),
|
|
'step': step,
|
|
'mode': session.mode,
|
|
'player': session.player,
|
|
'session_id': session.session_id,
|
|
}, timeout=1)
|
|
except Exception:
|
|
pass # Oracle is optional, never block the gateway
|
|
```
|
|
|
|
Then in `_model_driven_tool_loop`, after the tool result is captured (after line ~1338 `tool_trace.append(trace_entry)`), add:
|
|
|
|
```python
|
|
_oracle_trace(tool_name, tool_args, result, step, session)
|
|
```
|
|
|
|
Also post on session start/end — in `run_pipeline()` after `_model_driven_tool_loop` returns (after the tool loop result), add:
|
|
|
|
```python
|
|
# Notify Oracle of session end
|
|
_oracle_trace('_session_end', {'commands': commands, 'message': message}, {'ok': True}, -1, session)
|
|
```
|
|
|
|
- [ ] **Step 2: Verify gateway still starts cleanly**
|
|
|
|
Run: `python3 -c "import ast; ast.parse(open('/root/bin/Sethpc-Minecraft-PaperFork/langgraph_gateway.py').read()); print('OK')"`
|
|
Expected: `OK`
|
|
|
|
- [ ] **Step 3: Commit in PaperFork repo**
|
|
|
|
```bash
|
|
cd /root/bin/Sethpc-Minecraft-PaperFork
|
|
git add langgraph_gateway.py
|
|
git commit -m "feat: add Oracle Bot trace integration (fire-and-forget)"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Deployment — systemd + Caddy
|
|
|
|
**Files:**
|
|
- Create: `oracle-bot/oracle-bot.service`
|
|
|
|
- [ ] **Step 1: Write systemd service file**
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=Oracle Bot — Mortdecai Mind's Eye
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
WorkingDirectory=/opt/oracle-bot
|
|
ExecStart=/usr/bin/node server.js
|
|
Restart=always
|
|
RestartSec=5
|
|
Environment=PORT=3333
|
|
Environment=MC_HOST=192.168.0.244
|
|
Environment=MC_PORT=25568
|
|
StandardOutput=append:/var/log/oracle-bot.log
|
|
StandardError=append:/var/log/oracle-bot.log
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
- [ ] **Step 2: Write deploy instructions in README**
|
|
|
|
Create `oracle-bot/README.md` with:
|
|
- How to deploy to CT 644
|
|
- Caddy config for mind.mortdec.ai
|
|
- How to test locally
|
|
- How to check health
|
|
|
|
- [ ] **Step 3: Add Caddy config snippet**
|
|
|
|
For CT 600 Caddyfile:
|
|
```
|
|
mind.mortdec.ai {
|
|
@internal path /trace /command
|
|
respond @internal 404
|
|
|
|
reverse_proxy 192.168.0.244:3333
|
|
}
|
|
```
|
|
|
|
Note: WebSocket upgrade is handled automatically by Caddy's reverse_proxy.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add oracle-bot/oracle-bot.service oracle-bot/README.md
|
|
git commit -m "feat(oracle): systemd service + Caddy config + deploy docs"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Integration Test — End to End
|
|
|
|
- [ ] **Step 1: Start the bot locally**
|
|
|
|
Run: `cd oracle-bot && node server.js`
|
|
Expected: Bot connects, logs spawn position, server listening on 3333
|
|
|
|
- [ ] **Step 2: Open frontend in browser**
|
|
|
|
Navigate to `http://localhost:3333`
|
|
Expected: Dark themed page, "MORTDECAI — MIND'S EYE" title, idle mode, player dots visible
|
|
|
|
- [ ] **Step 3: Simulate a trace event**
|
|
|
|
```bash
|
|
curl -X POST http://localhost:3333/trace \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"tool":"rcon.execute","input":{"command":"give slingshooter08 minecraft:diamond 16"},"ok":true,"step":0,"mode":"god","player":"slingshooter08","session_id":"test-001"}'
|
|
```
|
|
|
|
Expected: Frontend switches to god mode (orange), trace appears in sidebar, map centers on player
|
|
|
|
- [ ] **Step 4: Simulate multiple traces (tool chain)**
|
|
|
|
```bash
|
|
for tool in "world.player_info" "world.nearby_entities" "rcon.execute" "journal.write"; do
|
|
curl -s -X POST http://localhost:3333/trace \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"tool\":\"$tool\",\"input\":{},\"ok\":true,\"step\":$((RANDOM % 8)),\"mode\":\"god\",\"player\":\"slingshooter08\",\"session_id\":\"test-001\"}"
|
|
sleep 1
|
|
done
|
|
```
|
|
|
|
Expected: Each trace appears in the sidebar sequentially, map updates with each event
|
|
|
|
- [ ] **Step 5: Verify idle timeout**
|
|
|
|
Wait 10 seconds after last trace.
|
|
Expected: Frontend fades back to idle mode (muted colors)
|
|
|
|
- [ ] **Step 6: Test health endpoint**
|
|
|
|
Run: `curl -s http://localhost:3333/health | python3 -m json.tool`
|
|
Expected: JSON with botConnected=true, wsClients=1 (your browser)
|
|
|
|
- [ ] **Step 7: Final commit**
|
|
|
|
```bash
|
|
git add -A oracle-bot/
|
|
git commit -m "feat(oracle): Oracle Bot v0.1.0 — Mind's Eye live viewport"
|
|
```
|