linear-agent-bridge 0.1.2
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/LICENSE +21 -0
- package/README.md +613 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/src/agent/context-builder.d.ts +9 -0
- package/dist/src/agent/context-builder.d.ts.map +1 -0
- package/dist/src/agent/context-builder.js +121 -0
- package/dist/src/agent/context-builder.js.map +1 -0
- package/dist/src/agent/plan-manager.d.ts +5 -0
- package/dist/src/agent/plan-manager.d.ts.map +1 -0
- package/dist/src/agent/plan-manager.js +11 -0
- package/dist/src/agent/plan-manager.js.map +1 -0
- package/dist/src/agent/response-tracker.d.ts +4 -0
- package/dist/src/agent/response-tracker.d.ts.map +1 -0
- package/dist/src/agent/response-tracker.js +11 -0
- package/dist/src/agent/response-tracker.js.map +1 -0
- package/dist/src/agent/session-token.d.ts +5 -0
- package/dist/src/agent/session-token.d.ts.map +1 -0
- package/dist/src/agent/session-token.js +15 -0
- package/dist/src/agent/session-token.js.map +1 -0
- package/dist/src/api/activity-ops.d.ts +2 -0
- package/dist/src/api/activity-ops.d.ts.map +1 -0
- package/dist/src/api/activity-ops.js +71 -0
- package/dist/src/api/activity-ops.js.map +1 -0
- package/dist/src/api/base-url.d.ts +3 -0
- package/dist/src/api/base-url.d.ts.map +1 -0
- package/dist/src/api/base-url.js +11 -0
- package/dist/src/api/base-url.js.map +1 -0
- package/dist/src/api/delegation-ops.d.ts +2 -0
- package/dist/src/api/delegation-ops.d.ts.map +1 -0
- package/dist/src/api/delegation-ops.js +51 -0
- package/dist/src/api/delegation-ops.js.map +1 -0
- package/dist/src/api/issue-ops.d.ts +2 -0
- package/dist/src/api/issue-ops.d.ts.map +1 -0
- package/dist/src/api/issue-ops.js +170 -0
- package/dist/src/api/issue-ops.js.map +1 -0
- package/dist/src/api/query-ops.d.ts +2 -0
- package/dist/src/api/query-ops.d.ts.map +1 -0
- package/dist/src/api/query-ops.js +77 -0
- package/dist/src/api/query-ops.js.map +1 -0
- package/dist/src/api/router.d.ts +12 -0
- package/dist/src/api/router.d.ts.map +1 -0
- package/dist/src/api/router.js +62 -0
- package/dist/src/api/router.js.map +1 -0
- package/dist/src/api/session-ops.d.ts +2 -0
- package/dist/src/api/session-ops.d.ts.map +1 -0
- package/dist/src/api/session-ops.js +96 -0
- package/dist/src/api/session-ops.js.map +1 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +47 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/graphql/mutations.d.ts +9 -0
- package/dist/src/graphql/mutations.d.ts.map +1 -0
- package/dist/src/graphql/mutations.js +74 -0
- package/dist/src/graphql/mutations.js.map +1 -0
- package/dist/src/graphql/queries.d.ts +10 -0
- package/dist/src/graphql/queries.d.ts.map +1 -0
- package/dist/src/graphql/queries.js +121 -0
- package/dist/src/graphql/queries.js.map +1 -0
- package/dist/src/linear-client.d.ts +7 -0
- package/dist/src/linear-client.d.ts.map +1 -0
- package/dist/src/linear-client.js +72 -0
- package/dist/src/linear-client.js.map +1 -0
- package/dist/src/types.d.ts +85 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/util.d.ts +13 -0
- package/dist/src/util.d.ts.map +1 -0
- package/dist/src/util.js +80 -0
- package/dist/src/util.js.map +1 -0
- package/dist/src/webhook/close-intent.d.ts +4 -0
- package/dist/src/webhook/close-intent.d.ts.map +1 -0
- package/dist/src/webhook/close-intent.js +37 -0
- package/dist/src/webhook/close-intent.js.map +1 -0
- package/dist/src/webhook/handler.d.ts +5 -0
- package/dist/src/webhook/handler.d.ts.map +1 -0
- package/dist/src/webhook/handler.js +441 -0
- package/dist/src/webhook/handler.js.map +1 -0
- package/dist/src/webhook/issue-policy.d.ts +7 -0
- package/dist/src/webhook/issue-policy.d.ts.map +1 -0
- package/dist/src/webhook/issue-policy.js +139 -0
- package/dist/src/webhook/issue-policy.js.map +1 -0
- package/dist/src/webhook/message-builder.d.ts +34 -0
- package/dist/src/webhook/message-builder.d.ts.map +1 -0
- package/dist/src/webhook/message-builder.js +144 -0
- package/dist/src/webhook/message-builder.js.map +1 -0
- package/dist/src/webhook/response-parser.d.ts +2 -0
- package/dist/src/webhook/response-parser.d.ts.map +1 -0
- package/dist/src/webhook/response-parser.js +51 -0
- package/dist/src/webhook/response-parser.js.map +1 -0
- package/dist/src/webhook/session-resolver.d.ts +6 -0
- package/dist/src/webhook/session-resolver.d.ts.map +1 -0
- package/dist/src/webhook/session-resolver.js +203 -0
- package/dist/src/webhook/session-resolver.js.map +1 -0
- package/dist/src/webhook/skip-filter.d.ts +4 -0
- package/dist/src/webhook/skip-filter.d.ts.map +1 -0
- package/dist/src/webhook/skip-filter.js +42 -0
- package/dist/src/webhook/skip-filter.js.map +1 -0
- package/dist/src/webhook/validation.d.ts +2 -0
- package/dist/src/webhook/validation.d.ts.map +1 -0
- package/dist/src/webhook/validation.js +11 -0
- package/dist/src/webhook/validation.js.map +1 -0
- package/openclaw.plugin.json +92 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tokezooo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
# linear-agent-bridge
|
|
2
|
+
|
|
3
|
+
An [OpenClaw](https://github.com/nicepkg/openclaw) plugin that turns Linear's Agent Sessions into fully autonomous AI agent runs. When someone @mentions or delegates an issue to your agent in Linear, this plugin receives the webhook, spins up an OpenClaw agent, and gives it a rich set of tools to manage issues, communicate progress, delegate work, and close tasks — all without leaving Linear.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [How It Works](#how-it-works)
|
|
8
|
+
- [Features](#features)
|
|
9
|
+
- [Prerequisites](#prerequisites)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Linear App Setup](#linear-app-setup)
|
|
12
|
+
- [Plugin Configuration](#plugin-configuration)
|
|
13
|
+
- [Webhook Setup](#webhook-setup)
|
|
14
|
+
- [Use Cases](#use-cases)
|
|
15
|
+
- [Agent API Reference](#agent-api-reference)
|
|
16
|
+
- [Issue Management](#issue-management)
|
|
17
|
+
- [Communication (Activities)](#communication-activities)
|
|
18
|
+
- [Session Management](#session-management)
|
|
19
|
+
- [Delegation](#delegation)
|
|
20
|
+
- [Queries](#queries)
|
|
21
|
+
- [Architecture](#architecture)
|
|
22
|
+
- [Development](#development)
|
|
23
|
+
- [Troubleshooting](#troubleshooting)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
|
|
26
|
+
## How It Works
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
┌──────────┐
|
|
30
|
+
│ Linear │
|
|
31
|
+
│ Workspace│
|
|
32
|
+
└────┬─────┘
|
|
33
|
+
│ Webhook (AgentSession / Comment)
|
|
34
|
+
▼
|
|
35
|
+
┌────────────────┐
|
|
36
|
+
│ This Plugin │
|
|
37
|
+
│ │
|
|
38
|
+
│ 1. Verify HMAC │
|
|
39
|
+
│ 2. Resolve │
|
|
40
|
+
│ session ID │
|
|
41
|
+
│ 3. Build │
|
|
42
|
+
│ enriched │
|
|
43
|
+
│ prompt │
|
|
44
|
+
│ 4. Issue │
|
|
45
|
+
│ per-session │
|
|
46
|
+
│ API token │
|
|
47
|
+
└───────┬────────┘
|
|
48
|
+
│ callGateway({ method: "agent", ... })
|
|
49
|
+
▼
|
|
50
|
+
┌────────────────┐
|
|
51
|
+
│ OpenClaw Agent │
|
|
52
|
+
│ │
|
|
53
|
+
│ Reads issue, │◄──── POST /plugins/linear/api
|
|
54
|
+
│ writes code, │ (bearer token auth)
|
|
55
|
+
│ posts updates │────► Linear GraphQL API
|
|
56
|
+
│ to Linear │
|
|
57
|
+
└────────────────┘
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
1. A user @mentions the agent or delegates an issue in Linear
|
|
61
|
+
2. Linear sends a webhook to this plugin
|
|
62
|
+
3. The plugin verifies the HMAC signature, resolves the agent session, and builds an enriched prompt containing the issue context and a full API reference
|
|
63
|
+
4. An OpenClaw agent is launched with that prompt and a short-lived bearer token
|
|
64
|
+
5. During execution, the agent calls back to the plugin's API proxy to post thoughts, update plans, create sub-issues, delegate, query data, and post final responses
|
|
65
|
+
6. When the agent finishes, the token is revoked and the response is posted to Linear
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
- **Full Linear Agent Protocol** — implements `created`, `prompted`, `stop` signal, agent plans, activities (thought/action/elicitation/response/error), proactive sessions
|
|
70
|
+
- **Rich Agent API** — during execution the agent can manage issues, post activities, update session plans, delegate work, query issue/team details, and more
|
|
71
|
+
- **Session Deduplication** — prevents duplicate agent runs when Linear sends both AgentSession and Comment webhooks for the same event
|
|
72
|
+
- **Close Intent Detection** — recognizes natural-language close commands in English and Russian ("close this task", "закрой задачу") and fast-paths them without a full agent run
|
|
73
|
+
- **Per-Session Security** — each agent run gets a unique cryptographic bearer token scoped to its session; revoked on completion
|
|
74
|
+
- **Issue Policies** — automatically moves issues to "started" state and delegates to the app user on session creation
|
|
75
|
+
- **Multi-Repo Routing** — maps Linear teams and projects to specific repository directories
|
|
76
|
+
- **Elicitation with Select** — the agent can present clickable option lists to users via the `select` signal
|
|
77
|
+
- **External URL Linking** — attaches external links (e.g. CI dashboard, PR) to the Linear session
|
|
78
|
+
- **Auto-Detection of Base URL** — works behind Tailscale or any reverse proxy; captures the public URL from the first webhook `Host` header
|
|
79
|
+
|
|
80
|
+
## Prerequisites
|
|
81
|
+
|
|
82
|
+
- **Node.js** >= 18
|
|
83
|
+
- A running **OpenClaw gateway** instance
|
|
84
|
+
- A **Linear workspace** with admin access (to install an OAuth application)
|
|
85
|
+
- A publicly reachable URL for webhooks (Tailscale, ngrok, cloud deploy, etc.)
|
|
86
|
+
|
|
87
|
+
## Installation
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# From npm (when published)
|
|
91
|
+
npm install linear-agent-bridge
|
|
92
|
+
|
|
93
|
+
# Or clone and build from source
|
|
94
|
+
git clone https://github.com/tokezooo/linear-agent-bridge.git
|
|
95
|
+
cd linear-agent-bridge
|
|
96
|
+
npm install
|
|
97
|
+
npm run build
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The plugin registers itself with OpenClaw via the `openclaw` field in `package.json`:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"openclaw": {
|
|
105
|
+
"extensions": ["./dist/index.js"]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Linear App Setup
|
|
111
|
+
|
|
112
|
+
### 1. Create a Linear Application
|
|
113
|
+
|
|
114
|
+
1. Go to **Linear Settings** > **API** > **Applications** > [Create new](https://linear.app/settings/api/applications/new)
|
|
115
|
+
2. Set a recognizable name (this is how users will see the agent in mentions and filters)
|
|
116
|
+
3. Enable **Webhooks**
|
|
117
|
+
4. Under webhook events, select **Agent session events**
|
|
118
|
+
5. Set the webhook URL to: `https://<your-host>/plugins/linear/linear`
|
|
119
|
+
|
|
120
|
+
### 2. OAuth Installation
|
|
121
|
+
|
|
122
|
+
Install the app into your workspace using the OAuth flow with `actor=app`:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
https://linear.app/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT&response_type=code&scope=read,write,issues:create,comments:create,app:assignable,app:mentionable&actor=app
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Key scopes:
|
|
129
|
+
| Scope | Purpose |
|
|
130
|
+
|-------|---------|
|
|
131
|
+
| `read`, `write` | Core issue/comment access |
|
|
132
|
+
| `issues:create` | Create issues and sub-issues |
|
|
133
|
+
| `comments:create` | Post comments |
|
|
134
|
+
| `app:assignable` | Allow delegation to the agent |
|
|
135
|
+
| `app:mentionable` | Allow @mentioning the agent |
|
|
136
|
+
|
|
137
|
+
### 3. Get the Webhook Signing Secret
|
|
138
|
+
|
|
139
|
+
In your Linear application settings, copy the **Webhook signing secret**. This is used for HMAC-SHA256 signature verification of incoming webhooks.
|
|
140
|
+
|
|
141
|
+
## Plugin Configuration
|
|
142
|
+
|
|
143
|
+
Configure the plugin in your OpenClaw config under the plugin's section. All options are defined in `openclaw.plugin.json`.
|
|
144
|
+
|
|
145
|
+
### Required
|
|
146
|
+
|
|
147
|
+
| Option | Type | Description |
|
|
148
|
+
|--------|------|-------------|
|
|
149
|
+
| `linearApiKey` | `string` | Linear OAuth token (from the OAuth flow above) |
|
|
150
|
+
| `linearWebhookSecret` | `string` | Webhook signing secret for HMAC verification |
|
|
151
|
+
|
|
152
|
+
### Recommended
|
|
153
|
+
|
|
154
|
+
| Option | Type | Default | Description |
|
|
155
|
+
|--------|------|---------|-------------|
|
|
156
|
+
| `devAgentId` | `string` | `"dev"` | OpenClaw agent ID to handle Linear issues |
|
|
157
|
+
| `defaultDir` | `string` | — | Default repository directory for agent work |
|
|
158
|
+
|
|
159
|
+
### Issue Policies
|
|
160
|
+
|
|
161
|
+
| Option | Type | Default | Description |
|
|
162
|
+
|--------|------|---------|-------------|
|
|
163
|
+
| `delegateOnCreate` | `boolean` | `true` | Auto-delegate issues to the app user when a session is created |
|
|
164
|
+
| `startOnCreate` | `boolean` | `true` | Move issues to "started" workflow state on session creation |
|
|
165
|
+
|
|
166
|
+
### Multi-Repo Routing
|
|
167
|
+
|
|
168
|
+
| Option | Type | Description |
|
|
169
|
+
|--------|------|-------------|
|
|
170
|
+
| `repoByTeam` | `object` | Map Linear team keys to repository directories. Example: `{ "ENG": "/home/code/backend", "WEB": "/home/code/frontend" }` |
|
|
171
|
+
| `repoByProject` | `object` | Map Linear project keys to repository directories. Takes precedence over `repoByTeam` |
|
|
172
|
+
|
|
173
|
+
### Agent API
|
|
174
|
+
|
|
175
|
+
| Option | Type | Default | Description |
|
|
176
|
+
|--------|------|---------|-------------|
|
|
177
|
+
| `enableAgentApi` | `boolean` | `true` | Enable the API proxy that agents call during execution |
|
|
178
|
+
| `apiBaseUrl` | `string` | auto-detected | Override the auto-detected base URL for agent API callbacks |
|
|
179
|
+
|
|
180
|
+
### External URLs
|
|
181
|
+
|
|
182
|
+
| Option | Type | Description |
|
|
183
|
+
|--------|------|-------------|
|
|
184
|
+
| `externalUrlBase` | `string` | URL template for session links. Supports `{session}` and `{issue}` placeholders. Example: `https://dash.example.com/sessions/{session}` |
|
|
185
|
+
| `externalUrlLabel` | `string` | Label for external links (default: `"OpenClaw session"`) |
|
|
186
|
+
|
|
187
|
+
### Notifications
|
|
188
|
+
|
|
189
|
+
| Option | Type | Description |
|
|
190
|
+
|--------|------|-------------|
|
|
191
|
+
| `notifyChannel` | `string` | Channel for delivery notifications (e.g. `"discord"`) |
|
|
192
|
+
| `notifyTo` | `string` | Target for notifications (e.g. `"channel:123456"`) |
|
|
193
|
+
| `notifyAccountId` | `string` | Account ID for notifications |
|
|
194
|
+
|
|
195
|
+
### Example Configuration
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"linearApiKey": "lin_oauth_...",
|
|
200
|
+
"linearWebhookSecret": "whsec_...",
|
|
201
|
+
"devAgentId": "dev",
|
|
202
|
+
"defaultDir": "/home/projects/main-repo",
|
|
203
|
+
"repoByTeam": {
|
|
204
|
+
"ENG": "/home/projects/backend",
|
|
205
|
+
"WEB": "/home/projects/frontend"
|
|
206
|
+
},
|
|
207
|
+
"delegateOnCreate": true,
|
|
208
|
+
"startOnCreate": true,
|
|
209
|
+
"enableAgentApi": true,
|
|
210
|
+
"externalUrlBase": "https://dash.example.com/sessions/{session}"
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Webhook Setup
|
|
215
|
+
|
|
216
|
+
The plugin registers a POST endpoint at `/plugins/linear/linear`.
|
|
217
|
+
|
|
218
|
+
### Security
|
|
219
|
+
|
|
220
|
+
- **HMAC-SHA256 Signature Verification** — every incoming webhook is verified against the `linearWebhookSecret` using the `linear-signature` header
|
|
221
|
+
- **Stale Webhook Rejection** — webhooks older than 60 seconds are rejected
|
|
222
|
+
- **Immediate 202 Response** — webhook processing happens asynchronously after responding to Linear
|
|
223
|
+
|
|
224
|
+
### What Gets Processed
|
|
225
|
+
|
|
226
|
+
| Event Type | Action | Result |
|
|
227
|
+
|------------|--------|--------|
|
|
228
|
+
| AgentSession `created` | New session | Full agent run with enriched prompt |
|
|
229
|
+
| AgentSession `prompted` | Follow-up message | Agent continues with new context |
|
|
230
|
+
| Comment (on agent thread) | Follow-up | Resolved to session, triggers `prompted` |
|
|
231
|
+
| Signal `stop` | Halt | Agent posts stop confirmation, no run |
|
|
232
|
+
| Close intent ("close task") | Fast-path | Issue closed directly, no agent run |
|
|
233
|
+
|
|
234
|
+
### What Gets Filtered
|
|
235
|
+
|
|
236
|
+
- `PermissionChange` and `OAuthApp` events (logged only)
|
|
237
|
+
- `AppUserNotification` events
|
|
238
|
+
- Self-authored comments (prevents feedback loops)
|
|
239
|
+
- System echo messages (e.g. "Starting work on...", "Agent run failed:")
|
|
240
|
+
- Empty prompts
|
|
241
|
+
- Duplicate events within the dedup window (5 seconds)
|
|
242
|
+
|
|
243
|
+
## Use Cases
|
|
244
|
+
|
|
245
|
+
### 1. Autonomous Issue Resolution
|
|
246
|
+
|
|
247
|
+
Delegate an issue to the agent in Linear. The agent receives the full issue context (title, description, labels, comments), writes code, creates sub-issues for subtasks, posts progress updates, and closes the issue when done.
|
|
248
|
+
|
|
249
|
+
### 2. Interactive Code Review
|
|
250
|
+
|
|
251
|
+
@mention the agent in a comment asking for review. The agent reads the issue context, examines the linked code, and posts structured feedback as a response activity.
|
|
252
|
+
|
|
253
|
+
### 3. Multi-Agent Delegation
|
|
254
|
+
|
|
255
|
+
The agent can delegate sub-tasks to other agents or reassign issues to human team members:
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{ "action": "delegate/assign", "issueId": "...", "delegateId": "other-agent-id" }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 4. Issue Triage and Breakdown
|
|
262
|
+
|
|
263
|
+
Ask the agent to break down a large issue. It creates sub-issues with appropriate priorities and links them to the parent:
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
{ "action": "issue/create-sub-issue", "title": "Implement auth middleware", "priority": 1 }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 5. Progress Tracking
|
|
270
|
+
|
|
271
|
+
The agent shows real-time progress via session plans — structured checklists visible in the Linear UI:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"action": "session/plan",
|
|
276
|
+
"plan": [
|
|
277
|
+
{ "content": "Analyze issue requirements", "status": "completed" },
|
|
278
|
+
{ "content": "Implement solution", "status": "inProgress" },
|
|
279
|
+
{ "content": "Write tests", "status": "pending" },
|
|
280
|
+
{ "content": "Post summary", "status": "pending" }
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 6. User Elicitation
|
|
286
|
+
|
|
287
|
+
The agent can ask the user to choose between options using the `select` signal:
|
|
288
|
+
|
|
289
|
+
```json
|
|
290
|
+
{
|
|
291
|
+
"action": "activity/elicitation",
|
|
292
|
+
"body": "Which approach should I take?",
|
|
293
|
+
"signal": "select",
|
|
294
|
+
"signalMeta": {
|
|
295
|
+
"options": [
|
|
296
|
+
{ "value": "Refactor the existing module" },
|
|
297
|
+
{ "value": "Write a new implementation from scratch" }
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### 7. Proactive Sessions
|
|
304
|
+
|
|
305
|
+
The agent can create new sessions on other issues or comments without being explicitly delegated:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{ "action": "session/create-on-issue", "issueId": "issue-uuid" }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Agent API Reference
|
|
312
|
+
|
|
313
|
+
During execution, the agent makes HTTP POST requests to a single endpoint. All requests use bearer token authentication and a JSON body with an `action` field.
|
|
314
|
+
|
|
315
|
+
**Endpoint:** `POST <apiBaseUrl>`
|
|
316
|
+
**Auth:** `Authorization: Bearer <per-session-token>`
|
|
317
|
+
**Content-Type:** `application/json`
|
|
318
|
+
|
|
319
|
+
Every request body **must** include an `action` field.
|
|
320
|
+
|
|
321
|
+
### Issue Management
|
|
322
|
+
|
|
323
|
+
#### `issue/create` — Create a New Issue
|
|
324
|
+
|
|
325
|
+
| Field | Type | Required | Description |
|
|
326
|
+
|-------|------|----------|-------------|
|
|
327
|
+
| `teamId` | `string` | No (defaults to current) | Team to create issue in |
|
|
328
|
+
| `title` | `string` | **Yes** | Issue title |
|
|
329
|
+
| `description` | `string` | No | Issue description (Markdown) |
|
|
330
|
+
| `priority` | `number` | No | Priority 0-4 (0 = no priority, 1 = urgent, 4 = low) |
|
|
331
|
+
| `labelIds` | `string[]` | No | Label IDs to attach |
|
|
332
|
+
| `assigneeId` | `string` | No | Assignee user ID |
|
|
333
|
+
| `parentId` | `string` | No | Parent issue ID (creates sub-issue) |
|
|
334
|
+
| `stateId` | `string` | No | Initial workflow state |
|
|
335
|
+
|
|
336
|
+
#### `issue/update` — Update Issue Fields
|
|
337
|
+
|
|
338
|
+
| Field | Type | Required | Description |
|
|
339
|
+
|-------|------|----------|-------------|
|
|
340
|
+
| `issueId` | `string` | No (defaults to current) | Issue to update |
|
|
341
|
+
| `title` | `string` | No | New title |
|
|
342
|
+
| `description` | `string` | No | New description |
|
|
343
|
+
| `stateId` | `string` | No | New workflow state |
|
|
344
|
+
| `priority` | `number` | No | New priority |
|
|
345
|
+
| `labelIds` | `string[]` | No | Replace labels |
|
|
346
|
+
| `assigneeId` | `string` | No | New assignee |
|
|
347
|
+
| `delegateId` | `string` | No | New delegate |
|
|
348
|
+
|
|
349
|
+
#### `issue/close` — Close an Issue
|
|
350
|
+
|
|
351
|
+
| Field | Type | Required | Description |
|
|
352
|
+
|-------|------|----------|-------------|
|
|
353
|
+
| `issueId` | `string` | No (defaults to current) | Issue to close |
|
|
354
|
+
|
|
355
|
+
Resolves the team's "completed" workflow state and transitions the issue. Returns `alreadyClosed: true` if the issue is already completed or canceled.
|
|
356
|
+
|
|
357
|
+
#### `issue/create-sub-issue` — Create a Child Issue
|
|
358
|
+
|
|
359
|
+
| Field | Type | Required | Description |
|
|
360
|
+
|-------|------|----------|-------------|
|
|
361
|
+
| `title` | `string` | **Yes** | Sub-issue title |
|
|
362
|
+
| `parentId` | `string` | No (defaults to current) | Parent issue |
|
|
363
|
+
| `description` | `string` | No | Description |
|
|
364
|
+
| `priority` | `number` | No | Priority 0-4 |
|
|
365
|
+
| `labelIds` | `string[]` | No | Labels |
|
|
366
|
+
| `assigneeId` | `string` | No | Assignee |
|
|
367
|
+
|
|
368
|
+
#### `issue/link` — Link Two Issues
|
|
369
|
+
|
|
370
|
+
| Field | Type | Required | Description |
|
|
371
|
+
|-------|------|----------|-------------|
|
|
372
|
+
| `issueId` | `string` | No (defaults to current) | Source issue |
|
|
373
|
+
| `relatedIssueId` | `string` | **Yes** | Target issue |
|
|
374
|
+
| `type` | `string` | **Yes** | One of: `blocks`, `blocked_by`, `related`, `duplicate` |
|
|
375
|
+
|
|
376
|
+
### Communication (Activities)
|
|
377
|
+
|
|
378
|
+
Activities are how the agent communicates with users in the Linear session UI.
|
|
379
|
+
|
|
380
|
+
#### `activity/thought` — Share Reasoning
|
|
381
|
+
|
|
382
|
+
| Field | Type | Required | Description |
|
|
383
|
+
|-------|------|----------|-------------|
|
|
384
|
+
| `body` | `string` | **Yes** | Markdown text |
|
|
385
|
+
| `ephemeral` | `boolean` | No | If `true`, shown temporarily and replaced by next activity |
|
|
386
|
+
|
|
387
|
+
#### `activity/action` — Show a Tool Call
|
|
388
|
+
|
|
389
|
+
| Field | Type | Required | Description |
|
|
390
|
+
|-------|------|----------|-------------|
|
|
391
|
+
| `activityAction` | `string` | **Yes** | Action verb (e.g. "Searching", "Building") |
|
|
392
|
+
| `parameter` | `string` | No | Subject of the action |
|
|
393
|
+
| `result` | `string` | No | Result (Markdown) |
|
|
394
|
+
|
|
395
|
+
#### `activity/elicitation` — Ask the User a Question
|
|
396
|
+
|
|
397
|
+
| Field | Type | Required | Description |
|
|
398
|
+
|-------|------|----------|-------------|
|
|
399
|
+
| `body` | `string` | **Yes** | Question text |
|
|
400
|
+
| `signal` | `string` | No | Set to `"select"` to show option buttons |
|
|
401
|
+
| `signalMeta` | `object` | No | `{ options: [{ value: "..." }] }` — list of choices |
|
|
402
|
+
|
|
403
|
+
#### `activity/response` — Post Final Response
|
|
404
|
+
|
|
405
|
+
| Field | Type | Required | Description |
|
|
406
|
+
|-------|------|----------|-------------|
|
|
407
|
+
| `body` | `string` | **Yes** | Final response (Markdown) |
|
|
408
|
+
|
|
409
|
+
Marks the session as having a posted response. The handler will skip auto-posting the agent's text output.
|
|
410
|
+
|
|
411
|
+
#### `activity/error` — Report an Error
|
|
412
|
+
|
|
413
|
+
| Field | Type | Required | Description |
|
|
414
|
+
|-------|------|----------|-------------|
|
|
415
|
+
| `body` | `string` | **Yes** | Error description |
|
|
416
|
+
|
|
417
|
+
### Session Management
|
|
418
|
+
|
|
419
|
+
#### `session/plan` — Update Progress Checklist
|
|
420
|
+
|
|
421
|
+
| Field | Type | Required | Description |
|
|
422
|
+
|-------|------|----------|-------------|
|
|
423
|
+
| `plan` | `array` | **Yes** | Array of plan steps |
|
|
424
|
+
|
|
425
|
+
Each step: `{ content: "Step description", status: "pending" | "inProgress" | "completed" | "canceled" }`
|
|
426
|
+
|
|
427
|
+
**Note:** Replaces the entire plan each time. Always include all steps.
|
|
428
|
+
|
|
429
|
+
#### `session/create-on-issue` — Create Session on Another Issue
|
|
430
|
+
|
|
431
|
+
| Field | Type | Required | Description |
|
|
432
|
+
|-------|------|----------|-------------|
|
|
433
|
+
| `issueId` | `string` | **Yes** | Issue to create session on |
|
|
434
|
+
|
|
435
|
+
#### `session/create-on-comment` — Create Session on a Comment
|
|
436
|
+
|
|
437
|
+
| Field | Type | Required | Description |
|
|
438
|
+
|-------|------|----------|-------------|
|
|
439
|
+
| `commentId` | `string` | **Yes** | Comment to create session on |
|
|
440
|
+
|
|
441
|
+
#### `session/external-url` — Set External URL
|
|
442
|
+
|
|
443
|
+
| Field | Type | Required | Description |
|
|
444
|
+
|-------|------|----------|-------------|
|
|
445
|
+
| `url` | `string` | **Yes** | URL to attach |
|
|
446
|
+
| `label` | `string` | No | Link label (default: `"Link"`) |
|
|
447
|
+
|
|
448
|
+
### Delegation
|
|
449
|
+
|
|
450
|
+
#### `delegate/assign` — Delegate to Agent or User
|
|
451
|
+
|
|
452
|
+
| Field | Type | Required | Description |
|
|
453
|
+
|-------|------|----------|-------------|
|
|
454
|
+
| `issueId` | `string` | No (defaults to current) | Issue to delegate |
|
|
455
|
+
| `delegateId` | `string` | **Yes** | Target agent or user ID |
|
|
456
|
+
|
|
457
|
+
#### `delegate/reassign` — Change Assignee
|
|
458
|
+
|
|
459
|
+
| Field | Type | Required | Description |
|
|
460
|
+
|-------|------|----------|-------------|
|
|
461
|
+
| `issueId` | `string` | No (defaults to current) | Issue to reassign |
|
|
462
|
+
| `assigneeId` | `string` | **Yes** | New assignee ID |
|
|
463
|
+
|
|
464
|
+
### Queries
|
|
465
|
+
|
|
466
|
+
#### `query/issue` — Get Full Issue Details
|
|
467
|
+
|
|
468
|
+
| Field | Type | Required | Description |
|
|
469
|
+
|-------|------|----------|-------------|
|
|
470
|
+
| `issueId` | `string` | No (defaults to current) | Issue ID |
|
|
471
|
+
|
|
472
|
+
Returns: labels, state, assignee, delegate, parent, children, relations, recent comments.
|
|
473
|
+
|
|
474
|
+
#### `query/team` — Get Team Info
|
|
475
|
+
|
|
476
|
+
| Field | Type | Required | Description |
|
|
477
|
+
|-------|------|----------|-------------|
|
|
478
|
+
| `teamId` | `string` | No (defaults to current) | Team ID |
|
|
479
|
+
|
|
480
|
+
Returns: workflow states, labels, members.
|
|
481
|
+
|
|
482
|
+
#### `query/repo-suggestions` — Get AI-Ranked Repository Suggestions
|
|
483
|
+
|
|
484
|
+
| Field | Type | Required | Description |
|
|
485
|
+
|-------|------|----------|-------------|
|
|
486
|
+
| `issueId` | `string` | No (defaults to current) | Issue for context |
|
|
487
|
+
| `candidateRepositories` | `array` | **Yes** | `[{ hostname, repositoryFullName }]` |
|
|
488
|
+
|
|
489
|
+
Returns ranked suggestions with confidence scores.
|
|
490
|
+
|
|
491
|
+
#### `query/viewer` — Get Current App Identity
|
|
492
|
+
|
|
493
|
+
No parameters. Returns the authenticated app's user ID.
|
|
494
|
+
|
|
495
|
+
## Architecture
|
|
496
|
+
|
|
497
|
+
```
|
|
498
|
+
index.ts ← Entry point: registers HTTP routes
|
|
499
|
+
├── src/
|
|
500
|
+
│ ├── types.ts ← Shared TypeScript interfaces
|
|
501
|
+
│ ├── config.ts ← Plugin config normalization
|
|
502
|
+
│ ├── util.ts ← HTTP/JSON helpers
|
|
503
|
+
│ ├── linear-client.ts ← Single gateway for all Linear GraphQL calls
|
|
504
|
+
│ ├── graphql/
|
|
505
|
+
│ │ ├── queries.ts ← GraphQL query strings
|
|
506
|
+
│ │ └── mutations.ts ← GraphQL mutation strings
|
|
507
|
+
│ ├── webhook/
|
|
508
|
+
│ │ ├── handler.ts ← Main webhook handler + agent orchestration
|
|
509
|
+
│ │ ├── validation.ts ← HMAC-SHA256 signature verification
|
|
510
|
+
│ │ ├── session-resolver.ts ← Session ID lookup (direct → cache → GraphQL)
|
|
511
|
+
│ │ ├── message-builder.ts ← Agent prompt construction
|
|
512
|
+
│ │ ├── response-parser.ts ← Parse agent output into response text
|
|
513
|
+
│ │ ├── issue-policy.ts ← Auto-start and auto-delegate policies
|
|
514
|
+
│ │ ├── close-intent.ts ← Natural language close detection
|
|
515
|
+
│ │ └── skip-filter.ts ← System echo and self-comment filtering
|
|
516
|
+
│ ├── api/
|
|
517
|
+
│ │ ├── router.ts ← API proxy router (bearer token auth)
|
|
518
|
+
│ │ ├── base-url.ts ← Auto-detect public URL from Host header
|
|
519
|
+
│ │ ├── issue-ops.ts ← Issue CRUD operations
|
|
520
|
+
│ │ ├── activity-ops.ts ← Agent activity posting
|
|
521
|
+
│ │ ├── session-ops.ts ← Session management (plans, proactive creation)
|
|
522
|
+
│ │ ├── delegation-ops.ts ← Issue delegation and reassignment
|
|
523
|
+
│ │ └── query-ops.ts ← Read-only queries (issue, team, viewer)
|
|
524
|
+
│ └── agent/
|
|
525
|
+
│ ├── session-token.ts ← Per-run bearer token lifecycle
|
|
526
|
+
│ ├── context-builder.ts ← Enriched prompt with API documentation
|
|
527
|
+
│ ├── response-tracker.ts ← Tracks whether agent already posted a response
|
|
528
|
+
│ └── plan-manager.ts ← In-memory plan state per session
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Key Design Patterns
|
|
532
|
+
|
|
533
|
+
| Pattern | Description |
|
|
534
|
+
|---------|-------------|
|
|
535
|
+
| **Single Linear Gateway** | All GraphQL communication goes through `callLinear()` which handles auth, errors, and logging |
|
|
536
|
+
| **Per-Session Tokens** | Each agent run gets a unique `crypto.randomBytes(32)` bearer token, revoked on completion |
|
|
537
|
+
| **Response Deduplication** | If the agent posts a response via the API, the handler skips auto-posting the text output |
|
|
538
|
+
| **Cascading Session Resolution** | Direct field → in-memory cache → GraphQL query with retry (120ms/350ms/800ms backoff) |
|
|
539
|
+
| **Side-Effect Registration** | API handlers register themselves via `registerApiHandler()` and are imported in `index.ts` |
|
|
540
|
+
| **Dedup Window** | Prevents double agent runs when Linear sends both AgentSession + Comment webhooks (5s window) |
|
|
541
|
+
|
|
542
|
+
## Development
|
|
543
|
+
|
|
544
|
+
### Build
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
npm run build # Runs tsc, outputs to dist/
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Project Structure
|
|
551
|
+
|
|
552
|
+
- **TypeScript** with `strict: true`, targeting ES2022
|
|
553
|
+
- **ESM** (`"type": "module"` in package.json)
|
|
554
|
+
- **Node16 module resolution**
|
|
555
|
+
- Zero runtime dependencies (only `@types/node` and `typescript` as dev deps)
|
|
556
|
+
|
|
557
|
+
### Adding a New API Operation
|
|
558
|
+
|
|
559
|
+
1. Create a file in `src/api/` (e.g. `my-ops.ts`)
|
|
560
|
+
2. Import and use `registerApiHandler`:
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
import { registerApiHandler } from "./router.js";
|
|
564
|
+
import { sendJson } from "../util.js";
|
|
565
|
+
|
|
566
|
+
registerApiHandler("/my/action", async ({ api, cfg, context, body, res }) => {
|
|
567
|
+
// context.sessionId, context.issueId, context.teamId are available
|
|
568
|
+
// body contains the parsed JSON request
|
|
569
|
+
sendJson(res, 200, { ok: true });
|
|
570
|
+
});
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
3. Add a side-effect import in `index.ts`:
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
import "./src/api/my-ops.js";
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
The handler is now available as `{ "action": "my/action" }` through the API proxy.
|
|
580
|
+
|
|
581
|
+
## Troubleshooting
|
|
582
|
+
|
|
583
|
+
### Webhook not reaching the plugin
|
|
584
|
+
|
|
585
|
+
- Verify the URL in Linear application settings matches `https://<host>/plugins/linear/linear`
|
|
586
|
+
- Ensure your host is publicly reachable (check with `curl`)
|
|
587
|
+
- Check that "Agent session events" is enabled in the Linear app webhook settings
|
|
588
|
+
|
|
589
|
+
### 401 Unauthorized on webhook
|
|
590
|
+
|
|
591
|
+
- Verify `linearWebhookSecret` matches the signing secret from Linear app settings
|
|
592
|
+
- Check that the webhook is not stale (>60 seconds old) — clock sync issues can cause this
|
|
593
|
+
|
|
594
|
+
### Agent doesn't respond in Linear
|
|
595
|
+
|
|
596
|
+
- Check that `linearApiKey` is a valid OAuth token with the required scopes
|
|
597
|
+
- Verify the agent ID in `devAgentId` matches a configured OpenClaw agent
|
|
598
|
+
- Check OpenClaw gateway logs for errors
|
|
599
|
+
|
|
600
|
+
### Agent API calls fail
|
|
601
|
+
|
|
602
|
+
- If `apiBaseUrl` is not set, the URL is auto-detected from the first webhook's `Host` header. Ensure your reverse proxy forwards the correct `Host`
|
|
603
|
+
- For Tailscale setups, the `Host` header with `.ts.net` domain is used automatically
|
|
604
|
+
- Set `apiBaseUrl` explicitly to bypass auto-detection
|
|
605
|
+
|
|
606
|
+
### Self-comment loop
|
|
607
|
+
|
|
608
|
+
- The plugin automatically filters out comments authored by the app itself using the `viewer` query
|
|
609
|
+
- If you see loops, verify that the `linearApiKey` belongs to the same OAuth application that sends the webhooks
|
|
610
|
+
|
|
611
|
+
## License
|
|
612
|
+
|
|
613
|
+
[MIT](LICENSE) © [tokezooo](https://github.com/tokezooo)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "./src/types.js";
|
|
2
|
+
import "./src/api/issue-ops.js";
|
|
3
|
+
import "./src/api/activity-ops.js";
|
|
4
|
+
import "./src/api/session-ops.js";
|
|
5
|
+
import "./src/api/delegation-ops.js";
|
|
6
|
+
import "./src/api/query-ops.js";
|
|
7
|
+
export default function register(api: OpenClawPluginApi): void;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAKxD,OAAO,wBAAwB,CAAC;AAChC,OAAO,2BAA2B,CAAC;AACnC,OAAO,0BAA0B,CAAC;AAClC,OAAO,6BAA6B,CAAC;AACrC,OAAO,wBAAwB,CAAC;AAEhC,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,CAU7D"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createLinearWebhook } from "./src/webhook/handler.js";
|
|
2
|
+
import { createApiRouter } from "./src/api/router.js";
|
|
3
|
+
// Side-effect imports: register all API endpoint handlers
|
|
4
|
+
import "./src/api/issue-ops.js";
|
|
5
|
+
import "./src/api/activity-ops.js";
|
|
6
|
+
import "./src/api/session-ops.js";
|
|
7
|
+
import "./src/api/delegation-ops.js";
|
|
8
|
+
import "./src/api/query-ops.js";
|
|
9
|
+
export default function register(api) {
|
|
10
|
+
api.registerHttpRoute({
|
|
11
|
+
path: "/plugins/linear/linear",
|
|
12
|
+
handler: createLinearWebhook(api),
|
|
13
|
+
});
|
|
14
|
+
api.registerHttpRoute({
|
|
15
|
+
path: "/plugins/linear/api",
|
|
16
|
+
handler: createApiRouter(api),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,0DAA0D;AAC1D,OAAO,wBAAwB,CAAC;AAChC,OAAO,2BAA2B,CAAC;AACnC,OAAO,0BAA0B,CAAC;AAClC,OAAO,6BAA6B,CAAC;AACrC,OAAO,wBAAwB,CAAC;AAEhC,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAsB;IACrD,GAAG,CAAC,iBAAiB,CAAC;QACpB,IAAI,EAAE,wBAAwB;QAC9B,OAAO,EAAE,mBAAmB,CAAC,GAAG,CAAC;KAClC,CAAC,CAAC;IAEH,GAAG,CAAC,iBAAiB,CAAC;QACpB,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,eAAe,CAAC,GAAG,CAAC;KAC9B,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type MessageParams } from "../webhook/message-builder.js";
|
|
2
|
+
export interface EnrichedMessageParams extends MessageParams {
|
|
3
|
+
apiBaseUrl: string;
|
|
4
|
+
apiToken: string;
|
|
5
|
+
issueId: string;
|
|
6
|
+
teamId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function buildEnrichedMessage(params: EnrichedMessageParams): string;
|
|
9
|
+
//# sourceMappingURL=context-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-builder.d.ts","sourceRoot":"","sources":["../../../src/agent/context-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEjF,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAwH1E"}
|