Agent integration rules

Paste this into AGENTS.md, Cursor rules, or your agent system prompt when adding Bootstrapware Chat. The same file ships inside the npm package.

# Integrating @bootstrapware/chat

Use this file as agent instructions.

## What this package does

Embeddable private messaging inside an authenticated product: 1:1 and groups (max 20), read/unread, plain text with light markup, attachments.

**Two modes**

- **BYO ($9.99):** you own conversations and files. Pass a `ChatAdapter`. Bootstrapware only hosts app config.
- **Hosted ($19.99):** omit adapter; pass `appId` + `publishableKey`. Messages and files live on Bootstrapware. Customer moderates in the dashboard.

Never send message body or file bytes through MCP. MCP configures apps only.

## Install algorithm (do this in order)

1. Read this file. Call `list_capabilities` on Chat MCP before inventing tools.
2. If the user only needs a working UI in this app: Path A. Do not invent keys.
3. If they need live Hosted or BYO config: Path B or C.
4. Never invent `user.id`. Wire the **real** session id from the host app.
5. Never mint API keys via MCP. If no publishable key is in env, **stop and ask the human**.

### Path A — local, zero keys (true seamless)

```bash
pnpm add @bootstrapware/chat
```

```tsx
import { Chat, createLocalAdapter } from "@bootstrapware/chat";
import "@bootstrapware/chat/styles.css";

<Chat
  user={{ id: session.user.id, name: session.user.name, avatarUrl: session.user.image }}
  people={workspaceMembers.map((m) => ({ id: m.id, name: m.name, avatarUrl: m.image }))}
  adapter={createLocalAdapter({ storageKey: "demo-chat" })}
/>
```

`people` is display and picker only. Identity stays `user.id`. If omitted, the New form still has a collapsed **Other id** field.

Optional corner launcher (default off):

```tsx
<Chat
  launcher
  user={{ id: session.user.id, name: session.user.name }}
  people={workspaceMembers}
  adapter={createLocalAdapter({ storageKey: "demo-chat" })}
/>
```

Nudge with `--bsw-ch-launcher-right` and `--bsw-ch-launcher-bottom`. Escape or the backdrop closes it. Unread total is the sum of `unreadCount`.

Optional browser notifications stay **off** until the end user clicks **Notify** in the widget header. That click is the only time the widget requests `Notification` permission. Incoming messages from others can then show a system notification while the tab is hidden. Visible-tab and self messages do not. Denied permission stays off with no re-prompt loop. No email, SMS, or server push.

### Path B — Hosted

1. Confirm Cursor MCP `bootstrapware-chat` at `https://chat.bootstrapware.co/mcp` (OAuth URL-only). If disconnected, tell the human to click **Add to Cursor (OAuth)** on https://app.bootstrapware.co/chat/keys
2. `list_capabilities` → `create_app` → `update_draft` (toggles + `allowedOrigins` for localhost and production) → `publish_app`
3. `get_install_snippet` (or the `nextSteps` from create/publish). `appId` is real. The key is a placeholder.
4. If env has no `bsw_test_pub_` / `bsw_live_pub_` value, **stop**. Ask the human to mint a test publishable key at https://app.bootstrapware.co/chat/keys and paste it into `.env.local`
5. Embed with the real session user id, never `"user_1"` in production.

```tsx
<Chat
  appId="cha_..."
  publishableKey={process.env.NEXT_PUBLIC_BSW_CHAT_PUBLISHABLE_KEY}
  user={{ id: session.user.id, name: session.user.name }}
  people={workspaceMembers}
/>
```

Optional `apiBaseUrl` defaults to `https://chat.bootstrapware.co`.

### Path C — BYO

Same MCP app config as Path B. Implement `ChatAdapter` on the customer backend. Bodies and file bytes never go to Bootstrapware.

`createConversation` / `addMembers` may send optional `participants: { id, name? }[]` (same ids as today). You can ignore names; the widget still titles and avatars from `people`.

```tsx
<Chat
  appId="cha_..."
  user={currentUser}
  adapter={{
    listConversations: async (input) =>
      fetch(`/api/chat/conversations?q=${input.q ?? ""}`).then((r) => r.json()),
    createConversation: async (input) =>
      fetch("/api/chat/conversations", { method: "POST", body: JSON.stringify(input) }).then((r) => r.json()),
    listMessages: async (input) =>
      fetch(`/api/chat/conversations/${input.conversationId}/messages`).then((r) => r.json()),
    sendMessage: async (input) =>
      fetch(`/api/chat/conversations/${input.conversationId}/messages`, {
        method: "POST",
        body: JSON.stringify(input),
      }).then((r) => r.json()),
    editMessage: async (input) =>
      fetch(`/api/chat/messages/${input.messageId}`, { method: "PATCH", body: JSON.stringify(input) }).then((r) => r.json()),
    deleteMessage: async (input) =>
      fetch(`/api/chat/messages/${input.messageId}`, { method: "POST", body: JSON.stringify({ action: "delete", ...input }) }).then((r) => r.json()),
    markRead: async (input) => {
      await fetch(`/api/chat/conversations/${input.conversationId}/read`, { method: "POST", body: JSON.stringify(input) });
    },
    uploadAttachment: async ({ file }) => {
      const body = new FormData();
      body.append("file", file);
      return fetch("/api/chat/attachments", { method: "POST", body }).then((r) => r.json());
    },
  }}
/>
```

## Identity

Bootstrapware does **not** authenticate end users. Your app asserts:

```tsx
user={{ id: "opaque-stable-id", name: "Ada" }}
```

`id` is opaque. Optional `email` / `avatarUrl` are display metadata only. Anonymous chat is not supported. Pass `people` for the directory (picker, titles, avatars). Avatars resolve from `people` then `user.avatarUrl`, else initials. Hosted can persist optional participant `name` on create/addMembers; there is no avatar column.

Optional `authorToken`: short-lived host-signed assertion minted with your Chat **secret** key via `POST /api/v1/author-tokens`. Required when the app has `requireAuthorToken: true`.

```bash
curl -s -X POST https://chat.bootstrapware.co/api/v1/author-tokens \
  -H "Authorization: Bearer $BSW_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"authorId":"user_123","appId":"cha_...","expiresInSec":3600}'
```

Pass the returned `authorToken` into `<Chat authorToken={...} />`. Never mint with the publishable key.

## Hosted management (secret key)

Secret keys stay server-side. `Authorization: Bearer` against `https://chat.bootstrapware.co`.

```bash
curl -s -X POST https://chat.bootstrapware.co/api/v1/apps \
  -H "Authorization: Bearer $BSW_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"name":"In-app chat"}'

curl -s -X PATCH https://chat.bootstrapware.co/api/v1/apps/APP_ID \
  -H "Authorization: Bearer $BSW_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"name":"In-app chat","config":{"allowDirect":true,"allowGroups":true,"allowAttachments":true,"allowMemberManage":true,"requireAuthorToken":false,"emptyState":"No conversations yet. Start one.","allowedOrigins":["http://localhost:3000"]}}'

curl -s -X POST https://chat.bootstrapware.co/api/v1/apps/APP_ID \
  -H "Authorization: Bearer $BSW_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"action":"publish"}'
```

Also available: `GET /api/v1/apps`, `GET /api/v1/apps/:id`, `GET /api/v1/usage`.

## Hosted MCP (Cursor)

HTTP MCP endpoint: `https://chat.bootstrapware.co/mcp`

**Preferred:** OAuth Connect, URL-only. **Add to Cursor (OAuth)** on https://app.bootstrapware.co/chat/keys

**Fallback:** test/live secret as `Authorization: Bearer`.

Tools: `list_apps`, `get_app`, `create_app`, `update_draft` (does not publish), `publish_app`, `get_published_config`, `get_install_snippet`, `list_capabilities`.

**Dashboard-only (do not invent MCP tools):** API key mint/revoke, webhooks, app delete, branding, billing, Hosted inbox (including Team send). Call `list_capabilities`.

Publishing stores hosted app config. Live `bsw_live_pub_` config fetch needs BYO ($9.99) or Hosted ($19.99). Live Hosted message and file storage needs Hosted ($19.99). Test publishable keys work without that.

Hosted files: 1 GB included. Optional Chat storage add-on $9.99/mo raises the cap to 10 GB. Uploads freeze at the cap; text still works.

## Limits

Plain text 4,000 characters with a tiny client-rendered subset (`**bold**`, `_italic_`, `` `code` ``, simple lists). Hosted inbox and JSON export stay raw text. 50 messages per page. Groups max 20. Attachments: jpeg/png/webp/gif/pdf, 10 MB, max 4 per message. Poll every 4s if there is no `subscribe` or the stream errors; about 20s heartbeat when SSE is healthy. Hosted `subscribe` is app-scoped so inbox unread updates while a thread is open. Conversation search is debounced (~250ms). Browser notifications are opt-in in the widget (default off); permission is never requested on load.

## Security

- Do not put `bsw_live_sec_` or `bsw_test_sec_` in client code.
- Do not upload message bodies or file bytes to Bootstrapware MCP.
- `user.id` is a trust boundary. Prefer origins + `requireAuthorToken` in production Hosted.

Related: Cursor guide · Agent brief · Prompt template · Quickstart · API