feat(perception): vendor mindcraft skills/world library + Pi tool wrappers
The bot was acting blind: it knew its own coordinates but nothing about
what was around it. mc_goto's over-strict safety guards refused every
real path. mc_dig was too low-level to drive a coherent farming loop.
The result: 4+ hours of "trying" with zero physical achievement.
This commit reframes the bridge around a perceive→decide→act loop using
proven primitives from Mindcraft (github.com/kolbytn/mindcraft, MIT —
LICENSE-MINDCRAFT vendored beside the library files).
Changes:
- extensions/lib/ (new, vendored from Mindcraft with attribution)
- world.js (431 LoC) — 21 perception functions: getNearbyBlockTypes,
getNearbyEntities, getInventoryCounts, getNearestBlock, getPosition,
getBiomeName, etc.
- skills.js (2093 LoC) — 30+ action primitives: collectBlock, placeBlock,
goToPosition, goToNearestBlock, craftRecipe, equip, consume,
defendSelf, avoidEnemies, pickupNearbyItems, stay, etc.
- mcdata.js (~600 LoC) — Mindcraft's mc-data adapter. Imports patched
to local paths; mineflayer-auto-eat removed (our installed 5.x has
a divergent API; skills.consume() falls back to bot.consume()).
Added attachPluginsAndInit(bot) export so mineflayer-bridge.ts can
wire plugins onto its externally-created bot.
- settings.js — minimal stub with farmer-bot defaults.
- extensions/mindcraft-skills.ts (new, 406 LoC) — Pi extension registering
15 high-level tools on top of the vendored library:
- Perception: mc_observe, mc_inventory, mc_nearby_blocks, mc_nearby_entities
- Action: mc_collect_block, mc_place_block, mc_go_to, mc_go_to_block,
mc_craft, mc_equip, mc_consume, mc_defend_self, mc_avoid_enemies,
mc_stay, mc_pickup_nearby
ES modules from extensions/lib/ are loaded via dynamic import() at
extension init so the cross-extension require()-race resolves cleanly.
- extensions/mineflayer-bridge.ts
- Expose the live Mineflayer bot on globalThis.__pepaPiBot so the
mindcraft-skills extension can use it (set on connect, cleared on
error/end/manual disconnect).
- Call attachPluginsAndInit(nextBot) right after createBot to load
pathfinder, pvp, collectblock, armorManager and prime
minecraft-data once login completes.
- package.json — new runtime deps: minecraft-data, vec3,
mineflayer-pvp, prismarine-item.
- AGENTS.md — new "Perception → decision → action" section before
"Your tools right now" with full tool catalog and a deprecation
note for the broken mc_goto / mc_build_pyramid_5x5 / low-level
mc_dig from the old bridge.
Smoke test (medium thinking): bot called mc_observe and received a
real JSON snapshot — nearbyBlocks listed coal_ore, oak_log, water,
sand; nearbyEntityTypes listed creeper, zombie, pillager, skeleton.
The bot can finally see what it could not see this morning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,15 @@ import type { Bot } from "mineflayer";
|
||||
const require = createRequire(import.meta.url);
|
||||
const dotenv = require("dotenv") as typeof import("dotenv");
|
||||
const mineflayer = require("mineflayer") as typeof import("mineflayer");
|
||||
|
||||
// mcdata is an ES module; load it lazily via dynamic import on first use to
|
||||
// avoid the "Cannot require() ES Module ... not yet fully loaded" race when
|
||||
// Pi loads multiple extensions in parallel.
|
||||
let mcdataPromise: Promise<{ attachPluginsAndInit: (bot: any) => any }> | null = null;
|
||||
function loadMcdata() {
|
||||
if (!mcdataPromise) mcdataPromise = import("./lib/mcdata.js" as any) as Promise<{ attachPluginsAndInit: (bot: any) => any }>;
|
||||
return mcdataPromise;
|
||||
}
|
||||
const pathfinderModule = require("mineflayer-pathfinder") as {
|
||||
pathfinder: (bot: Bot) => void;
|
||||
Movements: new (bot: Bot) => any;
|
||||
@@ -90,6 +99,12 @@ interface BuildPyramidInput {
|
||||
dry_run?: boolean;
|
||||
}
|
||||
|
||||
interface DigInput {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
type MemoryAction = "set_current_task" | "clear_current_task" | "append_diary" | "register_location";
|
||||
|
||||
interface MemoryInput {
|
||||
@@ -228,6 +243,17 @@ const BUILD_PYRAMID_PARAMS = {
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const DIG_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
x: { type: "number", description: "Block X coordinate to dig." },
|
||||
y: { type: "number", description: "Block Y coordinate to dig." },
|
||||
z: { type: "number", description: "Block Z coordinate to dig." },
|
||||
},
|
||||
required: ["x", "y", "z"],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const MEMORY_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1081,6 +1107,13 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
||||
}
|
||||
|
||||
bot = nextBot;
|
||||
(globalThis as any).__pepaPiBot = nextBot;
|
||||
// Attach Mindcraft-required plugins (pathfinder, pvp, collectblock,
|
||||
// armorManager) and prime mc_version once login completes. Skill calls
|
||||
// from mindcraft-skills.ts rely on this. Loaded lazily as ESM.
|
||||
loadMcdata()
|
||||
.then((m) => { try { m.attachPluginsAndInit(nextBot); } catch (e) { log("plugin-init-error", e); } })
|
||||
.catch((e) => log("mcdata-load-error", e));
|
||||
nextBot.once("login", () => {
|
||||
setConnectionState("connected");
|
||||
});
|
||||
@@ -1122,7 +1155,7 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
||||
lastDisconnectReason = `error: ${truncate(redact(stringifyUnknown(error), current), 200)}`;
|
||||
activeWorldTask = undefined;
|
||||
stopAuthTimer();
|
||||
if (bot === nextBot) bot = undefined;
|
||||
if (bot === nextBot) { bot = undefined; (globalThis as any).__pepaPiBot = undefined; }
|
||||
setConnectionState("disconnected");
|
||||
try {
|
||||
nextBot.end("connection error");
|
||||
@@ -1135,7 +1168,7 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
||||
nextBot.on("end", (reasonText) => {
|
||||
activeWorldTask = undefined;
|
||||
stopAuthTimer();
|
||||
if (bot === nextBot) bot = undefined;
|
||||
if (bot === nextBot) { bot = undefined; (globalThis as any).__pepaPiBot = undefined; }
|
||||
const reasonString = stringifyUnknown(reasonText || lastDisconnectReason || "end");
|
||||
lastDisconnectReason = reasonString;
|
||||
if (manualDisconnectRequested || shuttingDown) {
|
||||
@@ -1161,6 +1194,7 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
||||
}
|
||||
const currentBot = bot as Bot & { quit?: (reason?: string) => void; end?: (reason?: string) => void };
|
||||
bot = undefined;
|
||||
(globalThis as any).__pepaPiBot = undefined;
|
||||
setConnectionState("disconnected");
|
||||
if (typeof currentBot.quit === "function") {
|
||||
currentBot.quit();
|
||||
@@ -1866,6 +1900,31 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "mc_dig",
|
||||
label: "Minecraft Dig Block",
|
||||
description: "Dig one block at exact coordinates using Mineflayer bot.dig(bot.blockAt(new Vec3(x,y,z))).",
|
||||
promptSnippet: "Dig one block at exact Minecraft coordinates.",
|
||||
parameters: DIG_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_toolCallId, params: DigInput) {
|
||||
const currentBot = activeBot();
|
||||
const pos = new Vec3(
|
||||
Math.floor(finiteNumber(params.x, "x")),
|
||||
Math.floor(finiteNumber(params.y, "y")),
|
||||
Math.floor(finiteNumber(params.z, "z")),
|
||||
);
|
||||
const block = currentBot.blockAt(pos);
|
||||
if (!block) throw new Error(`No loaded block at ${pos.x},${pos.y},${pos.z}.`);
|
||||
if (block.name === "air") throw new Error(`Block at ${pos.x},${pos.y},${pos.z} is air.`);
|
||||
await currentBot.dig(block);
|
||||
return {
|
||||
content: [{ type: "text", text: `Dug ${block.name} at x=${pos.x}, y=${pos.y}, z=${pos.z}.` }],
|
||||
details: { x: pos.x, y: pos.y, z: pos.z, block: block.name },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "mc_goto",
|
||||
label: "Minecraft Guarded Go To",
|
||||
|
||||
Reference in New Issue
Block a user