Files
vercel__chat/packages/adapter-github/AGENTS.md
Ben Sabic 79227ae991 docs: refresh adapter pages with hand-authored MDX (#474)
## Summary

Refreshes the adapter docs end-to-end so every adapter — official,
vendor-official, and community — now ships hand-authored MDX, lives
under a clean URL structure, and renders on a polished
sidebar/right-rail layout dedicated to `/adapters` (the shared `/docs`
chrome is untouched).

```mermaid
flowchart LR
  subgraph Before
    direction TB
    OB[official] --> CB[community<br/>incl. 5 vendor pages]
  end
  subgraph After
    direction TB
    OA[official] --> VA[vendor-official<br/>5 pages] --> CA[community]
  end
  Before -.-> After
```

### Content & routing

- **New `/adapters/vendor-official/<slug>` route** for vendor-maintained
adapters (Beeper Matrix, Photon iMessage, Liveblocks, Resend, Zernio).
Sidebar gets a third labelled group ("Vendor-Official Adapters") between
Official and Community, with a top divider matching the existing
Community treatment.
- **All 13 vendor-official + community adapters migrated** from runtime
README fetching to hand-authored MDX with rich `features:` matrices and
full body content (install, quick start, configuration, auth,
gateway/streaming, troubleshooting). README fetch stays as a fallback
for any future community adapter that hasn't been migrated yet, gated by
a new `mdxBody: true` frontmatter flag.
- **Messenger filter pages removed** (`/adapters/for/<messenger>` + the
"Browse by messenger" chip row on `/adapters`). Existing URLs
308-redirect to `/adapters`.
- **Permanent redirects** from
`/adapters/community/{matrix,imessage,resend,zernio,liveblocks}` to
their new `/adapters/vendor-official/...` paths.
- **Fixed** `/docs/adapters` and `/docs/state` so the bare pages are
accessible again — the previous catch-all redirect (`:slug*`) was
swallowing them. Switched to `:slug+` so subpath URLs still 308 while
the bare pages render.

### Visual polish

- **Adapter-only sidebar variant** (`AdaptersDocsLayout` +
`AdaptersSidebar`) with uppercase eyebrow separators, tighter rows, and
a thin themed scrollbar utility class. The shared `/docs` sidebar is
untouched.
- **Restyled `AdapterHero`**: drops the badges row + packageName, sits
the title inline with the logo, larger 17 px tagline, horizontal divider
beneath the block.
- **Restyled `PackageInstall`** as a tabbed dark single-line snippet
with a `$` prompt prefix and a copy button — replaces the previous
multi-line `CodeBlock` layout.
- **New "Deploy your chat app on Vercel" upsell card** (`<Upsell />`)
replaces the old `EditSource / ScrollTop / Feedback / CopyPage` footer
cluster on every adapter detail page.
- **Listing & messenger pages**: align the H1 to a tighter `text-4xl
sm:text-[44px]`, and the section headers to `text-base font-medium
tracking-tight` with a one-line muted lede.

### Tooling & tests

- Added `mdxBody: true` opt-in to the adapter frontmatter schema
(`source.config.ts`), and updated both detail-page handlers
(`community/[slug]` and the new `vendor-official/[slug]`) to render the
MDX body when present, falling back to README fetch otherwise.
- Refactored both detail-page handlers to flatten the body-render
branches into a `renderBody()` helper, removing the nested ternaries
that were tripping `lint/style/noNestedTernary`.
- New test file
[`packages/integration-tests/src/docs-adapters.test.ts`](https://github.com/vercel/chat/blob/docs/refresh-adapters/packages/integration-tests/src/docs-adapters.test.ts)
— **220 new assertions** covering:
- Adapter MDX frontmatter completeness, slug ↔ filename consistency, and
`type ∈ {platform, state}`.
- Vendor-official invariants: exactly the expected slugs,
`vendorOfficial: true`, `community: true`, `author`, `mdxBody: true`,
`<FeatureSupport />` rendered.
- Community invariants: `community: true` (never vendor-official),
`mdxBody: true`, `<FeatureSupport />`.
- Official invariants: never flagged, `packageName` always under
`@chat-adapter/*`.
- `adapters.json` ↔ MDX sync on `packageName` / `type` / `community` /
`vendorOfficial`.
- Extended `VALID_DOC_PACKAGES` so `docs-content.test.ts` accepts the
new vendor-official + community packages, plus `@chat-adapter/web`,
`@chat-adapter/web/react`, and `@chat-adapter/messenger`.

### Per-package AGENTS.md

- Added `AGENTS.md` to every official adapter and state adapter (14
packages), each tailored to that adapter's surface — overview, directory
layout, build/test commands, public exports, thread ID format, webhook
flow, authentication, format conversion, cards/streaming, platform
quirks, testing approach, coding conventions, and release rules.
- Added a one-line `CLAUDE.md` (`@AGENTS.md`) beside each so Claude Code
picks up the same instructions through its built-in resolver — same
convention as the root.

### Web adapter copy

- Cleaned up the Web adapter tagline (removed inline backticks) and
dropped the now-redundant "v1 scope" section from the body.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-05-12 16:01:19 +10:00

9.8 KiB

AGENTS.md — @chat-adapter/github

Guidance for coding agents working inside the GitHub adapter package. The top-level repository AGENTS.md covers monorepo-wide build, lint, and release rules — read it first. This file documents the adapter-specific surface, conventions, and pitfalls.

Overview

@chat-adapter/github connects a Chat SDK bot to issue and pull-request comment threads on GitHub. It covers:

  • HTTP webhook endpoint at /api/webhooks/github for issues, issue_comment, pull_request, pull_request_review_comment, and pull_request_review events, validated with HMAC-SHA256 against the App secret.
  • Comment posting, editing, and deletion via the REST API (Issues, Pull Requests, and Reviews share comment endpoints).
  • Reactions on comments + issues + pull requests.
  • GitHub App authentication with installation-scoped tokens that auto-refresh.
  • DM-style routing — there's no native GitHub DM, so DMs map to a user-mentions-only thread on a designated repo (configurable).

Directory layout

packages/adapter-github/
├── src/
│   ├── index.ts             # GitHubAdapter + createGitHubAdapter
│   ├── index.test.ts
│   ├── cards.ts             # PostableMessage / Card → markdown comment body
│   ├── cards.test.ts
│   ├── markdown.ts          # GitHubFormatConverter (mdast ↔ GFM)
│   ├── markdown.test.ts
│   └── types.ts             # GitHub webhook event typings
├── sample-messages.md       # captured GitHub webhook deliveries
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── vitest.config.ts
└── README.md

sample-messages.md holds full webhook deliveries for the supported event types — extend it when adding handler coverage so the parser tests stay grounded in real payloads.

Build, test, typecheck

pnpm build
pnpm dev
pnpm test
pnpm test:watch
pnpm typecheck
pnpm clean

# from repo root
pnpm --filter @chat-adapter/github build
pnpm --filter @chat-adapter/github test

Replay tests live in packages/integration-tests/src/replay-github-*.test.ts.

Public surface

Main exports from src/index.ts:

  • createGitHubAdapter(config?) — primary factory. Auto-detects GITHUB_APP_ID, GITHUB_PRIVATE_KEY (PEM, may be base64-encoded), GITHUB_WEBHOOK_SECRET, and the optional GITHUB_INSTALLATION_ID for single-installation deployments.
  • GitHubAdapter class — implements Adapter<GitHubThreadId, unknown>. Public methods: handleWebhook, postMessage, editMessage, deleteMessage, addReaction, removeReaction, fetchThread, listThreads, fetchMessages, fetchSingleMessage, fetchChannelInfo, postChannelMessage, openDM, getInstallation, setInstallation.
  • Configuration: GitHubAdapterConfig, GitHubThreadId, GitHubInstallation.
  • Helpers: cardToMarkdown, cardToFallbackText, GitHubFormatConverter, decodeThreadId, encodeThreadId, isDM.

Thread ID format

GitHub URLs use {owner}/{repo}/issues/{number} and {owner}/{repo}/pull/{number}. The adapter encodes the same shape:

github:{owner}:{repo}:{type}:{number}

type is issue, pr, or review (review threads on a PR). encodeThreadId / decodeThreadId are the only sanctioned constructors.

isDM(threadId) returns true when the underlying issue is in the configured DM repo (per dmRepo option).

Webhook flow

GitHubAdapter.handleWebhook(request, options) is the entry point.

  1. Signature verification — every delivery includes X-Hub-Signature-256 (HMAC-SHA256 over the raw body). The adapter verifies it against webhookSecret with a timing-safe compare.
  2. Event routing
    • issues (opened, edited, closed, reopened) → optional hooks; for opened, also dispatches chat.handleIncomingMessage when the issue body @mentions the bot.
    • issue_comment → chat.handleIncomingMessage. Mentions resolve by matching @app-name against the bot's app login.
    • pull_request (opened, synchronize, etc.) → optional hooks.
    • pull_request_review_comment → chat.handleIncomingMessage scoped to the review thread.
    • pull_request_review (submitted) → review-level events.
    • reaction → chat.handleReaction (added / removed).
    • installation / installation_repositories → installation lifecycle hooks; the adapter calls setInstallation / deleteInstallation automatically when state is configured.
  3. Token resolution — every outbound API call goes through withInstallationToken(installationId, …), which reads the installation token from cache or mints a fresh one via the App private key.

Authentication (GitHub App)

Three pieces are required: the App ID, the App private key, and a webhook secret. For multi-installation apps, the adapter:

  • Reads the installation ID from the webhook payload (installation.id).
  • Mints an installation access token via POST /app/installations/{id}/access_tokens, signing the JWT with the App private key (RS256).
  • Caches the token in the configured state adapter under {installationKeyPrefix}:{installationId} until ~5 min before expiry.

For single-installation deployments, set GITHUB_INSTALLATION_ID explicitly and skip the lookup. Out-of-webhook code uses withInstallationToken(installationId, async () => { … }).

Format conversion

GitHubFormatConverter (in markdown.ts) maps:

  • mdast → GitHub Flavored Markdown — straightforward; GFM is a superset of mdast. Tables, fenced code with language hints, task lists, footnotes, and strikethrough all round-trip cleanly.
  • GFM → mdast — same. The tricky bits are GFM tables, autolinks (https://… becomes a link automatically), and @mentions / #issues / commit-sha references which the converter preserves as text by default.
  • Mentions of @app-name[bot] are detected via a leading-token check against the bot login.

renderPostable returns the body of an issue/PR/comment. Cards are rendered as a markdown body — GitHub has no card primitive.

Cards (markdown comments)

cardToMarkdown walks a Chat SDK Card JSX tree and emits a markdown comment body:

  • Header / Section → markdown headings + body paragraphs.
  • Field → bullet list with **label:** value entries.
  • Image → markdown image (![alt](url)); for binary uploads, the adapter posts to the user-content CDN first via the attachments.upload flow.
  • LinkButton / Button → markdown links. GitHub doesn't support interactive buttons inside comments, so callbacks fall back to a link to a deep-link URL the bot can host.
  • Divider → ---.
  • Table → GFM table.

cardToFallbackText is identical to cardToMarkdown for this adapter because GitHub renders markdown directly.

File uploads

GitHub comments can include image attachments that resolve to user- content URLs. postMessage handles binary uploads transparently:

  • Binary files get pre-uploaded to the user-content CDN via the authenticated attachments.upload endpoint.
  • The returned URL is inlined into the markdown body as ![alt](https://user-images.githubusercontent.com/…).
  • Larger-than-25 MB files are rejected with ValidationError.

GitHub quirks worth remembering

  • Bot accounts append [bot] to their login in mention syntax — always strip / re-add it consistently or mention detection breaks.
  • Reaction set is fixed (+1, -1, laugh, hooray, confused, heart, rocket, eyes). Custom reactions throw 422.
  • Review comments vs issue comments are different endpoints. The adapter chooses based on the type in the encoded thread id; never mix them.
  • Edit history. GitHub keeps an edit log on every comment; rapid-fire edits can hit the abuse-detection limit (~30 per minute per author). Streaming aborts gracefully when this triggers.
  • Webhook redeliveries — GitHub retries on non-2xx and may deliver the same event twice. Handlers must be idempotent.
  • X-GitHub-Delivery is the dedupe key. Persist it for replay protection if the bot performs side effects.

Testing approach

  • Unit tests colocated with each module (*.test.ts). The card → markdown round-trip suite is the most comprehensive because GitHub is forgiving and easy to fool with malformed markdown.
  • Replay tests in packages/integration-tests/src/replay-github-*.test.ts consume full webhook deliveries.
  • JWT signing uses the same private key in tests and production; test fixtures use a throwaway PEM checked into sample-messages.md.

When you add support for a new event type, capture a fresh delivery in sample-messages.md.

Coding conventions

  • Use named exports throughout. No default exports.
  • Webhook event types live in types.ts — extend rather than importing from @octokit/webhooks-types to keep the bundle small.
  • Errors map to @chat-adapter/shared (AuthenticationError, AdapterRateLimitError, NetworkError, ValidationError).
  • Top-level regex literals (MENTION_REGEX, ISSUE_NUMBER_REGEX, etc.).
  • The App private key is sensitive — never log it, never round-trip it through structured logging or error messages.

Releases

Behavioural changes need a changeset (pnpm changeset, choose @chat-adapter/github plus chat if a public type changed). Sample fixtures and AGENTS.md edits don't.

Where to look next