Maintainer-facing notes for this val. For the user-facing overview and setup, see README.md — setup is a wizard at the val's HTTP endpoint, not documented here.
townie/
├── events.ts # HTTP entry point — mounts the route modules below
├── prompt.md # System prompt for your agent
├── README.md
├── AGENTS.md
├── routes/
│ ├── setup.tsx # GET / , /source, /townie.png, /confirm-townie-token, /create-slack-app
│ │ # setup page + routes; /townie.png serves the app-icon
│ │ # image out of this val's blob storage
│ ├── connectors.tsx # GET /auth/* OAuth for external MCP services
│ └── slack.ts # POST /events intake → POST /process does the work
├── views/ # Server-rendered JSX (Hono JSX + Twind)
│ ├── layout.tsx # HTML shell + shared style tokens
│ ├── setup-page.tsx # Setup page and its components
│ ├── message-page.tsx # Generic success / error page
│ └── render.ts # JSX → HTML document
├── connectors/ # External MCP service integrations (optional)
│ ├── registry.ts # Service registry + tool loader (add connectors here)
│ └── oauth.ts # Generic OAuth 2.0 + PKCE engine
└── lib/
├── generate-response.ts # AI core (Claude + MCP + web search)
├── conversation-store.ts # SQLite-backed conversation persistence
├── confirm-townie-token.ts # Verifies TOWNIE_TOKEN (set / prefix / /v1/me)
├── handle-app-mention.ts # Handles @mentions in channels and threads
├── handle-message.ts # Handles DMs in assistant (Slack app) threads
├── handle-thread-started.ts # Greeting + suggested prompts on new thread
├── should-respond.ts # Should an *untagged* channel message get a reply?
├── should-skip-event.ts # Drops bot/self-triggered events before dispatch
├── progress.ts # One message, edited in place, then into the answer
├── people.ts # Val Town handle → the name people actually use
├── slack-files.ts # Downloads Slack images for model input
├── slack-manifest.ts # The manifest behind one-click app creation
├── slack-utils.ts # Slack client and utils
├── tool-labels.ts # Human labels for the agent's tools
└── val-links.ts # Links back into this val (code, env vars)
Pages are React-style components in views/*.tsx, rendered server-side by
Hono's JSX runtime — no markup in template
strings, so the setup page is type-checked and highlighted like the rest of the
code. Styling is Twind (Tailwind classes applied at
runtime from a CDN), so there's no build step and no stylesheet to keep in sync.
The token check runs on every page load — routes/setup.tsx calls
confirmTownieToken() in parallel with the connector/org-name lookups and
passes the result to the page as tokenStatus. Step 2's "here to confirm the
key is correctly set" link just reloads /, which re-runs the check and
re-renders the status box below the list: green "key looks good!" on success,
amber on a bad vtwn_ prefix, red on an invalid token (with the API detail),
and nothing when the token isn't set.
Step 3 ("Create the Slack app & install") is one flat numbered list (1–9) that
walks through creating the app and copying the two keys. Step 4 ("Add your
Anthropic API key") mirrors it: open the Claude console → Create Key → name it
Townie → paste it in. Each env var step links the user to this val's env var
editor prefilled with the key (${envVarsUrl}?key=…, opened in a new tab) —
there is no server-side setting of secrets; the user pastes them into the Val
Town env var editor themselves. Env vars set there are picked up on the very
next request (verified live — no redeploy needed), so reloading / shows the
updated checkmark.
Step 5 ("Talk to your bot") is marked done once the bot has actually been
chatted with — routes/setup.tsx counts rows in the conversations SQLite
table (conversationCount() in lib/conversation-store.ts) and passes it to
the page as threadCount.
Note: The setup page only ever reads Deno.env.get(key) to show a
set/not-set checkmark — it never renders the values themselves, so it's safe to
serve on a public endpoint.
This template uses Claude Opus 4.6 directly in
lib/generate-response.ts, which requires setting
an ANTHROPIC_API_KEY environment variable. Alternatively, you could use a
gateway to switch between models easily, like the
Vercel AI Gateway or the
Kilo Gateway.
This agent comes with the following tools:
You should of course edit and add your own tools! That's the whole point of BYOA :)
This val uses pinned imports (latest package version as of 2/19/26) to avoid breaking changes:
@slack/web-api@7.14.1hono@4.12.0ai@6.0.92@ai-sdk/anthropic@3.0.45@ai-sdk/mcp@1.0.21/events webhook anonymously. If the val's app access is set to
"restricted", Slack's calls get a 302 redirect to the Val Town login page and
the bot goes silent. When debugging "bot not responding", check the val's
httpPrivacy/app access is set to public first — not code privacy, the
separate app-access axis.