feat(runtime): script-driven reflex daemon + Ink TUI dashboard
Pure-Pi runtime had three failure modes in practice: - slow: 20-60s per decision because LLM was in the hot path - expensive: every tick (defend, eat, idle) paid for a reasoning pass - invisible: required tmux capture-pane to know what the bot was doing New runtime/ layer is a long-running Node daemon that owns the MC connection, ticks a priority-ordered reflex chain (defend > eat > sleep > idle) with NO LLM in the hot path, and exposes status + commands over a Unix-socket IPC. tui/ is an Ink dashboard that attaches over IPC and can detach freely — multiple TUI clients can connect at once. Pi/Codex are still available, but as on-demand escalation: TUI hotkey 'a' spawns `pi -p "<prompt>"` as a subprocess and streams stdout into the dashboard. The self-improvement loop (proposals → operator approval → Pi-driven patch → hot reload) is documented in docs/runtime.md but not yet wired. Reflex bodies are stubs today — they log decisions but don't drive Mineflayer actions yet. The priority chain, IPC contract, and TUI are fully working; subsequent commits will fill in defend/eat/sleep bodies and wire automatic escalation. Run with `npm run bot` + `npm run tui`. Pi-only fallback stays at `npm run agent`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
// Build a compact, JSON-safe snapshot of the world around the bot. Used both
|
||||
// for reflex decisions and for periodic IPC STATUS events.
|
||||
|
||||
function vec3ToObj(v) {
|
||||
if (!v) return null;
|
||||
return { x: Math.round(v.x * 100) / 100, y: Math.round(v.y * 100) / 100, z: Math.round(v.z * 100) / 100 };
|
||||
}
|
||||
|
||||
const HOSTILE = new Set([
|
||||
"zombie",
|
||||
"skeleton",
|
||||
"creeper",
|
||||
"spider",
|
||||
"witch",
|
||||
"pillager",
|
||||
"vindicator",
|
||||
"husk",
|
||||
"stray",
|
||||
"drowned",
|
||||
"phantom",
|
||||
"enderman",
|
||||
"slime",
|
||||
"magma_cube",
|
||||
"hoglin",
|
||||
"piglin_brute",
|
||||
"ravager",
|
||||
"warden",
|
||||
"breeze",
|
||||
"bogged",
|
||||
]);
|
||||
|
||||
export function snapshot(bot) {
|
||||
if (!bot || !bot.entity) {
|
||||
return { connected: false };
|
||||
}
|
||||
const pos = bot.entity.position;
|
||||
const entities = Object.values(bot.entities || {});
|
||||
const players = entities.filter((e) => e.type === "player" && e.username && e.username !== bot.username);
|
||||
const hostiles = entities.filter((e) => HOSTILE.has((e.name || "").toLowerCase()));
|
||||
const closestHostile = hostiles.reduce((best, e) => {
|
||||
const d = e.position.distanceTo(pos);
|
||||
return !best || d < best.d ? { d, e } : best;
|
||||
}, null);
|
||||
|
||||
const inventory = (bot.inventory?.items?.() ?? []).reduce((acc, item) => {
|
||||
acc[item.name] = (acc[item.name] ?? 0) + item.count;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
connected: true,
|
||||
username: bot.username,
|
||||
position: vec3ToObj(pos),
|
||||
health: bot.health,
|
||||
food: bot.food,
|
||||
saturation: bot.foodSaturation,
|
||||
experience: bot.experience?.level,
|
||||
time: bot.time?.timeOfDay,
|
||||
isDay: bot.time?.isDay,
|
||||
weather: { rain: bot.isRaining, thunder: bot.thundering },
|
||||
dimension: bot.game?.dimension,
|
||||
inventory,
|
||||
players: players.map((p) => ({ name: p.username, distance: Math.round(p.position.distanceTo(pos)) })),
|
||||
hostileCount: hostiles.length,
|
||||
closestHostile: closestHostile
|
||||
? { name: closestHostile.e.name, distance: Math.round(closestHostile.d * 10) / 10 }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user