@calltelemetry/openclaw-linear 0.2.0
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 +468 -0
- package/index.ts +56 -0
- package/openclaw.plugin.json +17 -0
- package/package.json +38 -0
- package/src/agent.ts +57 -0
- package/src/auth.ts +130 -0
- package/src/client.ts +93 -0
- package/src/linear-api.ts +384 -0
- package/src/oauth-callback.ts +113 -0
- package/src/pipeline.ts +212 -0
- package/src/tools.ts +84 -0
- package/src/webhook.test.ts +191 -0
- package/src/webhook.ts +852 -0
package/README.md
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# Linear Agent Plugin for OpenClaw
|
|
2
|
+
|
|
3
|
+
Webhook-driven Linear integration with OAuth support, multi-agent routing, and a 3-stage AI pipeline for issue triage and implementation.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
- **Issue triage** — When an issue is assigned/delegated to the app user, an agent estimates story points, applies labels, and posts an assessment
|
|
8
|
+
- **Agent sessions** — Full plan-approve-implement-audit pipeline triggered from Linear's agent UI
|
|
9
|
+
- **@mention routing** — Comment mentions like `@qa` or `@infra` route to specific role-based agents with different expertise
|
|
10
|
+
- **App notifications** — Responds to Linear app mentions and assignments via branded comments
|
|
11
|
+
- **Activity tracking** — Emits thought/action/response events visible in Linear's agent session UI
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- OpenClaw gateway running (systemd service)
|
|
16
|
+
- A Linear workspace with API access
|
|
17
|
+
- A Linear OAuth application (Settings > API > Applications)
|
|
18
|
+
- A public URL for webhook delivery (e.g., Cloudflare tunnel)
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
### 1. Create a Linear OAuth Application
|
|
23
|
+
|
|
24
|
+
1. Go to **Linear Settings > API > Applications**
|
|
25
|
+
2. Click **Create new application**
|
|
26
|
+
3. Fill in:
|
|
27
|
+
- **Application name:** your agent's name
|
|
28
|
+
- **Redirect URI:** `https://<your-domain>/linear/oauth/callback`
|
|
29
|
+
- **Webhook URL:** `https://<your-domain>/linear/webhook`
|
|
30
|
+
4. Note the **Client ID** and **Client Secret**
|
|
31
|
+
5. Enable the webhook events you need (Agent Sessions, Issues)
|
|
32
|
+
|
|
33
|
+
### 2. Create a Workspace Webhook
|
|
34
|
+
|
|
35
|
+
Separately from the OAuth app, create a workspace-level webhook:
|
|
36
|
+
|
|
37
|
+
1. Go to **Linear Settings > API > Webhooks**
|
|
38
|
+
2. Create a new webhook pointing to `https://<your-domain>/linear/webhook`
|
|
39
|
+
3. Enable these event types: **Comment**, **Issue**, **User**
|
|
40
|
+
|
|
41
|
+
> **Why two webhooks?** The OAuth app webhook handles `AgentSessionEvent` and `AppUserNotification` events (agent-specific). The workspace webhook handles `Comment` and `Issue` events (workspace-wide). Both must point to the same URL — the plugin routes internally.
|
|
42
|
+
|
|
43
|
+
### 3. Expose the Gateway via Cloudflare Tunnel
|
|
44
|
+
|
|
45
|
+
The OpenClaw gateway listens on `localhost:<port>` (default `18789`). Linear must reach it over HTTPS to deliver webhooks. A [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) is the recommended approach — no open inbound ports, no self-managed TLS.
|
|
46
|
+
|
|
47
|
+
#### Install `cloudflared`
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# RHEL / Rocky / Alma
|
|
51
|
+
sudo dnf install -y cloudflared
|
|
52
|
+
|
|
53
|
+
# Debian / Ubuntu
|
|
54
|
+
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
|
55
|
+
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
|
|
56
|
+
sudo apt update && sudo apt install -y cloudflared
|
|
57
|
+
|
|
58
|
+
# macOS
|
|
59
|
+
brew install cloudflare/cloudflare/cloudflared
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Authenticate with Cloudflare
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cloudflared tunnel login
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This opens your browser to Cloudflare's authorization page. You must:
|
|
69
|
+
|
|
70
|
+
1. Log in to your Cloudflare account
|
|
71
|
+
2. **Select the domain** (zone) you want the tunnel to use (e.g., `yourdomain.com`)
|
|
72
|
+
3. Click **Authorize**
|
|
73
|
+
|
|
74
|
+
Cloudflare writes an origin certificate to `~/.cloudflared/cert.pem`. This certificate grants `cloudflared` permission to create tunnels and DNS records under that domain. Without it, tunnel creation will fail.
|
|
75
|
+
|
|
76
|
+
#### Create a tunnel
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cloudflared tunnel create openclaw
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This outputs a **Tunnel ID** (a UUID like `da1f21bf-856e-...`) and writes a credentials file to `~/.cloudflared/<TUNNEL_ID>.json`.
|
|
83
|
+
|
|
84
|
+
#### Create a DNS subdomain for the tunnel
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cloudflared tunnel route dns openclaw linear.yourdomain.com
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This creates a **CNAME record** in your Cloudflare DNS:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
linear.yourdomain.com CNAME <TUNNEL_ID>.cfargotunnel.com
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
You can verify it in the Cloudflare dashboard under **DNS > Records** for your domain. The subdomain (`linear.yourdomain.com`) is what Linear will use for webhook delivery and OAuth callbacks.
|
|
97
|
+
|
|
98
|
+
> **Important:** Your domain must already be on Cloudflare (nameservers pointed to Cloudflare). If it's not, add it in the Cloudflare dashboard first.
|
|
99
|
+
|
|
100
|
+
#### Configure the tunnel
|
|
101
|
+
|
|
102
|
+
Create `~/.cloudflared/config.yml`:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
tunnel: <TUNNEL_ID>
|
|
106
|
+
credentials-file: /home/<user>/.cloudflared/<TUNNEL_ID>.json
|
|
107
|
+
|
|
108
|
+
ingress:
|
|
109
|
+
- hostname: linear.yourdomain.com
|
|
110
|
+
service: http://localhost:18789
|
|
111
|
+
- service: http_status:404
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The `ingress` rule routes all traffic for your subdomain to the OpenClaw gateway on localhost. The catch-all `http_status:404` rejects requests for any other hostname.
|
|
115
|
+
|
|
116
|
+
#### Run as a systemd service
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
sudo cloudflared service install
|
|
120
|
+
sudo systemctl enable --now cloudflared
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This installs a system-level service that starts on boot. To test without installing:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
cloudflared tunnel run openclaw
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Verify end-to-end
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
curl -s https://linear.yourdomain.com/linear/webhook \
|
|
133
|
+
-X POST -H "Content-Type: application/json" \
|
|
134
|
+
-d '{"type":"test","action":"ping"}'
|
|
135
|
+
# Should return: "ok"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
> **Note:** The hostname you choose here (`linear.yourdomain.com`) is what you'll use for the OAuth redirect URI and both webhook URLs in Linear. Make sure they all match.
|
|
139
|
+
|
|
140
|
+
### 4. Set Environment Variables
|
|
141
|
+
|
|
142
|
+
Required:
|
|
143
|
+
```bash
|
|
144
|
+
export LINEAR_CLIENT_ID="your_client_id"
|
|
145
|
+
export LINEAR_CLIENT_SECRET="your_client_secret"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Optional:
|
|
149
|
+
```bash
|
|
150
|
+
export LINEAR_REDIRECT_URI="https://your-domain.com/linear/oauth/callback"
|
|
151
|
+
export OPENCLAW_GATEWAY_PORT="18789" # if non-default
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 5. Install the Plugin
|
|
155
|
+
|
|
156
|
+
Add the plugin path to your OpenClaw config (`~/.openclaw/openclaw.json`):
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"plugins": {
|
|
161
|
+
"load": {
|
|
162
|
+
"paths": ["/path/to/claw-extensions/linear"]
|
|
163
|
+
},
|
|
164
|
+
"entries": {
|
|
165
|
+
"linear": {
|
|
166
|
+
"enabled": true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Restart the gateway to load the plugin:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
openclaw gateway restart
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 6. Run the OAuth Flow
|
|
180
|
+
|
|
181
|
+
There are two ways to authorize the plugin with Linear.
|
|
182
|
+
|
|
183
|
+
#### Option A: CLI Flow (Recommended)
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
openclaw auth linear oauth
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
This launches the OAuth flow interactively:
|
|
190
|
+
|
|
191
|
+
1. The plugin constructs the authorization URL with the required scopes
|
|
192
|
+
2. Your browser opens to Linear's authorization page
|
|
193
|
+
3. You approve the permissions
|
|
194
|
+
4. Linear redirects to the callback URL with an authorization code
|
|
195
|
+
5. The plugin exchanges the code for access + refresh tokens
|
|
196
|
+
6. Tokens are stored in `~/.openclaw/auth-profiles.json`
|
|
197
|
+
|
|
198
|
+
#### Option B: Manual URL
|
|
199
|
+
|
|
200
|
+
If the CLI flow doesn't work (headless server, tunnel issues), construct the URL yourself:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
https://linear.app/oauth/authorize
|
|
204
|
+
?client_id=YOUR_CLIENT_ID
|
|
205
|
+
&redirect_uri=https://your-domain.com/linear/oauth/callback
|
|
206
|
+
&response_type=code
|
|
207
|
+
&scope=read,write,app:assignable,app:mentionable
|
|
208
|
+
&state=random_string
|
|
209
|
+
&actor=app
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Key parameters:
|
|
213
|
+
|
|
214
|
+
| Parameter | Value | Why |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| `scope` | `read,write,app:assignable,app:mentionable` | `app:assignable` lets the agent appear in assignment menus. `app:mentionable` lets users @mention the agent. |
|
|
217
|
+
| `actor` | `app` | Makes the token act as the **application identity**, not a personal user. Agent sessions require this. |
|
|
218
|
+
| `redirect_uri` | Your callback URL | Must match what you registered in the OAuth app settings. |
|
|
219
|
+
|
|
220
|
+
Click the URL, authorize in Linear, and the callback handler at `/linear/oauth/callback` will exchange the code and store the tokens automatically.
|
|
221
|
+
|
|
222
|
+
#### What Gets Stored
|
|
223
|
+
|
|
224
|
+
After a successful OAuth flow, `~/.openclaw/auth-profiles.json` will contain:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"version": 1,
|
|
229
|
+
"profiles": {
|
|
230
|
+
"linear:default": {
|
|
231
|
+
"type": "oauth",
|
|
232
|
+
"provider": "linear",
|
|
233
|
+
"accessToken": "...",
|
|
234
|
+
"refreshToken": "...",
|
|
235
|
+
"expiresAt": 1708109280000,
|
|
236
|
+
"scope": "app:assignable app:mentionable read write"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
This file should be `chmod 600` (owner-only). The plugin auto-refreshes tokens 60 seconds before expiry and persists the new tokens back to this file.
|
|
243
|
+
|
|
244
|
+
### 7. Configure Agent Profiles
|
|
245
|
+
|
|
246
|
+
Create `~/.openclaw/agent-profiles.json` to define role-based agents:
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"agents": {
|
|
251
|
+
"lead": {
|
|
252
|
+
"label": "Lead",
|
|
253
|
+
"mission": "Product owner. Sets direction, makes scope decisions, prioritizes backlog.",
|
|
254
|
+
"isDefault": true,
|
|
255
|
+
"mentionAliases": ["lead", "product"],
|
|
256
|
+
"appAliases": ["myagent"],
|
|
257
|
+
"avatarUrl": "https://example.com/lead-avatar.png"
|
|
258
|
+
},
|
|
259
|
+
"qa": {
|
|
260
|
+
"label": "QA",
|
|
261
|
+
"mission": "Test engineer. Quality guardian, test strategy, release confidence.",
|
|
262
|
+
"mentionAliases": ["qa", "tester"]
|
|
263
|
+
},
|
|
264
|
+
"infra": {
|
|
265
|
+
"label": "Infra",
|
|
266
|
+
"mission": "Backend and infrastructure engineer. Performance, reliability, observability.",
|
|
267
|
+
"mentionAliases": ["infra", "backend"]
|
|
268
|
+
},
|
|
269
|
+
"ux": {
|
|
270
|
+
"label": "UX",
|
|
271
|
+
"mission": "User experience advocate. Accessibility, user journeys, pain points.",
|
|
272
|
+
"mentionAliases": ["ux", "design"]
|
|
273
|
+
},
|
|
274
|
+
"docs": {
|
|
275
|
+
"label": "Docs",
|
|
276
|
+
"mission": "Technical writer. Setup guides, API references, release notes.",
|
|
277
|
+
"mentionAliases": ["docs", "writer"]
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
| Field | Required | Description |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `label` | Yes | Display name in Linear comments |
|
|
286
|
+
| `mission` | Yes | Agent's role description (provided as context when dispatched) |
|
|
287
|
+
| `isDefault` | One agent | The default agent handles OAuth app events and assignment triage |
|
|
288
|
+
| `mentionAliases` | Yes | @mention triggers in comments (e.g., `@qa` in a comment routes to the QA agent) |
|
|
289
|
+
| `appAliases` | No | Triggers via OAuth app webhook (default agent only, for app-level @mentions) |
|
|
290
|
+
| `avatarUrl` | No | Avatar displayed on branded comments. Falls back to `[Label]` prefix if not set. |
|
|
291
|
+
|
|
292
|
+
Each agent ID (the JSON key) must match a configured OpenClaw agent in `openclaw.json`. The plugin dispatches to agents via `openclaw agent --agent <id>`.
|
|
293
|
+
|
|
294
|
+
### 8. Verify
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
openclaw gateway restart
|
|
298
|
+
openclaw logs | grep -i linear
|
|
299
|
+
# Should show: "Linear agent extension registered (agent: default, token: profile)"
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Test the webhook is reachable:
|
|
303
|
+
```bash
|
|
304
|
+
curl -s -X POST https://your-domain.com/linear/webhook \
|
|
305
|
+
-H "Content-Type: application/json" \
|
|
306
|
+
-d '{"type":"test","action":"ping"}'
|
|
307
|
+
# Should return: "ok"
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## How It Works
|
|
311
|
+
|
|
312
|
+
### Token Resolution
|
|
313
|
+
|
|
314
|
+
The plugin resolves an OAuth token from:
|
|
315
|
+
|
|
316
|
+
1. Plugin config `accessToken` (static, for testing)
|
|
317
|
+
2. Auth profile store `linear:default` (from the OAuth flow — this is the normal path)
|
|
318
|
+
|
|
319
|
+
OAuth is required. The plugin needs `app:assignable` and `app:mentionable` scopes to function — agent sessions, branded comments, assignment triage, and @mention routing all depend on the application identity that only OAuth provides.
|
|
320
|
+
|
|
321
|
+
### Webhook Event Routing
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
POST /linear/webhook
|
|
325
|
+
|
|
|
326
|
+
+-- AgentSessionEvent.created --> 3-stage pipeline (plan -> implement -> audit)
|
|
327
|
+
+-- AgentSessionEvent.prompted --> Resume pipeline (user approved plan)
|
|
328
|
+
+-- AppUserNotification --> Direct agent response to mention/assignment
|
|
329
|
+
+-- Comment.create --> Route @mention to role-based agent
|
|
330
|
+
+-- Issue.update --> Triage if assigned/delegated to app user
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
All handlers respond `200 OK` within 5 seconds (Linear requirement), then process asynchronously.
|
|
334
|
+
|
|
335
|
+
### Pipeline Stages
|
|
336
|
+
|
|
337
|
+
Triggered by `AgentSessionEvent.created`:
|
|
338
|
+
|
|
339
|
+
| Stage | Timeout | What It Does |
|
|
340
|
+
|---|---|---|
|
|
341
|
+
| **Planner** | 5 min | Analyzes issue, generates implementation plan, posts as comment, waits for approval |
|
|
342
|
+
| **Implementor** | 10 min | Follows the approved plan, makes changes, creates commits/PRs |
|
|
343
|
+
| **Auditor** | 5 min | Reviews implementation against plan, posts audit report |
|
|
344
|
+
|
|
345
|
+
The auditor stage can be disabled via plugin config: `"enableAudit": false`.
|
|
346
|
+
|
|
347
|
+
### Assignment Triage
|
|
348
|
+
|
|
349
|
+
When an issue is assigned or delegated to the app user:
|
|
350
|
+
|
|
351
|
+
1. Fetches full issue details and available team labels
|
|
352
|
+
2. Dispatches the default agent with a triage prompt
|
|
353
|
+
3. Agent returns JSON with story point estimate and label IDs
|
|
354
|
+
4. Plugin applies the estimate and labels to the issue
|
|
355
|
+
5. Posts the assessment as a branded comment
|
|
356
|
+
|
|
357
|
+
### @Mention Routing
|
|
358
|
+
|
|
359
|
+
When a comment contains `@qa`, `@infra`, or any configured `mentionAliases`:
|
|
360
|
+
|
|
361
|
+
1. Plugin matches the alias to an agent profile
|
|
362
|
+
2. Reacts with eyes emoji to acknowledge
|
|
363
|
+
3. Fetches full issue context (description, recent comments, labels, state)
|
|
364
|
+
4. Dispatches the matched agent with the comment context
|
|
365
|
+
5. Posts the agent's response as a branded comment on the issue
|
|
366
|
+
|
|
367
|
+
The default agent's `mentionAliases` are excluded from comment routing — the default agent is reached via `appAliases` through the OAuth app webhook instead.
|
|
368
|
+
|
|
369
|
+
### Comment Deduplication
|
|
370
|
+
|
|
371
|
+
Webhook events are deduplicated for 60 seconds using a key based on:
|
|
372
|
+
- Comment ID (for `Comment.create`)
|
|
373
|
+
- Session ID (for `AgentSessionEvent`)
|
|
374
|
+
- Assignment tuple (for `Issue.update`)
|
|
375
|
+
|
|
376
|
+
## Plugin Config Schema
|
|
377
|
+
|
|
378
|
+
Optional settings in `openclaw.json` under the plugin entry:
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"plugins": {
|
|
383
|
+
"entries": {
|
|
384
|
+
"linear": {
|
|
385
|
+
"enabled": true,
|
|
386
|
+
"clientId": "...",
|
|
387
|
+
"clientSecret": "...",
|
|
388
|
+
"redirectUri": "...",
|
|
389
|
+
"accessToken": "...",
|
|
390
|
+
"defaultAgentId": "...",
|
|
391
|
+
"enableAudit": true
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
All fields are optional — environment variables and auth profiles are the preferred configuration method.
|
|
399
|
+
|
|
400
|
+
## HTTP Routes
|
|
401
|
+
|
|
402
|
+
| Route | Method | Purpose |
|
|
403
|
+
|---|---|---|
|
|
404
|
+
| `/linear/webhook` | POST | Primary webhook endpoint |
|
|
405
|
+
| `/hooks/linear` | POST | Backward-compatible webhook endpoint |
|
|
406
|
+
| `/linear/oauth/callback` | GET | OAuth authorization callback |
|
|
407
|
+
|
|
408
|
+
## Agent Tools
|
|
409
|
+
|
|
410
|
+
Agents have access to these Linear tools during execution:
|
|
411
|
+
|
|
412
|
+
| Tool | Description |
|
|
413
|
+
|---|---|
|
|
414
|
+
| `linear_list_issues` | List issues (with optional team filter) |
|
|
415
|
+
| `linear_create_issue` | Create a new issue |
|
|
416
|
+
| `linear_add_comment` | Add a comment to an issue |
|
|
417
|
+
|
|
418
|
+
## File Structure
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
linear/
|
|
422
|
+
├── index.ts # Entry point, registers routes and provider
|
|
423
|
+
├── openclaw.plugin.json # Plugin metadata and config schema
|
|
424
|
+
├── package.json # Package definition (zero runtime deps)
|
|
425
|
+
├── README.md
|
|
426
|
+
└── src/
|
|
427
|
+
├── agent.ts # Agent dispatch via openclaw CLI
|
|
428
|
+
├── auth.ts # OAuth provider registration and token refresh
|
|
429
|
+
├── client.ts # Basic GraphQL client (for agent tools)
|
|
430
|
+
├── linear-api.ts # Full GraphQL API wrapper (LinearAgentApi)
|
|
431
|
+
├── oauth-callback.ts # OAuth callback handler
|
|
432
|
+
├── pipeline.ts # 3-stage pipeline (plan -> implement -> audit)
|
|
433
|
+
├── tools.ts # Agent tools (list, create, comment)
|
|
434
|
+
├── webhook.ts # Webhook dispatcher (5 event handlers)
|
|
435
|
+
└── webhook.test.ts # Tests (vitest)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Troubleshooting
|
|
439
|
+
|
|
440
|
+
**Plugin not loading:**
|
|
441
|
+
```bash
|
|
442
|
+
openclaw doctor --fix
|
|
443
|
+
openclaw logs | grep -i "linear\|plugin\|error"
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**Webhook not receiving events:**
|
|
447
|
+
- Verify both webhooks (workspace + OAuth app) point to the same URL
|
|
448
|
+
- Check that your tunnel/proxy is forwarding to the gateway port
|
|
449
|
+
- Linear requires `200 OK` within 5 seconds — check for gateway latency
|
|
450
|
+
|
|
451
|
+
**Agent sessions not working:**
|
|
452
|
+
- OAuth tokens require `app:assignable` and `app:mentionable` scopes
|
|
453
|
+
- Personal API keys cannot create agent sessions — use OAuth
|
|
454
|
+
- Re-run `openclaw auth linear oauth` to get fresh tokens
|
|
455
|
+
|
|
456
|
+
**"No defaultAgentId" error:**
|
|
457
|
+
- Set `defaultAgentId` in plugin config, OR
|
|
458
|
+
- Mark one agent as `"isDefault": true` in `agent-profiles.json`
|
|
459
|
+
|
|
460
|
+
**Token refresh failures:**
|
|
461
|
+
- Ensure `LINEAR_CLIENT_ID` and `LINEAR_CLIENT_SECRET` are set
|
|
462
|
+
- Check that the refresh token in `auth-profiles.json` hasn't been revoked
|
|
463
|
+
- Re-run the OAuth flow to get new tokens
|
|
464
|
+
|
|
465
|
+
**OAuth callback not working:**
|
|
466
|
+
- Verify the redirect URI in Linear's app settings matches your gateway URL
|
|
467
|
+
- If behind a reverse proxy, ensure `X-Forwarded-Proto` and `Host` headers are forwarded
|
|
468
|
+
- For local dev, the callback defaults to `http://localhost:<gateway-port>/linear/oauth/callback`
|
package/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { registerLinearProvider } from "./src/auth.js";
|
|
3
|
+
import { createLinearTools } from "./src/tools.js";
|
|
4
|
+
import { handleLinearWebhook } from "./src/webhook.js";
|
|
5
|
+
import { handleOAuthCallback } from "./src/oauth-callback.js";
|
|
6
|
+
import { resolveLinearToken } from "./src/linear-api.js";
|
|
7
|
+
|
|
8
|
+
export default function register(api: OpenClawPluginApi) {
|
|
9
|
+
const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
|
|
10
|
+
|
|
11
|
+
// Check token availability (config → env → auth profile store)
|
|
12
|
+
const tokenInfo = resolveLinearToken(pluginConfig);
|
|
13
|
+
if (!tokenInfo.accessToken) {
|
|
14
|
+
api.logger.warn(
|
|
15
|
+
"Linear: no access token found. Options: (1) run OAuth flow, (2) set LINEAR_ACCESS_TOKEN env var, " +
|
|
16
|
+
"(3) add accessToken to plugin config. Agent pipeline will not function without it.",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Register Linear as an auth provider (OAuth flow with agent scopes)
|
|
21
|
+
registerLinearProvider(api);
|
|
22
|
+
|
|
23
|
+
// Register Linear tools for the agent
|
|
24
|
+
api.registerTool((ctx) => {
|
|
25
|
+
return createLinearTools(api, ctx);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Register Linear webhook handler on a dedicated route
|
|
29
|
+
api.registerHttpRoute({
|
|
30
|
+
path: "/linear/webhook",
|
|
31
|
+
handler: async (req, res) => {
|
|
32
|
+
await handleLinearWebhook(api, req, res);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Back-compat route so existing production webhook URLs keep working.
|
|
37
|
+
api.registerHttpRoute({
|
|
38
|
+
path: "/hooks/linear",
|
|
39
|
+
handler: async (req, res) => {
|
|
40
|
+
await handleLinearWebhook(api, req, res);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Register OAuth callback route
|
|
45
|
+
api.registerHttpRoute({
|
|
46
|
+
path: "/linear/oauth/callback",
|
|
47
|
+
handler: async (req, res) => {
|
|
48
|
+
await handleOAuthCallback(api, req, res);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const agentId = (pluginConfig?.defaultAgentId as string) ?? "default";
|
|
53
|
+
api.logger.info(
|
|
54
|
+
`Linear agent extension registered (agent: ${agentId}, token: ${tokenInfo.source !== "none" ? `${tokenInfo.source}` : "missing"})`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "linear",
|
|
3
|
+
"name": "Linear Agent",
|
|
4
|
+
"description": "Linear integration with OAuth support, agent pipeline, and webhook-driven AI agent lifecycle",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"clientId": { "type": "string", "description": "Linear OAuth Client ID" },
|
|
10
|
+
"clientSecret": { "type": "string", "description": "Linear OAuth Client Secret", "sensitive": true },
|
|
11
|
+
"redirectUri": { "type": "string", "description": "Linear OAuth Redirect URI (optional, defaults to gateway URL)" },
|
|
12
|
+
"accessToken": { "type": "string", "description": "Linear API access token for agent activities", "sensitive": true },
|
|
13
|
+
"defaultAgentId": { "type": "string", "description": "OpenClaw agent ID to use for pipeline stages" },
|
|
14
|
+
"enableAudit": { "type": "boolean", "description": "Run auditor stage after implementation", "default": true }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@calltelemetry/openclaw-linear",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Linear Agent plugin for OpenClaw — webhook-driven AI pipeline with OAuth, multi-agent routing, and issue triage",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/calltelemetry/openclaw-linear-plugin.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/calltelemetry/openclaw-linear-plugin#readme",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"openclaw",
|
|
14
|
+
"linear",
|
|
15
|
+
"agent",
|
|
16
|
+
"webhook",
|
|
17
|
+
"oauth",
|
|
18
|
+
"ai-pipeline",
|
|
19
|
+
"issue-triage"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"index.ts",
|
|
23
|
+
"src/",
|
|
24
|
+
"openclaw.plugin.json",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"openclaw": "^2026.2.13"
|
|
32
|
+
},
|
|
33
|
+
"openclaw": {
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./index.ts"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
export interface AgentRunResult {
|
|
4
|
+
success: boolean;
|
|
5
|
+
output: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function runAgent(params: {
|
|
9
|
+
api: OpenClawPluginApi;
|
|
10
|
+
agentId: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
message: string;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}): Promise<AgentRunResult> {
|
|
15
|
+
const { api, agentId, sessionId, message, timeoutMs = 5 * 60_000 } = params;
|
|
16
|
+
|
|
17
|
+
api.logger.info(`Dispatching agent ${agentId} for session ${sessionId}`);
|
|
18
|
+
|
|
19
|
+
const command = [
|
|
20
|
+
"openclaw",
|
|
21
|
+
"agent",
|
|
22
|
+
"--agent",
|
|
23
|
+
agentId,
|
|
24
|
+
"--session-id",
|
|
25
|
+
sessionId,
|
|
26
|
+
"--message",
|
|
27
|
+
message,
|
|
28
|
+
"--timeout",
|
|
29
|
+
String(Math.floor(timeoutMs / 1000)),
|
|
30
|
+
"--json",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const result = await api.runtime.system.runCommandWithTimeout(command, { timeoutMs });
|
|
34
|
+
|
|
35
|
+
if (result.code !== 0) {
|
|
36
|
+
const error = result.stderr || result.stdout || "no output";
|
|
37
|
+
api.logger.error(`Agent ${agentId} failed (${result.code}): ${error}`);
|
|
38
|
+
return { success: false, output: error };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const raw = result.stdout || "";
|
|
42
|
+
api.logger.info(`Agent ${agentId} completed for session ${sessionId}`);
|
|
43
|
+
|
|
44
|
+
// Extract clean text from --json output
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
const payloads = parsed?.result?.payloads;
|
|
48
|
+
if (Array.isArray(payloads) && payloads.length > 0) {
|
|
49
|
+
const text = payloads.map((p: any) => p.text).filter(Boolean).join("\n\n");
|
|
50
|
+
if (text) return { success: true, output: text };
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Not JSON — use raw output as-is
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { success: true, output: raw };
|
|
57
|
+
}
|