Previous wiki_recipe only returned the page thumbnail, so "send me the
recipe image" delivered a picture of the item, not the recipe itself.
minecraft.wiki renders recipes as live HTML grids, not standalone PNGs,
so we now generate the image ourselves.
mcp/lib/minecraft-wiki-tools.ts:
- parseCraftingGrid(html): pulls the 3×3 input grid + result item URL
from <span class="mcui mcui-Crafting_Table">. Each <span class="invslot">
is either empty (chunk starts with </span>) or has an <img src="/images/
Invicon_X.png?h">. Reading-order chunks → grid; first non-null after
the 9th input slot → result.
- composeRecipePng(grid, result): fetches each unique sprite, resizes
to 64×64 via sharp (nearest-neighbour to keep pixel art crisp),
composites onto a #8b8b8b inventory-style background with #6f6f6f
slot squares + dark borders, places an SVG-rendered "→" arrow,
then the result cell next to it. PNG buffer returned.
- New tool wiki_recipe_image(title, chat, caption?) that runs the
pipeline end-to-end and uploads via tg.sendPhotoBytes — one tool
call, image already in the chat when it returns.
mcp/lib/types.ts:
- TgClient gains sendPhotoBytes({ chat_id, bytes, filename?, caption?,
parse_mode?, reply_to_message_id? }). Multipart/form-data POST to
sendPhoto, since Telegram's URL-mode can only fetch public URLs and
the composited PNG only lives in memory. 30s timeout (vs the 20s on
the JSON call) because uploads are bigger.
mcp/package.json + bot/package.json: add sharp ^0.33.5. mcp/ pulls it
in too so /mc/mcp/lib resolves it at the bot's runtime (the bot's
node_modules is at /app while the lib files live at /mc/mcp/lib).
Persona prompt: wiki_recipe_image is now the FIRST call for any
"recipe" / "send me the recipe image" request. Single tool turn ends
with the photo already in the chat. wiki_recipe stays as a fallback
when the page has no 3×3 crafting widget (smelting / brewing / smithing
use different mcui classes).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a new MCP tool that parses minecraft.wiki's <table data-description=
"Crafting recipes"> directly, pulling the ingredients column out of each
row (e.g. "Block of Iron + Iron Ingot" for Anvil), then chases up the
page's main item thumbnail via prop=pageimages so the bot can deliver
text + a real image in one tool round.
Why a dedicated tool: minecraft.wiki does not host standalone "recipe.png"
files — recipes are rendered live as HTML grids composed from individual
BlockSprite icons. Filtering wiki_page_images for 'recipe' / 'craft' /
'grid' returned empty for every craftable item, leaving the model with
nothing to call tg_send_photo with. Confirmed by inspecting the Anvil
page's image inventory (Anvil1-6.png + GUI + sound files, no recipe).
Tool returns {
title, page_url,
recipes: [{ ingredients: "…" }, …], // strips <br>/<a>/ cleanly
thumbnail_url, // for tg_send_photo
note?, // present when no recipe rows
}
Persona prompt tightened:
- wiki_recipe is now the FIRST call for "how do I craft X" / "recipe for X"
- wiki_page_images is downgraded to "specific non-thumbnail images only"
- IMAGE DELIVERY MANDATORY clause: must end the turn with tg_send_photo
using a real URL or say plainly that no thumbnail was found — no more
describing-the-image-in-prose-and-stopping failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reproduced: "send me the recipe for an anvil" got a TEXT reply describing
the recipe but no image. Three changes to fix this class of problem:
1) Iteration cap 4 → 6. The wiki+photo flow is naturally
wiki_search → wiki_page → wiki_page_images → tg_send_photo → final
text — five tool calls + one text response. With only 4 iterations
the model often ran out before reaching tg_send_photo or before
emitting closing text, returning {text:null, ranTool:true} which
the handler swallows silently.
2) Persona prompt: image delivery is now MANDATORY when the user asks
for a picture / image / recipe / "what it looks like". Added an
explicit fallback chain: filter='recipe' → 'craft' → 'grid' → the
wiki_page thumbnail_url. The previous phrasing was suggestive only,
which let the model answer in pure text.
3) Tool-call tracing. Both dispatchers now log `[redstone:or|gem]
tool -> name args` before each call and `tool <- name ok|err msg`
after. Without this, silent "model called tools but no image
showed up" failures had no log line at all and we had to guess.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two changes that came out of a real "I said hey steve, bot didn't reply"
report:
1) Persona-name addressing in groups. botAddressed now ALSO triggers
when the message text contains the bot's persona name (regex
/\b(redstone|steve)\b/i, case-insensitive, word-boundary). The
persona prompt already invites users to call the bot "Redstone" or
"Steve" — without this matcher, those messages were silently
dropped in groups because Telegram didn't recognise the plain
word as a @-mention. Private chats still trigger on every text
message (unchanged).
2) Slow-tool indicator for wiki_* calls. minecraft.wiki can lag /
rate-limit. New withSlowToolIndicator(chatId, toolName, fn) wraps
the tool execution: if the call takes longer than 1500 ms,
send a "🔍 looking it up…" placeholder message into the chat
and rotate through a 4-phrase loop ("📖 reading the wiki…",
"🔎 searching…", "🧭 cross-referencing…") every 900 ms. When
the tool resolves, the timer + interval are cleared and the
placeholder is deleted, so fast calls (<1.5 s) leave no trace
in the chat. Only fires for tools whose name matches /^wiki_/.
Wired through the dispatch chain — runRedstoneTurn,
runRedstoneTurnViaOpenRouter, and runRedstoneTurnViaGemini all
gained a `chatId` parameter so the indicator knows which chat to
post into.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Redstone can now query the official Minecraft wiki and deliver
crafting recipe / item images straight into the chat.
New: mcp/lib/minecraft-wiki-tools.ts exports three tools, all hitting
https://minecraft.wiki/api.php (MediaWiki API, formatversion=2):
- wiki_search(query, limit?) — opensearch top results, namespace 0,
returns { title, description, url } for each hit.
- wiki_page(title, thumb_size?) — extracts|pageimages → intro extract
(plain text, ~600 chars) + thumbnail_url + page_url.
- wiki_page_images(title, filter?, limit?) — generator=images +
prop=imageinfo (iiprop=url|mime|size) returns every image embedded
on the page along with its direct https URL. The `filter`
substring is case-insensitive against the filename — pass 'recipe'
/ 'craft' / 'grid' to surface crafting-recipe diagrams.
Bot wiring (bot/bot.ts):
- import minecraftWikiTools, merge into allTools so it flows through
the existing OpenRouter + Gemini function/tool schemas automatically.
- Persona prompt teaches the model the canonical flow:
wiki_search → wiki_page → wiki_page_images(filter='recipe') →
tg_send_photo(url, caption). Composes with the already-existing
tg_send_photo tool — no new telegram tool needed.
User-Agent on every wiki request, 15s timeout, JSON-only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the old "Up All / Server ▾ / Tunnel ▾" five-button grid with a
single power-toggle button (▶️ Start / 🛑 Stop) plus three flat-nav
buttons (Logs / Users / Refresh). Granular per-service control stays
reachable via the existing /server_up, /server_down, /pf_up, /pf_down
slash commands.
Message body redesigned: top-line health pill (🟢 Online / 🟡 Partial /
🔴 Offline), then aligned status rows for Server and Tunnel, then the
update timestamp. Uses the same 🟢/🔴 pair throughout for consistency,
dropping the mixed ✅/⛔ + 🟢/🟡 from before.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
actUpAll was running `docker compose up -d` with no service filter, so it
re-evaluated every service in the project (including the bot). docker
compose then recreated the bot container, SIGTERM'd the running process
mid-RPC, and left a renamed replacement (`<oldhash>_minecraft-bot`) stuck
in "Created" state because the original container hadn't yet released
the name. End result: /up silently took the bot offline and required
manual `docker rm + docker compose up -d bot` to recover.
Scope to SERVER_SVC + PF_SVC (matching actDownAll which already does the
same thing). The bot can't sensibly restart itself anyway.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
sendMessageDraft (Bot API 9.5) only works in private chats. Group,
supergroup and channel peers reject it with TEXTDRAFT_PEER_INVALID,
so Redstone was silently failing the entire reply path in groups
after the streaming feature landed in cab5337.
Keep replyWithStream for private chats (the typing animation is
the whole point); fall back to plain ctx.reply() everywhere else.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Redstone now hits OpenRouter's `openrouter/free` auto-router first,
falling back to direct Gemini if the OpenRouter call fails (no key,
network error, or `:free` upstream rejected tool use). The auto-
router filters its free-model pool by the request's tool-calling
requirement, so we don't have to pin a specific free model.
- bot/bot.ts:
- OPENROUTER_API_KEY / _FILE env (mirrors GEMINI_API_KEY_FILE)
- OPENROUTER_MODEL defaults to "openrouter/free"
- openRouterTools[] derived from existing geminiFunctionDeclarations
(OpenAI-style {type:"function", function:{name,description,parameters}})
- callOpenRouterOnce + runRedstoneTurnViaOpenRouter (parses JSON-string
tool_calls.arguments, replies with role:"tool" + tool_call_id)
- Existing Gemini path moved into runRedstoneTurnViaGemini
- runRedstoneTurn dispatches OpenRouter first, Gemini on null
- Early-return gate now passes if either key is configured
- docker-compose.yml:
- new openrouter_token secret -> ./openrouter.token (gitignored)
- OPENROUTER_API_KEY_FILE + OPENROUTER_MODEL env wired to the bot
- .gitignore: add openrouter.token (plus pre-existing gitea.token entry
that was sitting uncommitted).
The key file itself is NOT committed (verified via git check-ignore).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related changes to how Redstone behaves in chat.
Streaming: replies now animate word-by-word into Telegram using the new
sendMessageDraft endpoint (Bot API 9.5, March 2026) via the @grammyjs/stream
plugin, with @grammyjs/auto-retry on the API layer to swallow 429s
transparently. The previous editMessageText-based approach is gone —
sendMessageDraft is designed for this and animates natively on the client
without hitting the 1/sec-per-chat edit limit. Pace lives in
STREAM_WORD_DELAY_MS=50; tunable in one spot.
server_down footgun: the MCP tool was running `compose down` with no
service arg, which tore down the whole project — including the bot
container running the tool call, which then got SIGTERM mid-conversation
and didn't come back (unless-stopped doesn't restart on a clean compose
down). Behaviour now matches the existing /stop slash command:
compose stop minecraft autossh, leaving the bot up. Pass an explicit
service to stop just that one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Persona polish that grew out of using the bot in the group.
Voice: replace the generic 'friendly assistant' personality block with
laid-back hippy dialect — 'man', 'groovy', 'good vibes', occasional
loose grammar — wrapped around the existing Minecraft-themed flavor.
Capped to one or two hippy words per reply so it doesn't read as a
caricature; still helpful first, vibe second; dials down if the user
is terse.
Name: persona is now 'Redstone Steve' — combines the mineral and the
default player. Prompt notes it also answers to plain 'Redstone' or
'Steve' so existing chat history isn't suddenly off-character.
Typing indicator: startTypingLoop() re-ticks sendChatAction every 4s
for the whole AI roundtrip (Telegram auto-clears after ~5s), wrapped
in a try/finally so the indicator clears (fades naturally) on both
success and failure paths. Previously it fired once and faded mid-call
on long requests, making the bot look stalled.
Sticker swap: only 35% of palette-emoji matches actually fire a
sticker now (STICKER_SEND_PROBABILITY = 0.35). Keeps stickers as a
treat rather than a tic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A handful of related Telegram-bot improvements that piled up in one
working session.
Slash menu: /start now brings the whole stack up (was: panel render —
duplicated by /menu) and a new /stop takes it down. /up and /down kept
as undocumented aliases. /clear wipes the current chat's rows from the
messages table — DM-self for users, admin-only in groups — so Redstone's
recentTurns history can be reset on demand.
Sticker auto-collection: every incoming sticker is upserted into a new
sticker_library table (file_id PK, plus emoji, set_name, first_seen,
last_seen, seen_count). No /setsticker needed. The original event-keyed
stickers table is untouched and still drives the bot's own event
triggers (maybeSendSticker("up") etc).
Emoji → sticker swap (replaces the AI-driven sticker tool): instead of
listing 15+ file_ids in the system prompt and exposing tg_send_sticker
as a function call, the prompt now lists just the *emoji palette* the
library knows about. After Redstone replies, pickStickerForReply()
scans the text for the first palette-emoji that appears and fires the
matching sticker as a follow-up via ctx.replyWithSticker — without
inserting a row into the messages table, so the audit log stays
text-only and Redstone's next recentTurns isn't polluted. tg_send_sticker
and tg_stickers_known are filtered out of Redstone's geminiFunctionDeclarations
(still exposed via the MCP server for Claude / external clients).
Reliability: Gemini call timeout bumped from 25s to 45s after a real
timeout in production with the previous (too-large) sticker prompt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Redstone previously only saw the 23 tg_* tools — it had no idea the
Minecraft stack existed, so questions like "is the server up?" went
unanswered. This change extracts the 15 Minecraft tools (lifecycle,
rcon, backup, players, seen) into the same shared catalog the Telegram
tools already used, so both Gemini and external MCP clients see them.
mcp/lib/types.ts (new) holds the shared shape: Tool<P>, ToolCtx,
createTgClient, createMcRuntime (a Bun.spawn-based wrapper for docker
compose / docker exec), and toolToGeminiFunction (zod → Gemini schema,
now also stripping exclusiveMinimum/Maximum since Gemini rejects them).
mcp/lib/minecraft-tools.ts (new) is the catalog itself. Eight handlers
are flagged requiresAdmin: rcon, backup, and all destructive player_*
writes plus seen_list. mcp/server.ts trusts the caller (Claude / Paul
on the host) and ignores the flag; bot/bot.ts honours it at dispatch
time, returning {error: "...requires admin role..."} to Gemini so it
can explain to the user instead of attempting the call.
mcp/server.ts shrinks from 423 lines to 70 — a single loop over both
catalogs replaces the hand-rolled registrations.
bot/bot.ts wires both catalogs into the function declarations and adds
the admin gate. It also gains a defensive re-check on every incoming
group/DM text: the Redstone handler now does its own lookup of
ctx.from.id against the users table and refuses to reply unless
status='active'. This is belt-and-braces — the auth middleware already
short-circuits unauthorized callers earlier in the chain, but with
privacy-mode-off groups now feeding every message through, a future
refactor that reorders middleware shouldn't be able to make Redstone
respond to strangers.
Drops thinkingConfig from the Gemini call (gemini-2.5-flash-lite
rejects it outright with HTTP 400).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops two new capabilities on top of the existing Telegram bot + Minecraft
stack, both built around a shared toolkit so there's one source of truth
for the actions either side can take.
mcp/ — a Bun-native MCP server exposing 38 tools to MCP clients (Claude
Code, etc.): server lifecycle, rcon, backup, player/db CRUD, plus 23
Telegram Bot API methods (messaging, reactions, polls, dice, photos,
stickers, pins, forum topics, chat config). Runs over stdio.
mcp/lib/telegram-tools.ts — the Telegram tool catalog as Zod-typed
handlers. Imported by both mcp/server.ts (registers each as an MCP tool)
and bot/bot.ts (exposes each as a Gemini function declaration), so adding
a tool in one place lights it up everywhere.
bot/bot.ts — replaces the silent-on-unknown-text behaviour with Redstone,
an in-bot persona driven by gemini-2.5-flash-lite with native function
calling. In DMs it always responds; in groups only when @-mentioned or
replied to. The tool-use loop (max 4 rounds) lets it decide to send a
poll, react with an emoji, roll dice, etc. via the shared handlers rather
than just text. Thinking budget zeroed and system prompt locked down so
the model doesn't leak its reasoning into replies.
docker-compose.yml — adds google_key as a docker secret and passes
GEMINI_API_KEY_FILE + GEMINI_MODEL to the bot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the archive is produced, tag the current HEAD as backup-<stamp>,
create a Gitea release, and attach the tarball. Token is read from
$GITEA_TOKEN or ~/.gitea_token; upload is skipped if neither is set.
When the minecraft container is not running, archive data/world
directly from the host instead of failing — the files are already
consistent on disk with no process writing to them.
Stream tar from inside the container to the host, so all server
interaction (rcon + archive) happens through docker exec. Requires
the container to be running; fails fast otherwise.
Flushes world via rcon (save-off + save-all flush) before tarring
when the server is running; skips the flush and archives as-is when
the container is down. Outputs timestamped archives to backups/,
which is gitignored.
Tracks docker-compose config, server scripts, and server properties.
Runtime data (world, libraries, versions, logs, jar), credentials, and
backup snapshots are gitignored.