AGENTS.md

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.

File map

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)

UI

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.

Setup page structure

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.

Models

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.

Tools

This agent comes with the following tools:

  1. Anthropic web search
  2. Val Town MCP server

You should of course edit and add your own tools! That's the whole point of BYOA :)

Version pinning

This val uses pinned imports (latest package version as of 2/19/26) to avoid breaking changes:

  1. @slack/web-api@7.14.1
  2. hono@4.12.0
  3. ai@6.0.92
  4. @ai-sdk/anthropic@3.0.45
  5. @ai-sdk/mcp@1.0.21

Operational gotchas

  • Restricted access breaks Slack. Slack relays messages to this val's /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.