mirror of
https://github.com/vercel/chat.git
synced 2026-09-14 18:32:29 +08:00
4115c9431e
Adds Vercel Connect support to the Linear adapter: - `accessToken` now accepts a `() => string | Promise<string>` resolver in addition to a string, so tokens can be sourced from Vercel Connect at runtime. - A new optional `webhookVerifier` verifies inbound webhooks (Connect trigger-forwarded requests via a Vercel OIDC token) in place of the Linear webhook secret. - Connect-mode outbound calls outside webhook handling are supported via `withInstallation(organizationId, fn)`. Pair with `connectLinearAdapter()` from `@vercel/connect/chat`. Includes a changeset (`@chat-adapter/linear` minor). Stacked on #647 (base `vercel-connect/base`). ## Companion `@vercel/connect/chat` subpath: vercel/vercel#16826. <img width="929" height="664" alt="CleanShot 2026-06-30 at 12 35 30" src="https://github.com/user-attachments/assets/c5861cb9-d66b-42c6-b838-5b4983f48646" /> --------- Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com> Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
311 lines
10 KiB
Plaintext
311 lines
10 KiB
Plaintext
---
|
|
title: Linear
|
|
description: Respond to @mentions in Linear issue comment threads and agent sessions.
|
|
packageName: "@chat-adapter/linear"
|
|
slug: linear
|
|
type: platform
|
|
logo: linear
|
|
tagline: Automate Linear issue comment threads with bot responses. Supports both standard comments mode and Linear's app-actor agent sessions.
|
|
beta: true
|
|
features:
|
|
postMessage: yes
|
|
editMessage:
|
|
status: partial
|
|
label: ""
|
|
deleteMessage:
|
|
status: partial
|
|
label: ""
|
|
fileUploads: no
|
|
streaming:
|
|
status: partial
|
|
label: Agent sessions / Post+Edit
|
|
scheduledMessages: no
|
|
cardFormat:
|
|
status: yes
|
|
label: Markdown
|
|
buttons: no
|
|
linkButtons: no
|
|
selectMenus: no
|
|
tables:
|
|
status: yes
|
|
label: GFM
|
|
fields: yes
|
|
imagesInCards: no
|
|
modals: no
|
|
slashCommands: no
|
|
mentions: yes
|
|
addReactions: yes
|
|
removeReactions:
|
|
status: partial
|
|
label: ""
|
|
typingIndicator:
|
|
status: partial
|
|
label: Agent sessions
|
|
directMessages: no
|
|
ephemeralMessages: no
|
|
userLookup: yes
|
|
parentSubject: yes
|
|
nativeClient: yes
|
|
customApiEndpoint: yes
|
|
fetchMessages: yes
|
|
fetchSingleMessage: no
|
|
fetchThreadInfo: yes
|
|
fetchChannelMessages: no
|
|
listThreads: no
|
|
fetchChannelInfo: no
|
|
postChannelMessage: no
|
|
---
|
|
|
|
## Install
|
|
|
|
<PackageInstall package="@chat-adapter/linear" />
|
|
|
|
## Quick start
|
|
|
|
<Callout type="info">
|
|
The adapter auto-detects credentials from `LINEAR_API_KEY`, `LINEAR_ACCESS_TOKEN`, `LINEAR_CLIENT_CREDENTIALS_*`, or `LINEAR_CLIENT_ID`/`LINEAR_CLIENT_SECRET`, plus `LINEAR_WEBHOOK_SECRET` and `LINEAR_BOT_USERNAME`.
|
|
</Callout>
|
|
|
|
```typescript title="lib/bot.ts" lineNumbers
|
|
import { Chat } from "chat";
|
|
import { createLinearAdapter } from "@chat-adapter/linear";
|
|
|
|
const bot = new Chat({
|
|
userName: "my-bot",
|
|
adapters: {
|
|
linear: createLinearAdapter(),
|
|
},
|
|
});
|
|
|
|
bot.onNewMention(async (thread, message) => {
|
|
await thread.post("Hello from Linear!");
|
|
});
|
|
```
|
|
|
|
By default, the adapter runs in `mode: "comments"` and treats `Comment` webhooks as the inbound message source. For Linear app-actor installs, set `mode: "agent-sessions"` so inbound handling is driven by `AgentSessionEvent`.
|
|
|
|
## Configuration
|
|
|
|
<TypeTable
|
|
type={{
|
|
apiKey: {
|
|
type: "string",
|
|
description:
|
|
"Personal API key. Auto-detected from `LINEAR_API_KEY`.",
|
|
},
|
|
accessToken: {
|
|
type: "string | (() => string | Promise<string>)",
|
|
description:
|
|
"OAuth access token. Accepts a string, or (Vercel Connect) a resolver invoked per API call. Auto-detected from `LINEAR_ACCESS_TOKEN`.",
|
|
},
|
|
clientId: {
|
|
type: "string",
|
|
description:
|
|
"Multi-tenant OAuth client ID. Auto-detected from `LINEAR_CLIENT_ID`.",
|
|
},
|
|
clientSecret: {
|
|
type: "string",
|
|
description:
|
|
"Multi-tenant OAuth client secret. Auto-detected from `LINEAR_CLIENT_SECRET`.",
|
|
},
|
|
encryptionKey: {
|
|
type: "string",
|
|
description:
|
|
"AES-256-GCM key for encrypting stored OAuth tokens. Auto-detected from `LINEAR_ENCRYPTION_KEY`.",
|
|
},
|
|
clientCredentials: {
|
|
type: "ClientCredentialsConfig",
|
|
description:
|
|
"Single-tenant client credentials config (`clientId`, `clientSecret`, optional `scopes`).",
|
|
},
|
|
mode: {
|
|
type: '"comments" | "agent-sessions"',
|
|
default: '"comments"',
|
|
description:
|
|
"Inbound webhook handling mode. Use `agent-sessions` for app-actor installs.",
|
|
},
|
|
webhookSecret: {
|
|
type: "string",
|
|
description:
|
|
"Webhook signing secret. Auto-detected from `LINEAR_WEBHOOK_SECRET`.",
|
|
},
|
|
webhookVerifier: {
|
|
type: "(request, body) => unknown | Promise<unknown>",
|
|
description:
|
|
"Custom verifier used in place of `webhookSecret` (e.g. Vercel Connect OIDC). Takes precedence over the secret; required in Connect mode.",
|
|
},
|
|
userName: {
|
|
type: "string",
|
|
default: '"linear-bot"',
|
|
description: "Bot display name. Auto-detected from `LINEAR_BOT_USERNAME`.",
|
|
},
|
|
apiUrl: {
|
|
type: "string",
|
|
description:
|
|
"Override the Linear GraphQL API base URL.",
|
|
},
|
|
}}
|
|
/>
|
|
|
|
One of `apiKey`, `accessToken` (string or Vercel Connect resolver), top-level
|
|
`clientId`/`clientSecret`, or `clientCredentials` is required, plus either
|
|
`webhookSecret` or a `webhookVerifier`.
|
|
|
|
## Authentication
|
|
|
|
### Option A — Personal API key
|
|
|
|
Best for personal projects or single-workspace bots. Actions are attributed to you as an individual.
|
|
|
|
1. Go to [Settings then Security & Access](https://linear.app/settings/account/security).
|
|
2. Under **Personal API keys**, click **Create key**.
|
|
3. Choose **Only select permissions** and enable Create issues + Create comments.
|
|
4. Set `LINEAR_API_KEY`.
|
|
|
|
```typescript
|
|
createLinearAdapter({ apiKey: process.env.LINEAR_API_KEY! });
|
|
```
|
|
|
|
### Option B — OAuth access token
|
|
|
|
Use this when your app already manages the OAuth flow:
|
|
|
|
```typescript
|
|
createLinearAdapter({ accessToken: process.env.LINEAR_ACCESS_TOKEN! });
|
|
```
|
|
|
|
### Option C — Multi-tenant OAuth installs
|
|
|
|
Use top-level `clientId` / `clientSecret` for Slack-style multi-tenant installs. Each Linear workspace install is stored separately, webhook requests resolve the correct workspace token by `organizationId`, and `withInstallation()` lets you target a specific organization outside webhook handling.
|
|
|
|
1. Go to [Settings then API then Applications](https://linear.app/settings/api/applications/new).
|
|
2. Create the OAuth2 application.
|
|
3. Note the **Client ID** and **Client Secret**.
|
|
|
|
```typescript
|
|
const adapter = createLinearAdapter({
|
|
clientId: process.env.LINEAR_CLIENT_ID!,
|
|
clientSecret: process.env.LINEAR_CLIENT_SECRET!,
|
|
mode: "agent-sessions",
|
|
});
|
|
|
|
await bot.initialize();
|
|
const { organizationId } = await adapter.handleOAuthCallback(request, {
|
|
redirectUri: process.env.LINEAR_REDIRECT_URI!,
|
|
});
|
|
|
|
await adapter.withInstallation(organizationId, async () => {
|
|
await adapter.postMessage("linear:issue-id", "Hello from a background job");
|
|
});
|
|
```
|
|
|
|
### Option D — Single-tenant client credentials
|
|
|
|
App identity without multi-tenant installs. The adapter fetches and refreshes the token automatically.
|
|
|
|
```typescript
|
|
createLinearAdapter({
|
|
clientCredentials: {
|
|
clientId: process.env.LINEAR_CLIENT_CREDENTIALS_CLIENT_ID!,
|
|
clientSecret: process.env.LINEAR_CLIENT_CREDENTIALS_CLIENT_SECRET!,
|
|
scopes: ["read", "write", "comments:create", "issues:create"],
|
|
},
|
|
mode: "agent-sessions",
|
|
});
|
|
```
|
|
|
|
### Option E — Vercel Connect
|
|
|
|
Use [Vercel Connect](https://vercel.com/docs/connect) to source the Linear access token at runtime instead of storing a long-lived token or OAuth secret. The `connectLinearAdapter()` helper from [`@vercel/connect/chat`](https://www.npmjs.com/package/@vercel/connect) wires an `accessToken` resolver and a `webhookVerifier` for Connect trigger-forwarded webhooks:
|
|
|
|
```typescript
|
|
import { createLinearAdapter } from "@chat-adapter/linear";
|
|
import { connectLinearAdapter } from "@vercel/connect/chat";
|
|
|
|
createLinearAdapter({
|
|
...connectLinearAdapter("linear/acme-linear"),
|
|
mode: "agent-sessions",
|
|
});
|
|
```
|
|
|
|
`accessToken` accepts a `string` or `() => string | Promise<string>` resolver invoked per API call. When `webhookVerifier` is set it takes precedence over `webhookSecret` and `LINEAR_WEBHOOK_SECRET`.
|
|
|
|
## Advanced
|
|
|
|
### Token encryption
|
|
|
|
For multi-tenant OAuth installs, pass a base64-encoded 32-byte key as `encryptionKey` (or set `LINEAR_ENCRYPTION_KEY`) to encrypt stored access and refresh tokens at rest:
|
|
|
|
```bash
|
|
openssl rand -base64 32
|
|
```
|
|
|
|
When `encryptionKey` is set, `setInstallation()` encrypts tokens before writing to the configured state adapter. Existing plaintext records continue to work — you can roll the key in without flushing installs.
|
|
|
|
### Making the bot @-mentionable
|
|
|
|
To make the bot appear in Linear's `@`-mention dropdown as an Agent:
|
|
|
|
1. In your OAuth app settings, enable **Agent session events** under webhooks.
|
|
2. Have a workspace admin install the app with `actor=app` and the `app:mentionable` scope:
|
|
|
|
```
|
|
https://linear.app/oauth/authorize?
|
|
client_id=your_client_id&
|
|
redirect_uri=https://your-domain.com/callback&
|
|
response_type=code&
|
|
scope=read,write,comments:create,issues:create,app:mentionable&
|
|
actor=app
|
|
```
|
|
|
|
Once installed with `actor=app`, set `mode: "agent-sessions"` so the adapter treats `AgentSessionEvent` as the entrypoint:
|
|
|
|
- `onNewMention` fires from session-created events.
|
|
- `thread.startTyping()` sends an ephemeral Linear `thought`.
|
|
- `thread.post(stream)` uses agent activities and session plan updates.
|
|
- Session threads are append-only; `sent.edit()` / `sent.delete()` are not supported there.
|
|
|
|
See the [Linear Agents docs](https://linear.app/developers/agents) for full details.
|
|
|
|
### Direct API client
|
|
|
|
Access the underlying [LinearClient](https://github.com/linear/linear/tree/master/packages/sdk) via `.linearClient`:
|
|
|
|
```typescript
|
|
const linear = bot.getAdapter("linear").linearClient;
|
|
const issue = await linear.issue("ENG-123");
|
|
```
|
|
|
|
API key, access token, and single-tenant client-credentials modes return the same client anywhere. Multi-tenant OAuth requires webhook-handler context.
|
|
|
|
> The previous `.client` getter still works as a deprecated alias for `.linearClient`.
|
|
|
|
### Webhook setup
|
|
|
|
<Callout type="warn">
|
|
Webhook management requires workspace admin access. If you don't see the API settings page, ask a workspace admin.
|
|
</Callout>
|
|
|
|
1. Go to **Settings then API** then click **Create webhook**.
|
|
2. Set the URL to `https://your-domain.com/api/webhooks/linear`.
|
|
3. Copy the **Signing secret** to `LINEAR_WEBHOOK_SECRET`.
|
|
4. Under **Data change events**, select **Comments** (required for `mode: "comments"`), **Agent session events** (required for `mode: "agent-sessions"`), **Issues**, and optionally **Emoji reactions**.
|
|
5. Choose a team selection and click **Create webhook**.
|
|
|
|
### Thread model
|
|
|
|
Linear has four thread variants:
|
|
|
|
| Type | Description | Thread ID format |
|
|
|------|-------------|-----------------|
|
|
| Issue-level | Top-level comments on an issue | `linear:{issueId}` |
|
|
| Comment thread | Replies nested under a comment | `linear:{issueId}:c:{commentId}` |
|
|
| Agent session on issue | App-actor session on an issue | `linear:{issueId}:s:{agentSessionId}` |
|
|
| Agent session on comment thread | App-actor session on a comment thread | `linear:{issueId}:c:{commentId}:s:{agentSessionId}` |
|
|
|
|
When a user writes a comment, the bot replies within the same comment thread.
|
|
|
|
## Feature support
|
|
|
|
<FeatureSupport />
|