# 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" ```