This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Install dependencies
pnpm install
# Build all packages (uses Turborepo)
pnpm build
# Type-check all packages
pnpm typecheck
# Check all packages (linting and formatting)
pnpm -w run check
# Auto-fix linting and formatting issues
pnpm -w run check --write
# Check for unused exports/dependencies
pnpm knip
# Run all tests
pnpm test
# Run full validation. ALWAYS do this before declaring a task to be done.
pnpm validate
# Run dev mode (watch for changes)
pnpm dev
# Build a specific package
pnpm --filter chat build
pnpm --filter @chat-adapter/slack build
pnpm --filter @chat-adapter/gchat build
pnpm --filter @chat-adapter/teams build
# Run tests for a specific package
pnpm --filter chat test
pnpm --filter @chat-adapter/integration-tests test
# Run a single test file
pnpm --filter @chat-adapter/integration-tests test src/slack.test.ts- Install dependencies with
pnpm addrather than manually editing package.json sample-messages.mdfiles in adapter packages contain real-world webhook logs as examples
This is a pnpm monorepo using Turborepo for build orchestration. All packages use ESM ("type": "module"), TypeScript, and tsup for bundling.
packages/chat-sdk- Core SDK (chatpackage) withChatclass, types, and markdown utilities (mdast-based)packages/adapter-slack- Slack adapter using@slack/web-apipackages/adapter-gchat- Google Chat adapter usinggoogleapispackages/adapter-teams- Microsoft Teams adapter usingbotbuilderpackages/state-memory- In-memory state adapter (for development/testing)packages/state-redis- Redis state adapter (for production)packages/adapter-whatsapp- WhatsApp adapter using Meta Cloud APIpackages/integration-tests- Integration tests against real platform APIsexamples/nextjs-chat- Example Next.js app showing how to use the SDK
- Chat (
packages/chat-sdk/src/chat.tsinchatpackage) - Main entry point that coordinates adapters and handlers - Adapter - Platform-specific implementations (Slack, Teams, Google Chat). Each adapter:
- Handles webhook verification and parsing
- Converts platform-specific message formats to/from normalized format
- Provides
FormatConverterfor markdown/AST transformations
- StateAdapter - Persistence layer for subscriptions and distributed locking
- Thread - Represents a conversation thread with methods like
post(),subscribe(),startTyping() - Message - Normalized message format with
text,formatted(mdast AST), andraw(platform-specific)
All thread IDs follow the pattern: {adapter}:{channel}:{thread}
- Slack:
slack:C123ABC:1234567890.123456 - Teams:
teams:{base64(conversationId)}:{base64(serviceUrl)} - Google Chat:
gchat:spaces/ABC123:{base64(threadName)}
- Platform sends webhook to
/api/webhooks/{platform} - Adapter verifies request, parses message, calls
chat.handleIncomingMessage() - Chat class acquires lock on thread, then:
- Checks if thread is subscribed -> calls
onSubscribedMessagehandlers - Checks for @mention -> calls
onNewMentionhandlers - Checks message patterns -> calls matching
onNewMessagehandlers
- Checks if thread is subscribed -> calls
- Handler receives
ThreadandMessageobjects
Messages use mdast (Markdown AST) as the canonical format. Each adapter has a FormatConverter that:
toAst(platformText)- Converts platform format to mdastfromAst(ast)- Converts mdast to platform formatrenderPostable(message)- Renders aPostableMessageto platform string
The packages/chat/src/mock-adapter.ts file provides shared test utilities:
createMockAdapter(name)- Creates a mock Adapter with vi.fn() mocks for all methodscreateMockState()- Creates a mock StateAdapter with working in-memory subscriptions, locks, and cachecreateTestMessage(id, text, overrides?)- Creates a test Message objectmockLogger- A mock Logger that captures all log calls
Example usage:
import { createMockAdapter, createMockState, createTestMessage } from "./mock-adapter";
const adapter = createMockAdapter("slack");
const state = createMockState();
const message = createTestMessage("msg-1", "Hello world");Production webhook interactions can be recorded and converted into replay tests:
- Recording: Enable
RECORDING_ENABLED=truein deployed environment. Recordings are tagged with git SHA. - Export: Use
pnpm recording:listandpnpm recording:export <session-id>fromexamples/nextjs-chat - Convert: Extract webhook payloads and create JSON fixtures in
packages/integration-tests/fixtures/replay/ - Test: Write replay tests using helpers from
replay-test-utils.ts
See packages/integration-tests/fixtures/replay/README.md for detailed workflow.
When debugging production issues, download recordings for the current git SHA:
cd examples/nextjs-chat
# Get current SHA
git rev-parse HEAD
# List all recording sessions (look for sessions starting with your SHA)
pnpm recording:list
# Export a specific session to a file
pnpm recording:export session-<SHA>-<timestamp>-<random> 2>&1 | \
grep -v "^>" | grep -v "^\[dotenv" | grep -v "^$" > /tmp/recording.json
# View number of entries
cat /tmp/recording.json | jq 'length'
# Group webhooks by platform
cat /tmp/recording.json | jq '[.[] | select(.type == "webhook")] | group_by(.platform) | .[] | {platform: .[0].platform, count: length}'
# Extract and analyze platform-specific webhooks
cat /tmp/recording.json | jq '[.[] | select(.type == "webhook" and .platform == "teams") | .body | fromjson]' > /tmp/teams-webhooks.json
cat /tmp/recording.json | jq '[.[] | select(.type == "webhook" and .platform == "slack") | .body | fromjson]' > /tmp/slack-webhooks.json
cat /tmp/recording.json | jq '[.[] | select(.type == "webhook" and .platform == "gchat") | .body | fromjson]' > /tmp/gchat-webhooks.json
# Inspect specific webhook fields (e.g., Teams channelData)
cat /tmp/teams-webhooks.json | jq '[.[] | {type, text, channelData, value}]'This monorepo uses Changesets to manage versioning and changelogs. Every PR that changes a package's behavior must include a changeset.
pnpm changesetYou'll be prompted to:
- Select the affected package(s) — choose which packages your change touches (e.g.,
@chat-adapter/slack,chat) - Choose the semver bump —
patchfor fixes,minorfor new features,majorfor breaking changes - Write a summary — a short description of the change (this goes into the CHANGELOG)
This creates a markdown file in .changeset/ — commit it with your PR.
- patch — bug fixes, internal refactors with no API change
- minor — new features, new exports, new options
- major — breaking changes (removed exports, changed signatures, dropped support)
pnpm changeset
# → select: @chat-adapter/slack
# → bump: minor
# → summary: Add custom installation prefix support for preview deploymentsWhen changesets are merged to main, the Changesets GitHub Action opens a "Version Packages" PR that bumps versions and updates CHANGELOGs. Merging that PR triggers publishing to npm.
Key env vars used (see turbo.json for full list):
SLACK_BOT_TOKEN,SLACK_SIGNING_SECRET- Slack credentialsTEAMS_APP_ID,TEAMS_APP_PASSWORD,TEAMS_APP_TENANT_ID- Teams credentialsGOOGLE_CHAT_CREDENTIALSorGOOGLE_CHAT_USE_ADC- Google Chat authWHATSAPP_ACCESS_TOKEN,WHATSAPP_APP_SECRET,WHATSAPP_PHONE_NUMBER_ID,WHATSAPP_VERIFY_TOKEN- WhatsApp credentialsREDIS_URL- Redis connection for state adapterBOT_USERNAME- Default bot username
This project uses Ultracite, a zero-config preset that enforces strict code quality standards through automated formatting and linting.
- Format code:
pnpm dlx ultracite fix - Check for issues:
pnpm dlx ultracite check - Diagnose setup:
pnpm dlx ultracite doctor
Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable.
Write code that is accessible, performant, type-safe, and maintainable. Focus on clarity and explicit intent over brevity.
- Use explicit types for function parameters and return values when they enhance clarity
- Prefer
unknownoveranywhen the type is genuinely unknown - Use const assertions (
as const) for immutable values and literal types - Leverage TypeScript's type narrowing instead of type assertions
- Use meaningful variable names instead of magic numbers - extract constants with descriptive names
- Use arrow functions for callbacks and short functions
- Prefer
for...ofloops over.forEach()and indexedforloops - Use optional chaining (
?.) and nullish coalescing (??) for safer property access - Prefer template literals over string concatenation
- Use destructuring for object and array assignments
- Use
constby default,letonly when reassignment is needed, nevervar
- Always
awaitpromises in async functions - don't forget to use the return value - Use
async/awaitsyntax instead of promise chains for better readability - Handle errors appropriately in async code with try-catch blocks
- Don't use async functions as Promise executors
- Use function components over class components
- Call hooks at the top level only, never conditionally
- Specify all dependencies in hook dependency arrays correctly
- Use the
keyprop for elements in iterables (prefer unique IDs over array indices) - Nest children between opening and closing tags instead of passing as props
- Don't define components inside other components
- Use semantic HTML and ARIA attributes for accessibility:
- Provide meaningful alt text for images
- Use proper heading hierarchy
- Add labels for form inputs
- Include keyboard event handlers alongside mouse events
- Use semantic elements (
<button>,<nav>, etc.) instead of divs with roles
- Remove
console.log,debugger, andalertstatements from production code - Throw
Errorobjects with descriptive messages, not strings or other values - Use
try-catchblocks meaningfully - don't catch errors just to rethrow them - Prefer early returns over nested conditionals for error cases
- Keep functions focused and under reasonable cognitive complexity limits
- Extract complex conditions into well-named boolean variables
- Use early returns to reduce nesting
- Prefer simple conditionals over nested ternary operators
- Group related code together and separate concerns
- Add
rel="noopener"when usingtarget="_blank"on links - Avoid
dangerouslySetInnerHTMLunless absolutely necessary - Don't use
eval()or assign directly todocument.cookie - Validate and sanitize user input
- Avoid spread syntax in accumulators within loops
- Use top-level regex literals instead of creating them in loops
- Prefer specific imports over namespace imports
- Avoid barrel files (index files that re-export everything)
- Use proper image components (e.g., Next.js
<Image>) over<img>tags
Next.js:
- Use Next.js
<Image>component for images - Use
next/heador App Router metadata API for head elements - Use Server Components for async data fetching instead of async Client Components
React 19+:
- Use ref as a prop instead of
React.forwardRef
Solid/Svelte/Vue/Qwik:
- Use
classandforattributes (notclassNameorhtmlFor)
- Write assertions inside
it()ortest()blocks - Avoid done callbacks in async tests - use async/await instead
- Don't use
.onlyor.skipin committed code - Keep test suites reasonably flat - avoid excessive
describenesting
Biome's linter will catch most issues automatically. Focus your attention on:
- Business logic correctness - Biome can't validate your algorithms
- Meaningful naming - Use descriptive names for functions, variables, and types
- Architecture decisions - Component structure, data flow, and API design
- Edge cases - Handle boundary conditions and error states
- User experience - Accessibility, performance, and usability considerations
- Documentation - Add comments for complex logic, but prefer self-documenting code
Most formatting and common issues are automatically fixed by Biome. Run pnpm dlx ultracite fix before committing to ensure compliance.