@elnora-ai/linear 1.0.0 → 1.1.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.
Files changed (306) hide show
  1. package/.claude-plugin/marketplace.json +7 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +20 -1
  4. package/README.md +116 -26
  5. package/agents/linear-issue-creator.md +129 -17
  6. package/agents/linear-issue-reviewer.md +122 -23
  7. package/agents/linear-issue-updater.md +135 -23
  8. package/agents/linear-state-curator.md +173 -0
  9. package/agents/linear-url-to-issues.md +189 -26
  10. package/commands/linear-cleanup.md +64 -29
  11. package/dist/cli.js +83 -7
  12. package/dist/cli.js.map +1 -1
  13. package/dist/client/auth.d.ts.map +1 -1
  14. package/dist/client/auth.js +13 -2
  15. package/dist/client/auth.js.map +1 -1
  16. package/dist/client/linear-client.d.ts +7 -0
  17. package/dist/client/linear-client.d.ts.map +1 -1
  18. package/dist/client/linear-client.js +13 -1
  19. package/dist/client/linear-client.js.map +1 -1
  20. package/dist/commands/agent-activities.d.ts +3 -0
  21. package/dist/commands/agent-activities.d.ts.map +1 -0
  22. package/dist/commands/agent-activities.js +144 -0
  23. package/dist/commands/agent-activities.js.map +1 -0
  24. package/dist/commands/agent-sessions.d.ts +3 -0
  25. package/dist/commands/agent-sessions.d.ts.map +1 -0
  26. package/dist/commands/agent-sessions.js +132 -0
  27. package/dist/commands/agent-sessions.js.map +1 -0
  28. package/dist/commands/attachments.d.ts +3 -0
  29. package/dist/commands/attachments.d.ts.map +1 -0
  30. package/dist/commands/attachments.js +265 -0
  31. package/dist/commands/attachments.js.map +1 -0
  32. package/dist/commands/audit.d.ts +3 -0
  33. package/dist/commands/audit.d.ts.map +1 -0
  34. package/dist/commands/audit.js +73 -0
  35. package/dist/commands/audit.js.map +1 -0
  36. package/dist/commands/bulk.d.ts.map +1 -1
  37. package/dist/commands/bulk.js +6 -1
  38. package/dist/commands/bulk.js.map +1 -1
  39. package/dist/commands/cleanup.d.ts.map +1 -1
  40. package/dist/commands/cleanup.js +8 -1
  41. package/dist/commands/cleanup.js.map +1 -1
  42. package/dist/commands/comments.d.ts +3 -0
  43. package/dist/commands/comments.d.ts.map +1 -0
  44. package/dist/commands/comments.js +107 -0
  45. package/dist/commands/comments.js.map +1 -0
  46. package/dist/commands/completion.d.ts +3 -0
  47. package/dist/commands/completion.d.ts.map +1 -0
  48. package/dist/commands/completion.js +62 -0
  49. package/dist/commands/completion.js.map +1 -0
  50. package/dist/commands/context.d.ts +3 -0
  51. package/dist/commands/context.d.ts.map +1 -0
  52. package/dist/commands/context.js +94 -0
  53. package/dist/commands/context.js.map +1 -0
  54. package/dist/commands/curator.d.ts +14 -0
  55. package/dist/commands/curator.d.ts.map +1 -1
  56. package/dist/commands/curator.js +97 -19
  57. package/dist/commands/curator.js.map +1 -1
  58. package/dist/commands/customer-needs.d.ts +3 -0
  59. package/dist/commands/customer-needs.d.ts.map +1 -0
  60. package/dist/commands/customer-needs.js +198 -0
  61. package/dist/commands/customer-needs.js.map +1 -0
  62. package/dist/commands/customers.d.ts +5 -0
  63. package/dist/commands/customers.d.ts.map +1 -0
  64. package/dist/commands/customers.js +201 -0
  65. package/dist/commands/customers.js.map +1 -0
  66. package/dist/commands/cycles.d.ts +3 -0
  67. package/dist/commands/cycles.d.ts.map +1 -0
  68. package/dist/commands/cycles.js +67 -0
  69. package/dist/commands/cycles.js.map +1 -0
  70. package/dist/commands/documents.d.ts +3 -0
  71. package/dist/commands/documents.d.ts.map +1 -0
  72. package/dist/commands/documents.js +105 -0
  73. package/dist/commands/documents.js.map +1 -0
  74. package/dist/commands/favorites.d.ts +3 -0
  75. package/dist/commands/favorites.d.ts.map +1 -0
  76. package/dist/commands/favorites.js +101 -0
  77. package/dist/commands/favorites.js.map +1 -0
  78. package/dist/commands/index.d.ts +30 -0
  79. package/dist/commands/index.d.ts.map +1 -1
  80. package/dist/commands/index.js +30 -0
  81. package/dist/commands/index.js.map +1 -1
  82. package/dist/commands/initiatives.d.ts +3 -0
  83. package/dist/commands/initiatives.d.ts.map +1 -0
  84. package/dist/commands/initiatives.js +106 -0
  85. package/dist/commands/initiatives.js.map +1 -0
  86. package/dist/commands/issues.d.ts +21 -0
  87. package/dist/commands/issues.d.ts.map +1 -0
  88. package/dist/commands/issues.js +993 -0
  89. package/dist/commands/issues.js.map +1 -0
  90. package/dist/commands/labels.d.ts +3 -0
  91. package/dist/commands/labels.d.ts.map +1 -0
  92. package/dist/commands/labels.js +111 -0
  93. package/dist/commands/labels.js.map +1 -0
  94. package/dist/commands/milestones.d.ts +3 -0
  95. package/dist/commands/milestones.d.ts.map +1 -0
  96. package/dist/commands/milestones.js +94 -0
  97. package/dist/commands/milestones.js.map +1 -0
  98. package/dist/commands/notifications.d.ts +3 -0
  99. package/dist/commands/notifications.d.ts.map +1 -0
  100. package/dist/commands/notifications.js +130 -0
  101. package/dist/commands/notifications.js.map +1 -0
  102. package/dist/commands/project-labels.d.ts +3 -0
  103. package/dist/commands/project-labels.d.ts.map +1 -0
  104. package/dist/commands/project-labels.js +80 -0
  105. package/dist/commands/project-labels.js.map +1 -0
  106. package/dist/commands/project-relations.d.ts +3 -0
  107. package/dist/commands/project-relations.d.ts.map +1 -0
  108. package/dist/commands/project-relations.js +96 -0
  109. package/dist/commands/project-relations.js.map +1 -0
  110. package/dist/commands/projects.d.ts +3 -0
  111. package/dist/commands/projects.d.ts.map +1 -0
  112. package/dist/commands/projects.js +263 -0
  113. package/dist/commands/projects.js.map +1 -0
  114. package/dist/commands/quota.d.ts +3 -0
  115. package/dist/commands/quota.d.ts.map +1 -0
  116. package/dist/commands/quota.js +28 -0
  117. package/dist/commands/quota.js.map +1 -0
  118. package/dist/commands/reactions.d.ts +7 -0
  119. package/dist/commands/reactions.d.ts.map +1 -0
  120. package/dist/commands/reactions.js +53 -0
  121. package/dist/commands/reactions.js.map +1 -0
  122. package/dist/commands/relations.d.ts +3 -0
  123. package/dist/commands/relations.d.ts.map +1 -0
  124. package/dist/commands/relations.js +73 -0
  125. package/dist/commands/relations.js.map +1 -0
  126. package/dist/commands/states.d.ts +3 -0
  127. package/dist/commands/states.d.ts.map +1 -0
  128. package/dist/commands/states.js +52 -0
  129. package/dist/commands/states.js.map +1 -0
  130. package/dist/commands/status-updates.d.ts +3 -0
  131. package/dist/commands/status-updates.d.ts.map +1 -0
  132. package/dist/commands/status-updates.js +117 -0
  133. package/dist/commands/status-updates.js.map +1 -0
  134. package/dist/commands/sync.d.ts.map +1 -1
  135. package/dist/commands/sync.js +58 -18
  136. package/dist/commands/sync.js.map +1 -1
  137. package/dist/commands/teams.d.ts +3 -0
  138. package/dist/commands/teams.d.ts.map +1 -0
  139. package/dist/commands/teams.js +135 -0
  140. package/dist/commands/teams.js.map +1 -0
  141. package/dist/commands/templates.d.ts +3 -0
  142. package/dist/commands/templates.d.ts.map +1 -0
  143. package/dist/commands/templates.js +76 -0
  144. package/dist/commands/templates.js.map +1 -0
  145. package/dist/commands/users.d.ts +3 -0
  146. package/dist/commands/users.d.ts.map +1 -0
  147. package/dist/commands/users.js +40 -0
  148. package/dist/commands/users.js.map +1 -0
  149. package/dist/commands/views.d.ts +3 -0
  150. package/dist/commands/views.d.ts.map +1 -0
  151. package/dist/commands/views.js +177 -0
  152. package/dist/commands/views.js.map +1 -0
  153. package/dist/commands/webhooks.d.ts +3 -0
  154. package/dist/commands/webhooks.d.ts.map +1 -0
  155. package/dist/commands/webhooks.js +234 -0
  156. package/dist/commands/webhooks.js.map +1 -0
  157. package/dist/config/loader.d.ts.map +1 -1
  158. package/dist/config/loader.js +3 -0
  159. package/dist/config/loader.js.map +1 -1
  160. package/dist/config/types.d.ts +16 -2
  161. package/dist/config/types.d.ts.map +1 -1
  162. package/dist/config/types.js +1 -0
  163. package/dist/config/types.js.map +1 -1
  164. package/dist/curator/dispatch.d.ts +52 -0
  165. package/dist/curator/dispatch.d.ts.map +1 -0
  166. package/dist/curator/dispatch.js +144 -0
  167. package/dist/curator/dispatch.js.map +1 -0
  168. package/dist/curator/index.d.ts +5 -0
  169. package/dist/curator/index.d.ts.map +1 -0
  170. package/dist/curator/index.js +5 -0
  171. package/dist/curator/index.js.map +1 -0
  172. package/dist/curator/llm.d.ts +70 -0
  173. package/dist/curator/llm.d.ts.map +1 -0
  174. package/dist/curator/llm.js +107 -0
  175. package/dist/curator/llm.js.map +1 -0
  176. package/dist/curator/snapshot.d.ts +34 -0
  177. package/dist/curator/snapshot.d.ts.map +1 -0
  178. package/dist/curator/snapshot.js +127 -0
  179. package/dist/curator/snapshot.js.map +1 -0
  180. package/dist/curator/state.d.ts +50 -0
  181. package/dist/curator/state.d.ts.map +1 -0
  182. package/dist/curator/state.js +125 -0
  183. package/dist/curator/state.js.map +1 -0
  184. package/dist/lib/bulk-graphql.d.ts +144 -0
  185. package/dist/lib/bulk-graphql.d.ts.map +1 -0
  186. package/dist/lib/bulk-graphql.js +380 -0
  187. package/dist/lib/bulk-graphql.js.map +1 -0
  188. package/dist/lib/index.d.ts +2 -0
  189. package/dist/lib/index.d.ts.map +1 -0
  190. package/dist/lib/index.js +2 -0
  191. package/dist/lib/index.js.map +1 -0
  192. package/dist/output/cli.d.ts +17 -0
  193. package/dist/output/cli.d.ts.map +1 -0
  194. package/dist/output/cli.js +252 -0
  195. package/dist/output/cli.js.map +1 -0
  196. package/dist/output/formatter.d.ts +6 -0
  197. package/dist/output/formatter.d.ts.map +1 -1
  198. package/dist/output/formatter.js +10 -0
  199. package/dist/output/formatter.js.map +1 -1
  200. package/dist/output/index.d.ts +1 -0
  201. package/dist/output/index.d.ts.map +1 -1
  202. package/dist/output/index.js +1 -0
  203. package/dist/output/index.js.map +1 -1
  204. package/dist/scripts/sync-linear-templates.d.ts +26 -0
  205. package/dist/scripts/sync-linear-templates.d.ts.map +1 -0
  206. package/dist/scripts/sync-linear-templates.js +115 -0
  207. package/dist/scripts/sync-linear-templates.js.map +1 -0
  208. package/dist/signals/github-commits.d.ts +31 -0
  209. package/dist/signals/github-commits.d.ts.map +1 -0
  210. package/dist/signals/github-commits.js +127 -0
  211. package/dist/signals/github-commits.js.map +1 -0
  212. package/dist/signals/github-pr.d.ts +16 -0
  213. package/dist/signals/github-pr.d.ts.map +1 -0
  214. package/dist/signals/github-pr.js +98 -0
  215. package/dist/signals/github-pr.js.map +1 -0
  216. package/dist/signals/index.d.ts +4 -0
  217. package/dist/signals/index.d.ts.map +1 -1
  218. package/dist/signals/index.js +4 -0
  219. package/dist/signals/index.js.map +1 -1
  220. package/dist/signals/linear-issues.d.ts +20 -0
  221. package/dist/signals/linear-issues.d.ts.map +1 -0
  222. package/dist/signals/linear-issues.js +115 -0
  223. package/dist/signals/linear-issues.js.map +1 -0
  224. package/dist/signals/registry.d.ts +4 -3
  225. package/dist/signals/registry.d.ts.map +1 -1
  226. package/dist/signals/registry.js +33 -11
  227. package/dist/signals/registry.js.map +1 -1
  228. package/dist/signals/slack-messages.d.ts +20 -0
  229. package/dist/signals/slack-messages.d.ts.map +1 -0
  230. package/dist/signals/slack-messages.js +129 -0
  231. package/dist/signals/slack-messages.js.map +1 -0
  232. package/dist/utils/errors.d.ts +63 -0
  233. package/dist/utils/errors.d.ts.map +1 -0
  234. package/dist/utils/errors.js +94 -0
  235. package/dist/utils/errors.js.map +1 -0
  236. package/dist/utils/index.d.ts +9 -0
  237. package/dist/utils/index.d.ts.map +1 -0
  238. package/dist/utils/index.js +9 -0
  239. package/dist/utils/index.js.map +1 -0
  240. package/dist/utils/label-policy.d.ts +53 -0
  241. package/dist/utils/label-policy.d.ts.map +1 -0
  242. package/dist/utils/label-policy.js +93 -0
  243. package/dist/utils/label-policy.js.map +1 -0
  244. package/dist/utils/parse.d.ts +48 -0
  245. package/dist/utils/parse.d.ts.map +1 -0
  246. package/dist/utils/parse.js +133 -0
  247. package/dist/utils/parse.js.map +1 -0
  248. package/dist/utils/project-status.d.ts +6 -0
  249. package/dist/utils/project-status.d.ts.map +1 -0
  250. package/dist/utils/project-status.js +33 -0
  251. package/dist/utils/project-status.js.map +1 -0
  252. package/dist/utils/rate-limit.d.ts +24 -0
  253. package/dist/utils/rate-limit.d.ts.map +1 -0
  254. package/dist/utils/rate-limit.js +89 -0
  255. package/dist/utils/rate-limit.js.map +1 -0
  256. package/dist/utils/resolve.d.ts +84 -0
  257. package/dist/utils/resolve.d.ts.map +1 -0
  258. package/dist/utils/resolve.js +172 -0
  259. package/dist/utils/resolve.js.map +1 -0
  260. package/dist/utils/sleep.d.ts +2 -0
  261. package/dist/utils/sleep.d.ts.map +1 -0
  262. package/dist/utils/sleep.js +4 -0
  263. package/dist/utils/sleep.js.map +1 -0
  264. package/dist/utils/webhook-verify.d.ts +42 -0
  265. package/dist/utils/webhook-verify.d.ts.map +1 -0
  266. package/dist/utils/webhook-verify.js +65 -0
  267. package/dist/utils/webhook-verify.js.map +1 -0
  268. package/package.json +4 -3
  269. package/references/agent-description-template.md +31 -0
  270. package/references/cli-reference.md +227 -0
  271. package/references/curator-tiering-rules.md +76 -0
  272. package/references/label-policy.example.json +37 -0
  273. package/references/label-policy.placeholder.json +6 -0
  274. package/references/settings-template.md +30 -0
  275. package/references/sla-reference.md +70 -0
  276. package/references/template-index.md +34 -0
  277. package/references/workspace-labels.md +124 -0
  278. package/references/workspace-projects.md +56 -0
  279. package/references/workspace-routing.md +58 -0
  280. package/schemas/label-policy.json +72 -0
  281. package/schemas/signal-sources.json +1 -1
  282. package/skills/linear-workspace/SKILL.md +65 -4
  283. package/templates/ACC-PRO-provision.md +74 -0
  284. package/templates/ACC-PRV-privileged.md +66 -0
  285. package/templates/ACC-QTR-review.md +77 -0
  286. package/templates/ACC-REV-revoke.md +67 -0
  287. package/templates/AI-USE-capability.md +111 -0
  288. package/templates/AUD-CAP-corrective.md +89 -0
  289. package/templates/AUD-INT-internal.md +92 -0
  290. package/templates/AUD-MGT-management.md +110 -0
  291. package/templates/CHG-MAJ-major.md +110 -0
  292. package/templates/CHG-SIG-significant.md +83 -0
  293. package/templates/CHG-STD-standard.md +47 -0
  294. package/templates/LRN-DOC-lessons.md +75 -0
  295. package/templates/OPS-BCK-backup.md +99 -0
  296. package/templates/OPS-DAT-data-mod.md +98 -0
  297. package/templates/RCA-DOC-root-cause.md +105 -0
  298. package/templates/RSK-ASS-assessment.md +87 -0
  299. package/templates/RSK-VND-vendor.md +113 -0
  300. package/templates/SEC-INC-incident.md +76 -0
  301. package/templates/SEC-PEN-pentest.md +58 -0
  302. package/templates/SEC-VLN-vulnerability.md +69 -0
  303. package/templates/SLA-AVL-availability.md +86 -0
  304. package/templates/SLA-OPS-operational.md +70 -0
  305. package/templates/agent-server-template/README.md +88 -0
  306. package/templates/agent-server-template/server.example.ts +185 -0
@@ -0,0 +1,70 @@
1
+ # SLA-OPS: Operational Request
2
+
3
+ ## Quick Reference
4
+ - **SLA:** 3-14 days
5
+ - **Team:** *the team that owns this workflow in your workspace*
6
+ - **Project:** Operational Requests
7
+
8
+ ## Priority to SLA Mapping
9
+ | Priority | Resolution Timeframe |
10
+ |----------|---------------------|
11
+ | High | 3 days |
12
+ | Medium | 7 days |
13
+ | Low | 14 days |
14
+
15
+ ## Required Labels
16
+ - `Type: feature`
17
+ - `Layer: devops`
18
+ - `Infrastructure` (if infrastructure work)
19
+ - `Account Setup` (if account work)
20
+
21
+ ## Issue Template
22
+ ```markdown
23
+ ## Operational Request
24
+
25
+ **Request ID:** SLA-OPS-YYYY-XXX
26
+ **Request Date:** [YYYY-MM-DD]
27
+ **Requestor:** [Name]
28
+ **Priority:** [High / Medium / Low]
29
+ **SLA Deadline:** [YYYY-MM-DD]
30
+
31
+ ## Request Type
32
+ - [ ] Infrastructure provisioning
33
+ - [ ] Account setup
34
+ - [ ] Configuration change
35
+ - [ ] Access modification
36
+ - [ ] Other: ___
37
+
38
+ ## Request Details
39
+ [Detailed description of what is being requested]
40
+
41
+ ## Business Justification
42
+ [Why is this request needed?]
43
+
44
+ ## Requirements
45
+ [Specific technical requirements]
46
+
47
+ ## Dependencies
48
+ - [ ] [Dependency 1]
49
+ - [ ] [Dependency 2]
50
+
51
+ ## Implementation Plan
52
+ 1. [Step 1]
53
+ 2. [Step 2]
54
+ 3. [Step 3]
55
+
56
+ ## Verification Criteria
57
+ - [ ] [How to verify completion]
58
+
59
+ ## Approvals
60
+ - [ ] Manager approval: _________________ Date: _______
61
+ - [ ] Technical approval (if needed): _________________ Date: _______
62
+
63
+ ## Completion
64
+ - [ ] Request fulfilled
65
+ - [ ] Requestor notified
66
+ - [ ] Documentation updated
67
+
68
+ **Completion Date:** [YYYY-MM-DD]
69
+ **SLA Met:** [Yes / No]
70
+ ```
@@ -0,0 +1,88 @@
1
+ # Agent webhook receiver — reference template
2
+
3
+ This directory is **a template, not a running server**. The Linear plugin is a CLI; it doesn't host long-running HTTP listeners. If you want the plugin to act as a Linear agent (the `actor=app` model where users `@mention` it or assign issues to it), you need to host this receiver somewhere — copy `server.example.ts` into your own project and adapt.
4
+
5
+ ## What it does
6
+
7
+ 1. Listens for `POST /linear/webhook` from Linear.
8
+ 2. Verifies the `linear-signature` HMAC-SHA256 header against the body using your stored signing secret.
9
+ 3. On `agentSessionEvent.created`, emits a `thought` activity within 10 seconds via the plugin's CLI to ack the session.
10
+ 4. Hands off to your actual agent logic (which you write).
11
+
12
+ ## Where to host
13
+
14
+ | Host | Notes |
15
+ |---|---|
16
+ | Cloudflare Workers | Linear's own `linear/linear-agent-demo` uses Workers. Edit the example to use the Fetch API runtime. |
17
+ | Vercel / Netlify functions | Fine for low traffic. Same Fetch API edits. |
18
+ | Fly.io / Railway / Render | Run the example mostly as-is on Node. |
19
+ | Local + ngrok | For dev only — Linear retries 3× on failure (1 min / 1 hr / 6 hr). |
20
+
21
+ ## Setup
22
+
23
+ ```bash
24
+ # 1. Build the plugin CLI so the template can call it.
25
+ npm install -g @elnora-ai/linear
26
+
27
+ # 2. Copy this template into your own project.
28
+ cp -r templates/agent-server-template /path/to/your/agent-server
29
+
30
+ # 3. Register the webhook with Linear.
31
+ linear webhooks create \
32
+ --url https://your-public-url/linear/webhook \
33
+ --resource-types AgentSessionEvent \
34
+ --all-public-teams
35
+ # Save the secret it returns into LINEAR_WEBHOOK_SECRET.
36
+
37
+ # 4. Run the receiver.
38
+ LINEAR_API_KEY=lin_api_... \
39
+ LINEAR_WEBHOOK_SECRET=lin_wh_... \
40
+ node server.example.js
41
+ ```
42
+
43
+ ## What you fill in
44
+
45
+ The template stops after acking the session. The real agent logic — running Claude Code, opening a PR, posting back to Linear — goes in the `// TODO` block. Common patterns:
46
+
47
+ - **Quick echo bot:** emit a `response` activity with whatever you want to say.
48
+ - **Coding agent:** `child_process.spawn` your dev workflow (e.g. `claude` + a prompt that includes the issue context).
49
+ - **Triage routing:** read the issue, decide if it's for the bot, otherwise emit a `response` and exit.
50
+
51
+ Use the CLI for everything Linear-side:
52
+
53
+ ```bash
54
+ linear agent-activities create <sessionId> --type thought --body "Working on it"
55
+ linear agent-activities create <sessionId> --type response --body "Done. PR: <url>"
56
+ linear agent-sessions update-external-url <sessionId> --add https://github.com/.../pull/42
57
+ ```
58
+
59
+ ## Security notes
60
+
61
+ - **Always** verify the signature before reading the body. The template ships an inline `verifyLinearWebhook` helper (HMAC-SHA256 + `timingSafeEqual`) — don't bypass it.
62
+ - **Never** commit `LINEAR_WEBHOOK_SECRET` or `LINEAR_API_KEY`. Use your host's secret store.
63
+ - **Rotate** the webhook secret with `linear webhooks rotate-secret <id> --yes` if you suspect a leak. The new secret is shown ONCE.
64
+ - **Respond fast.** Linear retries on non-200 or >5s. ACK within 200ms by responding `200 ok` immediately, then doing the work async.
65
+
66
+ ## Activity types & lifecycle
67
+
68
+ | Type | Body | When to use |
69
+ |---|---|---|
70
+ | `thought` | text | Internal narration, including the mandatory 10s ack |
71
+ | `action` | `{action, parameter, result}` | Tool call summary (e.g. "ran `gh pr create`") |
72
+ | `elicitation` | text + `signal` (`select` / `auth`) | Ask the user (multi-choice or auth URL) |
73
+ | `response` | text | Final visible answer |
74
+ | `error` | text | Visible failure message |
75
+
76
+ The session is considered "stale" after 30 minutes of inactivity but is recoverable — emit any activity to revive.
77
+
78
+ ## Troubleshooting
79
+
80
+ - **401 in your logs**: signature mismatch. Check `LINEAR_WEBHOOK_SECRET` matches what `linear webhooks list` shows.
81
+ - **No webhook delivered**: confirm `agentSessionEvent` is in the webhook's `resourceTypes`.
82
+ - **"Agent unresponsive" in Linear UI**: you took longer than 10s to emit a thought. Move the ack BEFORE any expensive work.
83
+
84
+ ## See also
85
+
86
+ - Plugin CLI: `linear webhooks --help`, `linear agent-sessions --help`, `linear agent-activities --help`
87
+ - Linear's official agent docs: https://linear.app/developers/agents
88
+ - Linear's reference Cloudflare-Worker demo: https://github.com/linear/linear-agent-demo
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Reference webhook receiver for Linear's agent framework.
3
+ *
4
+ * This is a TEMPLATE — it ships with the plugin so operators can run their
5
+ * own agent receiver wherever they want (Cloudflare Worker, Vercel function,
6
+ * Fly machine, etc.). The plugin itself is a CLI; it does not host this
7
+ * server for you.
8
+ *
9
+ * 1. Register a webhook in Linear pointing at this server's URL:
10
+ * linear webhooks create \
11
+ * --url https://your-host/linear/webhook \
12
+ * --resource-types AgentSessionEvent \
13
+ * --all-public-teams
14
+ * (Linear will return a signing secret — store as LINEAR_WEBHOOK_SECRET.)
15
+ *
16
+ * 2. Run this server with both env vars set:
17
+ * LINEAR_API_KEY=lin_api_... \
18
+ * LINEAR_WEBHOOK_SECRET=lin_wh_... \
19
+ * node server.example.js
20
+ *
21
+ * 3. Linear delivers `agentSessionEvent` POSTs to /linear/webhook. We
22
+ * verify the signature, then must emit a `thought` activity within
23
+ * 10 seconds to acknowledge.
24
+ *
25
+ * The activity-create call shells out to the plugin's CLI so you don't end
26
+ * up with two implementations. The signature verifier is inlined below so
27
+ * this file works after `cp -r` out of the plugin tree — no relative
28
+ * imports back into the plugin source.
29
+ */
30
+
31
+ import http from "node:http";
32
+ import { execFile } from "node:child_process";
33
+ import { promisify } from "node:util";
34
+ import { createHmac, timingSafeEqual } from "node:crypto";
35
+
36
+ const execFileAsync = promisify(execFile);
37
+
38
+ /**
39
+ * Verify a Linear webhook payload's HMAC-SHA256 signature AND its replay
40
+ * window. Mirrors the implementation in the plugin's webhook-verify util —
41
+ * kept inline so this template stands alone after copy.
42
+ *
43
+ * Replay protection: when `timestamp` (the `webhookTimestamp` field from the
44
+ * parsed body, Unix ms) is provided, payloads older than `maxAgeMs` are
45
+ * rejected. Without this, an attacker who captures a valid signed request
46
+ * can replay it forever.
47
+ */
48
+ // Linear recommends 60 s — see https://linear.app/developers/webhooks
49
+ const DEFAULT_WEBHOOK_MAX_AGE_MS = 60 * 1000;
50
+
51
+ function verifyLinearWebhook(opts: {
52
+ rawBody: string | Buffer;
53
+ signature: string;
54
+ secret: string;
55
+ timestamp?: number;
56
+ maxAgeMs?: number;
57
+ }): boolean {
58
+ if (!opts.signature || !opts.secret) return false;
59
+ const body = typeof opts.rawBody === "string" ? Buffer.from(opts.rawBody, "utf-8") : opts.rawBody;
60
+ const expectedHex = createHmac("sha256", opts.secret).update(body).digest("hex");
61
+ const expected = Buffer.from(expectedHex, "hex");
62
+ let received: Buffer;
63
+ try {
64
+ received = Buffer.from(opts.signature, "hex");
65
+ } catch {
66
+ return false;
67
+ }
68
+ if (received.length !== expected.length) return false;
69
+ if (!timingSafeEqual(received, expected)) return false;
70
+
71
+ if (typeof opts.timestamp === "number") {
72
+ if (!Number.isFinite(opts.timestamp)) return false;
73
+ const maxAgeMs = opts.maxAgeMs ?? DEFAULT_WEBHOOK_MAX_AGE_MS;
74
+ if (Math.abs(Date.now() - opts.timestamp) > maxAgeMs) return false;
75
+ }
76
+ return true;
77
+ }
78
+
79
+ const PORT = Number(process.env.PORT ?? 8787);
80
+ const SECRET = process.env.LINEAR_WEBHOOK_SECRET;
81
+ const CLI_PATH = process.env.LINEAR_CLI_PATH ?? "../../cli/bin/linear.js";
82
+
83
+ if (!SECRET) {
84
+ console.error("LINEAR_WEBHOOK_SECRET not set — refusing to start.");
85
+ process.exit(1);
86
+ }
87
+ if (!process.env.LINEAR_API_KEY) {
88
+ console.error("LINEAR_API_KEY not set — refusing to start.");
89
+ process.exit(1);
90
+ }
91
+
92
+ interface AgentSessionEvent {
93
+ type: "AgentSessionEvent";
94
+ action: "created" | "prompted";
95
+ agentSession?: { id: string; type?: string };
96
+ promptContext?: string;
97
+ }
98
+
99
+ async function readBody(req: http.IncomingMessage): Promise<Buffer> {
100
+ const chunks: Buffer[] = [];
101
+ for await (const chunk of req) {
102
+ chunks.push(chunk as Buffer);
103
+ }
104
+ return Buffer.concat(chunks);
105
+ }
106
+
107
+ /** Emit a thought activity via the CLI (one process per call — fine for ack). */
108
+ async function emitThought(sessionId: string, body: string): Promise<void> {
109
+ await execFileAsync("node", [
110
+ CLI_PATH,
111
+ "agent-activities", "create", sessionId,
112
+ "--type", "thought",
113
+ "--body", body,
114
+ ]);
115
+ }
116
+
117
+ const server = http.createServer(async (req, res) => {
118
+ if (req.method !== "POST" || req.url !== "/linear/webhook") {
119
+ res.writeHead(404);
120
+ res.end();
121
+ return;
122
+ }
123
+ const signature = req.headers["linear-signature"];
124
+ if (typeof signature !== "string") {
125
+ res.writeHead(400);
126
+ res.end("Missing linear-signature header");
127
+ return;
128
+ }
129
+ const rawBody = await readBody(req);
130
+ // HMAC-verify on the raw bytes BEFORE touching JSON.parse — never let
131
+ // unverified input reach the parser. Replay-window check happens after,
132
+ // once we trust the body enough to read webhookTimestamp out of it.
133
+ if (!verifyLinearWebhook({ rawBody, signature, secret: SECRET })) {
134
+ res.writeHead(401);
135
+ res.end("Invalid signature");
136
+ return;
137
+ }
138
+ let parsedForTimestamp: { webhookTimestamp?: number } = {};
139
+ try {
140
+ parsedForTimestamp = JSON.parse(rawBody.toString("utf-8"));
141
+ } catch {
142
+ res.writeHead(400);
143
+ res.end("Body is not valid JSON");
144
+ return;
145
+ }
146
+ const timestamp = parsedForTimestamp.webhookTimestamp;
147
+ if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
148
+ res.writeHead(401);
149
+ res.end("Missing or invalid webhookTimestamp");
150
+ return;
151
+ }
152
+ if (Math.abs(Date.now() - timestamp) > DEFAULT_WEBHOOK_MAX_AGE_MS) {
153
+ res.writeHead(401);
154
+ res.end("Expired payload");
155
+ return;
156
+ }
157
+ // ACK Linear within 5s — non-200 triggers a retry and counts against agent
158
+ // responsiveness. Do work async after responding.
159
+ res.writeHead(200);
160
+ res.end("ok");
161
+
162
+ const payload = JSON.parse(rawBody.toString("utf-8")) as AgentSessionEvent;
163
+ if (payload.type !== "AgentSessionEvent") return;
164
+ if (payload.action !== "created") return;
165
+ const sessionId = payload.agentSession?.id;
166
+ if (!sessionId) return;
167
+
168
+ // Linear requires a thought within 10s — emit immediately then start work.
169
+ try {
170
+ await emitThought(sessionId, "Got it — picking this up now.");
171
+ } catch (e) {
172
+ console.error("Failed to emit ack thought:", e);
173
+ return;
174
+ }
175
+
176
+ // TODO: kick off whatever the agent actually does. Examples:
177
+ // - Run /linear-work as a subprocess (background coding agent)
178
+ // - Trigger your own task queue
179
+ // - Update the issue with progress via `linear issues update`
180
+ console.log(`[agent] session ${sessionId} acknowledged`);
181
+ });
182
+
183
+ server.listen(PORT, () => {
184
+ console.log(`Linear agent webhook receiver listening on :${PORT}/linear/webhook`);
185
+ });