@chat-adapter/linear 4.13.1 → 4.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -220
- package/dist/index.d.ts +20 -20
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/package.json +7 -8
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# @chat-adapter/linear
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@chat-adapter/linear)
|
|
4
|
+
[](https://www.npmjs.com/package/@chat-adapter/linear)
|
|
5
|
+
|
|
6
|
+
Linear adapter for [Chat SDK](https://chat-sdk.dev/docs). Enables bots to respond to @mentions in Linear issue comment threads.
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
@@ -13,241 +16,26 @@ npm install chat @chat-adapter/linear
|
|
|
13
16
|
```typescript
|
|
14
17
|
import { Chat } from "chat";
|
|
15
18
|
import { createLinearAdapter } from "@chat-adapter/linear";
|
|
16
|
-
import { MemoryState } from "@chat-adapter/state-memory";
|
|
17
19
|
|
|
18
|
-
const
|
|
20
|
+
const bot = new Chat({
|
|
19
21
|
userName: "my-bot",
|
|
20
22
|
adapters: {
|
|
21
23
|
linear: createLinearAdapter({
|
|
22
24
|
apiKey: process.env.LINEAR_API_KEY!,
|
|
23
25
|
webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,
|
|
24
26
|
userName: "my-bot",
|
|
25
|
-
logger: console,
|
|
26
27
|
}),
|
|
27
28
|
},
|
|
28
|
-
state: new MemoryState(),
|
|
29
|
-
logger: "info",
|
|
30
29
|
});
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
chat.onNewMention(async (thread, message) => {
|
|
31
|
+
bot.onNewMention(async (thread, message) => {
|
|
34
32
|
await thread.post("Hello from Linear!");
|
|
35
33
|
});
|
|
36
34
|
```
|
|
37
35
|
|
|
38
|
-
##
|
|
39
|
-
|
|
40
|
-
| Option | Required | Description |
|
|
41
|
-
| --------------- | -------- | --------------------------------------------------------------------------------------------------- |
|
|
42
|
-
| `apiKey` | Yes\* | [Personal API key](https://linear.app/docs/api-and-webhooks) from Settings > Security & Access |
|
|
43
|
-
| `clientId` | Yes\* | [OAuth app](https://linear.app/developers/oauth-2-0-authentication) client ID |
|
|
44
|
-
| `clientSecret` | Yes\* | OAuth app client secret |
|
|
45
|
-
| `accessToken` | Yes\* | Pre-obtained [OAuth access token](https://linear.app/developers/oauth-2-0-authentication) |
|
|
46
|
-
| `webhookSecret` | Yes | [Webhook signing secret](https://linear.app/developers/webhooks#securing-webhooks) for verification |
|
|
47
|
-
| `userName` | Yes | Bot display name for @mention detection |
|
|
48
|
-
|
|
49
|
-
\*One of: `apiKey`, `clientId`/`clientSecret`, or `accessToken` is required.
|
|
50
|
-
|
|
51
|
-
## Environment Variables
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
# API Key auth (simplest)
|
|
55
|
-
LINEAR_API_KEY=lin_api_xxxxxxxxxxxx
|
|
56
|
-
|
|
57
|
-
# OR OAuth app auth (recommended for production)
|
|
58
|
-
LINEAR_CLIENT_ID=your-client-id
|
|
59
|
-
LINEAR_CLIENT_SECRET=your-client-secret
|
|
60
|
-
|
|
61
|
-
# Webhook secret (required for all auth methods)
|
|
62
|
-
LINEAR_WEBHOOK_SECRET=your-webhook-secret
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## Linear Setup
|
|
66
|
-
|
|
67
|
-
### Option A: Personal API Key
|
|
68
|
-
|
|
69
|
-
Best for personal projects, testing, or single-workspace bots.
|
|
70
|
-
|
|
71
|
-
1. Go to [Settings > Security & Access](https://linear.app/settings/account/security) in Linear
|
|
72
|
-
2. Scroll to **Personal API keys** and click **Create key**
|
|
73
|
-
3. Select **Only select permissions** and enable:
|
|
74
|
-
- **Create issues** - Create and update issues
|
|
75
|
-
- **Create comments** - Create and update issue comments
|
|
76
|
-
4. Under **Team access**, choose **All teams** or select specific teams
|
|
77
|
-
5. Click **Create** and set `LINEAR_API_KEY` environment variable
|
|
78
|
-
|
|
79
|
-
> **Note:** When using a personal API key, all actions are attributed to you as an individual.
|
|
80
|
-
|
|
81
|
-
### Option B: OAuth Application (Recommended for Apps)
|
|
82
|
-
|
|
83
|
-
Use this if you want the bot to have its **own identity** in Linear (not attributed to you personally), or if you're building a public integration. The adapter handles token management internally -- no need to store tokens.
|
|
84
|
-
|
|
85
|
-
1. Go to [Settings > API > Applications](https://linear.app/settings/api/applications/new) in Linear
|
|
86
|
-
2. Create a new OAuth2 application with your bot's name and icon
|
|
87
|
-
3. **Enable client credentials tokens** in the app settings
|
|
88
|
-
4. Note your **Client ID** and **Client Secret**
|
|
89
|
-
5. Set `LINEAR_CLIENT_ID` and `LINEAR_CLIENT_SECRET` environment variables
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
createLinearAdapter({
|
|
93
|
-
clientId: process.env.LINEAR_CLIENT_ID!,
|
|
94
|
-
clientSecret: process.env.LINEAR_CLIENT_SECRET!,
|
|
95
|
-
webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,
|
|
96
|
-
userName: "my-bot",
|
|
97
|
-
logger: console,
|
|
98
|
-
});
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
The adapter uses the [client credentials grant](https://linear.app/developers/oauth-2-0-authentication#client-credentials-tokens) to obtain tokens automatically. Tokens are valid for 30 days and auto-refresh when expired.
|
|
102
|
-
|
|
103
|
-
> **When to use which?**
|
|
104
|
-
>
|
|
105
|
-
> - **API Key** -- Personal projects, testing, single workspace. Actions attributed to you.
|
|
106
|
-
> - **OAuth App** -- Production bots, public integrations, bot has its own identity.
|
|
107
|
-
> - For making the bot `@`-mentionable as an [Agent](https://linear.app/developers/agents), see the [OAuth setup guide](#oauth-setup-guide) below.
|
|
108
|
-
|
|
109
|
-
### Webhook Setup
|
|
110
|
-
|
|
111
|
-
See the [Linear Webhooks documentation](https://linear.app/developers/webhooks) for detailed instructions.
|
|
112
|
-
|
|
113
|
-
> **Note:** Webhook management requires **workspace admin** access. If you don't see the API settings page, ask a workspace admin to create the webhook for you.
|
|
114
|
-
|
|
115
|
-
1. Go to **Settings > API** in your Linear workspace and click **Create webhook**
|
|
116
|
-
2. Fill in:
|
|
117
|
-
- **Label**: A descriptive name (e.g., "Chat Bot")
|
|
118
|
-
- **URL**: `https://your-domain.com/api/webhooks/linear`
|
|
119
|
-
3. Copy the **Signing secret** and set it as `LINEAR_WEBHOOK_SECRET`
|
|
120
|
-
4. Under **Data change events**, select:
|
|
121
|
-
- **Comments** (required - for issue comments)
|
|
122
|
-
- **Issues** (recommended - for mentions in issue descriptions)
|
|
123
|
-
- **Emoji reactions** (optional - for reaction handling)
|
|
124
|
-
5. Under **Team selection**, choose **All public teams** or a specific team
|
|
125
|
-
6. Click **Create webhook**
|
|
126
|
-
|
|
127
|
-
## Features
|
|
128
|
-
|
|
129
|
-
- Message posting and editing
|
|
130
|
-
- Message deletion
|
|
131
|
-
- [Reaction handling](https://linear.app/docs/comment-on-issues) (add reactions via emoji)
|
|
132
|
-
- Issue comment threads
|
|
133
|
-
- Cards (rendered as [Markdown](https://linear.app/docs/comment-on-issues))
|
|
134
|
-
|
|
135
|
-
## Thread Model
|
|
136
|
-
|
|
137
|
-
Linear has two levels of comment threading:
|
|
138
|
-
|
|
139
|
-
| Type | Description | Thread ID Format |
|
|
140
|
-
| -------------- | --------------------------------------- | -------------------------------- |
|
|
141
|
-
| Issue-level | Top-level comments on an issue | `linear:{issueId}` |
|
|
142
|
-
| Comment thread | Replies nested under a specific comment | `linear:{issueId}:c:{commentId}` |
|
|
143
|
-
|
|
144
|
-
When a user writes a comment, the bot replies **within the same comment thread** (nested under the same card). This matches the expected Linear UX where conversations are grouped.
|
|
145
|
-
|
|
146
|
-
Example thread IDs:
|
|
147
|
-
|
|
148
|
-
- `linear:2174add1-f7c8-44e3-bbf3-2d60b5ea8bc9` (issue-level)
|
|
149
|
-
- `linear:2174add1-f7c8-44e3-bbf3-2d60b5ea8bc9:c:comment-abc123` (comment thread)
|
|
150
|
-
|
|
151
|
-
## Reactions
|
|
152
|
-
|
|
153
|
-
Linear supports standard [emoji reactions](https://linear.app/docs/comment-on-issues) on comments. The adapter maps SDK emoji names to unicode:
|
|
154
|
-
|
|
155
|
-
| SDK Emoji | Linear Emoji |
|
|
156
|
-
| ------------- | ------------ |
|
|
157
|
-
| `thumbs_up` | 👍 |
|
|
158
|
-
| `thumbs_down` | 👎 |
|
|
159
|
-
| `heart` | ❤️ |
|
|
160
|
-
| `fire` | 🔥 |
|
|
161
|
-
| `rocket` | 🚀 |
|
|
162
|
-
| `eyes` | 👀 |
|
|
163
|
-
| `sparkles` | ✨ |
|
|
164
|
-
| `wave` | 👋 |
|
|
165
|
-
|
|
166
|
-
## Limitations
|
|
167
|
-
|
|
168
|
-
- **No typing indicators** - Linear doesn't support typing indicators
|
|
169
|
-
- **No streaming** - Messages posted in full (editing supported for updates)
|
|
170
|
-
- **No DMs** - Linear doesn't have direct messages
|
|
171
|
-
- **No modals** - Linear doesn't support interactive modals
|
|
172
|
-
- **Action buttons** - Rendered as text; use link buttons for clickable actions
|
|
173
|
-
- **Remove reaction** - Requires reaction ID lookup (not directly supported)
|
|
174
|
-
|
|
175
|
-
## Troubleshooting
|
|
176
|
-
|
|
177
|
-
### "Invalid signature" error
|
|
178
|
-
|
|
179
|
-
- Verify `LINEAR_WEBHOOK_SECRET` matches the secret from your webhook configuration
|
|
180
|
-
- Ensure the request body isn't being modified before verification
|
|
181
|
-
- The webhook secret is shown only once at creation - regenerate if lost
|
|
182
|
-
|
|
183
|
-
### Bot not responding to mentions
|
|
184
|
-
|
|
185
|
-
- Verify webhook events are configured with `Comment` resource type
|
|
186
|
-
- Check that the webhook URL is correct and accessible
|
|
187
|
-
- Ensure the `userName` config matches how users mention the bot
|
|
188
|
-
- Check that the webhook is enabled (Linear may auto-disable after repeated failures)
|
|
189
|
-
|
|
190
|
-
### "Webhook expired" error
|
|
191
|
-
|
|
192
|
-
- This means the webhook timestamp is too old (> 5 minutes)
|
|
193
|
-
- Usually indicates a delivery delay or clock skew
|
|
194
|
-
- Check that your server time is synchronized
|
|
195
|
-
|
|
196
|
-
### Rate limiting
|
|
197
|
-
|
|
198
|
-
- Linear API has [rate limits](https://linear.app/developers/graphql#rate-limiting)
|
|
199
|
-
- The SDK handles rate limiting automatically in most cases
|
|
200
|
-
|
|
201
|
-
## OAuth Setup Guide
|
|
202
|
-
|
|
203
|
-
This section is only relevant if you chose [Option B: OAuth Application](#option-b-oauth-application-recommended-for-apps). If you're using a personal API key (Option A), you can skip this entirely.
|
|
204
|
-
|
|
205
|
-
Setting up an OAuth application gives the bot its own identity in Linear rather than acting as your personal account.
|
|
206
|
-
|
|
207
|
-
### 1. Create the OAuth Application
|
|
208
|
-
|
|
209
|
-
1. Go to [Settings > API > Applications](https://linear.app/settings/api/applications/new) in Linear (requires admin)
|
|
210
|
-
2. Fill in:
|
|
211
|
-
- **Application name**: Your bot's name (e.g., "v0") -- this is how it appears in Linear
|
|
212
|
-
- **Application icon**: Upload an icon for the bot
|
|
213
|
-
- **Redirect callback URLs**: Add a placeholder URL (e.g., `https://your-domain.com/callback`) -- not needed for client credentials auth
|
|
214
|
-
3. Click **Create**
|
|
215
|
-
4. **Enable client credentials tokens** in the app settings
|
|
216
|
-
5. Note your **Client ID** and **Client Secret**
|
|
217
|
-
|
|
218
|
-
### 2. Configure the Adapter
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
createLinearAdapter({
|
|
222
|
-
clientId: process.env.LINEAR_CLIENT_ID!,
|
|
223
|
-
clientSecret: process.env.LINEAR_CLIENT_SECRET!,
|
|
224
|
-
webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,
|
|
225
|
-
userName: "v0",
|
|
226
|
-
logger: console,
|
|
227
|
-
});
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
That's it. The adapter automatically obtains and refreshes tokens using the [client credentials grant](https://linear.app/developers/oauth-2-0-authentication#client-credentials-tokens). No callback endpoints, no token storage, no refresh logic on your end.
|
|
231
|
-
|
|
232
|
-
### 3. Making the Bot @-Mentionable (Optional)
|
|
233
|
-
|
|
234
|
-
To make the bot appear in Linear's `@` mention dropdown as an [Agent](https://linear.app/developers/agents):
|
|
235
|
-
|
|
236
|
-
1. In your OAuth app settings, enable **Agent session events** under webhooks
|
|
237
|
-
2. Have a workspace admin install the app using `actor=app` with the `app:mentionable` scope:
|
|
238
|
-
|
|
239
|
-
```
|
|
240
|
-
https://linear.app/oauth/authorize?
|
|
241
|
-
client_id=YOUR_CLIENT_ID&
|
|
242
|
-
redirect_uri=https://your-domain.com/callback&
|
|
243
|
-
response_type=code&
|
|
244
|
-
scope=read,write,comments:create,app:mentionable&
|
|
245
|
-
actor=app
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
Once installed, the bot shows up as a mentionable user in the workspace. See the [Linear Agents docs](https://linear.app/developers/agents) for full details.
|
|
36
|
+
## Documentation
|
|
249
37
|
|
|
250
|
-
|
|
38
|
+
Full setup instructions, configuration reference, and features at [chat-sdk.dev/docs/adapters/linear](https://chat-sdk.dev/docs/adapters/linear).
|
|
251
39
|
|
|
252
40
|
## License
|
|
253
41
|
|
package/dist/index.d.ts
CHANGED
|
@@ -13,17 +13,17 @@ import { Logger, Adapter, ChatInstance, WebhookOptions, AdapterPostableMessage,
|
|
|
13
13
|
interface LinearAdapterBaseConfig {
|
|
14
14
|
/** Logger instance for error reporting */
|
|
15
15
|
logger: Logger;
|
|
16
|
-
/**
|
|
17
|
-
* Webhook signing secret for HMAC-SHA256 verification.
|
|
18
|
-
* Found on the webhook detail page in Linear settings.
|
|
19
|
-
*/
|
|
20
|
-
webhookSecret: string;
|
|
21
16
|
/**
|
|
22
17
|
* Bot display name used for @-mention detection.
|
|
23
18
|
* For API key auth, this is typically the user's display name.
|
|
24
19
|
* For OAuth app auth with actor=app, this is the app name.
|
|
25
20
|
*/
|
|
26
21
|
userName: string;
|
|
22
|
+
/**
|
|
23
|
+
* Webhook signing secret for HMAC-SHA256 verification.
|
|
24
|
+
* Found on the webhook detail page in Linear settings.
|
|
25
|
+
*/
|
|
26
|
+
webhookSecret: string;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Configuration using a personal API key.
|
|
@@ -32,9 +32,9 @@ interface LinearAdapterBaseConfig {
|
|
|
32
32
|
* @see https://linear.app/docs/api-and-webhooks
|
|
33
33
|
*/
|
|
34
34
|
interface LinearAdapterAPIKeyConfig extends LinearAdapterBaseConfig {
|
|
35
|
+
accessToken?: never;
|
|
35
36
|
/** Personal API key from Linear Settings > Security & Access */
|
|
36
37
|
apiKey: string;
|
|
37
|
-
accessToken?: never;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Configuration using an OAuth access token (pre-obtained).
|
|
@@ -59,12 +59,12 @@ interface LinearAdapterOAuthConfig extends LinearAdapterBaseConfig {
|
|
|
59
59
|
* @see https://linear.app/developers/oauth-2-0-authentication#client-credentials-tokens
|
|
60
60
|
*/
|
|
61
61
|
interface LinearAdapterAppConfig extends LinearAdapterBaseConfig {
|
|
62
|
+
accessToken?: never;
|
|
63
|
+
apiKey?: never;
|
|
62
64
|
/** OAuth application client ID */
|
|
63
65
|
clientId: string;
|
|
64
66
|
/** OAuth application client secret */
|
|
65
67
|
clientSecret: string;
|
|
66
|
-
apiKey?: never;
|
|
67
|
-
accessToken?: never;
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
70
|
* Linear adapter configuration - API Key, OAuth token, or OAuth App (client credentials).
|
|
@@ -78,14 +78,14 @@ type LinearAdapterConfig = LinearAdapterAPIKeyConfig | LinearAdapterOAuthConfig
|
|
|
78
78
|
* - Comment thread: Replies nested under a specific root comment (has commentId)
|
|
79
79
|
*/
|
|
80
80
|
interface LinearThreadId {
|
|
81
|
-
/** Linear issue UUID */
|
|
82
|
-
issueId: string;
|
|
83
81
|
/**
|
|
84
82
|
* Root comment ID for comment-level threads.
|
|
85
83
|
* If present, this is a comment thread (replies nest under this comment).
|
|
86
84
|
* If absent, this is an issue-level thread (top-level comment).
|
|
87
85
|
*/
|
|
88
86
|
commentId?: string;
|
|
87
|
+
/** Linear issue UUID */
|
|
88
|
+
issueId: string;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* Comment data from a webhook payload.
|
|
@@ -96,22 +96,22 @@ interface LinearThreadId {
|
|
|
96
96
|
* @see https://linear.app/developers/webhooks#webhook-payload
|
|
97
97
|
*/
|
|
98
98
|
interface LinearCommentData {
|
|
99
|
-
/** Comment UUID */
|
|
100
|
-
id: string;
|
|
101
99
|
/** Comment body in markdown format */
|
|
102
100
|
body: string;
|
|
101
|
+
/** ISO 8601 creation date */
|
|
102
|
+
createdAt: string;
|
|
103
|
+
/** Comment UUID */
|
|
104
|
+
id: string;
|
|
103
105
|
/** Issue UUID the comment is associated with */
|
|
104
106
|
issueId: string;
|
|
105
|
-
/** User UUID who wrote the comment */
|
|
106
|
-
userId: string;
|
|
107
107
|
/** Parent comment UUID (for nested/threaded replies) */
|
|
108
108
|
parentId?: string;
|
|
109
|
-
/** ISO 8601 creation date */
|
|
110
|
-
createdAt: string;
|
|
111
109
|
/** ISO 8601 last update date */
|
|
112
110
|
updatedAt: string;
|
|
113
111
|
/** Direct URL to the comment */
|
|
114
112
|
url?: string;
|
|
113
|
+
/** User UUID who wrote the comment */
|
|
114
|
+
userId: string;
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
117
|
* Platform-specific raw message type for Linear.
|
|
@@ -171,12 +171,12 @@ declare class LinearAdapter implements Adapter<LinearThreadId, LinearRawMessage>
|
|
|
171
171
|
readonly name = "linear";
|
|
172
172
|
readonly userName: string;
|
|
173
173
|
private linearClient;
|
|
174
|
-
private webhookSecret;
|
|
174
|
+
private readonly webhookSecret;
|
|
175
175
|
private chat;
|
|
176
|
-
private logger;
|
|
176
|
+
private readonly logger;
|
|
177
177
|
private _botUserId;
|
|
178
|
-
private formatConverter;
|
|
179
|
-
private clientCredentials;
|
|
178
|
+
private readonly formatConverter;
|
|
179
|
+
private readonly clientCredentials;
|
|
180
180
|
private accessTokenExpiry;
|
|
181
181
|
/** Bot user ID used for self-message detection */
|
|
182
182
|
get botUserId(): string | undefined;
|
package/dist/index.js
CHANGED
|
@@ -124,6 +124,7 @@ var LinearFormatConverter = class extends BaseFormatConverter {
|
|
|
124
124
|
};
|
|
125
125
|
|
|
126
126
|
// src/index.ts
|
|
127
|
+
var COMMENT_THREAD_PATTERN = /^([^:]+):c:([^:]+)$/;
|
|
127
128
|
var LinearAdapter = class {
|
|
128
129
|
name = "linear";
|
|
129
130
|
userName;
|
|
@@ -184,7 +185,9 @@ var LinearAdapter = class {
|
|
|
184
185
|
* @see https://linear.app/developers/oauth-2-0-authentication#client-credentials-tokens
|
|
185
186
|
*/
|
|
186
187
|
async refreshClientCredentialsToken() {
|
|
187
|
-
if (!this.clientCredentials)
|
|
188
|
+
if (!this.clientCredentials) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
188
191
|
const { clientId, clientSecret } = this.clientCredentials;
|
|
189
192
|
const response = await fetch("https://api.linear.app/oauth/token", {
|
|
190
193
|
method: "POST",
|
|
@@ -650,7 +653,7 @@ var LinearAdapter = class {
|
|
|
650
653
|
`Invalid Linear thread ID format: ${threadId}`
|
|
651
654
|
);
|
|
652
655
|
}
|
|
653
|
-
const commentMatch = withoutPrefix.match(
|
|
656
|
+
const commentMatch = withoutPrefix.match(COMMENT_THREAD_PATTERN);
|
|
654
657
|
if (commentMatch) {
|
|
655
658
|
return {
|
|
656
659
|
issueId: commentMatch[1],
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/cards.ts","../src/markdown.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { extractCard, ValidationError } from \"@chat-adapter/shared\";\nimport type { LinearFetch, User } from \"@linear/sdk\";\nimport { LinearClient } from \"@linear/sdk\";\nimport type {\n Adapter,\n AdapterPostableMessage,\n Author,\n ChatInstance,\n EmojiValue,\n FetchOptions,\n FetchResult,\n FormattedContent,\n Logger,\n RawMessage,\n ThreadInfo,\n WebhookOptions,\n} from \"chat\";\nimport { convertEmojiPlaceholders, Message } from \"chat\";\nimport { cardToLinearMarkdown } from \"./cards\";\nimport { LinearFormatConverter } from \"./markdown\";\nimport type {\n CommentWebhookPayload,\n LinearAdapterConfig,\n LinearCommentData,\n LinearRawMessage,\n LinearThreadId,\n LinearWebhookActor,\n LinearWebhookPayload,\n ReactionWebhookPayload,\n} from \"./types\";\n\n// Re-export types\nexport type {\n LinearAdapterAPIKeyConfig,\n LinearAdapterAppConfig,\n LinearAdapterConfig,\n LinearAdapterOAuthConfig,\n LinearRawMessage,\n LinearThreadId,\n} from \"./types\";\n\n/**\n * Linear adapter for chat SDK.\n *\n * Supports comment threads on Linear issues.\n * Authentication via personal API key or OAuth access token.\n *\n * @example API Key auth\n * ```typescript\n * import { Chat } from \"chat\";\n * import { createLinearAdapter } from \"@chat-adapter/linear\";\n * import { MemoryState } from \"@chat-adapter/state-memory\";\n *\n * const chat = new Chat({\n * userName: \"my-bot\",\n * adapters: {\n * linear: createLinearAdapter({\n * apiKey: process.env.LINEAR_API_KEY!,\n * webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,\n * userName: \"my-bot\",\n * logger: console,\n * }),\n * },\n * state: new MemoryState(),\n * logger: \"info\",\n * });\n * ```\n *\n * @example OAuth auth\n * ```typescript\n * const chat = new Chat({\n * userName: \"my-bot\",\n * adapters: {\n * linear: createLinearAdapter({\n * accessToken: process.env.LINEAR_ACCESS_TOKEN!,\n * webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,\n * userName: \"my-bot\",\n * logger: console,\n * }),\n * },\n * state: new MemoryState(),\n * logger: \"info\",\n * });\n * ```\n */\nexport class LinearAdapter\n implements Adapter<LinearThreadId, LinearRawMessage>\n{\n readonly name = \"linear\";\n readonly userName: string;\n\n private linearClient!: LinearClient;\n private webhookSecret: string;\n private chat: ChatInstance | null = null;\n private logger: Logger;\n private _botUserId: string | null = null;\n private formatConverter = new LinearFormatConverter();\n\n // Client credentials auth state\n private clientCredentials: {\n clientId: string;\n clientSecret: string;\n } | null = null;\n private accessTokenExpiry: number | null = null;\n\n /** Bot user ID used for self-message detection */\n get botUserId(): string | undefined {\n return this._botUserId ?? undefined;\n }\n\n constructor(config: LinearAdapterConfig) {\n this.webhookSecret = config.webhookSecret;\n this.logger = config.logger;\n this.userName = config.userName;\n\n // Create LinearClient based on auth method\n // @see https://linear.app/developers/sdk\n if (\"apiKey\" in config && config.apiKey) {\n this.linearClient = new LinearClient({ apiKey: config.apiKey });\n } else if (\"accessToken\" in config && config.accessToken) {\n this.linearClient = new LinearClient({\n accessToken: config.accessToken,\n });\n } else if (\"clientId\" in config && config.clientId) {\n // Client credentials mode - token will be fetched during initialize()\n this.clientCredentials = {\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n };\n } else {\n throw new Error(\n \"LinearAdapter requires either apiKey, accessToken, or clientId/clientSecret\",\n );\n }\n }\n\n async initialize(chat: ChatInstance): Promise<void> {\n this.chat = chat;\n\n // For client credentials mode, fetch an access token first\n if (this.clientCredentials) {\n await this.refreshClientCredentialsToken();\n }\n\n // Fetch the bot's user ID for self-message detection\n // @see https://linear.app/developers/sdk-fetching-and-modifying-data\n try {\n const viewer = await this.linearClient.viewer;\n this._botUserId = viewer.id;\n this.logger.info(\"Linear auth completed\", {\n botUserId: this._botUserId,\n displayName: viewer.displayName,\n });\n } catch (error) {\n this.logger.warn(\"Could not fetch Linear bot user ID\", { error });\n }\n }\n\n /**\n * Fetch a new access token using client credentials grant.\n * The token is valid for 30 days. The adapter auto-refreshes on 401.\n *\n * @see https://linear.app/developers/oauth-2-0-authentication#client-credentials-tokens\n */\n private async refreshClientCredentialsToken(): Promise<void> {\n if (!this.clientCredentials) return;\n\n const { clientId, clientSecret } = this.clientCredentials;\n\n const response = await fetch(\"https://api.linear.app/oauth/token\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n grant_type: \"client_credentials\",\n client_id: clientId,\n client_secret: clientSecret,\n scope: \"read,write,comments:create,issues:create\",\n }),\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n throw new Error(\n `Failed to fetch Linear client credentials token: ${response.status} ${errorBody}`,\n );\n }\n\n const data = (await response.json()) as {\n access_token: string;\n expires_in: number;\n };\n\n this.linearClient = new LinearClient({\n accessToken: data.access_token,\n });\n\n // Track expiry so we can proactively refresh (with 1 hour buffer)\n this.accessTokenExpiry = Date.now() + data.expires_in * 1000 - 3600000;\n\n this.logger.info(\"Linear client credentials token obtained\", {\n expiresIn: `${Math.round(data.expires_in / 86400)} days`,\n });\n }\n\n /**\n * Ensure the client credentials token is still valid. Refresh if expired.\n */\n private async ensureValidToken(): Promise<void> {\n if (\n this.clientCredentials &&\n this.accessTokenExpiry &&\n Date.now() > this.accessTokenExpiry\n ) {\n this.logger.info(\"Linear access token expired, refreshing...\");\n await this.refreshClientCredentialsToken();\n }\n }\n\n /**\n * Handle incoming webhook from Linear.\n *\n * @see https://linear.app/developers/webhooks\n */\n async handleWebhook(\n request: Request,\n options?: WebhookOptions,\n ): Promise<Response> {\n const body = await request.text();\n this.logger.debug(\"Linear webhook raw body\", {\n body: body.substring(0, 500),\n });\n\n // Verify request signature (Linear-Signature header)\n // @see https://linear.app/developers/webhooks#securing-webhooks\n const signature = request.headers.get(\"linear-signature\");\n if (!this.verifySignature(body, signature)) {\n return new Response(\"Invalid signature\", { status: 401 });\n }\n\n // Parse the JSON payload\n let payload: LinearWebhookPayload;\n try {\n payload = JSON.parse(body);\n } catch {\n this.logger.error(\"Linear webhook invalid JSON\", {\n contentType: request.headers.get(\"content-type\"),\n bodyPreview: body.substring(0, 200),\n });\n return new Response(\"Invalid JSON\", { status: 400 });\n }\n\n // Validate webhook timestamp to prevent replay attacks (within 5 minutes)\n if (payload.webhookTimestamp) {\n const timeDiff = Math.abs(Date.now() - payload.webhookTimestamp);\n if (timeDiff > 5 * 60 * 1000) {\n this.logger.warn(\"Linear webhook timestamp too old\", {\n webhookTimestamp: payload.webhookTimestamp,\n timeDiff,\n });\n return new Response(\"Webhook expired\", { status: 401 });\n }\n }\n\n // Handle events based on type\n if (payload.type === \"Comment\") {\n const commentPayload = payload as CommentWebhookPayload;\n if (commentPayload.action === \"create\") {\n this.handleCommentCreated(commentPayload, options);\n }\n } else if (payload.type === \"Reaction\") {\n const reactionPayload = payload as ReactionWebhookPayload;\n this.handleReaction(reactionPayload);\n }\n\n return new Response(\"ok\", { status: 200 });\n }\n\n /**\n * Verify Linear webhook signature using HMAC-SHA256.\n *\n * @see https://linear.app/developers/webhooks#securing-webhooks\n */\n private verifySignature(body: string, signature: string | null): boolean {\n if (!signature) {\n return false;\n }\n\n const computedSignature = createHmac(\"sha256\", this.webhookSecret)\n .update(body)\n .digest(\"hex\");\n\n try {\n return timingSafeEqual(\n Buffer.from(computedSignature, \"hex\"),\n Buffer.from(signature, \"hex\"),\n );\n } catch {\n return false;\n }\n }\n\n /**\n * Handle a new comment created on an issue.\n *\n * Threading logic:\n * - If the comment has a parentId, it's a reply -> thread under the parent (root comment)\n * - If no parentId, this is a root comment -> thread under this comment's own ID\n */\n private handleCommentCreated(\n payload: CommentWebhookPayload,\n options?: WebhookOptions,\n ): void {\n if (!this.chat) {\n this.logger.warn(\"Chat instance not initialized, ignoring comment\");\n return;\n }\n\n const { data, actor } = payload;\n\n // Skip if the comment has no issueId (e.g., project update comment)\n if (!data.issueId) {\n this.logger.debug(\"Ignoring non-issue comment\", {\n commentId: data.id,\n });\n return;\n }\n\n // Determine thread: use parentId as root if it's a reply, otherwise this comment is the root\n const rootCommentId = data.parentId || data.id;\n const threadId = this.encodeThreadId({\n issueId: data.issueId,\n commentId: rootCommentId,\n });\n\n // Build message\n const message = this.buildMessage(data, actor, threadId);\n\n // Skip bot's own messages\n if (data.userId === this._botUserId) {\n this.logger.debug(\"Ignoring message from self\", {\n messageId: data.id,\n });\n return;\n }\n\n this.chat.processMessage(this, threadId, message, options);\n }\n\n /**\n * Handle reaction events (logging only - reactions don't include issueId).\n */\n private handleReaction(payload: ReactionWebhookPayload): void {\n if (!this.chat) {\n return;\n }\n\n const { data, actor } = payload;\n\n // Reactions on comments need a commentId to find the thread.\n // Since reaction webhooks don't include issueId directly,\n // we'd need an additional API call to look it up.\n this.logger.debug(\"Received reaction webhook\", {\n reactionId: data.id,\n emoji: data.emoji,\n commentId: data.commentId,\n action: payload.action,\n actorName: actor.name,\n });\n }\n\n /**\n * Build a Message from a Linear comment and actor.\n */\n private buildMessage(\n comment: LinearCommentData,\n actor: LinearWebhookActor,\n threadId: string,\n ): Message<LinearRawMessage> {\n const text = comment.body || \"\";\n\n const author: Author = {\n userId: comment.userId,\n userName: actor.name || \"unknown\",\n fullName: actor.name || \"unknown\",\n isBot: actor.type !== \"user\",\n isMe: comment.userId === this._botUserId,\n };\n\n const formatted: FormattedContent = this.formatConverter.toAst(text);\n\n const raw: LinearRawMessage = {\n comment,\n organizationId: undefined,\n };\n\n return new Message<LinearRawMessage>({\n id: comment.id,\n threadId,\n text,\n formatted,\n raw,\n author,\n metadata: {\n dateSent: comment.createdAt ? new Date(comment.createdAt) : new Date(),\n edited: comment.createdAt !== comment.updatedAt,\n editedAt:\n comment.createdAt !== comment.updatedAt && comment.updatedAt\n ? new Date(comment.updatedAt)\n : undefined,\n },\n attachments: [],\n });\n }\n\n /**\n * Post a message to a thread (create a comment on an issue).\n *\n * For comment-level threads, uses parentId to reply under the root comment.\n * For issue-level threads, creates a top-level comment.\n *\n * Uses LinearClient.createComment({ issueId, body, parentId? }).\n * @see https://linear.app/developers/sdk-fetching-and-modifying-data#mutations\n */\n async postMessage(\n threadId: string,\n message: AdapterPostableMessage,\n ): Promise<RawMessage<LinearRawMessage>> {\n await this.ensureValidToken();\n const { issueId, commentId } = this.decodeThreadId(threadId);\n\n // Render message to markdown\n let body: string;\n const card = extractCard(message);\n if (card) {\n body = cardToLinearMarkdown(card);\n } else {\n body = this.formatConverter.renderPostable(message);\n }\n\n // Convert emoji placeholders to unicode\n body = convertEmojiPlaceholders(body, \"linear\");\n\n // Create the comment via Linear SDK\n // If commentId is present, reply under that comment (comment-level thread)\n const commentPayload = await this.linearClient.createComment({\n issueId,\n body,\n parentId: commentId,\n });\n\n const comment = await commentPayload.comment;\n if (!comment) {\n throw new Error(\"Failed to create comment on Linear issue\");\n }\n\n return {\n id: comment.id,\n threadId,\n raw: {\n comment: {\n id: comment.id,\n body: comment.body,\n issueId,\n userId: this._botUserId || \"\",\n createdAt: comment.createdAt.toISOString(),\n updatedAt: comment.updatedAt.toISOString(),\n url: comment.url,\n },\n },\n };\n }\n\n /**\n * Edit an existing message (update a comment).\n *\n * Uses LinearClient.updateComment(id, { body }).\n * @see https://linear.app/developers/sdk-fetching-and-modifying-data#mutations\n */\n async editMessage(\n threadId: string,\n messageId: string,\n message: AdapterPostableMessage,\n ): Promise<RawMessage<LinearRawMessage>> {\n await this.ensureValidToken();\n const { issueId } = this.decodeThreadId(threadId);\n\n // Render message to markdown\n let body: string;\n const card = extractCard(message);\n if (card) {\n body = cardToLinearMarkdown(card);\n } else {\n body = this.formatConverter.renderPostable(message);\n }\n\n // Convert emoji placeholders to unicode\n body = convertEmojiPlaceholders(body, \"linear\");\n\n // Update the comment via Linear SDK\n const commentPayload = await this.linearClient.updateComment(messageId, {\n body,\n });\n\n const comment = await commentPayload.comment;\n if (!comment) {\n throw new Error(\"Failed to update comment on Linear\");\n }\n\n return {\n id: comment.id,\n threadId,\n raw: {\n comment: {\n id: comment.id,\n body: comment.body,\n issueId,\n userId: this._botUserId || \"\",\n createdAt: comment.createdAt.toISOString(),\n updatedAt: comment.updatedAt.toISOString(),\n url: comment.url,\n },\n },\n };\n }\n\n /**\n * Delete a message (delete a comment).\n *\n * Uses LinearClient.deleteComment(id).\n */\n async deleteMessage(_threadId: string, messageId: string): Promise<void> {\n await this.ensureValidToken();\n await this.linearClient.deleteComment(messageId);\n }\n\n /**\n * Add a reaction to a comment.\n *\n * Uses LinearClient.createReaction({ commentId, emoji }).\n * Linear reactions use emoji strings (unicode).\n */\n async addReaction(\n _threadId: string,\n messageId: string,\n emoji: EmojiValue | string,\n ): Promise<void> {\n await this.ensureValidToken();\n const emojiStr = this.resolveEmoji(emoji);\n await this.linearClient.createReaction({\n commentId: messageId,\n emoji: emojiStr,\n });\n }\n\n /**\n * Remove a reaction from a comment.\n *\n * Linear doesn't have a direct \"remove reaction by emoji + user\" API.\n * Removing requires knowing the reaction ID, which would require fetching\n * the comment's reactions first. This is a known limitation.\n */\n async removeReaction(\n _threadId: string,\n _messageId: string,\n _emoji: EmojiValue | string,\n ): Promise<void> {\n this.logger.warn(\n \"removeReaction is not fully supported on Linear - reaction ID lookup would be required\",\n );\n }\n\n /**\n * Start typing indicator. Not supported by Linear.\n */\n async startTyping(_threadId: string): Promise<void> {\n // Linear doesn't support typing indicators\n }\n\n /**\n * Fetch messages from a thread.\n *\n * For issue-level threads: fetches all top-level issue comments.\n * For comment-level threads: fetches the root comment and its children (replies).\n */\n async fetchMessages(\n threadId: string,\n options?: FetchOptions,\n ): Promise<FetchResult<LinearRawMessage>> {\n await this.ensureValidToken();\n const { issueId, commentId } = this.decodeThreadId(threadId);\n\n if (commentId) {\n // Comment-level thread: fetch root comment's children\n return this.fetchCommentThread(threadId, issueId, commentId, options);\n }\n\n // Issue-level thread: fetch all top-level comments\n return this.fetchIssueComments(threadId, issueId, options);\n }\n\n /**\n * Fetch top-level comments on an issue.\n */\n private async fetchIssueComments(\n threadId: string,\n issueId: string,\n options?: FetchOptions,\n ): Promise<FetchResult<LinearRawMessage>> {\n const issue = await this.linearClient.issue(issueId);\n const commentsConnection = await issue.comments({\n first: options?.limit ?? 50,\n });\n\n const messages = await this.commentsToMessages(\n commentsConnection.nodes,\n threadId,\n issueId,\n );\n\n return {\n messages,\n nextCursor: commentsConnection.pageInfo.hasNextPage\n ? commentsConnection.pageInfo.endCursor\n : undefined,\n };\n }\n\n /**\n * Fetch a comment thread (root comment + its children/replies).\n */\n private async fetchCommentThread(\n threadId: string,\n issueId: string,\n commentId: string,\n options?: FetchOptions,\n ): Promise<FetchResult<LinearRawMessage>> {\n const rootComment = await this.linearClient.comment({ id: commentId });\n if (!rootComment) {\n return { messages: [] };\n }\n\n // Get the children (replies) of the root comment\n const childrenConnection = await rootComment.children({\n first: options?.limit ?? 50,\n });\n\n // Include the root comment as the first message, then its children\n const rootMessages = await this.commentsToMessages(\n [rootComment],\n threadId,\n issueId,\n );\n const childMessages = await this.commentsToMessages(\n childrenConnection.nodes,\n threadId,\n issueId,\n );\n\n return {\n messages: [...rootMessages, ...childMessages],\n nextCursor: childrenConnection.pageInfo.hasNextPage\n ? childrenConnection.pageInfo.endCursor\n : undefined,\n };\n }\n\n /**\n * Convert an array of Linear SDK Comment objects to Message instances.\n */\n private async commentsToMessages(\n comments: Array<{\n id: string;\n body: string;\n createdAt: Date;\n updatedAt: Date;\n url: string;\n user: LinearFetch<User> | undefined;\n }>,\n threadId: string,\n issueId: string,\n ): Promise<Message<LinearRawMessage>[]> {\n const messages: Message<LinearRawMessage>[] = [];\n\n for (const comment of comments) {\n const user = await comment.user;\n const author: Author = {\n userId: user?.id || \"unknown\",\n userName: user?.displayName || \"unknown\",\n fullName: user?.name || user?.displayName || \"unknown\",\n isBot: false,\n isMe: user?.id === this._botUserId,\n };\n\n const formatted: FormattedContent = this.formatConverter.toAst(\n comment.body,\n );\n\n messages.push(\n new Message<LinearRawMessage>({\n id: comment.id,\n threadId,\n text: comment.body,\n formatted,\n author,\n metadata: {\n dateSent: new Date(comment.createdAt),\n edited: comment.createdAt.getTime() !== comment.updatedAt.getTime(),\n editedAt:\n comment.createdAt.getTime() !== comment.updatedAt.getTime()\n ? new Date(comment.updatedAt)\n : undefined,\n },\n attachments: [],\n raw: {\n comment: {\n id: comment.id,\n body: comment.body,\n issueId,\n userId: user?.id || \"unknown\",\n createdAt: comment.createdAt.toISOString(),\n updatedAt: comment.updatedAt.toISOString(),\n url: comment.url,\n },\n },\n }),\n );\n }\n\n return messages;\n }\n\n /**\n * Fetch thread info for a Linear issue.\n */\n async fetchThread(threadId: string): Promise<ThreadInfo> {\n await this.ensureValidToken();\n const { issueId } = this.decodeThreadId(threadId);\n\n const issue = await this.linearClient.issue(issueId);\n\n return {\n id: threadId,\n channelId: issueId,\n channelName: `${issue.identifier}: ${issue.title}`,\n isDM: false,\n metadata: {\n issueId,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n },\n };\n }\n\n /**\n * Encode a Linear thread ID.\n *\n * Formats:\n * - Issue-level: linear:{issueId}\n * - Comment thread: linear:{issueId}:c:{commentId}\n */\n encodeThreadId(platformData: LinearThreadId): string {\n if (platformData.commentId) {\n return `linear:${platformData.issueId}:c:${platformData.commentId}`;\n }\n return `linear:${platformData.issueId}`;\n }\n\n /**\n * Decode a Linear thread ID.\n *\n * Formats:\n * - Issue-level: linear:{issueId}\n * - Comment thread: linear:{issueId}:c:{commentId}\n */\n decodeThreadId(threadId: string): LinearThreadId {\n if (!threadId.startsWith(\"linear:\")) {\n throw new ValidationError(\n \"linear\",\n `Invalid Linear thread ID: ${threadId}`,\n );\n }\n\n const withoutPrefix = threadId.slice(7);\n if (!withoutPrefix) {\n throw new ValidationError(\n \"linear\",\n `Invalid Linear thread ID format: ${threadId}`,\n );\n }\n\n // Check for comment thread format: {issueId}:c:{commentId}\n const commentMatch = withoutPrefix.match(/^([^:]+):c:([^:]+)$/);\n if (commentMatch) {\n return {\n issueId: commentMatch[1],\n commentId: commentMatch[2],\n };\n }\n\n // Issue-level format: {issueId}\n return { issueId: withoutPrefix };\n }\n\n /**\n * Derive channel ID from a Linear thread ID.\n * linear:{issueId}:c:{commentId} -> linear:{issueId}\n * linear:{issueId} -> linear:{issueId}\n */\n channelIdFromThreadId(threadId: string): string {\n const { issueId } = this.decodeThreadId(threadId);\n return `linear:${issueId}`;\n }\n\n /**\n * Parse platform message format to normalized format.\n */\n parseMessage(raw: LinearRawMessage): Message<LinearRawMessage> {\n const text = raw.comment.body || \"\";\n const formatted: FormattedContent = this.formatConverter.toAst(text);\n\n return new Message<LinearRawMessage>({\n id: raw.comment.id,\n threadId: \"\",\n text,\n formatted,\n author: {\n userId: raw.comment.userId,\n userName: \"unknown\",\n fullName: \"unknown\",\n isBot: false,\n isMe: raw.comment.userId === this._botUserId,\n },\n metadata: {\n dateSent: raw.comment.createdAt\n ? new Date(raw.comment.createdAt)\n : new Date(),\n edited: raw.comment.createdAt !== raw.comment.updatedAt,\n editedAt:\n raw.comment.createdAt !== raw.comment.updatedAt &&\n raw.comment.updatedAt\n ? new Date(raw.comment.updatedAt)\n : undefined,\n },\n attachments: [],\n raw,\n });\n }\n\n /**\n * Render formatted content to Linear markdown.\n */\n renderFormatted(content: FormattedContent): string {\n return this.formatConverter.fromAst(content);\n }\n\n /**\n * Resolve an emoji value to a unicode string.\n * Linear uses standard unicode emoji for reactions.\n */\n private resolveEmoji(emoji: EmojiValue | string): string {\n const emojiName = typeof emoji === \"string\" ? emoji : emoji.name;\n\n const mapping: Record<string, string> = {\n thumbs_up: \"\\u{1F44D}\",\n thumbs_down: \"\\u{1F44E}\",\n heart: \"\\u{2764}\\u{FE0F}\",\n fire: \"\\u{1F525}\",\n rocket: \"\\u{1F680}\",\n eyes: \"\\u{1F440}\",\n check: \"\\u{2705}\",\n warning: \"\\u{26A0}\\u{FE0F}\",\n sparkles: \"\\u{2728}\",\n wave: \"\\u{1F44B}\",\n raised_hands: \"\\u{1F64C}\",\n laugh: \"\\u{1F604}\",\n hooray: \"\\u{1F389}\",\n confused: \"\\u{1F615}\",\n };\n\n return mapping[emojiName] || emojiName;\n }\n}\n\n/**\n * Factory function to create a Linear adapter.\n *\n * @example\n * ```typescript\n * const adapter = createLinearAdapter({\n * apiKey: process.env.LINEAR_API_KEY!,\n * webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,\n * userName: \"my-bot\",\n * logger: console,\n * });\n * ```\n */\nexport function createLinearAdapter(\n config: LinearAdapterConfig,\n): LinearAdapter {\n return new LinearAdapter(config);\n}\n","/**\n * Convert CardElement to Linear-compatible markdown.\n *\n * Since Linear doesn't support rich cards natively, we render cards\n * as clean formatted markdown. Linear comments support standard\n * markdown syntax.\n *\n * @see https://linear.app/docs/comment-on-issues\n */\n\nimport type {\n ActionsElement,\n CardChild,\n CardElement,\n FieldsElement,\n TextElement,\n} from \"chat\";\n\n/**\n * Convert a CardElement to Linear-compatible markdown.\n *\n * Cards are rendered as clean markdown with:\n * - Bold title and subtitle\n * - Text content\n * - Fields as key-value pairs\n * - Buttons as markdown links (action buttons become bold text since Linear has no interactivity)\n *\n * @example\n * ```typescript\n * const card = Card({\n * title: \"Order #1234\",\n * subtitle: \"Status update\",\n * children: [\n * Text(\"Your order has been shipped!\"),\n * Fields([\n * Field({ label: \"Tracking\", value: \"ABC123\" }),\n * ]),\n * Actions([\n * LinkButton({ url: \"https://track.example.com\", label: \"Track Order\" }),\n * ]),\n * ],\n * });\n *\n * // Output:\n * // **Order #1234**\n * // Status update\n * //\n * // Your order has been shipped!\n * //\n * // **Tracking:** ABC123\n * //\n * // [Track Order](https://track.example.com)\n * ```\n */\nexport function cardToLinearMarkdown(card: CardElement): string {\n const lines: string[] = [];\n\n // Title (bold)\n if (card.title) {\n lines.push(`**${escapeMarkdown(card.title)}**`);\n }\n\n // Subtitle\n if (card.subtitle) {\n lines.push(escapeMarkdown(card.subtitle));\n }\n\n // Add spacing after header if there are children\n if ((card.title || card.subtitle) && card.children.length > 0) {\n lines.push(\"\");\n }\n\n // Header image\n if (card.imageUrl) {\n lines.push(``);\n lines.push(\"\");\n }\n\n // Children\n for (let i = 0; i < card.children.length; i++) {\n const child = card.children[i];\n const childLines = renderChild(child);\n\n if (childLines.length > 0) {\n lines.push(...childLines);\n\n // Add spacing between children (except last)\n if (i < card.children.length - 1) {\n lines.push(\"\");\n }\n }\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Render a card child element to markdown lines.\n */\nfunction renderChild(child: CardChild): string[] {\n switch (child.type) {\n case \"text\":\n return renderText(child);\n\n case \"fields\":\n return renderFields(child);\n\n case \"actions\":\n return renderActions(child);\n\n case \"section\":\n // Flatten section children\n return child.children.flatMap(renderChild);\n\n case \"image\":\n if (child.alt) {\n return [``];\n }\n return [``];\n\n case \"divider\":\n return [\"---\"];\n\n default:\n return [];\n }\n}\n\n/**\n * Render text element.\n */\nfunction renderText(text: TextElement): string[] {\n const content = text.content;\n\n switch (text.style) {\n case \"bold\":\n return [`**${content}**`];\n case \"muted\":\n return [`_${content}_`];\n default:\n return [content];\n }\n}\n\n/**\n * Render fields as key-value pairs.\n */\nfunction renderFields(fields: FieldsElement): string[] {\n return fields.children.map(\n (field) =>\n `**${escapeMarkdown(field.label)}:** ${escapeMarkdown(field.value)}`,\n );\n}\n\n/**\n * Render actions (buttons) as markdown links or bold text.\n */\nfunction renderActions(actions: ActionsElement): string[] {\n const buttonTexts = actions.children.map((button) => {\n if (button.type === \"link-button\") {\n return `[${escapeMarkdown(button.label)}](${button.url})`;\n }\n // Action buttons become bold text (no interactivity in Linear comments)\n return `**[${escapeMarkdown(button.label)}]**`;\n });\n\n return [buttonTexts.join(\" \\u2022 \")];\n}\n\n/**\n * Escape special markdown characters in text.\n */\nfunction escapeMarkdown(text: string): string {\n return text\n .replace(/\\*/g, \"\\\\*\")\n .replace(/_/g, \"\\\\_\")\n .replace(/\\[/g, \"\\\\[\")\n .replace(/\\]/g, \"\\\\]\");\n}\n\n/**\n * Generate plain text fallback from a card (no markdown).\n */\nexport function cardToPlainText(card: CardElement): string {\n const parts: string[] = [];\n\n if (card.title) {\n parts.push(card.title);\n }\n\n if (card.subtitle) {\n parts.push(card.subtitle);\n }\n\n for (const child of card.children) {\n const text = childToPlainText(child);\n if (text) {\n parts.push(text);\n }\n }\n\n return parts.join(\"\\n\");\n}\n\n/**\n * Convert card child to plain text.\n */\nfunction childToPlainText(child: CardChild): string | null {\n switch (child.type) {\n case \"text\":\n return child.content;\n case \"fields\":\n return child.children.map((f) => `${f.label}: ${f.value}`).join(\"\\n\");\n case \"actions\":\n // Actions are interactive-only — exclude from fallback text.\n // See: https://docs.slack.dev/reference/methods/chat.postMessage\n return null;\n case \"section\":\n return child.children.map(childToPlainText).filter(Boolean).join(\"\\n\");\n default:\n return null;\n }\n}\n","/**\n * Linear-specific format conversion using AST-based parsing.\n *\n * Linear uses standard Markdown for comments, which is very close\n * to the mdast format used by the SDK. This converter is mostly\n * pass-through, similar to the GitHub adapter.\n *\n * @see https://linear.app/docs/comment-on-issues\n */\n\nimport {\n type AdapterPostableMessage,\n BaseFormatConverter,\n parseMarkdown,\n type Root,\n stringifyMarkdown,\n} from \"chat\";\n\nexport class LinearFormatConverter extends BaseFormatConverter {\n /**\n * Convert an AST to standard markdown.\n * Linear uses standard markdown, so we use remark-stringify directly.\n */\n fromAst(ast: Root): string {\n return stringifyMarkdown(ast).trim();\n }\n\n /**\n * Parse markdown into an AST.\n * Linear uses standard markdown, so we use the standard parser.\n */\n toAst(markdown: string): Root {\n return parseMarkdown(markdown);\n }\n\n /**\n * Render a postable message to Linear markdown string.\n */\n override renderPostable(message: AdapterPostableMessage): string {\n if (typeof message === \"string\") {\n return message;\n }\n if (\"raw\" in message) {\n return message.raw;\n }\n if (\"markdown\" in message) {\n return this.fromMarkdown(message.markdown);\n }\n if (\"ast\" in message) {\n return this.fromAst(message.ast);\n }\n // Handle cards via base class\n return super.renderPostable(message);\n }\n}\n"],"mappings":";AAAA,SAAS,YAAY,uBAAuB;AAC5C,SAAS,aAAa,uBAAuB;AAE7C,SAAS,oBAAoB;AAe7B,SAAS,0BAA0B,eAAe;;;ACoC3C,SAAS,qBAAqB,MAA2B;AAC9D,QAAM,QAAkB,CAAC;AAGzB,MAAI,KAAK,OAAO;AACd,UAAM,KAAK,KAAK,eAAe,KAAK,KAAK,CAAC,IAAI;AAAA,EAChD;AAGA,MAAI,KAAK,UAAU;AACjB,UAAM,KAAK,eAAe,KAAK,QAAQ,CAAC;AAAA,EAC1C;AAGA,OAAK,KAAK,SAAS,KAAK,aAAa,KAAK,SAAS,SAAS,GAAG;AAC7D,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,MAAI,KAAK,UAAU;AACjB,UAAM,KAAK,OAAO,KAAK,QAAQ,GAAG;AAClC,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,WAAS,IAAI,GAAG,IAAI,KAAK,SAAS,QAAQ,KAAK;AAC7C,UAAM,QAAQ,KAAK,SAAS,CAAC;AAC7B,UAAM,aAAa,YAAY,KAAK;AAEpC,QAAI,WAAW,SAAS,GAAG;AACzB,YAAM,KAAK,GAAG,UAAU;AAGxB,UAAI,IAAI,KAAK,SAAS,SAAS,GAAG;AAChC,cAAM,KAAK,EAAE;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAKA,SAAS,YAAY,OAA4B;AAC/C,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,WAAW,KAAK;AAAA,IAEzB,KAAK;AACH,aAAO,aAAa,KAAK;AAAA,IAE3B,KAAK;AACH,aAAO,cAAc,KAAK;AAAA,IAE5B,KAAK;AAEH,aAAO,MAAM,SAAS,QAAQ,WAAW;AAAA,IAE3C,KAAK;AACH,UAAI,MAAM,KAAK;AACb,eAAO,CAAC,KAAK,eAAe,MAAM,GAAG,CAAC,KAAK,MAAM,GAAG,GAAG;AAAA,MACzD;AACA,aAAO,CAAC,OAAO,MAAM,GAAG,GAAG;AAAA,IAE7B,KAAK;AACH,aAAO,CAAC,KAAK;AAAA,IAEf;AACE,aAAO,CAAC;AAAA,EACZ;AACF;AAKA,SAAS,WAAW,MAA6B;AAC/C,QAAM,UAAU,KAAK;AAErB,UAAQ,KAAK,OAAO;AAAA,IAClB,KAAK;AACH,aAAO,CAAC,KAAK,OAAO,IAAI;AAAA,IAC1B,KAAK;AACH,aAAO,CAAC,IAAI,OAAO,GAAG;AAAA,IACxB;AACE,aAAO,CAAC,OAAO;AAAA,EACnB;AACF;AAKA,SAAS,aAAa,QAAiC;AACrD,SAAO,OAAO,SAAS;AAAA,IACrB,CAAC,UACC,KAAK,eAAe,MAAM,KAAK,CAAC,OAAO,eAAe,MAAM,KAAK,CAAC;AAAA,EACtE;AACF;AAKA,SAAS,cAAc,SAAmC;AACxD,QAAM,cAAc,QAAQ,SAAS,IAAI,CAAC,WAAW;AACnD,QAAI,OAAO,SAAS,eAAe;AACjC,aAAO,IAAI,eAAe,OAAO,KAAK,CAAC,KAAK,OAAO,GAAG;AAAA,IACxD;AAEA,WAAO,MAAM,eAAe,OAAO,KAAK,CAAC;AAAA,EAC3C,CAAC;AAED,SAAO,CAAC,YAAY,KAAK,UAAU,CAAC;AACtC;AAKA,SAAS,eAAe,MAAsB;AAC5C,SAAO,KACJ,QAAQ,OAAO,KAAK,EACpB,QAAQ,MAAM,KAAK,EACnB,QAAQ,OAAO,KAAK,EACpB,QAAQ,OAAO,KAAK;AACzB;;;ACxKA;AAAA,EAEE;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AAEA,IAAM,wBAAN,cAAoC,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA,EAK7D,QAAQ,KAAmB;AACzB,WAAO,kBAAkB,GAAG,EAAE,KAAK;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAwB;AAC5B,WAAO,cAAc,QAAQ;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKS,eAAe,SAAyC;AAC/D,QAAI,OAAO,YAAY,UAAU;AAC/B,aAAO;AAAA,IACT;AACA,QAAI,SAAS,SAAS;AACpB,aAAO,QAAQ;AAAA,IACjB;AACA,QAAI,cAAc,SAAS;AACzB,aAAO,KAAK,aAAa,QAAQ,QAAQ;AAAA,IAC3C;AACA,QAAI,SAAS,SAAS;AACpB,aAAO,KAAK,QAAQ,QAAQ,GAAG;AAAA,IACjC;AAEA,WAAO,MAAM,eAAe,OAAO;AAAA,EACrC;AACF;;;AFgCO,IAAM,gBAAN,MAEP;AAAA,EACW,OAAO;AAAA,EACP;AAAA,EAED;AAAA,EACA;AAAA,EACA,OAA4B;AAAA,EAC5B;AAAA,EACA,aAA4B;AAAA,EAC5B,kBAAkB,IAAI,sBAAsB;AAAA;AAAA,EAG5C,oBAGG;AAAA,EACH,oBAAmC;AAAA;AAAA,EAG3C,IAAI,YAAgC;AAClC,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA,EAEA,YAAY,QAA6B;AACvC,SAAK,gBAAgB,OAAO;AAC5B,SAAK,SAAS,OAAO;AACrB,SAAK,WAAW,OAAO;AAIvB,QAAI,YAAY,UAAU,OAAO,QAAQ;AACvC,WAAK,eAAe,IAAI,aAAa,EAAE,QAAQ,OAAO,OAAO,CAAC;AAAA,IAChE,WAAW,iBAAiB,UAAU,OAAO,aAAa;AACxD,WAAK,eAAe,IAAI,aAAa;AAAA,QACnC,aAAa,OAAO;AAAA,MACtB,CAAC;AAAA,IACH,WAAW,cAAc,UAAU,OAAO,UAAU;AAElD,WAAK,oBAAoB;AAAA,QACvB,UAAU,OAAO;AAAA,QACjB,cAAc,OAAO;AAAA,MACvB;AAAA,IACF,OAAO;AACL,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,MAAmC;AAClD,SAAK,OAAO;AAGZ,QAAI,KAAK,mBAAmB;AAC1B,YAAM,KAAK,8BAA8B;AAAA,IAC3C;AAIA,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,aAAa;AACvC,WAAK,aAAa,OAAO;AACzB,WAAK,OAAO,KAAK,yBAAyB;AAAA,QACxC,WAAW,KAAK;AAAA,QAChB,aAAa,OAAO;AAAA,MACtB,CAAC;AAAA,IACH,SAAS,OAAO;AACd,WAAK,OAAO,KAAK,sCAAsC,EAAE,MAAM,CAAC;AAAA,IAClE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,gCAA+C;AAC3D,QAAI,CAAC,KAAK,kBAAmB;AAE7B,UAAM,EAAE,UAAU,aAAa,IAAI,KAAK;AAExC,UAAM,WAAW,MAAM,MAAM,sCAAsC;AAAA,MACjE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,eAAe;AAAA,QACf,OAAO;AAAA,MACT,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAK;AACtC,YAAM,IAAI;AAAA,QACR,oDAAoD,SAAS,MAAM,IAAI,SAAS;AAAA,MAClF;AAAA,IACF;AAEA,UAAM,OAAQ,MAAM,SAAS,KAAK;AAKlC,SAAK,eAAe,IAAI,aAAa;AAAA,MACnC,aAAa,KAAK;AAAA,IACpB,CAAC;AAGD,SAAK,oBAAoB,KAAK,IAAI,IAAI,KAAK,aAAa,MAAO;AAE/D,SAAK,OAAO,KAAK,4CAA4C;AAAA,MAC3D,WAAW,GAAG,KAAK,MAAM,KAAK,aAAa,KAAK,CAAC;AAAA,IACnD,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,mBAAkC;AAC9C,QACE,KAAK,qBACL,KAAK,qBACL,KAAK,IAAI,IAAI,KAAK,mBAClB;AACA,WAAK,OAAO,KAAK,4CAA4C;AAC7D,YAAM,KAAK,8BAA8B;AAAA,IAC3C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,cACJ,SACA,SACmB;AACnB,UAAM,OAAO,MAAM,QAAQ,KAAK;AAChC,SAAK,OAAO,MAAM,2BAA2B;AAAA,MAC3C,MAAM,KAAK,UAAU,GAAG,GAAG;AAAA,IAC7B,CAAC;AAID,UAAM,YAAY,QAAQ,QAAQ,IAAI,kBAAkB;AACxD,QAAI,CAAC,KAAK,gBAAgB,MAAM,SAAS,GAAG;AAC1C,aAAO,IAAI,SAAS,qBAAqB,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1D;AAGA,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,QAAQ;AACN,WAAK,OAAO,MAAM,+BAA+B;AAAA,QAC/C,aAAa,QAAQ,QAAQ,IAAI,cAAc;AAAA,QAC/C,aAAa,KAAK,UAAU,GAAG,GAAG;AAAA,MACpC,CAAC;AACD,aAAO,IAAI,SAAS,gBAAgB,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrD;AAGA,QAAI,QAAQ,kBAAkB;AAC5B,YAAM,WAAW,KAAK,IAAI,KAAK,IAAI,IAAI,QAAQ,gBAAgB;AAC/D,UAAI,WAAW,IAAI,KAAK,KAAM;AAC5B,aAAK,OAAO,KAAK,oCAAoC;AAAA,UACnD,kBAAkB,QAAQ;AAAA,UAC1B;AAAA,QACF,CAAC;AACD,eAAO,IAAI,SAAS,mBAAmB,EAAE,QAAQ,IAAI,CAAC;AAAA,MACxD;AAAA,IACF;AAGA,QAAI,QAAQ,SAAS,WAAW;AAC9B,YAAM,iBAAiB;AACvB,UAAI,eAAe,WAAW,UAAU;AACtC,aAAK,qBAAqB,gBAAgB,OAAO;AAAA,MACnD;AAAA,IACF,WAAW,QAAQ,SAAS,YAAY;AACtC,YAAM,kBAAkB;AACxB,WAAK,eAAe,eAAe;AAAA,IACrC;AAEA,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,gBAAgB,MAAc,WAAmC;AACvE,QAAI,CAAC,WAAW;AACd,aAAO;AAAA,IACT;AAEA,UAAM,oBAAoB,WAAW,UAAU,KAAK,aAAa,EAC9D,OAAO,IAAI,EACX,OAAO,KAAK;AAEf,QAAI;AACF,aAAO;AAAA,QACL,OAAO,KAAK,mBAAmB,KAAK;AAAA,QACpC,OAAO,KAAK,WAAW,KAAK;AAAA,MAC9B;AAAA,IACF,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,qBACN,SACA,SACM;AACN,QAAI,CAAC,KAAK,MAAM;AACd,WAAK,OAAO,KAAK,iDAAiD;AAClE;AAAA,IACF;AAEA,UAAM,EAAE,MAAM,MAAM,IAAI;AAGxB,QAAI,CAAC,KAAK,SAAS;AACjB,WAAK,OAAO,MAAM,8BAA8B;AAAA,QAC9C,WAAW,KAAK;AAAA,MAClB,CAAC;AACD;AAAA,IACF;AAGA,UAAM,gBAAgB,KAAK,YAAY,KAAK;AAC5C,UAAM,WAAW,KAAK,eAAe;AAAA,MACnC,SAAS,KAAK;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAGD,UAAM,UAAU,KAAK,aAAa,MAAM,OAAO,QAAQ;AAGvD,QAAI,KAAK,WAAW,KAAK,YAAY;AACnC,WAAK,OAAO,MAAM,8BAA8B;AAAA,QAC9C,WAAW,KAAK;AAAA,MAClB,CAAC;AACD;AAAA,IACF;AAEA,SAAK,KAAK,eAAe,MAAM,UAAU,SAAS,OAAO;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAe,SAAuC;AAC5D,QAAI,CAAC,KAAK,MAAM;AACd;AAAA,IACF;AAEA,UAAM,EAAE,MAAM,MAAM,IAAI;AAKxB,SAAK,OAAO,MAAM,6BAA6B;AAAA,MAC7C,YAAY,KAAK;AAAA,MACjB,OAAO,KAAK;AAAA,MACZ,WAAW,KAAK;AAAA,MAChB,QAAQ,QAAQ;AAAA,MAChB,WAAW,MAAM;AAAA,IACnB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKQ,aACN,SACA,OACA,UAC2B;AAC3B,UAAM,OAAO,QAAQ,QAAQ;AAE7B,UAAM,SAAiB;AAAA,MACrB,QAAQ,QAAQ;AAAA,MAChB,UAAU,MAAM,QAAQ;AAAA,MACxB,UAAU,MAAM,QAAQ;AAAA,MACxB,OAAO,MAAM,SAAS;AAAA,MACtB,MAAM,QAAQ,WAAW,KAAK;AAAA,IAChC;AAEA,UAAM,YAA8B,KAAK,gBAAgB,MAAM,IAAI;AAEnE,UAAM,MAAwB;AAAA,MAC5B;AAAA,MACA,gBAAgB;AAAA,IAClB;AAEA,WAAO,IAAI,QAA0B;AAAA,MACnC,IAAI,QAAQ;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU;AAAA,QACR,UAAU,QAAQ,YAAY,IAAI,KAAK,QAAQ,SAAS,IAAI,oBAAI,KAAK;AAAA,QACrE,QAAQ,QAAQ,cAAc,QAAQ;AAAA,QACtC,UACE,QAAQ,cAAc,QAAQ,aAAa,QAAQ,YAC/C,IAAI,KAAK,QAAQ,SAAS,IAC1B;AAAA,MACR;AAAA,MACA,aAAa,CAAC;AAAA,IAChB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,YACJ,UACA,SACuC;AACvC,UAAM,KAAK,iBAAiB;AAC5B,UAAM,EAAE,SAAS,UAAU,IAAI,KAAK,eAAe,QAAQ;AAG3D,QAAI;AACJ,UAAM,OAAO,YAAY,OAAO;AAChC,QAAI,MAAM;AACR,aAAO,qBAAqB,IAAI;AAAA,IAClC,OAAO;AACL,aAAO,KAAK,gBAAgB,eAAe,OAAO;AAAA,IACpD;AAGA,WAAO,yBAAyB,MAAM,QAAQ;AAI9C,UAAM,iBAAiB,MAAM,KAAK,aAAa,cAAc;AAAA,MAC3D;AAAA,MACA;AAAA,MACA,UAAU;AAAA,IACZ,CAAC;AAED,UAAM,UAAU,MAAM,eAAe;AACrC,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,0CAA0C;AAAA,IAC5D;AAEA,WAAO;AAAA,MACL,IAAI,QAAQ;AAAA,MACZ;AAAA,MACA,KAAK;AAAA,QACH,SAAS;AAAA,UACP,IAAI,QAAQ;AAAA,UACZ,MAAM,QAAQ;AAAA,UACd;AAAA,UACA,QAAQ,KAAK,cAAc;AAAA,UAC3B,WAAW,QAAQ,UAAU,YAAY;AAAA,UACzC,WAAW,QAAQ,UAAU,YAAY;AAAA,UACzC,KAAK,QAAQ;AAAA,QACf;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,YACJ,UACA,WACA,SACuC;AACvC,UAAM,KAAK,iBAAiB;AAC5B,UAAM,EAAE,QAAQ,IAAI,KAAK,eAAe,QAAQ;AAGhD,QAAI;AACJ,UAAM,OAAO,YAAY,OAAO;AAChC,QAAI,MAAM;AACR,aAAO,qBAAqB,IAAI;AAAA,IAClC,OAAO;AACL,aAAO,KAAK,gBAAgB,eAAe,OAAO;AAAA,IACpD;AAGA,WAAO,yBAAyB,MAAM,QAAQ;AAG9C,UAAM,iBAAiB,MAAM,KAAK,aAAa,cAAc,WAAW;AAAA,MACtE;AAAA,IACF,CAAC;AAED,UAAM,UAAU,MAAM,eAAe;AACrC,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AAEA,WAAO;AAAA,MACL,IAAI,QAAQ;AAAA,MACZ;AAAA,MACA,KAAK;AAAA,QACH,SAAS;AAAA,UACP,IAAI,QAAQ;AAAA,UACZ,MAAM,QAAQ;AAAA,UACd;AAAA,UACA,QAAQ,KAAK,cAAc;AAAA,UAC3B,WAAW,QAAQ,UAAU,YAAY;AAAA,UACzC,WAAW,QAAQ,UAAU,YAAY;AAAA,UACzC,KAAK,QAAQ;AAAA,QACf;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,cAAc,WAAmB,WAAkC;AACvE,UAAM,KAAK,iBAAiB;AAC5B,UAAM,KAAK,aAAa,cAAc,SAAS;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,YACJ,WACA,WACA,OACe;AACf,UAAM,KAAK,iBAAiB;AAC5B,UAAM,WAAW,KAAK,aAAa,KAAK;AACxC,UAAM,KAAK,aAAa,eAAe;AAAA,MACrC,WAAW;AAAA,MACX,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,eACJ,WACA,YACA,QACe;AACf,SAAK,OAAO;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,WAAkC;AAAA,EAEpD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,cACJ,UACA,SACwC;AACxC,UAAM,KAAK,iBAAiB;AAC5B,UAAM,EAAE,SAAS,UAAU,IAAI,KAAK,eAAe,QAAQ;AAE3D,QAAI,WAAW;AAEb,aAAO,KAAK,mBAAmB,UAAU,SAAS,WAAW,OAAO;AAAA,IACtE;AAGA,WAAO,KAAK,mBAAmB,UAAU,SAAS,OAAO;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,mBACZ,UACA,SACA,SACwC;AACxC,UAAM,QAAQ,MAAM,KAAK,aAAa,MAAM,OAAO;AACnD,UAAM,qBAAqB,MAAM,MAAM,SAAS;AAAA,MAC9C,OAAO,SAAS,SAAS;AAAA,IAC3B,CAAC;AAED,UAAM,WAAW,MAAM,KAAK;AAAA,MAC1B,mBAAmB;AAAA,MACnB;AAAA,MACA;AAAA,IACF;AAEA,WAAO;AAAA,MACL;AAAA,MACA,YAAY,mBAAmB,SAAS,cACpC,mBAAmB,SAAS,YAC5B;AAAA,IACN;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,mBACZ,UACA,SACA,WACA,SACwC;AACxC,UAAM,cAAc,MAAM,KAAK,aAAa,QAAQ,EAAE,IAAI,UAAU,CAAC;AACrE,QAAI,CAAC,aAAa;AAChB,aAAO,EAAE,UAAU,CAAC,EAAE;AAAA,IACxB;AAGA,UAAM,qBAAqB,MAAM,YAAY,SAAS;AAAA,MACpD,OAAO,SAAS,SAAS;AAAA,IAC3B,CAAC;AAGD,UAAM,eAAe,MAAM,KAAK;AAAA,MAC9B,CAAC,WAAW;AAAA,MACZ;AAAA,MACA;AAAA,IACF;AACA,UAAM,gBAAgB,MAAM,KAAK;AAAA,MAC/B,mBAAmB;AAAA,MACnB;AAAA,MACA;AAAA,IACF;AAEA,WAAO;AAAA,MACL,UAAU,CAAC,GAAG,cAAc,GAAG,aAAa;AAAA,MAC5C,YAAY,mBAAmB,SAAS,cACpC,mBAAmB,SAAS,YAC5B;AAAA,IACN;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,mBACZ,UAQA,UACA,SACsC;AACtC,UAAM,WAAwC,CAAC;AAE/C,eAAW,WAAW,UAAU;AAC9B,YAAM,OAAO,MAAM,QAAQ;AAC3B,YAAM,SAAiB;AAAA,QACrB,QAAQ,MAAM,MAAM;AAAA,QACpB,UAAU,MAAM,eAAe;AAAA,QAC/B,UAAU,MAAM,QAAQ,MAAM,eAAe;AAAA,QAC7C,OAAO;AAAA,QACP,MAAM,MAAM,OAAO,KAAK;AAAA,MAC1B;AAEA,YAAM,YAA8B,KAAK,gBAAgB;AAAA,QACvD,QAAQ;AAAA,MACV;AAEA,eAAS;AAAA,QACP,IAAI,QAA0B;AAAA,UAC5B,IAAI,QAAQ;AAAA,UACZ;AAAA,UACA,MAAM,QAAQ;AAAA,UACd;AAAA,UACA;AAAA,UACA,UAAU;AAAA,YACR,UAAU,IAAI,KAAK,QAAQ,SAAS;AAAA,YACpC,QAAQ,QAAQ,UAAU,QAAQ,MAAM,QAAQ,UAAU,QAAQ;AAAA,YAClE,UACE,QAAQ,UAAU,QAAQ,MAAM,QAAQ,UAAU,QAAQ,IACtD,IAAI,KAAK,QAAQ,SAAS,IAC1B;AAAA,UACR;AAAA,UACA,aAAa,CAAC;AAAA,UACd,KAAK;AAAA,YACH,SAAS;AAAA,cACP,IAAI,QAAQ;AAAA,cACZ,MAAM,QAAQ;AAAA,cACd;AAAA,cACA,QAAQ,MAAM,MAAM;AAAA,cACpB,WAAW,QAAQ,UAAU,YAAY;AAAA,cACzC,WAAW,QAAQ,UAAU,YAAY;AAAA,cACzC,KAAK,QAAQ;AAAA,YACf;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,UAAuC;AACvD,UAAM,KAAK,iBAAiB;AAC5B,UAAM,EAAE,QAAQ,IAAI,KAAK,eAAe,QAAQ;AAEhD,UAAM,QAAQ,MAAM,KAAK,aAAa,MAAM,OAAO;AAEnD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,WAAW;AAAA,MACX,aAAa,GAAG,MAAM,UAAU,KAAK,MAAM,KAAK;AAAA,MAChD,MAAM;AAAA,MACN,UAAU;AAAA,QACR;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,OAAO,MAAM;AAAA,QACb,KAAK,MAAM;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,eAAe,cAAsC;AACnD,QAAI,aAAa,WAAW;AAC1B,aAAO,UAAU,aAAa,OAAO,MAAM,aAAa,SAAS;AAAA,IACnE;AACA,WAAO,UAAU,aAAa,OAAO;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,eAAe,UAAkC;AAC/C,QAAI,CAAC,SAAS,WAAW,SAAS,GAAG;AACnC,YAAM,IAAI;AAAA,QACR;AAAA,QACA,6BAA6B,QAAQ;AAAA,MACvC;AAAA,IACF;AAEA,UAAM,gBAAgB,SAAS,MAAM,CAAC;AACtC,QAAI,CAAC,eAAe;AAClB,YAAM,IAAI;AAAA,QACR;AAAA,QACA,oCAAoC,QAAQ;AAAA,MAC9C;AAAA,IACF;AAGA,UAAM,eAAe,cAAc,MAAM,qBAAqB;AAC9D,QAAI,cAAc;AAChB,aAAO;AAAA,QACL,SAAS,aAAa,CAAC;AAAA,QACvB,WAAW,aAAa,CAAC;AAAA,MAC3B;AAAA,IACF;AAGA,WAAO,EAAE,SAAS,cAAc;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,sBAAsB,UAA0B;AAC9C,UAAM,EAAE,QAAQ,IAAI,KAAK,eAAe,QAAQ;AAChD,WAAO,UAAU,OAAO;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,KAAkD;AAC7D,UAAM,OAAO,IAAI,QAAQ,QAAQ;AACjC,UAAM,YAA8B,KAAK,gBAAgB,MAAM,IAAI;AAEnE,WAAO,IAAI,QAA0B;AAAA,MACnC,IAAI,IAAI,QAAQ;AAAA,MAChB,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,QACN,QAAQ,IAAI,QAAQ;AAAA,QACpB,UAAU;AAAA,QACV,UAAU;AAAA,QACV,OAAO;AAAA,QACP,MAAM,IAAI,QAAQ,WAAW,KAAK;AAAA,MACpC;AAAA,MACA,UAAU;AAAA,QACR,UAAU,IAAI,QAAQ,YAClB,IAAI,KAAK,IAAI,QAAQ,SAAS,IAC9B,oBAAI,KAAK;AAAA,QACb,QAAQ,IAAI,QAAQ,cAAc,IAAI,QAAQ;AAAA,QAC9C,UACE,IAAI,QAAQ,cAAc,IAAI,QAAQ,aACtC,IAAI,QAAQ,YACR,IAAI,KAAK,IAAI,QAAQ,SAAS,IAC9B;AAAA,MACR;AAAA,MACA,aAAa,CAAC;AAAA,MACd;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,SAAmC;AACjD,WAAO,KAAK,gBAAgB,QAAQ,OAAO;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,aAAa,OAAoC;AACvD,UAAM,YAAY,OAAO,UAAU,WAAW,QAAQ,MAAM;AAE5D,UAAM,UAAkC;AAAA,MACtC,WAAW;AAAA,MACX,aAAa;AAAA,MACb,OAAO;AAAA,MACP,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS;AAAA,MACT,UAAU;AAAA,MACV,MAAM;AAAA,MACN,cAAc;AAAA,MACd,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,UAAU;AAAA,IACZ;AAEA,WAAO,QAAQ,SAAS,KAAK;AAAA,EAC/B;AACF;AAeO,SAAS,oBACd,QACe;AACf,SAAO,IAAI,cAAc,MAAM;AACjC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/cards.ts","../src/markdown.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { extractCard, ValidationError } from \"@chat-adapter/shared\";\nimport type { LinearFetch, User } from \"@linear/sdk\";\nimport { LinearClient } from \"@linear/sdk\";\nimport type {\n Adapter,\n AdapterPostableMessage,\n Author,\n ChatInstance,\n EmojiValue,\n FetchOptions,\n FetchResult,\n FormattedContent,\n Logger,\n RawMessage,\n ThreadInfo,\n WebhookOptions,\n} from \"chat\";\nimport { convertEmojiPlaceholders, Message } from \"chat\";\nimport { cardToLinearMarkdown } from \"./cards\";\nimport { LinearFormatConverter } from \"./markdown\";\nimport type {\n CommentWebhookPayload,\n LinearAdapterConfig,\n LinearCommentData,\n LinearRawMessage,\n LinearThreadId,\n LinearWebhookActor,\n LinearWebhookPayload,\n ReactionWebhookPayload,\n} from \"./types\";\n\nconst COMMENT_THREAD_PATTERN = /^([^:]+):c:([^:]+)$/;\n\n// Re-export types\nexport type {\n LinearAdapterAPIKeyConfig,\n LinearAdapterAppConfig,\n LinearAdapterConfig,\n LinearAdapterOAuthConfig,\n LinearRawMessage,\n LinearThreadId,\n} from \"./types\";\n\n/**\n * Linear adapter for chat SDK.\n *\n * Supports comment threads on Linear issues.\n * Authentication via personal API key or OAuth access token.\n *\n * @example API Key auth\n * ```typescript\n * import { Chat } from \"chat\";\n * import { createLinearAdapter } from \"@chat-adapter/linear\";\n * import { MemoryState } from \"@chat-adapter/state-memory\";\n *\n * const chat = new Chat({\n * userName: \"my-bot\",\n * adapters: {\n * linear: createLinearAdapter({\n * apiKey: process.env.LINEAR_API_KEY!,\n * webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,\n * userName: \"my-bot\",\n * logger: console,\n * }),\n * },\n * state: new MemoryState(),\n * logger: \"info\",\n * });\n * ```\n *\n * @example OAuth auth\n * ```typescript\n * const chat = new Chat({\n * userName: \"my-bot\",\n * adapters: {\n * linear: createLinearAdapter({\n * accessToken: process.env.LINEAR_ACCESS_TOKEN!,\n * webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,\n * userName: \"my-bot\",\n * logger: console,\n * }),\n * },\n * state: new MemoryState(),\n * logger: \"info\",\n * });\n * ```\n */\nexport class LinearAdapter\n implements Adapter<LinearThreadId, LinearRawMessage>\n{\n readonly name = \"linear\";\n readonly userName: string;\n\n private linearClient!: LinearClient;\n private readonly webhookSecret: string;\n private chat: ChatInstance | null = null;\n private readonly logger: Logger;\n private _botUserId: string | null = null;\n private readonly formatConverter = new LinearFormatConverter();\n\n // Client credentials auth state\n private readonly clientCredentials: {\n clientId: string;\n clientSecret: string;\n } | null = null;\n private accessTokenExpiry: number | null = null;\n\n /** Bot user ID used for self-message detection */\n get botUserId(): string | undefined {\n return this._botUserId ?? undefined;\n }\n\n constructor(config: LinearAdapterConfig) {\n this.webhookSecret = config.webhookSecret;\n this.logger = config.logger;\n this.userName = config.userName;\n\n // Create LinearClient based on auth method\n // @see https://linear.app/developers/sdk\n if (\"apiKey\" in config && config.apiKey) {\n this.linearClient = new LinearClient({ apiKey: config.apiKey });\n } else if (\"accessToken\" in config && config.accessToken) {\n this.linearClient = new LinearClient({\n accessToken: config.accessToken,\n });\n } else if (\"clientId\" in config && config.clientId) {\n // Client credentials mode - token will be fetched during initialize()\n this.clientCredentials = {\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n };\n } else {\n throw new Error(\n \"LinearAdapter requires either apiKey, accessToken, or clientId/clientSecret\"\n );\n }\n }\n\n async initialize(chat: ChatInstance): Promise<void> {\n this.chat = chat;\n\n // For client credentials mode, fetch an access token first\n if (this.clientCredentials) {\n await this.refreshClientCredentialsToken();\n }\n\n // Fetch the bot's user ID for self-message detection\n // @see https://linear.app/developers/sdk-fetching-and-modifying-data\n try {\n const viewer = await this.linearClient.viewer;\n this._botUserId = viewer.id;\n this.logger.info(\"Linear auth completed\", {\n botUserId: this._botUserId,\n displayName: viewer.displayName,\n });\n } catch (error) {\n this.logger.warn(\"Could not fetch Linear bot user ID\", { error });\n }\n }\n\n /**\n * Fetch a new access token using client credentials grant.\n * The token is valid for 30 days. The adapter auto-refreshes on 401.\n *\n * @see https://linear.app/developers/oauth-2-0-authentication#client-credentials-tokens\n */\n private async refreshClientCredentialsToken(): Promise<void> {\n if (!this.clientCredentials) {\n return;\n }\n\n const { clientId, clientSecret } = this.clientCredentials;\n\n const response = await fetch(\"https://api.linear.app/oauth/token\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n grant_type: \"client_credentials\",\n client_id: clientId,\n client_secret: clientSecret,\n scope: \"read,write,comments:create,issues:create\",\n }),\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n throw new Error(\n `Failed to fetch Linear client credentials token: ${response.status} ${errorBody}`\n );\n }\n\n const data = (await response.json()) as {\n access_token: string;\n expires_in: number;\n };\n\n this.linearClient = new LinearClient({\n accessToken: data.access_token,\n });\n\n // Track expiry so we can proactively refresh (with 1 hour buffer)\n this.accessTokenExpiry = Date.now() + data.expires_in * 1000 - 3600000;\n\n this.logger.info(\"Linear client credentials token obtained\", {\n expiresIn: `${Math.round(data.expires_in / 86400)} days`,\n });\n }\n\n /**\n * Ensure the client credentials token is still valid. Refresh if expired.\n */\n private async ensureValidToken(): Promise<void> {\n if (\n this.clientCredentials &&\n this.accessTokenExpiry &&\n Date.now() > this.accessTokenExpiry\n ) {\n this.logger.info(\"Linear access token expired, refreshing...\");\n await this.refreshClientCredentialsToken();\n }\n }\n\n /**\n * Handle incoming webhook from Linear.\n *\n * @see https://linear.app/developers/webhooks\n */\n async handleWebhook(\n request: Request,\n options?: WebhookOptions\n ): Promise<Response> {\n const body = await request.text();\n this.logger.debug(\"Linear webhook raw body\", {\n body: body.substring(0, 500),\n });\n\n // Verify request signature (Linear-Signature header)\n // @see https://linear.app/developers/webhooks#securing-webhooks\n const signature = request.headers.get(\"linear-signature\");\n if (!this.verifySignature(body, signature)) {\n return new Response(\"Invalid signature\", { status: 401 });\n }\n\n // Parse the JSON payload\n let payload: LinearWebhookPayload;\n try {\n payload = JSON.parse(body);\n } catch {\n this.logger.error(\"Linear webhook invalid JSON\", {\n contentType: request.headers.get(\"content-type\"),\n bodyPreview: body.substring(0, 200),\n });\n return new Response(\"Invalid JSON\", { status: 400 });\n }\n\n // Validate webhook timestamp to prevent replay attacks (within 5 minutes)\n if (payload.webhookTimestamp) {\n const timeDiff = Math.abs(Date.now() - payload.webhookTimestamp);\n if (timeDiff > 5 * 60 * 1000) {\n this.logger.warn(\"Linear webhook timestamp too old\", {\n webhookTimestamp: payload.webhookTimestamp,\n timeDiff,\n });\n return new Response(\"Webhook expired\", { status: 401 });\n }\n }\n\n // Handle events based on type\n if (payload.type === \"Comment\") {\n const commentPayload = payload as CommentWebhookPayload;\n if (commentPayload.action === \"create\") {\n this.handleCommentCreated(commentPayload, options);\n }\n } else if (payload.type === \"Reaction\") {\n const reactionPayload = payload as ReactionWebhookPayload;\n this.handleReaction(reactionPayload);\n }\n\n return new Response(\"ok\", { status: 200 });\n }\n\n /**\n * Verify Linear webhook signature using HMAC-SHA256.\n *\n * @see https://linear.app/developers/webhooks#securing-webhooks\n */\n private verifySignature(body: string, signature: string | null): boolean {\n if (!signature) {\n return false;\n }\n\n const computedSignature = createHmac(\"sha256\", this.webhookSecret)\n .update(body)\n .digest(\"hex\");\n\n try {\n return timingSafeEqual(\n Buffer.from(computedSignature, \"hex\"),\n Buffer.from(signature, \"hex\")\n );\n } catch {\n return false;\n }\n }\n\n /**\n * Handle a new comment created on an issue.\n *\n * Threading logic:\n * - If the comment has a parentId, it's a reply -> thread under the parent (root comment)\n * - If no parentId, this is a root comment -> thread under this comment's own ID\n */\n private handleCommentCreated(\n payload: CommentWebhookPayload,\n options?: WebhookOptions\n ): void {\n if (!this.chat) {\n this.logger.warn(\"Chat instance not initialized, ignoring comment\");\n return;\n }\n\n const { data, actor } = payload;\n\n // Skip if the comment has no issueId (e.g., project update comment)\n if (!data.issueId) {\n this.logger.debug(\"Ignoring non-issue comment\", {\n commentId: data.id,\n });\n return;\n }\n\n // Determine thread: use parentId as root if it's a reply, otherwise this comment is the root\n const rootCommentId = data.parentId || data.id;\n const threadId = this.encodeThreadId({\n issueId: data.issueId,\n commentId: rootCommentId,\n });\n\n // Build message\n const message = this.buildMessage(data, actor, threadId);\n\n // Skip bot's own messages\n if (data.userId === this._botUserId) {\n this.logger.debug(\"Ignoring message from self\", {\n messageId: data.id,\n });\n return;\n }\n\n this.chat.processMessage(this, threadId, message, options);\n }\n\n /**\n * Handle reaction events (logging only - reactions don't include issueId).\n */\n private handleReaction(payload: ReactionWebhookPayload): void {\n if (!this.chat) {\n return;\n }\n\n const { data, actor } = payload;\n\n // Reactions on comments need a commentId to find the thread.\n // Since reaction webhooks don't include issueId directly,\n // we'd need an additional API call to look it up.\n this.logger.debug(\"Received reaction webhook\", {\n reactionId: data.id,\n emoji: data.emoji,\n commentId: data.commentId,\n action: payload.action,\n actorName: actor.name,\n });\n }\n\n /**\n * Build a Message from a Linear comment and actor.\n */\n private buildMessage(\n comment: LinearCommentData,\n actor: LinearWebhookActor,\n threadId: string\n ): Message<LinearRawMessage> {\n const text = comment.body || \"\";\n\n const author: Author = {\n userId: comment.userId,\n userName: actor.name || \"unknown\",\n fullName: actor.name || \"unknown\",\n isBot: actor.type !== \"user\",\n isMe: comment.userId === this._botUserId,\n };\n\n const formatted: FormattedContent = this.formatConverter.toAst(text);\n\n const raw: LinearRawMessage = {\n comment,\n organizationId: undefined,\n };\n\n return new Message<LinearRawMessage>({\n id: comment.id,\n threadId,\n text,\n formatted,\n raw,\n author,\n metadata: {\n dateSent: comment.createdAt ? new Date(comment.createdAt) : new Date(),\n edited: comment.createdAt !== comment.updatedAt,\n editedAt:\n comment.createdAt !== comment.updatedAt && comment.updatedAt\n ? new Date(comment.updatedAt)\n : undefined,\n },\n attachments: [],\n });\n }\n\n /**\n * Post a message to a thread (create a comment on an issue).\n *\n * For comment-level threads, uses parentId to reply under the root comment.\n * For issue-level threads, creates a top-level comment.\n *\n * Uses LinearClient.createComment({ issueId, body, parentId? }).\n * @see https://linear.app/developers/sdk-fetching-and-modifying-data#mutations\n */\n async postMessage(\n threadId: string,\n message: AdapterPostableMessage\n ): Promise<RawMessage<LinearRawMessage>> {\n await this.ensureValidToken();\n const { issueId, commentId } = this.decodeThreadId(threadId);\n\n // Render message to markdown\n let body: string;\n const card = extractCard(message);\n if (card) {\n body = cardToLinearMarkdown(card);\n } else {\n body = this.formatConverter.renderPostable(message);\n }\n\n // Convert emoji placeholders to unicode\n body = convertEmojiPlaceholders(body, \"linear\");\n\n // Create the comment via Linear SDK\n // If commentId is present, reply under that comment (comment-level thread)\n const commentPayload = await this.linearClient.createComment({\n issueId,\n body,\n parentId: commentId,\n });\n\n const comment = await commentPayload.comment;\n if (!comment) {\n throw new Error(\"Failed to create comment on Linear issue\");\n }\n\n return {\n id: comment.id,\n threadId,\n raw: {\n comment: {\n id: comment.id,\n body: comment.body,\n issueId,\n userId: this._botUserId || \"\",\n createdAt: comment.createdAt.toISOString(),\n updatedAt: comment.updatedAt.toISOString(),\n url: comment.url,\n },\n },\n };\n }\n\n /**\n * Edit an existing message (update a comment).\n *\n * Uses LinearClient.updateComment(id, { body }).\n * @see https://linear.app/developers/sdk-fetching-and-modifying-data#mutations\n */\n async editMessage(\n threadId: string,\n messageId: string,\n message: AdapterPostableMessage\n ): Promise<RawMessage<LinearRawMessage>> {\n await this.ensureValidToken();\n const { issueId } = this.decodeThreadId(threadId);\n\n // Render message to markdown\n let body: string;\n const card = extractCard(message);\n if (card) {\n body = cardToLinearMarkdown(card);\n } else {\n body = this.formatConverter.renderPostable(message);\n }\n\n // Convert emoji placeholders to unicode\n body = convertEmojiPlaceholders(body, \"linear\");\n\n // Update the comment via Linear SDK\n const commentPayload = await this.linearClient.updateComment(messageId, {\n body,\n });\n\n const comment = await commentPayload.comment;\n if (!comment) {\n throw new Error(\"Failed to update comment on Linear\");\n }\n\n return {\n id: comment.id,\n threadId,\n raw: {\n comment: {\n id: comment.id,\n body: comment.body,\n issueId,\n userId: this._botUserId || \"\",\n createdAt: comment.createdAt.toISOString(),\n updatedAt: comment.updatedAt.toISOString(),\n url: comment.url,\n },\n },\n };\n }\n\n /**\n * Delete a message (delete a comment).\n *\n * Uses LinearClient.deleteComment(id).\n */\n async deleteMessage(_threadId: string, messageId: string): Promise<void> {\n await this.ensureValidToken();\n await this.linearClient.deleteComment(messageId);\n }\n\n /**\n * Add a reaction to a comment.\n *\n * Uses LinearClient.createReaction({ commentId, emoji }).\n * Linear reactions use emoji strings (unicode).\n */\n async addReaction(\n _threadId: string,\n messageId: string,\n emoji: EmojiValue | string\n ): Promise<void> {\n await this.ensureValidToken();\n const emojiStr = this.resolveEmoji(emoji);\n await this.linearClient.createReaction({\n commentId: messageId,\n emoji: emojiStr,\n });\n }\n\n /**\n * Remove a reaction from a comment.\n *\n * Linear doesn't have a direct \"remove reaction by emoji + user\" API.\n * Removing requires knowing the reaction ID, which would require fetching\n * the comment's reactions first. This is a known limitation.\n */\n async removeReaction(\n _threadId: string,\n _messageId: string,\n _emoji: EmojiValue | string\n ): Promise<void> {\n this.logger.warn(\n \"removeReaction is not fully supported on Linear - reaction ID lookup would be required\"\n );\n }\n\n /**\n * Start typing indicator. Not supported by Linear.\n */\n async startTyping(_threadId: string): Promise<void> {\n // Linear doesn't support typing indicators\n }\n\n /**\n * Fetch messages from a thread.\n *\n * For issue-level threads: fetches all top-level issue comments.\n * For comment-level threads: fetches the root comment and its children (replies).\n */\n async fetchMessages(\n threadId: string,\n options?: FetchOptions\n ): Promise<FetchResult<LinearRawMessage>> {\n await this.ensureValidToken();\n const { issueId, commentId } = this.decodeThreadId(threadId);\n\n if (commentId) {\n // Comment-level thread: fetch root comment's children\n return this.fetchCommentThread(threadId, issueId, commentId, options);\n }\n\n // Issue-level thread: fetch all top-level comments\n return this.fetchIssueComments(threadId, issueId, options);\n }\n\n /**\n * Fetch top-level comments on an issue.\n */\n private async fetchIssueComments(\n threadId: string,\n issueId: string,\n options?: FetchOptions\n ): Promise<FetchResult<LinearRawMessage>> {\n const issue = await this.linearClient.issue(issueId);\n const commentsConnection = await issue.comments({\n first: options?.limit ?? 50,\n });\n\n const messages = await this.commentsToMessages(\n commentsConnection.nodes,\n threadId,\n issueId\n );\n\n return {\n messages,\n nextCursor: commentsConnection.pageInfo.hasNextPage\n ? commentsConnection.pageInfo.endCursor\n : undefined,\n };\n }\n\n /**\n * Fetch a comment thread (root comment + its children/replies).\n */\n private async fetchCommentThread(\n threadId: string,\n issueId: string,\n commentId: string,\n options?: FetchOptions\n ): Promise<FetchResult<LinearRawMessage>> {\n const rootComment = await this.linearClient.comment({ id: commentId });\n if (!rootComment) {\n return { messages: [] };\n }\n\n // Get the children (replies) of the root comment\n const childrenConnection = await rootComment.children({\n first: options?.limit ?? 50,\n });\n\n // Include the root comment as the first message, then its children\n const rootMessages = await this.commentsToMessages(\n [rootComment],\n threadId,\n issueId\n );\n const childMessages = await this.commentsToMessages(\n childrenConnection.nodes,\n threadId,\n issueId\n );\n\n return {\n messages: [...rootMessages, ...childMessages],\n nextCursor: childrenConnection.pageInfo.hasNextPage\n ? childrenConnection.pageInfo.endCursor\n : undefined,\n };\n }\n\n /**\n * Convert an array of Linear SDK Comment objects to Message instances.\n */\n private async commentsToMessages(\n comments: Array<{\n id: string;\n body: string;\n createdAt: Date;\n updatedAt: Date;\n url: string;\n user: LinearFetch<User> | undefined;\n }>,\n threadId: string,\n issueId: string\n ): Promise<Message<LinearRawMessage>[]> {\n const messages: Message<LinearRawMessage>[] = [];\n\n for (const comment of comments) {\n const user = await comment.user;\n const author: Author = {\n userId: user?.id || \"unknown\",\n userName: user?.displayName || \"unknown\",\n fullName: user?.name || user?.displayName || \"unknown\",\n isBot: false,\n isMe: user?.id === this._botUserId,\n };\n\n const formatted: FormattedContent = this.formatConverter.toAst(\n comment.body\n );\n\n messages.push(\n new Message<LinearRawMessage>({\n id: comment.id,\n threadId,\n text: comment.body,\n formatted,\n author,\n metadata: {\n dateSent: new Date(comment.createdAt),\n edited: comment.createdAt.getTime() !== comment.updatedAt.getTime(),\n editedAt:\n comment.createdAt.getTime() !== comment.updatedAt.getTime()\n ? new Date(comment.updatedAt)\n : undefined,\n },\n attachments: [],\n raw: {\n comment: {\n id: comment.id,\n body: comment.body,\n issueId,\n userId: user?.id || \"unknown\",\n createdAt: comment.createdAt.toISOString(),\n updatedAt: comment.updatedAt.toISOString(),\n url: comment.url,\n },\n },\n })\n );\n }\n\n return messages;\n }\n\n /**\n * Fetch thread info for a Linear issue.\n */\n async fetchThread(threadId: string): Promise<ThreadInfo> {\n await this.ensureValidToken();\n const { issueId } = this.decodeThreadId(threadId);\n\n const issue = await this.linearClient.issue(issueId);\n\n return {\n id: threadId,\n channelId: issueId,\n channelName: `${issue.identifier}: ${issue.title}`,\n isDM: false,\n metadata: {\n issueId,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n },\n };\n }\n\n /**\n * Encode a Linear thread ID.\n *\n * Formats:\n * - Issue-level: linear:{issueId}\n * - Comment thread: linear:{issueId}:c:{commentId}\n */\n encodeThreadId(platformData: LinearThreadId): string {\n if (platformData.commentId) {\n return `linear:${platformData.issueId}:c:${platformData.commentId}`;\n }\n return `linear:${platformData.issueId}`;\n }\n\n /**\n * Decode a Linear thread ID.\n *\n * Formats:\n * - Issue-level: linear:{issueId}\n * - Comment thread: linear:{issueId}:c:{commentId}\n */\n decodeThreadId(threadId: string): LinearThreadId {\n if (!threadId.startsWith(\"linear:\")) {\n throw new ValidationError(\n \"linear\",\n `Invalid Linear thread ID: ${threadId}`\n );\n }\n\n const withoutPrefix = threadId.slice(7);\n if (!withoutPrefix) {\n throw new ValidationError(\n \"linear\",\n `Invalid Linear thread ID format: ${threadId}`\n );\n }\n\n // Check for comment thread format: {issueId}:c:{commentId}\n const commentMatch = withoutPrefix.match(COMMENT_THREAD_PATTERN);\n if (commentMatch) {\n return {\n issueId: commentMatch[1],\n commentId: commentMatch[2],\n };\n }\n\n // Issue-level format: {issueId}\n return { issueId: withoutPrefix };\n }\n\n /**\n * Derive channel ID from a Linear thread ID.\n * linear:{issueId}:c:{commentId} -> linear:{issueId}\n * linear:{issueId} -> linear:{issueId}\n */\n channelIdFromThreadId(threadId: string): string {\n const { issueId } = this.decodeThreadId(threadId);\n return `linear:${issueId}`;\n }\n\n /**\n * Parse platform message format to normalized format.\n */\n parseMessage(raw: LinearRawMessage): Message<LinearRawMessage> {\n const text = raw.comment.body || \"\";\n const formatted: FormattedContent = this.formatConverter.toAst(text);\n\n return new Message<LinearRawMessage>({\n id: raw.comment.id,\n threadId: \"\",\n text,\n formatted,\n author: {\n userId: raw.comment.userId,\n userName: \"unknown\",\n fullName: \"unknown\",\n isBot: false,\n isMe: raw.comment.userId === this._botUserId,\n },\n metadata: {\n dateSent: raw.comment.createdAt\n ? new Date(raw.comment.createdAt)\n : new Date(),\n edited: raw.comment.createdAt !== raw.comment.updatedAt,\n editedAt:\n raw.comment.createdAt !== raw.comment.updatedAt &&\n raw.comment.updatedAt\n ? new Date(raw.comment.updatedAt)\n : undefined,\n },\n attachments: [],\n raw,\n });\n }\n\n /**\n * Render formatted content to Linear markdown.\n */\n renderFormatted(content: FormattedContent): string {\n return this.formatConverter.fromAst(content);\n }\n\n /**\n * Resolve an emoji value to a unicode string.\n * Linear uses standard unicode emoji for reactions.\n */\n private resolveEmoji(emoji: EmojiValue | string): string {\n const emojiName = typeof emoji === \"string\" ? emoji : emoji.name;\n\n const mapping: Record<string, string> = {\n thumbs_up: \"\\u{1F44D}\",\n thumbs_down: \"\\u{1F44E}\",\n heart: \"\\u{2764}\\u{FE0F}\",\n fire: \"\\u{1F525}\",\n rocket: \"\\u{1F680}\",\n eyes: \"\\u{1F440}\",\n check: \"\\u{2705}\",\n warning: \"\\u{26A0}\\u{FE0F}\",\n sparkles: \"\\u{2728}\",\n wave: \"\\u{1F44B}\",\n raised_hands: \"\\u{1F64C}\",\n laugh: \"\\u{1F604}\",\n hooray: \"\\u{1F389}\",\n confused: \"\\u{1F615}\",\n };\n\n return mapping[emojiName] || emojiName;\n }\n}\n\n/**\n * Factory function to create a Linear adapter.\n *\n * @example\n * ```typescript\n * const adapter = createLinearAdapter({\n * apiKey: process.env.LINEAR_API_KEY!,\n * webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,\n * userName: \"my-bot\",\n * logger: console,\n * });\n * ```\n */\nexport function createLinearAdapter(\n config: LinearAdapterConfig\n): LinearAdapter {\n return new LinearAdapter(config);\n}\n","/**\n * Convert CardElement to Linear-compatible markdown.\n *\n * Since Linear doesn't support rich cards natively, we render cards\n * as clean formatted markdown. Linear comments support standard\n * markdown syntax.\n *\n * @see https://linear.app/docs/comment-on-issues\n */\n\nimport type {\n ActionsElement,\n CardChild,\n CardElement,\n FieldsElement,\n TextElement,\n} from \"chat\";\n\n/**\n * Convert a CardElement to Linear-compatible markdown.\n *\n * Cards are rendered as clean markdown with:\n * - Bold title and subtitle\n * - Text content\n * - Fields as key-value pairs\n * - Buttons as markdown links (action buttons become bold text since Linear has no interactivity)\n *\n * @example\n * ```typescript\n * const card = Card({\n * title: \"Order #1234\",\n * subtitle: \"Status update\",\n * children: [\n * Text(\"Your order has been shipped!\"),\n * Fields([\n * Field({ label: \"Tracking\", value: \"ABC123\" }),\n * ]),\n * Actions([\n * LinkButton({ url: \"https://track.example.com\", label: \"Track Order\" }),\n * ]),\n * ],\n * });\n *\n * // Output:\n * // **Order #1234**\n * // Status update\n * //\n * // Your order has been shipped!\n * //\n * // **Tracking:** ABC123\n * //\n * // [Track Order](https://track.example.com)\n * ```\n */\nexport function cardToLinearMarkdown(card: CardElement): string {\n const lines: string[] = [];\n\n // Title (bold)\n if (card.title) {\n lines.push(`**${escapeMarkdown(card.title)}**`);\n }\n\n // Subtitle\n if (card.subtitle) {\n lines.push(escapeMarkdown(card.subtitle));\n }\n\n // Add spacing after header if there are children\n if ((card.title || card.subtitle) && card.children.length > 0) {\n lines.push(\"\");\n }\n\n // Header image\n if (card.imageUrl) {\n lines.push(``);\n lines.push(\"\");\n }\n\n // Children\n for (let i = 0; i < card.children.length; i++) {\n const child = card.children[i];\n const childLines = renderChild(child);\n\n if (childLines.length > 0) {\n lines.push(...childLines);\n\n // Add spacing between children (except last)\n if (i < card.children.length - 1) {\n lines.push(\"\");\n }\n }\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Render a card child element to markdown lines.\n */\nfunction renderChild(child: CardChild): string[] {\n switch (child.type) {\n case \"text\":\n return renderText(child);\n\n case \"fields\":\n return renderFields(child);\n\n case \"actions\":\n return renderActions(child);\n\n case \"section\":\n // Flatten section children\n return child.children.flatMap(renderChild);\n\n case \"image\":\n if (child.alt) {\n return [``];\n }\n return [``];\n\n case \"divider\":\n return [\"---\"];\n\n default:\n return [];\n }\n}\n\n/**\n * Render text element.\n */\nfunction renderText(text: TextElement): string[] {\n const content = text.content;\n\n switch (text.style) {\n case \"bold\":\n return [`**${content}**`];\n case \"muted\":\n return [`_${content}_`];\n default:\n return [content];\n }\n}\n\n/**\n * Render fields as key-value pairs.\n */\nfunction renderFields(fields: FieldsElement): string[] {\n return fields.children.map(\n (field) =>\n `**${escapeMarkdown(field.label)}:** ${escapeMarkdown(field.value)}`\n );\n}\n\n/**\n * Render actions (buttons) as markdown links or bold text.\n */\nfunction renderActions(actions: ActionsElement): string[] {\n const buttonTexts = actions.children.map((button) => {\n if (button.type === \"link-button\") {\n return `[${escapeMarkdown(button.label)}](${button.url})`;\n }\n // Action buttons become bold text (no interactivity in Linear comments)\n return `**[${escapeMarkdown(button.label)}]**`;\n });\n\n return [buttonTexts.join(\" \\u2022 \")];\n}\n\n/**\n * Escape special markdown characters in text.\n */\nfunction escapeMarkdown(text: string): string {\n return text\n .replace(/\\*/g, \"\\\\*\")\n .replace(/_/g, \"\\\\_\")\n .replace(/\\[/g, \"\\\\[\")\n .replace(/\\]/g, \"\\\\]\");\n}\n\n/**\n * Generate plain text fallback from a card (no markdown).\n */\nexport function cardToPlainText(card: CardElement): string {\n const parts: string[] = [];\n\n if (card.title) {\n parts.push(card.title);\n }\n\n if (card.subtitle) {\n parts.push(card.subtitle);\n }\n\n for (const child of card.children) {\n const text = childToPlainText(child);\n if (text) {\n parts.push(text);\n }\n }\n\n return parts.join(\"\\n\");\n}\n\n/**\n * Convert card child to plain text.\n */\nfunction childToPlainText(child: CardChild): string | null {\n switch (child.type) {\n case \"text\":\n return child.content;\n case \"fields\":\n return child.children.map((f) => `${f.label}: ${f.value}`).join(\"\\n\");\n case \"actions\":\n // Actions are interactive-only — exclude from fallback text.\n // See: https://docs.slack.dev/reference/methods/chat.postMessage\n return null;\n case \"section\":\n return child.children.map(childToPlainText).filter(Boolean).join(\"\\n\");\n default:\n return null;\n }\n}\n","/**\n * Linear-specific format conversion using AST-based parsing.\n *\n * Linear uses standard Markdown for comments, which is very close\n * to the mdast format used by the SDK. This converter is mostly\n * pass-through, similar to the GitHub adapter.\n *\n * @see https://linear.app/docs/comment-on-issues\n */\n\nimport {\n type AdapterPostableMessage,\n BaseFormatConverter,\n parseMarkdown,\n type Root,\n stringifyMarkdown,\n} from \"chat\";\n\nexport class LinearFormatConverter extends BaseFormatConverter {\n /**\n * Convert an AST to standard markdown.\n * Linear uses standard markdown, so we use remark-stringify directly.\n */\n fromAst(ast: Root): string {\n return stringifyMarkdown(ast).trim();\n }\n\n /**\n * Parse markdown into an AST.\n * Linear uses standard markdown, so we use the standard parser.\n */\n toAst(markdown: string): Root {\n return parseMarkdown(markdown);\n }\n\n /**\n * Render a postable message to Linear markdown string.\n */\n override renderPostable(message: AdapterPostableMessage): string {\n if (typeof message === \"string\") {\n return message;\n }\n if (\"raw\" in message) {\n return message.raw;\n }\n if (\"markdown\" in message) {\n return this.fromMarkdown(message.markdown);\n }\n if (\"ast\" in message) {\n return this.fromAst(message.ast);\n }\n // Handle cards via base class\n return super.renderPostable(message);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chat-adapter/linear",
|
|
3
|
-
"version": "4.13.
|
|
3
|
+
"version": "4.13.3",
|
|
4
4
|
"description": "Linear adapter for chat - issue comment threads",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@linear/sdk": "^37.0.0",
|
|
20
|
-
"@chat-adapter/shared": "4.13.
|
|
21
|
-
"chat": "4.13.
|
|
20
|
+
"@chat-adapter/shared": "4.13.3",
|
|
21
|
+
"chat": "4.13.3"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^22.10.2",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
},
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "git+https://github.com/vercel
|
|
31
|
+
"url": "git+https://github.com/vercel/chat.git",
|
|
32
32
|
"directory": "packages/adapter-linear"
|
|
33
33
|
},
|
|
34
|
-
"homepage": "https://github.com/vercel
|
|
34
|
+
"homepage": "https://github.com/vercel/chat#readme",
|
|
35
35
|
"bugs": {
|
|
36
|
-
"url": "https://github.com/vercel
|
|
36
|
+
"url": "https://github.com/vercel/chat/issues"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
@@ -50,10 +50,9 @@
|
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "tsup",
|
|
52
52
|
"dev": "tsup --watch",
|
|
53
|
-
"test": "vitest run",
|
|
53
|
+
"test": "vitest run --coverage",
|
|
54
54
|
"test:watch": "vitest",
|
|
55
55
|
"typecheck": "tsc --noEmit",
|
|
56
|
-
"lint": "biome check src",
|
|
57
56
|
"clean": "rm -rf dist"
|
|
58
57
|
}
|
|
59
58
|
}
|