-
released this
2026-09-10 21:59:33 -05:00 | 57 commits to main since this releaseAdded
-
ESPNFF_TEAM_MAP— who is who in the league. Comma-separated
discordUserId:teamIdpairs, telling the bot which ESPN team each Discord
user manages.This closes a wrong-answer bug, not a missing feature. The fantasy
server is configured with exactly one team id and flags itis_your_team.
That is correct for whoever set it up and wrong for every other manager in
the league — and the flag says nothing about who is typing. So "how did my
team do?" from a second manager came back complete, confident and about
somebody else's team, with nothing in the answer to give it away. The
asker's own team id now goes into the system prompt for every request, and
theis_your_teamflag is explicitly demoted where they disagree.Nothing can derive this mapping: ESPN knows a manager by an ESPN display
name, Discord knows a snowflake, and no field joins them. An unmapped user
is told apart from a mapped one and asked which team they mean rather than
being given a guess;/aufhocker espnff helptells each person which team
the bot has them as, or that it has none. A malformed entry is dropped with
a warning — including123:and123:0, which would otherwise map someone
onto a team that does not exist.
Changed
-
/aufhocker espnff mcpis no longer admin-only. The ten fantasy tools
are read-only and see one league, so the reach that makesweb_search
admin-gated does not apply; the rate limiter is the cost control, as it is
for every other command.The gate this required is in
ClaudeClient: naming an explicittools
list now authorizes that list.isAdminstill gates the DEFAULT set —
general-purpose reach onto the open internet, spending an API key, for
anyone who can type in a channel. A named list is a narrower claim the
calling command has already made. Without this the command would have kept
working for non-admins while quietly dropping its tools, which produces a
fluent fantasy answer with nothing behind it.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-09-10 21:48:24 -05:00 | 58 commits to main since this releaseAdded
-
/aufhocker espnff help— the fantasy commands, and what is behind
them. Lists every tool the fantasy server currently publishes with a
one-line gloss, plus example questions to copy.Rendered from the tools actually registered, never from a list kept in
this repo. So a tool added to the fantasy server appears here on the bot's
next restart, and — the part that matters when something has gone wrong — a
server that is not connected says exactly that instead of showing a
confident, empty list. It answers from local state: no Claude call, no
deferral, and deliberately not subject to the rate limit, because it is the
command you reach for when you are already stuck.
Changed
/aufhocker espnffis now/aufhocker espnff mcp query: <text>. The
fantasy commands moved under a subcommand group to make room forhelp.
Shipped in 1.15.0 and renamed before it ever reached a deployment.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-09-10 21:39:40 -05:00 | 59 commits to main since this releaseAdded
-
/aufhocker espnff query: <text>— a command of its own for the ESPN
fantasy league. Previously the fantasy tools were only reachable through
/aufhocker query-chat, mixed in withweb_searchand the channel history,
and whether they were used at all was left to the model.It is the same Claude client pointed at one subject, and the three
differences fromquery-chatall remove something:- Only the borrowed fantasy tools are offered.
web_searchis not on
the table, so a question this league's data cannot answer comes back as
"I don't know" rather than as a confident answer assembled from the open
web — which is the failure that looks most like success. - No channel history is built or sent. A fantasy question is about the
league, not the conversation. That also removes the prompt-injection
surface the general system prompt spends a paragraph defending against. - It refuses outright when the tools are missing, rather than answering
without them. A fantasy answer with no tool call behind it is a guess, and
in Discord it would read exactly like a real one.
Admin-gated, like every other tool-using path in this bot, and it says so
when it refuses. Rate limiting, channel-permission checks and the "Make
Public" button are shared withquery-chatrather than reimplemented —
the publish rules are the security-sensitive part of that plugin, and a
second copy is a second place for them to drift. - Only the borrowed fantasy tools are offered.
Changed
ClaudeRequestaccepts an optionalsystemprompt and an optional explicit
toolslist. An emptytools: []means no tools and is deliberately not
the same as omitting the field, which still means "the built-ins plus
everything borrowed from MCP".
Fixed
- Tool use was gated on
config.admin.availableTools.length > 0, so a
deployment that configured no built-in tools would also have withheld every
tool borrowed from an MCP server. The gate now counts the tools actually
being offered.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-09-10 21:05:39 -05:00 | 60 commits to main since this releaseTools borrowed from MCP servers
MCP_SERVERStakes comma-separatedname=urlpairs. At startup the bot connects to each,
asks what tools it publishes, and hands them to Claude alongside the built-inweb_search
andweb_fetch. A tool added on the server appears in Discord on the next restart, with
no change to this repo — which is the point, and also the gotcha: the tool list is no
longer in this repo, so "where isget_standingsimplemented" is answered by the server
rather than bygrep.The first consumer is
mcp-gateway's ESPN fantasy football tenant, so
/aufhocker query-chatcan now answer about a league roster, matchups, standings, the
waiver wire and recent transactions.Four deliberate non-behaviours
connect()never throws. Plugininit()errors are fatal to startup, so a fantasy
server restarting for thirty seconds would otherwise take Discord's general-purpose chat
down with it. It reports instead, and the bot starts with whatever tools it got.- Borrowed names are not prefixed. The servers' own descriptions cross-reference each
other by name — "get the team_id from list_teams first" — so prefixing would silently
break every one of those sentences. The cost is possible collisions, so a name matching a
built-in is refused rather than overwritten. - A server refusal comes back as content, not an error. Those refusals are written for
the model to read and act on: "that week has not been played yet; get_matchups shows the
scheduled pairings instead". Throwing would replace that with "tool execution failed". - Nothing is reached over the public internet. Service aliases on the Swarm's internal
network, not proxied hostnames.
Authenticated with a single bearer token (
MCP_AUTH_TOKEN) rather than OAuth: a bot has no
browser to complete a consent screen with, which is why the server side runs in token mode.An eighth preflight check
preflight/mcpreaches every configured server at boot and reports its tool count — the
same "prove it works before something needs it hours later" the other seven exist for. It
connects separately from the session the query plugin holds and pays a second handshake
to do it: a line reporting what some other component happened to find is a line that goes
stale when that component changes. A server that connects but publishes zero tools is a
failure rather than anokcarrying a zero — that is what a misconfigured tenant looks
like, and a green line is exactly what gets read as fine.Also
browserslistpinned to^4.28.7inoverrides(High: unbounded memory growth, and a
prototype write innormalizeStats). Dev-only — reached solely viajest → babel, so
never in the production image. Newly published rather than newly introduced: it would have
failednpm audit --audit-level=highon the next push regardless.
833 tests green.
Deploying this
Two one-time prerequisites on rpi5a:
docker network create -d overlay mcp_net printf %s '<token>' | docker secret create espnff_mcp_token -Then a yaml stack update for both
aufhockerandmcp-gateway.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-08-31 22:19:41 -05:00 | 61 commits to main since this releaseChanged
- Recent form is back to five games. The live-game embed had been showing a
three-game win/loss record since 1.13.0, where the count was cut to hold a
detected game's enrichment burst to 40 calls on a Riot personal key shared
with lol-companion. Three games is too small a sample to read as form, and the
two icons the formatter reserves for a clean sweep (five straight wins, five
straight losses) could never fire at that count. Back at five, a detected game
asks for ~70 calls;riotMaxRequestsPer2Minstill caps us at 40 per window,
so the limiter spreads that burst rather than taking 429s — the cost is that
the Phase 2 embed edit lands a few minutes after the notification instead of
seconds after it. SetLOL_RIOT_MAX_REQUESTS_PER_2MINhigher to shorten that
wait, but only alongside lowering lol-companion's ceiling: the two must still
sum under the key's 100.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Recent form is back to five games. The live-game embed had been showing a
-
released this
2026-08-26 10:47:02 -05:00 | 62 commits to main since this releaseChanged
-
A 429 now names which side refused it. An idle poll rotation was logging a
steady drip ofRiot API rate limited, retrying in 12swarnings with nobody in
a game — which reads as our own pacing misbehaving, when the header on every
one of them said otherwise. Riot'sX-Rate-Limit-Typedistinguishes our key's
quota (application/method— actionable, pace down) from Riot's own
backend shedding load (service, andserveris observed in the wild), which
no pacing on our side prevents: spectator-v5 refuses the question before it can
answer "not in a game", so an idle rotation draws as many of these as a busy
one.Each 429 line now carries a
blamefield (ours/riot/unknown) and
says the same in prose, so the two incidents wearing one status code are
tellable apart at a glance instead of by reconstructing the raw header. A 429
Riot omits the header from isunknown, neverriot— quietly blaming Riot
for what might be our own quota is the misread the field exists to prevent.Riot-side retries that succeed dropped to
debug: the wait is the whole cost,
and the drip was burying real warnings. The lines that mean a poll actually
lost its answer — the too-long-to-wait skip and exhausted retries — stay at
warnregardless of blame.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-25 22:47:28 -05:00 | 63 commits to main since this releaseFixed
Lane detection assigned no roles at all. Every live game logged
Lane detection response contained no text blocksand came back withrolesAssigned=2— and those two were the Smite junglers, whichlaneDetectionServicepre-assigns locally before Claude is ever called. The other eight players fell back toUnknown, so the sorted-by-role embed and the role hints handed to the prediction prompt were both running empty.The call asked for
max_tokens: 150and never setthinking. Current models think by default, and the defaultdisplayisomitted— so the whole 150-token budget was spent inside a thinking block that carries no visible text, and the response came back with zero text blocks.callClaudereturnednull, andif (!raw) breakskipped the retry, so the single warning per game was the whole story.Fixed by sending
thinking: { type: 'disabled' }. Role assignment is deterministic extraction from a fixed champion list, not reasoning, so the small budget was always the right size — it just needed to be spent on the answer.gamePredictionServicewas never hit by this: its 500-token budget leaves room for both.The warning now carries
stopReasonand the block types that did come back, so the next variant of this failure names itself instead of looking like a parse problem.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
Source code (ZIP)
-
released this
2026-08-25 21:13:00 -05:00 | 64 commits to main since this releaseAdded
-
The running build is now the first line of the boot log, on its own.
2026-08-25 21:09:59 INF bot/version > aufhocker v1.13.3The version was previously one field among three on the sixth line
(Starting Aufhocker Discord Bot... | environment=… nodeVersion=… version=…),
below five lines emitted by import side effects — unfindable by a human
scrolling container output. That cost real time during the 1.13.x rollout: a
deploy that had genuinely rolled forward was read as one that had not.Emitted from a new
src/utils/bootBanner.tsat import time, not from
main(). Everything appearing abovemain()'s first line does so from its own
import side effects (the database pool, the shutdown handler, the admin
services), and ES module imports evaluate in source order — so importing the
banner first inindex.tsis the only thing that puts it at the top. There is
no import-order lint rule, so that import carries a comment saying it must
stay first.Deliberately field-free, since a bare message is what makes it scannable. New
bot/versionphase so it can be grepped on its own. The structuredversion
fields on the startup andconfig/runtimelines are unchanged for anything
parsing the logs.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-25 21:04:13 -05:00 | 65 commits to main since this releaseFixed
- A passing network blip was logged as an unfixable bug.
pollSummoner
sorts poll failures witherr instanceof TypeError, on the reasoning that a
TypeError can only come from our own mapper reading a field Riot omitted. But
fetchreports every transport failure as a bareTypeError: fetch failed,
so DNS hiccups, resets and timeouts all landed in that branch and were logged
at ERROR asBug mapping Riot spectator response — game will NOT be announced. Both claims were false: nothing was wrong with the mapper, and the
next cycle announced the game normally. Production logs show the contradiction
a millisecond apart —Riot API network errorimmediately followed by
Bug mapping Riot spectator responsefor the same failure. RiotApiClientBasenow wraps transport failures in a new exported
RiotNetworkError, preserving the original ascause. It is not a
TypeError, so it falls to the transient branch and a genuine mapping bug
still reaches the error branch — which is what theinstanceof TypeErrortest
meant all along. That test was added to stop mapping bugs hiding under
"transient network error" (the missing-perkscrash on Classic games); this
is the same misfiling in reverse, and both directions are now pinned by tests.
Behaviour is unchanged — both branches already returned
'skipped', counted
the summoner in the cycle summary, and retried next cycle. This is log accuracy
only, which matters because these logs are the instrument used to diagnose the
shared-key 429s in 1.13.0.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- A passing network blip was logged as an unfixable bug.
-
released this
2026-08-25 20:47:05 -05:00 | 66 commits to main since this releaseFixed
- Restart recovery discarded a live game every time the bot restarted.
activeGameTrackerpersists theplatformIdthat came back on spectator data
(NA1), butgetPlatformaccepted only user-facing region codes (NA) and
threwUnknown region: NA1.recoverActiveGamescaught the throw, marked the
game stale and cleaned it up, so a genuinely in-progress game lost its
end-of-game notification on every deploy — observed twice in ten minutes
across the 1.13.0 rollout (games 5628417643 and 5628441631).getPlatformnow
accepts either form, matchinggetRoutingFromRegionOrPlatform, which had
handled both all along. Unrecognised values still throw, so a typo fails
loudly rather than reaching Riot as a malformed hostname.
Changed
.env.configdropsLOL_RIOT_MAX_REQUESTS_PER_2MINfrom 75 to 40, so the
repo's mirror agrees with the code default 1.13.0 introduced. The deployed
stack pins these same three variables, which is why the 1.13.0 rollout came up
still reportingriotMaxPer2Min=75— the image had changed but the
environment overrode it. The comment now says outright that leaving them unset
is preferred, since pinning a copy of the default is what made the default
dead config.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Restart recovery discarded a live game every time the bot restarted.