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:
2026-05-25 14:24:23 +03:00
co-authored by Claude Opus 4.7
parent 7060daac46
commit 3e61b87f19
10 changed files with 3816 additions and 7 deletions
+61 -2
View File
@@ -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",