@mastra/factory 0.0.0 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE.md +30 -0
  3. package/dist/auth.d.ts +181 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +370 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/capabilities/connection.d.ts +9 -0
  8. package/dist/capabilities/connection.d.ts.map +1 -0
  9. package/dist/capabilities/connection.js +1 -0
  10. package/dist/capabilities/connection.js.map +1 -0
  11. package/dist/capabilities/intake.d.ts +93 -0
  12. package/dist/capabilities/intake.d.ts.map +1 -0
  13. package/dist/capabilities/intake.js +1 -0
  14. package/dist/capabilities/intake.js.map +1 -0
  15. package/dist/capabilities/version-control.d.ts +239 -0
  16. package/dist/capabilities/version-control.d.ts.map +1 -0
  17. package/dist/capabilities/version-control.js +1 -0
  18. package/dist/capabilities/version-control.js.map +1 -0
  19. package/dist/factory.d.ts +145 -0
  20. package/dist/factory.d.ts.map +1 -0
  21. package/dist/factory.js +38245 -0
  22. package/dist/factory.js.map +1 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +38245 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/integrations/base.d.ts +197 -0
  28. package/dist/integrations/base.d.ts.map +1 -0
  29. package/dist/integrations/base.js +1 -0
  30. package/dist/integrations/base.js.map +1 -0
  31. package/dist/integrations/github/config.d.ts +60 -0
  32. package/dist/integrations/github/config.d.ts.map +1 -0
  33. package/dist/integrations/github/config.js +28 -0
  34. package/dist/integrations/github/config.js.map +1 -0
  35. package/dist/integrations/github/factory-session.d.ts +12 -0
  36. package/dist/integrations/github/factory-session.d.ts.map +1 -0
  37. package/dist/integrations/github/factory-session.js +41 -0
  38. package/dist/integrations/github/factory-session.js.map +1 -0
  39. package/dist/integrations/github/integration.d.ts +191 -0
  40. package/dist/integrations/github/integration.d.ts.map +1 -0
  41. package/dist/integrations/github/integration.js +3001 -0
  42. package/dist/integrations/github/integration.js.map +1 -0
  43. package/dist/integrations/github/issue-triage.d.ts +27 -0
  44. package/dist/integrations/github/issue-triage.d.ts.map +1 -0
  45. package/dist/integrations/github/issue-triage.js +82 -0
  46. package/dist/integrations/github/issue-triage.js.map +1 -0
  47. package/dist/integrations/github/project-lock.d.ts +78 -0
  48. package/dist/integrations/github/project-lock.d.ts.map +1 -0
  49. package/dist/integrations/github/project-lock.js +70 -0
  50. package/dist/integrations/github/project-lock.js.map +1 -0
  51. package/dist/integrations/github/provenance.d.ts +27 -0
  52. package/dist/integrations/github/provenance.d.ts.map +1 -0
  53. package/dist/integrations/github/provenance.js +104 -0
  54. package/dist/integrations/github/provenance.js.map +1 -0
  55. package/dist/integrations/github/routes.d.ts +72 -0
  56. package/dist/integrations/github/routes.d.ts.map +1 -0
  57. package/dist/integrations/github/routes.js +1834 -0
  58. package/dist/integrations/github/routes.js.map +1 -0
  59. package/dist/integrations/github/sandbox.d.ts +268 -0
  60. package/dist/integrations/github/sandbox.d.ts.map +1 -0
  61. package/dist/integrations/github/sandbox.js +381 -0
  62. package/dist/integrations/github/sandbox.js.map +1 -0
  63. package/dist/integrations/github/session-subscriptions.d.ts +23 -0
  64. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
  65. package/dist/integrations/github/session-subscriptions.js +190 -0
  66. package/dist/integrations/github/session-subscriptions.js.map +1 -0
  67. package/dist/integrations/github/subscriptions.d.ts +54 -0
  68. package/dist/integrations/github/subscriptions.d.ts.map +1 -0
  69. package/dist/integrations/github/subscriptions.js +70 -0
  70. package/dist/integrations/github/subscriptions.js.map +1 -0
  71. package/dist/integrations/github/webhook.d.ts +87 -0
  72. package/dist/integrations/github/webhook.d.ts.map +1 -0
  73. package/dist/integrations/github/webhook.js +381 -0
  74. package/dist/integrations/github/webhook.js.map +1 -0
  75. package/dist/integrations/linear/agent-tools.d.ts +34 -0
  76. package/dist/integrations/linear/agent-tools.d.ts.map +1 -0
  77. package/dist/integrations/linear/agent-tools.js +108 -0
  78. package/dist/integrations/linear/agent-tools.js.map +1 -0
  79. package/dist/integrations/linear/integration.d.ts +233 -0
  80. package/dist/integrations/linear/integration.d.ts.map +1 -0
  81. package/dist/integrations/linear/integration.js +962 -0
  82. package/dist/integrations/linear/integration.js.map +1 -0
  83. package/dist/integrations/linear/routes.d.ts +61 -0
  84. package/dist/integrations/linear/routes.d.ts.map +1 -0
  85. package/dist/integrations/linear/routes.js +242 -0
  86. package/dist/integrations/linear/routes.js.map +1 -0
  87. package/dist/integrations/linear/storage.d.ts +65 -0
  88. package/dist/integrations/linear/storage.d.ts.map +1 -0
  89. package/dist/integrations/linear/storage.js +1 -0
  90. package/dist/integrations/linear/storage.js.map +1 -0
  91. package/dist/integrations/platform/api-client.d.ts +24 -0
  92. package/dist/integrations/platform/api-client.d.ts.map +1 -0
  93. package/dist/integrations/platform/api-client.js +153 -0
  94. package/dist/integrations/platform/api-client.js.map +1 -0
  95. package/dist/integrations/platform/github/event-worker.d.ts +44 -0
  96. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
  97. package/dist/integrations/platform/github/event-worker.js +585 -0
  98. package/dist/integrations/platform/github/event-worker.js.map +1 -0
  99. package/dist/integrations/platform/github/integration.d.ts +36 -0
  100. package/dist/integrations/platform/github/integration.d.ts.map +1 -0
  101. package/dist/integrations/platform/github/integration.js +3253 -0
  102. package/dist/integrations/platform/github/integration.js.map +1 -0
  103. package/dist/integrations/platform/linear/integration.d.ts +46 -0
  104. package/dist/integrations/platform/linear/integration.d.ts.map +1 -0
  105. package/dist/integrations/platform/linear/integration.js +891 -0
  106. package/dist/integrations/platform/linear/integration.js.map +1 -0
  107. package/dist/integrations/workos/integration.d.ts +39 -0
  108. package/dist/integrations/workos/integration.d.ts.map +1 -0
  109. package/dist/integrations/workos/integration.js +96 -0
  110. package/dist/integrations/workos/integration.js.map +1 -0
  111. package/dist/routes/config.d.ts +220 -0
  112. package/dist/routes/config.d.ts.map +1 -0
  113. package/dist/routes/config.js +770 -0
  114. package/dist/routes/config.js.map +1 -0
  115. package/dist/routes/fs.d.ts +105 -0
  116. package/dist/routes/fs.d.ts.map +1 -0
  117. package/dist/routes/fs.js +294 -0
  118. package/dist/routes/fs.js.map +1 -0
  119. package/dist/routes/intake.d.ts +23 -0
  120. package/dist/routes/intake.d.ts.map +1 -0
  121. package/dist/routes/intake.js +179 -0
  122. package/dist/routes/intake.js.map +1 -0
  123. package/dist/routes/oauth.d.ts +43 -0
  124. package/dist/routes/oauth.d.ts.map +1 -0
  125. package/dist/routes/oauth.js +25058 -0
  126. package/dist/routes/oauth.js.map +1 -0
  127. package/dist/routes/projects.d.ts +19 -0
  128. package/dist/routes/projects.d.ts.map +1 -0
  129. package/dist/routes/projects.js +395 -0
  130. package/dist/routes/projects.js.map +1 -0
  131. package/dist/routes/provider-credentials.d.ts +81 -0
  132. package/dist/routes/provider-credentials.d.ts.map +1 -0
  133. package/dist/routes/provider-credentials.js +68 -0
  134. package/dist/routes/provider-credentials.js.map +1 -0
  135. package/dist/routes/route.d.ts +48 -0
  136. package/dist/routes/route.d.ts.map +1 -0
  137. package/dist/routes/route.js +11 -0
  138. package/dist/routes/route.js.map +1 -0
  139. package/dist/routes/skills.d.ts +30 -0
  140. package/dist/routes/skills.d.ts.map +1 -0
  141. package/dist/routes/skills.js +181 -0
  142. package/dist/routes/skills.js.map +1 -0
  143. package/dist/routes/surface.d.ts +83 -0
  144. package/dist/routes/surface.d.ts.map +1 -0
  145. package/dist/routes/surface.js +28519 -0
  146. package/dist/routes/surface.js.map +1 -0
  147. package/dist/routes/tenant-credentials.d.ts +74 -0
  148. package/dist/routes/tenant-credentials.d.ts.map +1 -0
  149. package/dist/routes/tenant-credentials.js +181 -0
  150. package/dist/routes/tenant-credentials.js.map +1 -0
  151. package/dist/routes/work-items.d.ts +40 -0
  152. package/dist/routes/work-items.d.ts.map +1 -0
  153. package/dist/routes/work-items.js +902 -0
  154. package/dist/routes/work-items.js.map +1 -0
  155. package/dist/rules/binding-context.d.ts +5 -0
  156. package/dist/rules/binding-context.d.ts.map +1 -0
  157. package/dist/rules/binding-context.js +40 -0
  158. package/dist/rules/binding-context.js.map +1 -0
  159. package/dist/rules/defaults.d.ts +9 -0
  160. package/dist/rules/defaults.d.ts.map +1 -0
  161. package/dist/rules/defaults.js +334 -0
  162. package/dist/rules/defaults.js.map +1 -0
  163. package/dist/rules/dispatcher.d.ts +40 -0
  164. package/dist/rules/dispatcher.d.ts.map +1 -0
  165. package/dist/rules/dispatcher.js +695 -0
  166. package/dist/rules/dispatcher.js.map +1 -0
  167. package/dist/rules/github-service.d.ts +29 -0
  168. package/dist/rules/github-service.d.ts.map +1 -0
  169. package/dist/rules/github-service.js +474 -0
  170. package/dist/rules/github-service.js.map +1 -0
  171. package/dist/rules/index.d.ts +7 -0
  172. package/dist/rules/index.d.ts.map +1 -0
  173. package/dist/rules/index.js +583 -0
  174. package/dist/rules/index.js.map +1 -0
  175. package/dist/rules/linear-service.d.ts +27 -0
  176. package/dist/rules/linear-service.d.ts.map +1 -0
  177. package/dist/rules/linear-service.js +343 -0
  178. package/dist/rules/linear-service.js.map +1 -0
  179. package/dist/rules/processor.d.ts +54 -0
  180. package/dist/rules/processor.d.ts.map +1 -0
  181. package/dist/rules/processor.js +629 -0
  182. package/dist/rules/processor.js.map +1 -0
  183. package/dist/rules/resolve.d.ts +17 -0
  184. package/dist/rules/resolve.d.ts.map +1 -0
  185. package/dist/rules/resolve.js +27 -0
  186. package/dist/rules/resolve.js.map +1 -0
  187. package/dist/rules/start-coordinator.d.ts +58 -0
  188. package/dist/rules/start-coordinator.d.ts.map +1 -0
  189. package/dist/rules/start-coordinator.js +136 -0
  190. package/dist/rules/start-coordinator.js.map +1 -0
  191. package/dist/rules/tools.d.ts +10 -0
  192. package/dist/rules/tools.d.ts.map +1 -0
  193. package/dist/rules/tools.js +93 -0
  194. package/dist/rules/tools.js.map +1 -0
  195. package/dist/rules/transition-service.d.ts +32 -0
  196. package/dist/rules/transition-service.d.ts.map +1 -0
  197. package/dist/rules/transition-service.js +474 -0
  198. package/dist/rules/transition-service.js.map +1 -0
  199. package/dist/rules/types.d.ts +225 -0
  200. package/dist/rules/types.d.ts.map +1 -0
  201. package/dist/rules/types.js +33 -0
  202. package/dist/rules/types.js.map +1 -0
  203. package/dist/rules/validation.d.ts +11 -0
  204. package/dist/rules/validation.d.ts.map +1 -0
  205. package/dist/rules/validation.js +310 -0
  206. package/dist/rules/validation.js.map +1 -0
  207. package/dist/sandbox/fleet.d.ts +208 -0
  208. package/dist/sandbox/fleet.d.ts.map +1 -0
  209. package/dist/sandbox/fleet.js +259 -0
  210. package/dist/sandbox/fleet.js.map +1 -0
  211. package/dist/sandbox/reattach.d.ts +3 -0
  212. package/dist/sandbox/reattach.d.ts.map +1 -0
  213. package/dist/sandbox/reattach.js +9 -0
  214. package/dist/sandbox/reattach.js.map +1 -0
  215. package/dist/server-error.d.ts +12 -0
  216. package/dist/server-error.d.ts.map +1 -0
  217. package/dist/server-error.js +20 -0
  218. package/dist/server-error.js.map +1 -0
  219. package/dist/skills/service.d.ts +48 -0
  220. package/dist/skills/service.d.ts.map +1 -0
  221. package/dist/skills/service.js +45 -0
  222. package/dist/skills/service.js.map +1 -0
  223. package/dist/spa-static.d.ts +34 -0
  224. package/dist/spa-static.d.ts.map +1 -0
  225. package/dist/spa-static.js +73 -0
  226. package/dist/spa-static.js.map +1 -0
  227. package/dist/state-signing.d.ts +48 -0
  228. package/dist/state-signing.d.ts.map +1 -0
  229. package/dist/state-signing.js +48 -0
  230. package/dist/state-signing.js.map +1 -0
  231. package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
  232. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
  233. package/dist/storage/domains/audit/agent-audit.js +65 -0
  234. package/dist/storage/domains/audit/agent-audit.js.map +1 -0
  235. package/dist/storage/domains/audit/base.d.ts +140 -0
  236. package/dist/storage/domains/audit/base.d.ts.map +1 -0
  237. package/dist/storage/domains/audit/base.js +127 -0
  238. package/dist/storage/domains/audit/base.js.map +1 -0
  239. package/dist/storage/domains/audit/domain.d.ts +69 -0
  240. package/dist/storage/domains/audit/domain.d.ts.map +1 -0
  241. package/dist/storage/domains/audit/domain.js +165 -0
  242. package/dist/storage/domains/audit/domain.js.map +1 -0
  243. package/dist/storage/domains/credentials/base.d.ts +134 -0
  244. package/dist/storage/domains/credentials/base.d.ts.map +1 -0
  245. package/dist/storage/domains/credentials/base.js +257 -0
  246. package/dist/storage/domains/credentials/base.js.map +1 -0
  247. package/dist/storage/domains/intake/base.d.ts +38 -0
  248. package/dist/storage/domains/intake/base.d.ts.map +1 -0
  249. package/dist/storage/domains/intake/base.js +62 -0
  250. package/dist/storage/domains/intake/base.js.map +1 -0
  251. package/dist/storage/domains/integrations/base.d.ts +126 -0
  252. package/dist/storage/domains/integrations/base.d.ts.map +1 -0
  253. package/dist/storage/domains/integrations/base.js +252 -0
  254. package/dist/storage/domains/integrations/base.js.map +1 -0
  255. package/dist/storage/domains/model-packs/base.d.ts +49 -0
  256. package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
  257. package/dist/storage/domains/model-packs/base.js +95 -0
  258. package/dist/storage/domains/model-packs/base.js.map +1 -0
  259. package/dist/storage/domains/projects/base.d.ts +55 -0
  260. package/dist/storage/domains/projects/base.d.ts.map +1 -0
  261. package/dist/storage/domains/projects/base.js +99 -0
  262. package/dist/storage/domains/projects/base.js.map +1 -0
  263. package/dist/storage/domains/queue-health/base.d.ts +52 -0
  264. package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
  265. package/dist/storage/domains/queue-health/base.js +87 -0
  266. package/dist/storage/domains/queue-health/base.js.map +1 -0
  267. package/dist/storage/domains/source-control/base.d.ts +288 -0
  268. package/dist/storage/domains/source-control/base.d.ts.map +1 -0
  269. package/dist/storage/domains/source-control/base.js +722 -0
  270. package/dist/storage/domains/source-control/base.js.map +1 -0
  271. package/dist/storage/domains/source-control/inmemory.d.ts +158 -0
  272. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
  273. package/dist/storage/domains/source-control/inmemory.js +348 -0
  274. package/dist/storage/domains/source-control/inmemory.js.map +1 -0
  275. package/dist/storage/domains/work-items/base.d.ts +412 -0
  276. package/dist/storage/domains/work-items/base.d.ts.map +1 -0
  277. package/dist/storage/domains/work-items/base.js +1174 -0
  278. package/dist/storage/domains/work-items/base.js.map +1 -0
  279. package/dist/storage/domains/work-items/metrics.d.ts +101 -0
  280. package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
  281. package/dist/storage/domains/work-items/metrics.js +203 -0
  282. package/dist/storage/domains/work-items/metrics.js.map +1 -0
  283. package/dist/workspace.d.ts +20 -0
  284. package/dist/workspace.d.ts.map +1 -0
  285. package/dist/workspace.js +364 -0
  286. package/dist/workspace.js.map +1 -0
  287. package/package.json +19 -19
@@ -0,0 +1,962 @@
1
+ // src/integrations/linear/agent-tools.ts
2
+ import { createTool } from "@mastra/core/tools";
3
+ import { z } from "zod";
4
+ function createLinearGetIssueTool(linear, orgId) {
5
+ return createTool({
6
+ id: "linear_get_issue",
7
+ description: "Fetch a Linear issue's full details \u2014 title, description, state, assignee, labels, priority, and discussion comments. Use this whenever you're working on a Linear issue (e.g. ENG-123) to get its complete context.",
8
+ inputSchema: z.object({
9
+ issue: z.string().min(1).describe('The Linear issue identifier (e.g. "ENG-123") or issue UUID.')
10
+ }),
11
+ execute: async ({ issue }) => {
12
+ const connection = await linear.loadConnection(orgId);
13
+ if (!connection) {
14
+ return { error: "Linear is not connected for this repository. Connect Linear in Settings to fetch issues." };
15
+ }
16
+ try {
17
+ const accessToken = await linear.getFreshAccessToken(connection);
18
+ const detail = await linear.intake.getIssue({
19
+ connection: { type: "oauth", accessToken },
20
+ issueId: issue.trim()
21
+ });
22
+ if (!detail) {
23
+ return { error: `Linear issue "${issue}" was not found in this workspace.` };
24
+ }
25
+ return detail;
26
+ } catch (err) {
27
+ if (err instanceof LinearReauthRequiredError) {
28
+ return { error: err.message };
29
+ }
30
+ return { error: `Failed to fetch Linear issue: ${err instanceof Error ? err.message : String(err)}` };
31
+ }
32
+ }
33
+ });
34
+ }
35
+ function createLinearCommentTool(linear, orgId) {
36
+ return createTool({
37
+ id: "linear_create_comment",
38
+ description: "Post a comment on a Linear issue (e.g. to report investigation findings, link a PR, or ask a clarifying question). The comment is posted as the connected Linear integration, so make clear it comes from the agent.",
39
+ inputSchema: z.object({
40
+ issue: z.string().min(1).describe('The Linear issue identifier (e.g. "ENG-123") or issue UUID.'),
41
+ body: z.string().min(1).describe("The comment body, as Linear-flavored markdown.")
42
+ }),
43
+ execute: async ({ issue, body }) => {
44
+ const connection = await linear.loadConnection(orgId);
45
+ if (!connection) {
46
+ return { error: "Linear is not connected for this repository. Connect Linear in Settings to post comments." };
47
+ }
48
+ if (!linear.canPostComments(connection)) {
49
+ return {
50
+ error: "The Linear connection does not have comment permissions. Reconnect Linear in Settings to grant them."
51
+ };
52
+ }
53
+ try {
54
+ const accessToken = await linear.getFreshAccessToken(connection);
55
+ const comment = await linear.intake.createComment({
56
+ connection: { type: "oauth", accessToken },
57
+ issueId: issue.trim(),
58
+ body
59
+ });
60
+ if (!comment) {
61
+ return { error: `Linear issue "${issue}" was not found in this workspace.` };
62
+ }
63
+ return { posted: true, url: comment.url };
64
+ } catch (err) {
65
+ if (err instanceof LinearReauthRequiredError) {
66
+ return { error: err.message };
67
+ }
68
+ return { error: `Failed to post Linear comment: ${err instanceof Error ? err.message : String(err)}` };
69
+ }
70
+ }
71
+ });
72
+ }
73
+ async function buildLinearAgentTools({
74
+ requestContext,
75
+ linear
76
+ }) {
77
+ if (!linear.authEnabled) return {};
78
+ const ctx = requestContext.get("controller");
79
+ const resourceId = ctx?.resourceId;
80
+ if (!resourceId) return {};
81
+ const orgId = await linear.resolveOrgId(resourceId);
82
+ if (!orgId) return {};
83
+ const check = await linear.checkConnection(orgId);
84
+ if (!check.connected) return {};
85
+ return {
86
+ linear_get_issue: createLinearGetIssueTool(linear, orgId),
87
+ // Only offered when the granted OAuth scope allows posting comments —
88
+ // connections made before `comments:create` was requested are read-only
89
+ // until the org reconnects Linear.
90
+ ...check.canComment ? { linear_create_comment: createLinearCommentTool(linear, orgId) } : {}
91
+ };
92
+ }
93
+
94
+ // src/integrations/linear/routes.ts
95
+ import { registerApiRoute } from "@mastra/core/server";
96
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
97
+ function loose(c) {
98
+ return c;
99
+ }
100
+ async function resolveOrgTenant(c, auth) {
101
+ await auth.ensureUser(c);
102
+ const tenant = auth.tenant(c);
103
+ if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
104
+ if (!tenant.orgId) {
105
+ return {
106
+ response: c.json(
107
+ {
108
+ error: "organization_required",
109
+ message: "Linear intake requires an organization. Personal accounts cannot connect Linear."
110
+ },
111
+ 403
112
+ )
113
+ };
114
+ }
115
+ return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };
116
+ }
117
+ function parseAfterCursor(raw) {
118
+ if (raw === void 0 || raw === "") return void 0;
119
+ if (raw.length > 512 || !/^[\w+/=.:-]+$/.test(raw)) return null;
120
+ return raw;
121
+ }
122
+ function linearFetchError(c, err) {
123
+ if (err instanceof LinearReauthRequiredError || err.status === 401) {
124
+ return c.json({ error: "linear_reauth_required", message: new LinearReauthRequiredError().message }, 409);
125
+ }
126
+ return c.json({ error: "linear_fetch_failed", message: err instanceof Error ? err.message : String(err) }, 502);
127
+ }
128
+ function buildLinearRoutes(options) {
129
+ const routes = [];
130
+ const { linear, auth, stateSigner, intake } = options;
131
+ const enabled = Boolean(linear) && auth.enabled();
132
+ const diagnostics = () => ({
133
+ linearAppConfigured: Boolean(linear),
134
+ factoryAuthEnabled: auth.enabled(),
135
+ appDbConfigured: true
136
+ });
137
+ routes.push(
138
+ registerApiRoute("/web/linear/status", {
139
+ method: "GET",
140
+ requiresAuth: false,
141
+ handler: async (c) => {
142
+ if (!enabled || !linear || !stateSigner) {
143
+ return c.json({
144
+ enabled: false,
145
+ connected: false,
146
+ workspace: null,
147
+ reason: "missing_config",
148
+ diagnostics: diagnostics()
149
+ });
150
+ }
151
+ await auth.ensureUser(loose(c));
152
+ const tenant = auth.tenant(loose(c));
153
+ if (!tenant) return c.json({ error: "unauthorized", reason: "auth_required" }, 401);
154
+ if (!tenant.orgId) {
155
+ return c.json({
156
+ enabled: true,
157
+ organizationRequired: true,
158
+ connected: false,
159
+ workspace: null,
160
+ reason: "organization_required",
161
+ diagnostics: diagnostics()
162
+ });
163
+ }
164
+ const connection = await linear.loadConnection(tenant.orgId);
165
+ return c.json({
166
+ enabled: true,
167
+ connected: Boolean(connection),
168
+ workspace: connection ? { name: connection.workspaceName, urlKey: connection.workspaceUrlKey } : null,
169
+ reason: connection ? "ready" : "not_connected",
170
+ diagnostics: diagnostics()
171
+ });
172
+ }
173
+ })
174
+ );
175
+ if (!enabled || !linear || !stateSigner || !intake) {
176
+ return routes;
177
+ }
178
+ const redirectUri = options.redirectUri ?? `${(options.baseUrl ?? "").replace(/\/$/, "")}/auth/linear/callback`;
179
+ routes.push(
180
+ registerApiRoute("/auth/linear/connect", {
181
+ method: "GET",
182
+ requiresAuth: false,
183
+ handler: async (c) => {
184
+ const resolved = await resolveOrgTenant(loose(c), auth);
185
+ if ("response" in resolved) return resolved.response;
186
+ const state = stateSigner.sign(resolved.tenant.orgId, resolved.tenant.userId);
187
+ return c.redirect(linear.buildAuthorizeUrl(state, redirectUri));
188
+ }
189
+ })
190
+ );
191
+ routes.push(
192
+ registerApiRoute("/auth/linear/callback", {
193
+ method: "GET",
194
+ requiresAuth: false,
195
+ handler: async (c) => {
196
+ const resolved = await resolveOrgTenant(loose(c), auth);
197
+ if ("response" in resolved) return resolved.response;
198
+ const { orgId, userId } = resolved.tenant;
199
+ const stateTenant = stateSigner.verify(c.req.query("state"));
200
+ if (!stateTenant || stateTenant.userId !== userId || stateTenant.orgId !== orgId) {
201
+ console.warn("[Linear] OAuth callback rejected: state/tenant mismatch.");
202
+ return c.redirect("/?linear=error");
203
+ }
204
+ const code = c.req.query("code");
205
+ if (!code) {
206
+ return c.redirect("/?linear=error");
207
+ }
208
+ try {
209
+ const tokens = await linear.exchangeOAuthCode(code, redirectUri);
210
+ const workspace = await linear.fetchWorkspace(tokens.accessToken);
211
+ await linear.upsertConnection({
212
+ orgId,
213
+ userId,
214
+ accessToken: tokens.accessToken,
215
+ refreshToken: tokens.refreshToken,
216
+ expiresAt: tokens.expiresAt,
217
+ scope: tokens.scope,
218
+ workspaceName: workspace.name,
219
+ workspaceUrlKey: workspace.urlKey
220
+ });
221
+ } catch (error) {
222
+ console.warn(`[Linear] OAuth callback failed to persist connection for org ${orgId}.`, error);
223
+ return c.redirect("/?linear=error");
224
+ }
225
+ return c.redirect("/?linear=connected");
226
+ }
227
+ })
228
+ );
229
+ routes.push(
230
+ registerApiRoute("/web/linear/projects", {
231
+ method: "GET",
232
+ requiresAuth: false,
233
+ handler: async (c) => {
234
+ const resolved = await resolveOrgTenant(loose(c), auth);
235
+ if ("response" in resolved) return resolved.response;
236
+ const connection = await linear.loadConnection(resolved.tenant.orgId);
237
+ if (!connection) {
238
+ return c.json({ error: "linear_not_connected", message: "Connect Linear to list Linear projects." }, 409);
239
+ }
240
+ try {
241
+ const accessToken = await linear.getFreshAccessToken(connection);
242
+ const projects = await linear.listProjects(accessToken);
243
+ return c.json({ projects });
244
+ } catch (err) {
245
+ return linearFetchError(loose(c), err);
246
+ }
247
+ }
248
+ })
249
+ );
250
+ routes.push(
251
+ registerApiRoute("/web/linear/issues", {
252
+ method: "GET",
253
+ requiresAuth: false,
254
+ handler: async (c) => {
255
+ const resolved = await resolveOrgTenant(loose(c), auth);
256
+ if ("response" in resolved) return resolved.response;
257
+ const after = parseAfterCursor(c.req.query("after"));
258
+ if (after === null) return c.json({ error: "invalid_cursor" }, 400);
259
+ const factoryProjectId = c.req.query("factoryProjectId");
260
+ if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {
261
+ return c.json({ error: "invalid_factory_project_id" }, 400);
262
+ }
263
+ const connection = await linear.loadConnection(resolved.tenant.orgId);
264
+ if (!connection) {
265
+ return c.json({ error: "linear_not_connected", message: "Connect Linear to see intake issues." }, 409);
266
+ }
267
+ await intake.ensureReady();
268
+ const config = await intake.getConfig({
269
+ orgId: resolved.tenant.orgId,
270
+ userId: resolved.tenant.userId,
271
+ integrationIds: ["linear"]
272
+ });
273
+ const selection = config.linear;
274
+ if (!selection.enabled) {
275
+ return c.json({ error: "linear_intake_disabled", message: "Linear intake is turned off in Settings." }, 404);
276
+ }
277
+ const projectIds = selection.sourceIds ?? [];
278
+ if (projectIds.length === 0) {
279
+ return c.json({ issues: [], nextCursor: null });
280
+ }
281
+ try {
282
+ const accessToken = await linear.getFreshAccessToken(connection);
283
+ const { issues, nextCursor } = await linear.intake.listIssues({
284
+ connection: { type: "oauth", accessToken },
285
+ sourceIds: projectIds,
286
+ cursor: after
287
+ });
288
+ const issuePayload = issues.map((issue) => ({
289
+ id: issue.id,
290
+ identifier: issue.identifier,
291
+ title: issue.title,
292
+ url: issue.url,
293
+ state: issue.state ?? "",
294
+ stateType: issue.stateType ?? "",
295
+ priorityLabel: issue.priority ?? "",
296
+ assignee: issue.assignee,
297
+ team: issue.source,
298
+ labels: issue.labels,
299
+ createdAt: issue.createdAt,
300
+ updatedAt: issue.updatedAt
301
+ }));
302
+ if (factoryProjectId && options.hooks?.ingestLinearIssues) {
303
+ await options.hooks.ingestLinearIssues({
304
+ orgId: resolved.tenant.orgId,
305
+ userId: resolved.tenant.userId,
306
+ factoryProjectId,
307
+ issues: issuePayload
308
+ });
309
+ }
310
+ return c.json({ issues: issuePayload, nextCursor });
311
+ } catch (err) {
312
+ return linearFetchError(loose(c), err);
313
+ }
314
+ }
315
+ })
316
+ );
317
+ return routes;
318
+ }
319
+
320
+ // src/integrations/linear/integration.ts
321
+ var LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
322
+ var LINEAR_TOKEN_URL = "https://api.linear.app/oauth/token";
323
+ var LINEAR_AUTHORIZE_URL = "https://linear.app/oauth/authorize";
324
+ var LINEAR_ISSUES_PAGE_SIZE = 30;
325
+ var ISSUE_COMMENTS_PAGE_SIZE = 50;
326
+ var ISSUE_COMMENTS_MAX_PAGES = 20;
327
+ var TOKEN_REFRESH_SKEW_MS = 6e4;
328
+ var CONNECTION_TTL_MS = 6e4;
329
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
330
+ var LinearReauthRequiredError = class extends Error {
331
+ constructor() {
332
+ super("Linear authorization expired. Reconnect Linear to keep syncing intake issues.");
333
+ }
334
+ };
335
+ async function linearGraphql(accessToken, query, variables) {
336
+ const res = await fetch(LINEAR_GRAPHQL_URL, {
337
+ method: "POST",
338
+ signal: AbortSignal.timeout(15e3),
339
+ headers: {
340
+ "content-type": "application/json",
341
+ authorization: `Bearer ${accessToken}`
342
+ },
343
+ body: JSON.stringify({ query, variables })
344
+ });
345
+ if (!res.ok) {
346
+ let detail = null;
347
+ try {
348
+ const errBody = await res.json();
349
+ detail = errBody.errors?.[0]?.message ?? null;
350
+ } catch {
351
+ }
352
+ const err = new Error(`Linear API request failed (${res.status})${detail ? `: ${detail}` : ""}`);
353
+ err.status = res.status;
354
+ throw err;
355
+ }
356
+ const body = await res.json();
357
+ if (body.errors?.length) {
358
+ throw new Error(`Linear API error: ${body.errors[0]?.message ?? "unknown error"}`);
359
+ }
360
+ if (!body.data) {
361
+ throw new Error("Linear API returned no data.");
362
+ }
363
+ return body.data;
364
+ }
365
+ var LinearIntegration = class {
366
+ /** Stable integration identifier (see `../base.ts`). */
367
+ id = "linear";
368
+ /** Bound once by the factory via `initialize()` before any surface is used. */
369
+ #storage;
370
+ #projects;
371
+ #auth;
372
+ /** Bind Linear's slice of the generic integration storage, the projects domain, and the host auth seam. */
373
+ initialize({
374
+ storage,
375
+ projects,
376
+ auth
377
+ }) {
378
+ this.#storage = storage;
379
+ this.#projects = projects;
380
+ this.#auth = auth;
381
+ }
382
+ get storage() {
383
+ if (!this.#storage) {
384
+ throw new Error("LinearIntegration is not initialized \u2014 the factory binds storage during prepare().");
385
+ }
386
+ return this.#storage;
387
+ }
388
+ /** Factory projects domain — maps a session's resourceId to its owning org. */
389
+ get projects() {
390
+ if (!this.#projects) {
391
+ throw new Error("LinearIntegration is not initialized \u2014 the factory binds storage during prepare().");
392
+ }
393
+ return this.#projects;
394
+ }
395
+ /**
396
+ * Whether the host runs with web auth enabled. Linear connections are
397
+ * org-owned, so every Linear surface is inert without a tenant auth seam.
398
+ */
399
+ get authEnabled() {
400
+ return this.#auth?.enabled() ?? false;
401
+ }
402
+ // ── Connection + OAuth token lifecycle ───────────────────────────────────
403
+ /** Load the org's Linear connection, or `null` when not connected. */
404
+ async loadConnection(orgId) {
405
+ const connection = await this.storage.connections.get(orgId);
406
+ if (!connection) return null;
407
+ const { data } = connection;
408
+ return {
409
+ id: connection.id,
410
+ orgId: connection.orgId,
411
+ userId: connection.userId,
412
+ accessToken: data.accessToken,
413
+ scope: data.scope ?? null,
414
+ refreshToken: data.refreshToken ?? null,
415
+ expiresAt: data.expiresAtMs === null || data.expiresAtMs === void 0 ? null : new Date(data.expiresAtMs),
416
+ workspaceName: data.workspaceName ?? null,
417
+ workspaceUrlKey: data.workspaceUrlKey ?? null,
418
+ createdAt: connection.createdAt,
419
+ updatedAt: connection.updatedAt
420
+ };
421
+ }
422
+ /** Insert or replace the org's connection (one per org). */
423
+ async upsertConnection(input) {
424
+ await this.storage.connections.upsert(input.orgId, {
425
+ userId: input.userId,
426
+ data: {
427
+ accessToken: input.accessToken,
428
+ refreshToken: input.refreshToken,
429
+ expiresAtMs: input.expiresAt?.getTime() ?? null,
430
+ scope: input.scope,
431
+ workspaceName: input.workspaceName,
432
+ workspaceUrlKey: input.workspaceUrlKey
433
+ }
434
+ });
435
+ this.invalidateConnectionCache(input.orgId);
436
+ }
437
+ /** Persist a rotated token set on the org's existing connection row. */
438
+ async #updateTokens(orgId, tokens) {
439
+ await this.storage.connections.update(orgId, (data) => ({
440
+ ...data,
441
+ accessToken: tokens.accessToken,
442
+ refreshToken: tokens.refreshToken,
443
+ expiresAtMs: tokens.expiresAt?.getTime() ?? null,
444
+ // Refresh responses may omit scope; keep the recorded grant.
445
+ scope: tokens.scope ?? data.scope
446
+ }));
447
+ }
448
+ /**
449
+ * Whether the connection's token can post issue comments. Legacy rows
450
+ * without a recorded scope were minted with `read` only, so they count as
451
+ * read-only until the org reconnects Linear.
452
+ */
453
+ canPostComments(connection) {
454
+ const scopes = (connection.scope ?? "").split(/[\s,]+/).filter(Boolean);
455
+ return scopes.some((scope) => scope === "comments:create" || scope === "write" || scope === "admin");
456
+ }
457
+ /**
458
+ * In-flight refreshes keyed by org. Linear rotates refresh tokens, so two
459
+ * concurrent refreshes with the same token would invalidate each other —
460
+ * single-flight ensures one exchange per org and shares the result.
461
+ */
462
+ #inflightRefreshes = /* @__PURE__ */ new Map();
463
+ /**
464
+ * Return a usable access token for the connection, proactively refreshing
465
+ * it when the recorded expiry is past (or imminent). Throws
466
+ * `LinearReauthRequiredError` when the token is expired and cannot be
467
+ * refreshed — the org has to go through the OAuth flow again.
468
+ */
469
+ async getFreshAccessToken(connection) {
470
+ const expired = connection.expiresAt !== null && connection.expiresAt.getTime() - TOKEN_REFRESH_SKEW_MS <= Date.now();
471
+ if (!expired) return connection.accessToken;
472
+ if (!connection.refreshToken) {
473
+ throw new LinearReauthRequiredError();
474
+ }
475
+ const existing = this.#inflightRefreshes.get(connection.orgId);
476
+ if (existing) return existing;
477
+ const latest = await this.loadConnection(connection.orgId);
478
+ if (!latest) throw new LinearReauthRequiredError();
479
+ const concurrent = this.#inflightRefreshes.get(connection.orgId);
480
+ if (concurrent) return concurrent;
481
+ const latestExpired = latest.expiresAt !== null && latest.expiresAt.getTime() - TOKEN_REFRESH_SKEW_MS <= Date.now();
482
+ if (!latestExpired) return latest.accessToken;
483
+ if (!latest.refreshToken) throw new LinearReauthRequiredError();
484
+ const refreshToken = latest.refreshToken;
485
+ const refresh = (async () => {
486
+ try {
487
+ const tokens = await this.refreshAccessToken(refreshToken);
488
+ await this.#updateTokens(connection.orgId, tokens);
489
+ return tokens.accessToken;
490
+ } catch (err) {
491
+ const status = err.status;
492
+ if (status === 400 || status === 401) throw new LinearReauthRequiredError();
493
+ throw err;
494
+ } finally {
495
+ this.#inflightRefreshes.delete(connection.orgId);
496
+ }
497
+ })();
498
+ this.#inflightRefreshes.set(connection.orgId, refresh);
499
+ return refresh;
500
+ }
501
+ // ── Connection checks for agent tools ────────────────────────────────────
502
+ /** Re-check an org's Linear connection (and its scopes) at most this often. */
503
+ #connectionChecks = /* @__PURE__ */ new Map();
504
+ /**
505
+ * Whether the org has an active connection and whether its granted scope
506
+ * allows posting comments. Cached per org with a short TTL — tool-set
507
+ * resolution runs on every request.
508
+ */
509
+ async checkConnection(orgId) {
510
+ const cached = this.#connectionChecks.get(orgId);
511
+ if (cached && Date.now() - cached.checkedAt < CONNECTION_TTL_MS) return cached;
512
+ const connection = await this.loadConnection(orgId);
513
+ const check = {
514
+ connected: connection !== null,
515
+ canComment: connection !== null && this.canPostComments(connection),
516
+ checkedAt: Date.now()
517
+ };
518
+ this.#connectionChecks.set(orgId, check);
519
+ return check;
520
+ }
521
+ /**
522
+ * Drop the cached connection check for an org. Called after a connection is
523
+ * persisted so the tools show up on the very next run instead of after the
524
+ * TTL lapses.
525
+ */
526
+ invalidateConnectionCache(orgId) {
527
+ this.#connectionChecks.delete(orgId);
528
+ }
529
+ /**
530
+ * A project's org never changes, so the resourceId → orgId mapping is
531
+ * cached forever. `null` marks resource ids that aren't factory projects
532
+ * (e.g. local default resources) so we don't re-query them on every
533
+ * tool-set resolution.
534
+ */
535
+ #orgIdByResourceId = /* @__PURE__ */ new Map();
536
+ /** Map a session's resourceId to its owning org, or `null` when it isn't a project. */
537
+ async resolveOrgId(resourceId) {
538
+ const cached = this.#orgIdByResourceId.get(resourceId);
539
+ if (cached !== void 0) return cached;
540
+ if (!UUID_PATTERN.test(resourceId)) {
541
+ this.#orgIdByResourceId.set(resourceId, null);
542
+ return null;
543
+ }
544
+ let orgId;
545
+ try {
546
+ await this.projects.ensureReady();
547
+ const project = await this.projects.getById({ id: resourceId });
548
+ orgId = project?.orgId ?? null;
549
+ } catch {
550
+ return null;
551
+ }
552
+ this.#orgIdByResourceId.set(resourceId, orgId);
553
+ return orgId;
554
+ }
555
+ /** Test hook: clear the org/connection caches between specs. */
556
+ clearCaches() {
557
+ this.#orgIdByResourceId.clear();
558
+ this.#connectionChecks.clear();
559
+ }
560
+ intake = {
561
+ listSources: async ({ orgId }) => {
562
+ const connection = await this.loadConnection(orgId);
563
+ if (!connection) return [];
564
+ const accessToken = await this.getFreshAccessToken(connection);
565
+ const projects = await this.listProjects(accessToken);
566
+ return projects.map((project) => ({
567
+ id: project.id,
568
+ name: project.name,
569
+ type: "project"
570
+ }));
571
+ },
572
+ listItems: async ({ orgId, sourceIds, cursor }) => {
573
+ if (sourceIds.length === 0) return { items: [], nextCursor: null };
574
+ const connection = await this.loadConnection(orgId);
575
+ if (!connection) return { items: [], nextCursor: null };
576
+ const accessToken = await this.getFreshAccessToken(connection);
577
+ const page = await this.listActiveIssues(accessToken, cursor, sourceIds);
578
+ return {
579
+ items: page.issues.map((issue) => ({
580
+ source: { type: "issue", externalId: issue.id, url: issue.url },
581
+ sourceId: issue.projectId,
582
+ title: `${issue.identifier}: ${issue.title}`,
583
+ status: issue.state,
584
+ labels: issue.labels,
585
+ assignee: issue.assignee,
586
+ createdAt: issue.createdAt,
587
+ updatedAt: issue.updatedAt,
588
+ metadata: {
589
+ identifier: issue.identifier,
590
+ stateType: issue.stateType,
591
+ priority: issue.priorityLabel,
592
+ team: issue.team
593
+ }
594
+ })),
595
+ nextCursor: page.nextCursor
596
+ };
597
+ },
598
+ listIssues: (input) => this.#listIntakeIssues(input),
599
+ getIssue: (input) => this.#getIntakeIssue(input),
600
+ createComment: (input) => this.#createIntakeComment(input)
601
+ };
602
+ /**
603
+ * The OAuth connect/callback flow round-trips a signed `state` through
604
+ * Linear, so a multi-replica deploy needs a deployment-stable state secret.
605
+ */
606
+ requiresStableStateSigner = true;
607
+ #clientId;
608
+ #clientSecret;
609
+ constructor(config) {
610
+ const missing = ["clientId", "clientSecret"].filter((key) => !config[key]);
611
+ if (missing.length > 0) {
612
+ throw new Error(`LinearIntegration is missing required config: ${missing.join(", ")}.`);
613
+ }
614
+ this.#clientId = config.clientId;
615
+ this.#clientSecret = config.clientSecret;
616
+ }
617
+ // ── OAuth ────────────────────────────────────────────────────────────────
618
+ /**
619
+ * Build the OAuth authorize URL. `prompt=consent` forces the workspace
620
+ * picker even for an already-authorized user, so "reconnect" can switch
621
+ * workspaces.
622
+ */
623
+ buildAuthorizeUrl(state, redirectUri) {
624
+ const url = new URL(LINEAR_AUTHORIZE_URL);
625
+ url.searchParams.set("client_id", this.#clientId);
626
+ url.searchParams.set("redirect_uri", redirectUri);
627
+ url.searchParams.set("response_type", "code");
628
+ url.searchParams.set("scope", "read,comments:create");
629
+ url.searchParams.set("state", state);
630
+ url.searchParams.set("prompt", "consent");
631
+ return url.toString();
632
+ }
633
+ /** Exchange an OAuth `code` for a workspace-scoped token set. */
634
+ async exchangeOAuthCode(code, redirectUri) {
635
+ return this.#requestTokens({ grant_type: "authorization_code", code, redirect_uri: redirectUri }, "token exchange");
636
+ }
637
+ /**
638
+ * Exchange a refresh token for a new token set. Linear rotates refresh
639
+ * tokens, so the returned set replaces the stored one entirely. A 400/401
640
+ * here means the refresh token is invalid/revoked and the org must
641
+ * re-authorize.
642
+ */
643
+ async refreshAccessToken(refreshToken) {
644
+ return this.#requestTokens({ grant_type: "refresh_token", refresh_token: refreshToken }, "token refresh");
645
+ }
646
+ /** POST to Linear's token endpoint and normalize the response. */
647
+ async #requestTokens(params, label) {
648
+ const res = await fetch(LINEAR_TOKEN_URL, {
649
+ method: "POST",
650
+ signal: AbortSignal.timeout(1e4),
651
+ headers: { "content-type": "application/x-www-form-urlencoded" },
652
+ body: new URLSearchParams({
653
+ ...params,
654
+ client_id: this.#clientId,
655
+ client_secret: this.#clientSecret
656
+ })
657
+ });
658
+ if (!res.ok) {
659
+ const err = new Error(`Linear ${label} failed (${res.status})`);
660
+ err.status = res.status;
661
+ throw err;
662
+ }
663
+ const body = await res.json();
664
+ if (!body.access_token) {
665
+ throw new Error(`Linear ${label} returned no access token.`);
666
+ }
667
+ return {
668
+ accessToken: body.access_token,
669
+ refreshToken: body.refresh_token ?? null,
670
+ expiresAt: typeof body.expires_in === "number" ? new Date(Date.now() + body.expires_in * 1e3) : null,
671
+ scope: body.scope ?? null
672
+ };
673
+ }
674
+ // ── GraphQL reads/writes ─────────────────────────────────────────────────
675
+ /** Fetch the workspace (organization) the access token is scoped to. */
676
+ async fetchWorkspace(accessToken) {
677
+ const data = await linearGraphql(
678
+ accessToken,
679
+ `query { organization { name urlKey } }`
680
+ );
681
+ return { name: data.organization.name, urlKey: data.organization.urlKey };
682
+ }
683
+ async #listIntakeIssues(input) {
684
+ const accessToken = getLinearAccessToken(input.connection);
685
+ const result = await this.listActiveIssues(accessToken, input.cursor, input.sourceIds, input.labels);
686
+ return {
687
+ issues: result.issues.map((issue) => linearIssueToIntakeIssue(issue)),
688
+ nextCursor: result.nextCursor
689
+ };
690
+ }
691
+ async #getIntakeIssue(input) {
692
+ const accessToken = getLinearAccessToken(input.connection);
693
+ const issue = await this.fetchIssueDetail(accessToken, input.issueId);
694
+ if (!issue) return null;
695
+ return {
696
+ ...linearIssueToIntakeIssue(issue),
697
+ description: issue.description,
698
+ commentCount: issue.comments.length,
699
+ comments: issue.comments
700
+ };
701
+ }
702
+ async #createIntakeComment(input) {
703
+ const accessToken = getLinearAccessToken(input.connection);
704
+ return this.createIssueComment(accessToken, input.issueId, input.body);
705
+ }
706
+ /** List the workspace's projects (for the Settings intake-source picker). */
707
+ async listProjects(accessToken) {
708
+ const data = await linearGraphql(
709
+ accessToken,
710
+ `query { projects(first: 100) { nodes { id name state teams(first: 10) { nodes { id key name } } } } }`
711
+ );
712
+ return data.projects.nodes.map((node) => ({
713
+ id: node.id,
714
+ name: node.name,
715
+ state: node.state,
716
+ teams: node.teams.nodes.map((team) => ({ id: team.id, key: team.key, name: team.name }))
717
+ }));
718
+ }
719
+ /**
720
+ * List one page of the workspace's active issues (triage/backlog/unstarted/
721
+ * started — completed and canceled are excluded), most recently updated
722
+ * first. When `projectIds` is provided, only issues from those projects are
723
+ * returned.
724
+ */
725
+ async listActiveIssues(accessToken, after, projectIds, labels) {
726
+ const normalizedLabels = [...new Set((labels ?? []).map((label) => label.trim()).filter(Boolean))];
727
+ const projectFilter = projectIds?.length ? ", project: { id: { in: $projectIds } }" : "";
728
+ const projectVar = projectIds?.length ? ", $projectIds: [ID!]" : "";
729
+ const labelFilter = normalizedLabels.length > 0 ? ", labels: { name: { in: $labels } }" : "";
730
+ const labelVar = normalizedLabels.length > 0 ? ", $labels: [String!]" : "";
731
+ const data = await linearGraphql(
732
+ accessToken,
733
+ `query Intake($first: Int!, $after: String${projectVar}${labelVar}) {
734
+ issues(
735
+ first: $first
736
+ after: $after
737
+ orderBy: updatedAt
738
+ filter: { state: { type: { in: ["triage", "backlog", "unstarted", "started"] } }${projectFilter}${labelFilter} }
739
+ ) {
740
+ nodes {
741
+ id
742
+ identifier
743
+ title
744
+ url
745
+ priorityLabel
746
+ createdAt
747
+ updatedAt
748
+ state { name type }
749
+ project { id }
750
+ assignee { name }
751
+ team { key }
752
+ labels { nodes { name } }
753
+ }
754
+ pageInfo { hasNextPage endCursor }
755
+ }
756
+ }`,
757
+ {
758
+ first: LINEAR_ISSUES_PAGE_SIZE,
759
+ after: after ?? null,
760
+ ...projectIds?.length ? { projectIds } : {},
761
+ ...normalizedLabels.length > 0 ? { labels: normalizedLabels } : {}
762
+ }
763
+ );
764
+ const { nodes, pageInfo } = data.issues;
765
+ return {
766
+ issues: nodes.map((node) => ({
767
+ id: node.id,
768
+ projectId: node.project.id,
769
+ identifier: node.identifier,
770
+ title: node.title,
771
+ url: node.url,
772
+ state: node.state.name,
773
+ stateType: node.state.type,
774
+ priorityLabel: node.priorityLabel,
775
+ assignee: node.assignee?.name ?? null,
776
+ team: node.team?.key ?? null,
777
+ labels: node.labels.nodes.map((label) => label.name),
778
+ createdAt: node.createdAt,
779
+ updatedAt: node.updatedAt
780
+ })),
781
+ nextCursor: pageInfo.hasNextPage ? pageInfo.endCursor : null
782
+ };
783
+ }
784
+ /** Follow `comments.pageInfo` until exhausted so long discussions aren't truncated. */
785
+ async #fetchRemainingIssueComments(accessToken, issueId, firstPage) {
786
+ const nodes = [...firstPage.nodes];
787
+ let { hasNextPage, endCursor } = firstPage.pageInfo;
788
+ for (let page = 1; hasNextPage && endCursor && page < ISSUE_COMMENTS_MAX_PAGES; page++) {
789
+ const data = await linearGraphql(
790
+ accessToken,
791
+ `query IssueComments($id: String!, $first: Int!, $after: String!) {
792
+ issue(id: $id) {
793
+ comments(first: $first, after: $after) {
794
+ nodes { body createdAt user { name } }
795
+ pageInfo { hasNextPage endCursor }
796
+ }
797
+ }
798
+ }`,
799
+ { id: issueId, first: ISSUE_COMMENTS_PAGE_SIZE, after: endCursor }
800
+ );
801
+ const comments = data.issue?.comments;
802
+ if (!comments) break;
803
+ nodes.push(...comments.nodes);
804
+ ({ hasNextPage, endCursor } = comments.pageInfo);
805
+ }
806
+ return nodes;
807
+ }
808
+ /**
809
+ * Fetch one issue with its description and comments. `idOrIdentifier`
810
+ * accepts both the Linear UUID and the human key (`ENG-123`). Returns
811
+ * `null` when the issue doesn't exist (Linear reports it as an "Entity not
812
+ * found" error).
813
+ */
814
+ async fetchIssueDetail(accessToken, idOrIdentifier) {
815
+ let data;
816
+ try {
817
+ data = await linearGraphql(
818
+ accessToken,
819
+ `query IssueDetail($id: String!, $commentsFirst: Int!) {
820
+ issue(id: $id) {
821
+ id
822
+ identifier
823
+ title
824
+ description
825
+ url
826
+ priorityLabel
827
+ createdAt
828
+ updatedAt
829
+ state { name type }
830
+ project { id }
831
+ assignee { name }
832
+ team { key }
833
+ labels { nodes { name } }
834
+ comments(first: $commentsFirst) {
835
+ nodes { body createdAt user { name } }
836
+ pageInfo { hasNextPage endCursor }
837
+ }
838
+ }
839
+ }`,
840
+ { id: idOrIdentifier, commentsFirst: ISSUE_COMMENTS_PAGE_SIZE }
841
+ );
842
+ } catch (err) {
843
+ if (err instanceof Error && /entity not found/i.test(err.message)) return null;
844
+ throw err;
845
+ }
846
+ const issue = data.issue;
847
+ if (!issue) return null;
848
+ const allComments = await this.#fetchRemainingIssueComments(accessToken, issue.id, issue.comments);
849
+ const comments = allComments.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
850
+ return {
851
+ id: issue.id,
852
+ projectId: issue.project.id,
853
+ identifier: issue.identifier,
854
+ title: issue.title,
855
+ description: issue.description?.trim() ? issue.description : null,
856
+ url: issue.url,
857
+ state: issue.state.name,
858
+ stateType: issue.state.type,
859
+ priorityLabel: issue.priorityLabel,
860
+ assignee: issue.assignee?.name ?? null,
861
+ team: issue.team?.key ?? null,
862
+ labels: issue.labels.nodes.map((label) => label.name),
863
+ createdAt: issue.createdAt,
864
+ updatedAt: issue.updatedAt,
865
+ comments: comments.map((comment) => ({
866
+ author: comment.user?.name ?? null,
867
+ body: comment.body,
868
+ createdAt: comment.createdAt
869
+ }))
870
+ };
871
+ }
872
+ /**
873
+ * Post a comment on an issue. `idOrIdentifier` accepts both the Linear UUID
874
+ * and the human key (`ENG-123`) — the identifier is resolved to a UUID
875
+ * first because `commentCreate` only accepts UUIDs. Returns `null` when the
876
+ * issue doesn't exist.
877
+ */
878
+ async createIssueComment(accessToken, idOrIdentifier, body) {
879
+ let issueId;
880
+ try {
881
+ const data2 = await linearGraphql(
882
+ accessToken,
883
+ `query IssueId($id: String!) { issue(id: $id) { id } }`,
884
+ { id: idOrIdentifier }
885
+ );
886
+ if (!data2.issue) return null;
887
+ issueId = data2.issue.id;
888
+ } catch (err) {
889
+ if (err instanceof Error && /entity not found/i.test(err.message)) return null;
890
+ throw err;
891
+ }
892
+ const data = await linearGraphql(
893
+ accessToken,
894
+ `mutation CommentCreate($input: CommentCreateInput!) {
895
+ commentCreate(input: $input) { success comment { id url } }
896
+ }`,
897
+ { input: { issueId, body } }
898
+ );
899
+ if (!data.commentCreate.success || !data.commentCreate.comment) {
900
+ throw new Error("Linear did not accept the comment.");
901
+ }
902
+ return data.commentCreate.comment;
903
+ }
904
+ // ── FactoryIntegration surface ───────────────────────────────────────────
905
+ /**
906
+ * The integration's HTTP surface: `/web/linear/*` + `/auth/linear/*` Mastra
907
+ * `apiRoutes` (status, OAuth connect/callback, projects + issues for
908
+ * Intake). Handlers operate on this instance.
909
+ */
910
+ routes(ctx) {
911
+ return buildLinearRoutes({
912
+ linear: this,
913
+ auth: ctx.auth,
914
+ stateSigner: ctx.stateSigner,
915
+ baseUrl: ctx.baseUrl,
916
+ intake: ctx.storage.intake,
917
+ hooks: ctx.hooks
918
+ });
919
+ }
920
+ /**
921
+ * Org-scoped agent tools: issue detail + comment tools for sessions whose
922
+ * project belongs to an org with an active Linear connection.
923
+ */
924
+ async agentTools(args) {
925
+ return buildLinearAgentTools({ requestContext: args.requestContext, linear: this });
926
+ }
927
+ /** Non-secret config snapshot for system diagnostics/startup logs. */
928
+ diagnostics() {
929
+ return {
930
+ oauthAppConfigured: true
931
+ };
932
+ }
933
+ };
934
+ function getLinearAccessToken(connection) {
935
+ if (connection.type !== "oauth") {
936
+ throw new Error("Linear capabilities require an OAuth connection.");
937
+ }
938
+ return connection.accessToken;
939
+ }
940
+ function linearIssueToIntakeIssue(issue) {
941
+ return {
942
+ id: issue.id,
943
+ identifier: issue.identifier,
944
+ title: issue.title,
945
+ url: issue.url,
946
+ author: null,
947
+ state: issue.state,
948
+ stateType: issue.stateType,
949
+ priority: issue.priorityLabel,
950
+ assignee: issue.assignee,
951
+ source: issue.team,
952
+ labels: issue.labels,
953
+ commentCount: null,
954
+ createdAt: issue.createdAt,
955
+ updatedAt: issue.updatedAt
956
+ };
957
+ }
958
+ export {
959
+ LinearIntegration,
960
+ LinearReauthRequiredError
961
+ };
962
+ //# sourceMappingURL=integration.js.map