Malte Ubl d2ffcc2cbe The great rename
We've acquired `chat` on npm. This plan covers renaming all packages and setting up automated publishing.

| Current Name | New Name | Version |
|--------------|----------|---------|
| `chat-sdk` | `chat` | 4.0.0 |
| `@chat-sdk/slack` | `@chat-adapter/slack` | 4.0.0 |
| `@chat-sdk/gchat` | `@chat-adapter/gchat` | 4.0.0 |
| `@chat-sdk/teams` | `@chat-adapter/teams` | 4.0.0 |
| `@chat-sdk/state-memory` | `@chat-adapter/state-memory` | 4.0.0 |
| `@chat-sdk/state-redis` | `@chat-adapter/state-redis` | 4.0.0 |
| `@chat-sdk/state-ioredis` | `@chat-adapter/state-ioredis` | 4.0.0 |

1. **Main package** (`packages/chat-sdk/package.json`):
   - Change `"name": "chat-sdk"` → `"name": "chat"`
   - Change `"version": "0.1.0"` → `"version": "4.0.0"`

2. **Adapter packages**:
   - `packages/adapter-slack/package.json`: `@chat-sdk/slack` → `@chat-adapter/slack`
   - `packages/adapter-gchat/package.json`: `@chat-sdk/gchat` → `@chat-adapter/gchat`
   - `packages/adapter-teams/package.json`: `@chat-sdk/teams` → `@chat-adapter/teams`
   - Update all versions to `4.0.0`
   - Update dependency on `chat-sdk` → `chat`

3. **State packages**:
   - `packages/state-memory/package.json`: `@chat-sdk/state-memory` → `@chat-adapter/state-memory`
   - `packages/state-redis/package.json`: `@chat-sdk/state-redis` → `@chat-adapter/state-redis`
   - `packages/state-ioredis/package.json`: `@chat-sdk/state-ioredis` → `@chat-adapter/state-ioredis`
   - Update all versions to `4.0.0`
   - Update dependency on `chat-sdk` → `chat`

Update `peerDependencies` and `dependencies` in each package:

```json
// Before
"peerDependencies": {
  "chat-sdk": "workspace:*"
}

// After
"peerDependencies": {
  "chat": "workspace:*"
}
```

Search and replace across the codebase:

| Find | Replace |
|------|---------|
| `from "chat-sdk"` | `from "chat"` |
| `from 'chat-sdk'` | `from 'chat'` |
| `import("chat-sdk")` | `import("chat")` |
| `from "@chat-sdk/slack"` | `from "@chat-adapter/slack"` |
| `from "@chat-sdk/gchat"` | `from "@chat-adapter/gchat"` |
| `from "@chat-sdk/teams"` | `from "@chat-adapter/teams"` |
| `from "@chat-sdk/state-memory"` | `from "@chat-adapter/state-memory"` |
| `from "@chat-sdk/state-redis"` | `from "@chat-adapter/state-redis"` |
| `from "@chat-sdk/state-ioredis"` | `from "@chat-adapter/state-ioredis"` |

Update any references to old package names in:
- `pnpm-workspace.yaml`
- `turbo.json` (task filters)
- Root `package.json` scripts

1. **README.md files** in each package
2. **Examples** (`examples/nextjs-chat/`)
3. **AGENTS.md** and other docs
4. **JSDoc comments** referencing package names

1. **Install changesets**:
   ```bash
   pnpm add -D @changesets/cli
   pnpm changeset init
   ```

2. **Configure `.changeset/config.json`**:
   ```json
   {
     "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
     "changelog": "@changesets/cli/changelog",
     "commit": false,
     "fixed": [],
     "linked": [
       ["chat", "@chat-adapter/*"]
     ],
     "access": "public",
     "baseBranch": "main",
     "updateInternalDependencies": "patch",
     "ignore": ["example-nextjs-chat"]
   }
   ```

3. **Add GitHub Action** (`.github/workflows/release.yml`):
   ```yaml
   name: Release

   on:
     push:
       branches:
         - main

   concurrency: ${{ github.workflow }}-${{ github.ref }}

   jobs:
     release:
       name: Release
       runs-on: ubuntu-latest
       steps:
         - uses: actions/checkout@v4
         - uses: pnpm/action-setup@v2
           with:
             version: 9
         - uses: actions/setup-node@v4
           with:
             node-version: 20
             cache: 'pnpm'

         - run: pnpm install
         - run: pnpm build
         - run: pnpm test

         - name: Create Release Pull Request or Publish
           uses: changesets/action@v1
           with:
             publish: pnpm changeset publish
           env:
             GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
             NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
   ```

4. **Add publish config to each package.json**:
   ```json
   "publishConfig": {
     "access": "public"
   }
   ```

1. Run full build: `pnpm build`
2. Run typecheck: `pnpm typecheck`
3. Run lint: `pnpm lint`
4. Run tests: `pnpm test`
5. Test local linking in example app

| File | Changes |
|------|---------|
| `packages/chat-sdk/package.json` | Rename to `chat`, version 4.0.0 |
| `packages/adapter-slack/package.json` | Rename to `@chat-adapter/slack`, update deps |
| `packages/adapter-gchat/package.json` | Rename to `@chat-adapter/gchat`, update deps |
| `packages/adapter-teams/package.json` | Rename to `@chat-adapter/teams`, update deps |
| `packages/state-memory/package.json` | Rename to `@chat-adapter/state-memory`, update deps |
| `packages/state-redis/package.json` | Rename to `@chat-adapter/state-redis`, update deps |
| `packages/state-ioredis/package.json` | Rename to `@chat-adapter/state-ioredis`, update deps |
| `examples/nextjs-chat/package.json` | Update all dependencies |
| `packages/integration-tests/package.json` | Update all dependencies |
| `turbo.json` | Update package references |
| All `*.ts` files | Update import statements |
| All `README.md` files | Update package names in docs |
| `.changeset/config.json` | Create new |
| `.github/workflows/release.yml` | Create new |

1. Update all `package.json` files (names, versions, dependencies)
2. Run `pnpm install` to update lockfile
3. Update all import statements in source files
4. Update all import statements in test files
5. Update documentation
6. Set up changesets
7. Run full validation
8. Create initial changeset for 4.0.0 release
9. Commit and push

If issues arise:
1. Git revert the rename commit
2. Run `pnpm install` to restore lockfile
3. Previous npm versions remain available
2026-01-02 13:56:55 -08:00
2026-01-02 13:56:55 -08:00
2026-01-02 13:56:55 -08:00
2026-01-02 13:56:55 -08:00
2026-01-02 13:56:55 -08:00
2026-01-02 10:21:07 -08:00
2025-12-21 20:48:14 -08:00
2026-01-02 13:56:55 -08:00
JSX
2026-01-01 19:01:37 -08:00
2026-01-02 13:56:55 -08:00
2025-12-31 13:14:26 -08:00
2026-01-02 13:56:55 -08:00
2025-12-22 18:21:52 -08:00
2026-01-02 13:56:55 -08:00
2025-12-21 20:48:14 -08:00
2026-01-02 13:56:55 -08:00
2026-01-02 13:56:55 -08:00
2026-01-02 13:56:55 -08:00
2026-01-02 06:00:29 -08:00
2026-01-01 17:25:27 -08:00
2025-12-21 20:48:14 -08:00
2025-12-31 14:24:46 -08:00

Chat SDK

A unified SDK for building chat bots across Slack, Microsoft Teams, and Google Chat.

Features

  • Multi-platform support with a single codebase
  • Mention-based thread subscriptions
  • Reaction handling with type-safe emoji
  • Cross-platform emoji helper for consistent rendering
  • Rich cards with buttons - TSX or object-based cards
  • Action callbacks - Handle button clicks across platforms
  • File uploads - Send files with messages
  • DM support - Initiate direct messages programmatically
  • Message deduplication for platform quirks
  • Serverless-ready with pluggable state backends

Packages

Package Description
chat Core SDK with thread management and handlers
@chat-adapter/slack Slack adapter
@chat-adapter/teams Microsoft Teams adapter
@chat-adapter/gchat Google Chat adapter with Workspace Events
@chat-adapter/state-memory In-memory state (development)
@chat-adapter/state-redis Redis state using redis package (production)
@chat-adapter/state-ioredis Redis state using ioredis package (production)

Quick Start

1. Create your bot (lib/bot.ts)

import { Chat, emoji } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createTeamsAdapter } from "@chat-adapter/teams";
import { createGoogleChatAdapter } from "@chat-adapter/gchat";
import { createRedisState } from "@chat-adapter/state-redis";

export const bot = new Chat({
  userName: "mybot",
  adapters: {
    slack: createSlackAdapter({
      botToken: process.env.SLACK_BOT_TOKEN!,
      signingSecret: process.env.SLACK_SIGNING_SECRET!,
    }),
    teams: createTeamsAdapter({
      appId: process.env.TEAMS_APP_ID!,
      appPassword: process.env.TEAMS_APP_PASSWORD!,
    }),
    gchat: createGoogleChatAdapter({
      credentials: JSON.parse(process.env.GOOGLE_CHAT_CREDENTIALS!),
    }),
  },
  state: createRedisState({ url: process.env.REDIS_URL! }),
});

// Handle @mentions - works across all platforms
bot.onNewMention(async (thread) => {
  await thread.subscribe();
  // Emoji auto-converts to platform format: :wave: on Slack, 👋 on Teams/GChat
  await thread.post(`${emoji.wave} Hello! I'm now listening to this thread.`);
});

// Handle follow-up messages in subscribed threads
bot.onSubscribedMessage(async (thread, message) => {
  await thread.post(`${emoji.check} You said: ${message.text}`);
});

// Handle emoji reactions (type-safe emoji values)
bot.onReaction([emoji.thumbs_up, emoji.heart, emoji.fire], async (event) => {
  if (!event.added) return; // Only respond to added reactions
  await event.adapter.addReaction(event.threadId, event.messageId, event.emoji);
});

2. Create a webhook handler (app/api/webhooks/[platform]/route.ts)

import { after } from "next/server";
import { bot } from "@/lib/bot";

type Platform = keyof typeof bot.webhooks;

export async function POST(
  request: Request,
  { params }: { params: Promise<{ platform: string }> },
) {
  const { platform } = await params;

  const handler = bot.webhooks[platform as Platform];
  if (!handler) {
    return new Response(`Unknown platform: ${platform}`, { status: 404 });
  }

  return handler(request, {
    waitUntil: (task) => after(() => task),
  });
}

This creates endpoints for each platform:

  • POST /api/webhooks/slack
  • POST /api/webhooks/teams
  • POST /api/webhooks/gchat

The waitUntil option ensures message processing completes after the response is sent (required for serverless).

Setup

See SETUP.md for platform configuration instructions including:

  • Slack app creation and OAuth scopes
  • Microsoft Teams Azure Bot setup
  • Google Chat service account and Pub/Sub configuration
  • Environment variables reference

Emoji Helper

The emoji helper provides type-safe, cross-platform emoji that automatically convert to each platform's format. Use it with thread.post():

await thread.post(`${emoji.thumbs_up} Great job!`);
// Slack: ":+1: Great job!"
// Teams/GChat: "👍 Great job!"

Available emoji:

Name Emoji Name Emoji
emoji.thumbs_up 👍 emoji.thumbs_down 👎
emoji.heart ❤️ emoji.smile 😊
emoji.laugh 😂 emoji.thinking 🤔
emoji.eyes 👀 emoji.fire 🔥
emoji.check ✅ emoji.x ❌
emoji.question ❓ emoji.party 🎉
emoji.rocket 🚀 emoji.star ⭐
emoji.wave 👋 emoji.clap 👏
emoji["100"] 💯 emoji.warning ⚠️

For one-off custom emoji, use emoji.custom("name").

Custom Emoji (Type-Safe)

For workspace-specific emoji with full type safety, use createEmoji():

import { createEmoji } from "chat";

// Create emoji helper with custom emoji
const myEmoji = createEmoji({
  unicorn: { slack: "unicorn_face", gchat: "🦄" },
  company_logo: { slack: "company", gchat: "🏢" },
});

// Type-safe access to custom emoji (with autocomplete)
const message = `${myEmoji.unicorn} Magic! ${myEmoji.company_logo}`;
// Slack: ":unicorn_face: Magic! :company:"
// GChat: "🦄 Magic! 🏢"

Rich Cards with Buttons

Send interactive cards with buttons that work across all platforms. Cards automatically convert to Block Kit (Slack), Adaptive Cards (Teams), and Google Chat Cards.

Configure your tsconfig.json to use the chat JSX runtime:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "chat"
  }
}

Then use JSX syntax:

import { Card, CardText, Button, Actions, Section, Fields, Field, Divider, Image } from "chat";

// Simple card with buttons
await thread.post(
  <Card title="Order #1234">
    <CardText>Your order has been received!</CardText>
    <Section>
      <CardText style="bold">Total: $50.00</CardText>
    </Section>
    <Actions>
      <Button id="approve" style="primary">Approve</Button>
      <Button id="reject" style="danger">Reject</Button>
    </Actions>
  </Card>
);

// Card with fields (key-value pairs)
await thread.post(
  <Card title="User Profile">
    <Fields>
      <Field label="Name" value="John Doe" />
      <Field label="Role" value="Developer" />
      <Field label="Team" value="Platform" />
    </Fields>
    <Divider />
    <Actions>
      <Button id="edit">Edit Profile</Button>
    </Actions>
  </Card>
);

// Card with image
await thread.post(
  <Card title="Product Update">
    <Image url="https://example.com/product.png" alt="Product screenshot" />
    <CardText>Check out our new feature!</CardText>
  </Card>
);

Note: Use CardText (not Text) when using JSX to avoid conflicts with React's built-in types.

Action Callbacks

Handle button clicks from cards:

import { Chat, type ActionEvent } from "chat";
declare const bot: Chat;

// Handle a specific action
bot.onAction("approve", async (event: ActionEvent) => {
  await event.thread.post(`Order approved by ${event.user.fullName}!`);
});

// Handle multiple actions
bot.onAction(["approve", "reject"], async (event: ActionEvent) => {
  const action = event.actionId === "approve" ? "approved" : "rejected";
  await event.thread.post(`Order ${action}!`);
});

// Catch-all action handler
bot.onAction(async (event: ActionEvent) => {
  console.log(`Action: ${event.actionId}, Value: ${event.value}`);
});

The ActionEvent includes actionId, value, user, thread, messageId, threadId, adapter, and raw properties.

File Uploads

Send files along with messages:

import type { Thread } from "chat";
declare const thread: Thread;

// Send a file with a message
const reportBuffer = Buffer.from("PDF content");
await thread.post({
  markdown: "Here's the report you requested:",
  files: [
    {
      data: reportBuffer,
      filename: "report.pdf",
      mimeType: "application/pdf",
    },
  ],
});

// Send multiple files
const image1 = Buffer.from("image1");
const image2 = Buffer.from("image2");
await thread.post({
  markdown: "Attached are the images:",
  files: [
    { data: image1, filename: "screenshot1.png" },
    { data: image2, filename: "screenshot2.png" },
  ],
});

// Files only (with minimal text)
const buffer = Buffer.from("document content");
await thread.post({
  markdown: "",
  files: [{ data: buffer, filename: "document.xlsx" }],
});

Reading Attachments

Access attachments from incoming messages:

import { Chat } from "chat";
declare const bot: Chat;

bot.onSubscribedMessage(async (thread, message) => {
  for (const attachment of message.attachments ?? []) {
    console.log(`File: ${attachment.name}, Type: ${attachment.mimeType}`);

    // Download the file data
    if (attachment.fetchData) {
      const data = await attachment.fetchData();
      // Process the file...
      console.log(`Downloaded ${data.length} bytes`);
    }
  }
});

The Attachment interface includes type, url, name, mimeType, size, width, height, and fetchData properties.

Direct Messages

Initiate DM conversations programmatically. The adapter is automatically inferred from the userId format:

import { Chat } from "chat";
declare const bot: Chat;

// Open a DM using Author object (convenient in handlers)
bot.onSubscribedMessage(async (thread, message) => {
  if (message.text === "DM me") {
    const dmThread = await bot.openDM(message.author);
    await dmThread.post("Hello! This is a direct message.");
  }
});

// Or use userId string directly - adapter inferred from format:
// - Slack: U... (e.g., "U1234567890")
// - Teams: 29:... (e.g., "29:abc123...")
// - Google Chat: users/... (e.g., "users/123456789")
const dmThread = await bot.openDM("U1234567890");

// Check if a thread is a DM
bot.onSubscribedMessage(async (thread, message) => {
  if (thread.isDM) {
    await thread.post("This is a private conversation.");
  }
});

Development

pnpm install
pnpm build
pnpm dev         # Run example app
pnpm typecheck
pnpm lint

License

MIT

S
Description
Build multi-platform chat bots with Chat SDK (chat npm package). Use when developers want to scaffold a bot with create-chat-sdk, build a Slack, Teams,…
Readme MIT 23 MiB
Languages
TypeScript 89%
MDX 10.9%