@mastra/factory 0.15.0 → 0.16.0-alpha.10

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 (212) hide show
  1. package/README.md +4 -3
  2. package/dist/boards/index.d.ts +1 -1
  3. package/dist/boards/index.d.ts.map +1 -1
  4. package/dist/boards/index.js +2 -2
  5. package/dist/boards/index.js.map +1 -1
  6. package/dist/boards/review.d.ts.map +1 -1
  7. package/dist/boards/review.js +4 -1
  8. package/dist/boards/review.js.map +1 -1
  9. package/dist/boards/semantics.d.ts +7 -0
  10. package/dist/boards/semantics.d.ts.map +1 -1
  11. package/dist/boards/semantics.js +15 -1
  12. package/dist/boards/semantics.js.map +1 -1
  13. package/dist/boards/transition-policy.d.ts +4 -0
  14. package/dist/boards/transition-policy.d.ts.map +1 -1
  15. package/dist/boards/transition-policy.js.map +1 -1
  16. package/dist/boards/work-transition-policy.d.ts.map +1 -1
  17. package/dist/boards/work-transition-policy.js +6 -1
  18. package/dist/boards/work-transition-policy.js.map +1 -1
  19. package/dist/boards/work.d.ts.map +1 -1
  20. package/dist/boards/work.js +37 -3
  21. package/dist/boards/work.js.map +1 -1
  22. package/dist/capabilities/intake.d.ts +4 -0
  23. package/dist/capabilities/intake.d.ts.map +1 -1
  24. package/dist/factory.d.ts +6 -3
  25. package/dist/factory.d.ts.map +1 -1
  26. package/dist/factory.js +66 -18
  27. package/dist/factory.js.map +1 -1
  28. package/dist/integrations/base.d.ts +6 -2
  29. package/dist/integrations/base.d.ts.map +1 -1
  30. package/dist/integrations/incidentio/agent-tools.d.ts +49 -0
  31. package/dist/integrations/incidentio/agent-tools.d.ts.map +1 -0
  32. package/dist/integrations/incidentio/agent-tools.js +90 -0
  33. package/dist/integrations/incidentio/agent-tools.js.map +1 -0
  34. package/dist/integrations/incidentio/claim.d.ts +8 -0
  35. package/dist/integrations/incidentio/claim.d.ts.map +1 -0
  36. package/dist/integrations/incidentio/claim.js +14 -0
  37. package/dist/integrations/incidentio/claim.js.map +1 -0
  38. package/dist/integrations/incidentio/default-rules.d.ts +46 -0
  39. package/dist/integrations/incidentio/default-rules.d.ts.map +1 -0
  40. package/dist/integrations/incidentio/default-rules.js +66 -0
  41. package/dist/integrations/incidentio/default-rules.js.map +1 -0
  42. package/dist/integrations/incidentio/integration.d.ts +48 -2
  43. package/dist/integrations/incidentio/integration.d.ts.map +1 -1
  44. package/dist/integrations/incidentio/integration.js +88 -2
  45. package/dist/integrations/incidentio/integration.js.map +1 -1
  46. package/dist/integrations/incidentio/issue-reconciler.d.ts +2 -0
  47. package/dist/integrations/incidentio/issue-reconciler.d.ts.map +1 -1
  48. package/dist/integrations/incidentio/issue-reconciler.js +38 -2
  49. package/dist/integrations/incidentio/issue-reconciler.js.map +1 -1
  50. package/dist/integrations/incidentio/routes.d.ts +71 -0
  51. package/dist/integrations/incidentio/routes.d.ts.map +1 -0
  52. package/dist/integrations/incidentio/routes.js +245 -0
  53. package/dist/integrations/incidentio/routes.js.map +1 -0
  54. package/dist/integrations/incidentio/rules.d.ts +55 -0
  55. package/dist/integrations/incidentio/rules.d.ts.map +1 -0
  56. package/dist/integrations/incidentio/rules.js +175 -0
  57. package/dist/integrations/incidentio/rules.js.map +1 -0
  58. package/dist/integrations/jira/adf.d.ts +26 -0
  59. package/dist/integrations/jira/adf.d.ts.map +1 -0
  60. package/dist/integrations/jira/adf.js +73 -0
  61. package/dist/integrations/jira/adf.js.map +1 -0
  62. package/dist/integrations/jira/agent-tools.d.ts +45 -0
  63. package/dist/integrations/jira/agent-tools.d.ts.map +1 -0
  64. package/dist/integrations/jira/agent-tools.js +87 -0
  65. package/dist/integrations/jira/agent-tools.js.map +1 -0
  66. package/dist/integrations/jira/api.d.ts +157 -0
  67. package/dist/integrations/jira/api.d.ts.map +1 -0
  68. package/dist/integrations/jira/api.js +141 -0
  69. package/dist/integrations/jira/api.js.map +1 -0
  70. package/dist/integrations/jira/claim.d.ts +9 -0
  71. package/dist/integrations/jira/claim.d.ts.map +1 -0
  72. package/dist/integrations/jira/claim.js +15 -0
  73. package/dist/integrations/jira/claim.js.map +1 -0
  74. package/dist/integrations/jira/default-rules.d.ts +47 -0
  75. package/dist/integrations/jira/default-rules.d.ts.map +1 -0
  76. package/dist/integrations/jira/default-rules.js +67 -0
  77. package/dist/integrations/jira/default-rules.js.map +1 -0
  78. package/dist/integrations/jira/integration.d.ts +114 -0
  79. package/dist/integrations/jira/integration.d.ts.map +1 -0
  80. package/dist/integrations/jira/integration.js +401 -0
  81. package/dist/integrations/jira/integration.js.map +1 -0
  82. package/dist/integrations/jira/issue-reconciler.d.ts +10 -0
  83. package/dist/integrations/jira/issue-reconciler.d.ts.map +1 -0
  84. package/dist/integrations/jira/issue-reconciler.js +70 -0
  85. package/dist/integrations/jira/issue-reconciler.js.map +1 -0
  86. package/dist/integrations/jira/reconciliation-config.d.ts +3 -0
  87. package/dist/integrations/jira/reconciliation-config.d.ts.map +1 -0
  88. package/dist/integrations/jira/reconciliation-config.js +17 -0
  89. package/dist/integrations/jira/reconciliation-config.js.map +1 -0
  90. package/dist/integrations/jira/routes.d.ts +54 -0
  91. package/dist/integrations/jira/routes.d.ts.map +1 -0
  92. package/dist/integrations/jira/routes.js +260 -0
  93. package/dist/integrations/jira/routes.js.map +1 -0
  94. package/dist/integrations/jira/rules.d.ts +57 -0
  95. package/dist/integrations/jira/rules.d.ts.map +1 -0
  96. package/dist/integrations/jira/rules.js +175 -0
  97. package/dist/integrations/jira/rules.js.map +1 -0
  98. package/dist/integrations/linear/claim.d.ts +8 -0
  99. package/dist/integrations/linear/claim.d.ts.map +1 -0
  100. package/dist/integrations/linear/claim.js +14 -0
  101. package/dist/integrations/linear/claim.js.map +1 -0
  102. package/dist/integrations/linear/default-rules.d.ts +1 -0
  103. package/dist/integrations/linear/default-rules.d.ts.map +1 -1
  104. package/dist/integrations/linear/default-rules.js +2 -0
  105. package/dist/integrations/linear/default-rules.js.map +1 -1
  106. package/dist/integrations/linear/integration.d.ts +24 -6
  107. package/dist/integrations/linear/integration.d.ts.map +1 -1
  108. package/dist/integrations/linear/integration.js +257 -37
  109. package/dist/integrations/linear/integration.js.map +1 -1
  110. package/dist/integrations/linear/routes.d.ts +10 -0
  111. package/dist/integrations/linear/routes.d.ts.map +1 -1
  112. package/dist/integrations/linear/routes.js +74 -12
  113. package/dist/integrations/linear/routes.js.map +1 -1
  114. package/dist/integrations/linear/rules.d.ts +1 -1
  115. package/dist/integrations/linear/rules.d.ts.map +1 -1
  116. package/dist/integrations/linear/rules.js +27 -2
  117. package/dist/integrations/linear/rules.js.map +1 -1
  118. package/dist/integrations/platform/connect/routes.d.ts +39 -0
  119. package/dist/integrations/platform/connect/routes.d.ts.map +1 -0
  120. package/dist/integrations/platform/connect/routes.js +129 -0
  121. package/dist/integrations/platform/connect/routes.js.map +1 -0
  122. package/dist/integrations/platform/github/event-worker.js +1 -1
  123. package/dist/integrations/platform/github/integration.js +1 -1
  124. package/dist/integrations/platform/incidentio/integration.d.ts +61 -4
  125. package/dist/integrations/platform/incidentio/integration.d.ts.map +1 -1
  126. package/dist/integrations/platform/incidentio/integration.js +371 -22
  127. package/dist/integrations/platform/incidentio/integration.js.map +1 -1
  128. package/dist/integrations/platform/jira/agent-tools.d.ts +36 -0
  129. package/dist/integrations/platform/jira/agent-tools.d.ts.map +1 -0
  130. package/dist/integrations/platform/jira/agent-tools.js +85 -0
  131. package/dist/integrations/platform/jira/agent-tools.js.map +1 -0
  132. package/dist/integrations/platform/jira/integration.d.ts +65 -0
  133. package/dist/integrations/platform/jira/integration.d.ts.map +1 -0
  134. package/dist/integrations/platform/jira/integration.js +540 -0
  135. package/dist/integrations/platform/jira/integration.js.map +1 -0
  136. package/dist/integrations/platform/jira/routes.d.ts +55 -0
  137. package/dist/integrations/platform/jira/routes.d.ts.map +1 -0
  138. package/dist/integrations/platform/jira/routes.js +274 -0
  139. package/dist/integrations/platform/jira/routes.js.map +1 -0
  140. package/dist/integrations/platform/linear/integration.d.ts +34 -1
  141. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  142. package/dist/integrations/platform/linear/integration.js +271 -48
  143. package/dist/integrations/platform/linear/integration.js.map +1 -1
  144. package/dist/routes/intake.d.ts.map +1 -1
  145. package/dist/routes/intake.js +15 -5
  146. package/dist/routes/intake.js.map +1 -1
  147. package/dist/routes/oauth.js +2 -5
  148. package/dist/routes/oauth.js.map +1 -1
  149. package/dist/routes/surface.d.ts.map +1 -1
  150. package/dist/routes/surface.js +24 -4
  151. package/dist/routes/surface.js.map +1 -1
  152. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  153. package/dist/rules/dispatch-errors.js +8 -0
  154. package/dist/rules/dispatch-errors.js.map +1 -1
  155. package/dist/rules/dispatcher.d.ts +1 -0
  156. package/dist/rules/dispatcher.d.ts.map +1 -1
  157. package/dist/rules/dispatcher.js +180 -106
  158. package/dist/rules/dispatcher.js.map +1 -1
  159. package/dist/rules/index.d.ts +2 -2
  160. package/dist/rules/index.d.ts.map +1 -1
  161. package/dist/rules/index.js +2 -2
  162. package/dist/rules/start-coordinator.d.ts.map +1 -1
  163. package/dist/rules/start-coordinator.js +4 -1
  164. package/dist/rules/start-coordinator.js.map +1 -1
  165. package/dist/rules/terminal-cleanup.d.ts +14 -1
  166. package/dist/rules/terminal-cleanup.d.ts.map +1 -1
  167. package/dist/rules/terminal-cleanup.js +20 -0
  168. package/dist/rules/terminal-cleanup.js.map +1 -1
  169. package/dist/rules/tools.d.ts +2 -0
  170. package/dist/rules/tools.d.ts.map +1 -1
  171. package/dist/rules/tools.js +30 -2
  172. package/dist/rules/tools.js.map +1 -1
  173. package/dist/rules/transition-service.d.ts +13 -0
  174. package/dist/rules/transition-service.d.ts.map +1 -1
  175. package/dist/rules/transition-service.js +11 -2
  176. package/dist/rules/transition-service.js.map +1 -1
  177. package/dist/rules/types.d.ts +72 -3
  178. package/dist/rules/types.d.ts.map +1 -1
  179. package/dist/rules/types.js +9 -1
  180. package/dist/rules/types.js.map +1 -1
  181. package/dist/rules/validation.d.ts.map +1 -1
  182. package/dist/rules/validation.js +13 -2
  183. package/dist/rules/validation.js.map +1 -1
  184. package/dist/session/run-audit.d.ts +9 -0
  185. package/dist/session/run-audit.d.ts.map +1 -1
  186. package/dist/session/run-audit.js +37 -6
  187. package/dist/session/run-audit.js.map +1 -1
  188. package/dist/skills/service.d.ts +12 -0
  189. package/dist/skills/service.d.ts.map +1 -1
  190. package/dist/skills/service.js +27 -1
  191. package/dist/skills/service.js.map +1 -1
  192. package/dist/storage/domains/intake/base.d.ts +11 -8
  193. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  194. package/dist/storage/domains/intake/base.js +69 -24
  195. package/dist/storage/domains/intake/base.js.map +1 -1
  196. package/dist/storage/domains/work-items/base.d.ts +41 -2
  197. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  198. package/dist/storage/domains/work-items/base.js +159 -12
  199. package/dist/storage/domains/work-items/base.js.map +1 -1
  200. package/dist/supervisor/session-messaging.d.ts +23 -0
  201. package/dist/supervisor/session-messaging.d.ts.map +1 -0
  202. package/dist/supervisor/session-messaging.js +15 -0
  203. package/dist/supervisor/session-messaging.js.map +1 -0
  204. package/dist/supervisor/write-tools.d.ts +2 -1
  205. package/dist/supervisor/write-tools.d.ts.map +1 -1
  206. package/dist/supervisor/write-tools.js +11 -7
  207. package/dist/supervisor/write-tools.js.map +1 -1
  208. package/dist/work-item-branch.d.ts +1 -1
  209. package/dist/work-item-branch.d.ts.map +1 -1
  210. package/dist/work-item-branch.js +4 -2
  211. package/dist/work-item-branch.js.map +1 -1
  212. package/package.json +11 -11
@@ -7,6 +7,7 @@ import { buildLinearRoutes } from "../../linear/routes.js";
7
7
  import { buildLinearAgentTools } from "../../linear/agent-tools.js";
8
8
  import { PlatformLinearEventWorker } from "./event-worker.js";
9
9
  import { registerApiRoute } from "@mastra/core/server";
10
+ import { createHash } from "crypto";
10
11
  //#region src/integrations/platform/linear/integration.ts
11
12
  const API_PREFIX = "/v1/server/linear";
12
13
  const PAGE_SIZE = 30;
@@ -34,7 +35,8 @@ var PlatformLinearIntegration = class {
34
35
  intake = {
35
36
  resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
36
37
  listSources: async () => {
37
- return (await this.#listProjectSources()).map(({ workspace, project }) => ({
38
+ const [projectSources, teamSources] = await Promise.all([this.#listProjectSources(), this.#listTeamSources()]);
39
+ const projects = projectSources.map(({ workspace, project }) => ({
38
40
  id: encodeSourceId(workspace.linearWorkspaceId, project.id),
39
41
  name: project.name,
40
42
  type: "project",
@@ -46,17 +48,29 @@ var PlatformLinearIntegration = class {
46
48
  teams: project.teams
47
49
  }
48
50
  }));
51
+ const teams = teamSources.map(({ workspace, team }) => ({
52
+ id: encodeTeamSourceId(workspace.linearWorkspaceId, team.id),
53
+ name: team.name,
54
+ type: "team",
55
+ metadata: {
56
+ workspaceId: workspace.linearWorkspaceId,
57
+ workspaceName: workspace.linearWorkspaceName,
58
+ workspaceUrlKey: workspace.urlKey,
59
+ teamKey: team.key
60
+ }
61
+ }));
62
+ return [...projects, ...teams];
49
63
  },
50
- listItems: async ({ sourceIds, cursor }) => {
51
- const result = await this.#listIssues(sourceIds, cursor);
64
+ listItems: async ({ sourceIds, attributionSourceIds, cursor }) => {
65
+ const result = await this.#listIssues(sourceIds, cursor, void 0, attributionSourceIds);
52
66
  return {
53
- items: result.issues.map(({ issue, source }) => ({
67
+ items: result.issues.map(({ issue, sourceId, workspace }) => ({
54
68
  source: {
55
69
  type: "issue",
56
70
  externalId: issue.id,
57
71
  url: issue.url
58
72
  },
59
- sourceId: encodeSourceId(source.workspace.linearWorkspaceId, source.project.id),
73
+ sourceId,
60
74
  title: issue.title,
61
75
  status: issue.state.name,
62
76
  labels: issue.labels.map((label) => label.name),
@@ -65,10 +79,9 @@ var PlatformLinearIntegration = class {
65
79
  updatedAt: issue.updatedAt,
66
80
  metadata: {
67
81
  identifier: issue.identifier,
68
- workspaceId: source.workspace.linearWorkspaceId,
69
- workspaceName: source.workspace.linearWorkspaceName,
70
- projectId: source.project.id,
71
- projectName: source.project.name,
82
+ workspaceId: workspace.linearWorkspaceId,
83
+ workspaceName: workspace.linearWorkspaceName,
84
+ projectId: issue.project?.id ?? null,
72
85
  team: issue.team.key,
73
86
  priority: issue.priorityLabel
74
87
  }
@@ -76,13 +89,13 @@ var PlatformLinearIntegration = class {
76
89
  nextCursor: result.nextCursor
77
90
  };
78
91
  },
79
- listIssues: async ({ connection, sourceIds, labels, cursor }) => {
92
+ listIssues: async ({ connection, sourceIds, attributionSourceIds, labels, cursor }) => {
80
93
  requireLinearConnection(connection);
81
- const result = await this.#listIssues(sourceIds, cursor, labels);
94
+ const result = await this.#listIssues(sourceIds, cursor, labels, attributionSourceIds);
82
95
  return {
83
- issues: result.issues.map(({ issue, source }) => ({
96
+ issues: result.issues.map(({ issue, sourceId }) => ({
84
97
  ...parseIssue(issue),
85
- sourceId: encodeSourceId(source.workspace.linearWorkspaceId, source.project.id)
98
+ sourceId
86
99
  })),
87
100
  nextCursor: result.nextCursor
88
101
  };
@@ -285,7 +298,9 @@ var PlatformLinearIntegration = class {
285
298
  baseUrl: ctx.baseUrl,
286
299
  intake: ctx.storage.intake,
287
300
  projects: ctx.storage.projects,
288
- ingestFactoryIssues: attachLinearRules(this, ctx)
301
+ ingestFactoryIssues: attachLinearRules(this, ctx),
302
+ workItems: ctx.runtime?.workItems,
303
+ boards: ctx.runtime?.boards
289
304
  }).filter((route) => !route.path.startsWith("/auth/linear/"))];
290
305
  }
291
306
  #connectRoute(ctx) {
@@ -331,9 +346,52 @@ var PlatformLinearIntegration = class {
331
346
  workspaceId: workspace.linearWorkspaceId
332
347
  }));
333
348
  }
334
- async #listProjectSources() {
349
+ async listTeams(_accessToken) {
350
+ return (await this.#listTeamSources()).map(({ workspace, team }) => ({
351
+ ...team,
352
+ workspaceId: workspace.linearWorkspaceId,
353
+ sourceId: encodeTeamSourceId(workspace.linearWorkspaceId, team.id)
354
+ }));
355
+ }
356
+ async fetchIssueDetail(_accessToken, idOrIdentifier, sourceIds, routedSourceIds) {
357
+ const located = await this.#findIssue(void 0, idOrIdentifier, sourceIds, routedSourceIds);
358
+ if (!located) return null;
359
+ const comments = await this.#loadComments(located.workspaceId, located.issue.id, located.issue.comments);
360
+ const issue = located.issue;
361
+ return {
362
+ id: issue.id,
363
+ projectId: issue.project?.id ?? null,
364
+ workspaceId: located.workspaceId,
365
+ teamId: issue.team.id,
366
+ identifier: issue.identifier,
367
+ title: issue.title,
368
+ description: issue.description?.trim() ? issue.description : null,
369
+ url: issue.url,
370
+ state: issue.state.name,
371
+ stateType: issue.state.type,
372
+ priorityLabel: issue.priorityLabel,
373
+ assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,
374
+ creator: issue.creator?.displayName ?? issue.creator?.name ?? null,
375
+ team: issue.team.key,
376
+ labels: issue.labels.map((label) => label.name),
377
+ createdAt: issue.createdAt,
378
+ updatedAt: issue.updatedAt,
379
+ comments: comments.map((comment) => ({
380
+ author: comment.user?.displayName ?? comment.user?.name ?? null,
381
+ body: comment.body,
382
+ createdAt: comment.createdAt
383
+ }))
384
+ };
385
+ }
386
+ sourceMatchesIssue(sourceId, issue) {
387
+ const source = parseSourceId(sourceId);
388
+ if (issue.workspaceId !== source.workspaceId) return false;
389
+ return source.kind === "team" ? issue.teamId === source.teamId : issue.projectId === source.projectId;
390
+ }
391
+ async #listProjectSources(workspaceIds) {
335
392
  const workspaces = await this.#listWorkspaces();
336
- return (await Promise.all(workspaces.map(async (workspace) => {
393
+ const scopedWorkspaces = workspaceIds ? workspaces.filter((workspace) => workspaceIds.has(workspace.linearWorkspaceId)) : workspaces;
394
+ return (await Promise.all(scopedWorkspaces.map(async (workspace) => {
337
395
  const projects = [];
338
396
  let after;
339
397
  for (let page = 0; page < MAX_REFERENCE_PAGES; page += 1) {
@@ -342,6 +400,7 @@ var PlatformLinearIntegration = class {
342
400
  const result = await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/projects?${query}`);
343
401
  projects.push(...result.projects);
344
402
  if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor) break;
403
+ if (result.pageInfo.endCursor === after) throw invalidLinearCursor();
345
404
  after = result.pageInfo.endCursor;
346
405
  }
347
406
  return projects.map((project) => ({
@@ -350,57 +409,133 @@ var PlatformLinearIntegration = class {
350
409
  }));
351
410
  }))).flat();
352
411
  }
412
+ async #listTeamSources(workspaceIds) {
413
+ const workspaces = await this.#listWorkspaces();
414
+ const scopedWorkspaces = workspaceIds ? workspaces.filter((workspace) => workspaceIds.has(workspace.linearWorkspaceId)) : workspaces;
415
+ return (await Promise.all(scopedWorkspaces.map(async (workspace) => {
416
+ const teams = [];
417
+ let after;
418
+ for (let page = 0; page < MAX_REFERENCE_PAGES; page += 1) {
419
+ const query = new URLSearchParams({ first: "200" });
420
+ if (after) query.set("after", after);
421
+ const result = await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/teams?${query}`);
422
+ teams.push(...result.teams);
423
+ if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor) break;
424
+ if (result.pageInfo.endCursor === after) throw invalidLinearCursor();
425
+ after = result.pageInfo.endCursor;
426
+ }
427
+ return teams.map((team) => ({
428
+ workspace,
429
+ team
430
+ }));
431
+ }))).flat();
432
+ }
353
433
  async listWorkspaces() {
354
434
  return this.#listWorkspaces();
355
435
  }
356
436
  async #listWorkspaces() {
357
437
  return (await this.#client.request("GET", `${API_PREFIX}/workspaces`)).workspaces.filter((workspace) => workspace.connected);
358
438
  }
359
- async #listIssues(sourceIds, cursor, labels) {
439
+ async #listIssues(sourceIds, cursor, labels, attributionSourceIds = sourceIds) {
360
440
  if (sourceIds.length === 0) return {
361
441
  issues: [],
362
442
  nextCursor: null
363
443
  };
364
- const sources = await this.#listProjectSources();
365
- const sourceMap = new Map(sources.map((source) => [encodeSourceId(source.workspace.linearWorkspaceId, source.project.id), source]));
366
- const selected = sourceIds.map((sourceId) => sourceMap.get(sourceId)).filter((source) => !!source);
367
- const cursors = decodeCursor(cursor, sourceIds);
444
+ const parsedSources = sourceIds.map((sourceId) => parseSourceId(sourceId));
445
+ const projectWorkspaceIds = new Set(parsedSources.flatMap((source) => source.kind === "project" ? [source.workspaceId] : []));
446
+ const teamWorkspaceIds = new Set(parsedSources.flatMap((source) => source.kind === "team" ? [source.workspaceId] : []));
447
+ const [projectSources, teamSources] = await Promise.all([projectWorkspaceIds.size > 0 ? this.#listProjectSources(projectWorkspaceIds) : Promise.resolve([]), teamWorkspaceIds.size > 0 ? this.#listTeamSources(teamWorkspaceIds) : Promise.resolve([])]);
448
+ const projectMap = new Map(projectSources.map((source) => [encodeSourceId(source.workspace.linearWorkspaceId, source.project.id), source]));
449
+ const teamMap = new Map(teamSources.map((source) => [encodeTeamSourceId(source.workspace.linearWorkspaceId, source.team.id), source]));
450
+ const descriptors = [];
451
+ const selectedProjectIdsByWorkspace = /* @__PURE__ */ new Map();
452
+ for (const attributionSourceId of attributionSourceIds) {
453
+ const source = parseSourceId(attributionSourceId);
454
+ if (source.kind !== "project") continue;
455
+ const selectedProjectIds = selectedProjectIdsByWorkspace.get(source.workspaceId) ?? /* @__PURE__ */ new Set();
456
+ selectedProjectIds.add(source.projectId);
457
+ selectedProjectIdsByWorkspace.set(source.workspaceId, selectedProjectIds);
458
+ }
459
+ for (const sourceId of sourceIds) {
460
+ const projectSource = projectMap.get(sourceId);
461
+ if (projectSource) {
462
+ const query = new URLSearchParams({
463
+ first: String(PAGE_SIZE),
464
+ projectIds: projectSource.project.id,
465
+ stateType: "triage,backlog,unstarted,started",
466
+ orderBy: "updatedAt"
467
+ });
468
+ const workspaceId = projectSource.workspace.linearWorkspaceId;
469
+ const selectedProjectIds = selectedProjectIdsByWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Set();
470
+ selectedProjectIds.add(projectSource.project.id);
471
+ selectedProjectIdsByWorkspace.set(workspaceId, selectedProjectIds);
472
+ descriptors.push({
473
+ kind: "project",
474
+ sourceId,
475
+ workspace: projectSource.workspace,
476
+ query
477
+ });
478
+ continue;
479
+ }
480
+ const teamSource = teamMap.get(sourceId);
481
+ if (teamSource) {
482
+ const query = new URLSearchParams({
483
+ first: String(PAGE_SIZE),
484
+ teamId: teamSource.team.id,
485
+ stateType: "triage,backlog,unstarted,started",
486
+ orderBy: "updatedAt"
487
+ });
488
+ descriptors.push({
489
+ kind: "team",
490
+ sourceId,
491
+ workspace: teamSource.workspace,
492
+ query
493
+ });
494
+ }
495
+ }
496
+ const cursors = decodeCursor(cursor, sourceIds, attributionSourceIds);
368
497
  const normalizedLabels = normalizeLabels(labels);
369
498
  const nextState = {};
370
499
  let hasNextPage = false;
371
500
  return {
372
- issues: (await Promise.all(selected.map(async (source) => {
373
- const sourceId = encodeSourceId(source.workspace.linearWorkspaceId, source.project.id);
501
+ issues: dedupeIssuesBySource((await Promise.all(descriptors.map(async ({ kind, sourceId, workspace, query }) => {
374
502
  if (cursors[sourceId] === null) {
375
503
  nextState[sourceId] = null;
376
504
  return [];
377
505
  }
378
- const query = new URLSearchParams({
379
- first: String(PAGE_SIZE),
380
- projectIds: source.project.id,
381
- stateType: "triage,backlog,unstarted,started",
382
- orderBy: "updatedAt"
383
- });
384
506
  const after = cursors[sourceId];
385
507
  if (after) query.set("after", after);
386
- const result = await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(source.workspace.linearWorkspaceId)}/issues?${query}`);
508
+ const result = await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/issues?${query}`);
387
509
  const next = result.pageInfo.hasNextPage ? result.pageInfo.endCursor : null;
510
+ if (next !== null && next === after) throw invalidLinearCursor();
388
511
  nextState[sourceId] = next;
389
512
  hasNextPage ||= next !== null;
390
- return result.issues.filter((issue) => normalizedLabels.length === 0 || issue.labels.some((label) => normalizedLabels.includes(label.name))).map((issue) => ({
513
+ return result.issues.filter((issue) => normalizedLabels.length === 0 || issue.labels.some((label) => normalizedLabels.includes(label.name))).filter((issue) => kind === "project" || issue.project === null || !selectedProjectIdsByWorkspace.get(workspace.linearWorkspaceId)?.has(issue.project.id)).map((issue) => ({
391
514
  issue,
392
- source
515
+ sourceId,
516
+ workspace
393
517
  }));
394
- }))).flat(),
395
- nextCursor: hasNextPage ? encodeCursor(nextState, sourceIds) : null
518
+ }))).flat(), sourceIds),
519
+ nextCursor: hasNextPage ? encodeCursor(nextState, sourceIds, attributionSourceIds) : null
396
520
  };
397
521
  }
398
- async #findIssue(sourceId, issueId) {
399
- const workspaceIds = await this.#candidateWorkspaceIds(sourceId);
522
+ async #findIssue(sourceId, issueId, sourceIds, routedSourceIds) {
523
+ const scopedSources = (sourceIds ?? (sourceId ? [sourceId] : void 0))?.map((sourceKey) => ({
524
+ sourceKey,
525
+ source: parseSourceId(sourceKey)
526
+ }));
527
+ const routedSourceSet = routedSourceIds ? new Set(routedSourceIds) : void 0;
528
+ const workspaceIds = routedSourceIds ? [...new Set(routedSourceIds.map((sourceKey) => parseSourceId(sourceKey).workspaceId))] : scopedSources ? [...new Set(scopedSources.map(({ source }) => source.workspaceId))] : await this.#candidateWorkspaceIds(void 0);
400
529
  for (const workspaceId of workspaceIds) try {
530
+ const issue = await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}?include=comments`);
531
+ if (scopedSources) {
532
+ const matching = scopedSources.filter(({ source }) => source.workspaceId === workspaceId && (source.kind === "team" ? issue.team.id === source.teamId : issue.project?.id === source.projectId));
533
+ const winner = matching.find(({ source }) => source.kind === "project") ?? matching[0];
534
+ if (!winner || routedSourceSet && !routedSourceSet.has(winner.sourceKey)) continue;
535
+ }
401
536
  return {
402
537
  workspaceId,
403
- issue: await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}?include=comments`)
538
+ issue
404
539
  };
405
540
  } catch (error) {
406
541
  if (!isNotFound(error)) throw error;
@@ -427,7 +562,7 @@ var PlatformLinearIntegration = class {
427
562
  return result.workflowStates;
428
563
  }
429
564
  async #candidateWorkspaceIds(sourceId) {
430
- if (sourceId) return [decodeSourceId(sourceId).workspaceId];
565
+ if (sourceId) return [parseSourceId(sourceId).workspaceId];
431
566
  return (await this.#listWorkspaces()).map((workspace) => workspace.linearWorkspaceId);
432
567
  }
433
568
  async #loadComments(workspaceId, issueId, embedded) {
@@ -437,6 +572,7 @@ var PlatformLinearIntegration = class {
437
572
  while (pageInfo?.hasNextPage && pageInfo.endCursor && page < MAX_COMMENT_PAGES) {
438
573
  const result = await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments?first=200&after=${encodeURIComponent(pageInfo.endCursor)}`);
439
574
  comments.push(...result.comments);
575
+ if (result.pageInfo.hasNextPage && result.pageInfo.endCursor === pageInfo.endCursor) throw invalidLinearCursor();
440
576
  pageInfo = result.pageInfo;
441
577
  page += 1;
442
578
  }
@@ -473,6 +609,43 @@ function parseIssueDetail(issue, comments) {
473
609
  }))
474
610
  };
475
611
  }
612
+ /**
613
+ * Deduplicate issues that surfaced from more than one selected source (a team
614
+ * source and one of its projects both select the same issue). Keeps one entry
615
+ * per Linear issue UUID.
616
+ *
617
+ * Tie-breaker — MOST-SPECIFIC-WINS: a project source beats a team source, so an
618
+ * issue that belongs to a selected project is attributed to that project's
619
+ * source (and therefore routes to the project's board). The team source covers
620
+ * only the remainder — projectless issues and issues in projects that were not
621
+ * separately selected. `selectionOrder` breaks a same-kind tie deterministically
622
+ * by the order the sources were selected.
623
+ *
624
+ * This function is the single precedence point; change the comparison here to
625
+ * change the routing policy.
626
+ */
627
+ function dedupeIssuesBySource(entries, selectionOrder) {
628
+ const rank = new Map(selectionOrder.map((id, index) => [id, index]));
629
+ const specificity = (sourceId) => parseSourceId(sourceId).kind === "project" ? 0 : 1;
630
+ const winners = /* @__PURE__ */ new Map();
631
+ for (const entry of entries) {
632
+ const existing = winners.get(entry.issue.id);
633
+ if (!existing) {
634
+ winners.set(entry.issue.id, entry);
635
+ continue;
636
+ }
637
+ const bySpecificity = specificity(entry.sourceId) - specificity(existing.sourceId);
638
+ if (bySpecificity < 0 || bySpecificity === 0 && (rank.get(entry.sourceId) ?? Number.MAX_SAFE_INTEGER) < (rank.get(existing.sourceId) ?? Number.MAX_SAFE_INTEGER)) winners.set(entry.issue.id, entry);
639
+ }
640
+ const seen = /* @__PURE__ */ new Set();
641
+ const ordered = [];
642
+ for (const entry of entries) {
643
+ if (seen.has(entry.issue.id)) continue;
644
+ seen.add(entry.issue.id);
645
+ ordered.push(winners.get(entry.issue.id));
646
+ }
647
+ return ordered;
648
+ }
476
649
  function encodeSourceId(workspaceId, projectId) {
477
650
  return `linear-project:${Buffer.from(JSON.stringify({
478
651
  workspaceId,
@@ -493,23 +666,73 @@ function decodeSourceId(sourceId) {
493
666
  throw new Error("Linear project source id is invalid.");
494
667
  }
495
668
  }
669
+ const TEAM_SOURCE_PREFIX = "linear-team:";
670
+ function encodeTeamSourceId(workspaceId, teamId) {
671
+ return `${TEAM_SOURCE_PREFIX}${Buffer.from(JSON.stringify({
672
+ workspaceId,
673
+ teamId
674
+ })).toString("base64url")}`;
675
+ }
676
+ function decodeTeamSourceId(sourceId) {
677
+ if (!sourceId.startsWith(TEAM_SOURCE_PREFIX)) throw new Error("Linear team source id is invalid.");
678
+ try {
679
+ const parsed = JSON.parse(Buffer.from(sourceId.slice(12), "base64url").toString("utf8"));
680
+ if (typeof parsed.workspaceId !== "string" || !parsed.workspaceId) throw new Error();
681
+ if (typeof parsed.teamId !== "string" || !parsed.teamId) throw new Error();
682
+ return {
683
+ workspaceId: parsed.workspaceId,
684
+ teamId: parsed.teamId
685
+ };
686
+ } catch {
687
+ throw new Error("Linear team source id is invalid.");
688
+ }
689
+ }
690
+ /** Discriminate any Linear source id by its prefix and decode it. */
691
+ function parseSourceId(sourceId) {
692
+ if (sourceId.startsWith(TEAM_SOURCE_PREFIX)) return {
693
+ kind: "team",
694
+ ...decodeTeamSourceId(sourceId)
695
+ };
696
+ return {
697
+ kind: "project",
698
+ ...decodeSourceId(sourceId)
699
+ };
700
+ }
496
701
  function normalizeLabels(labels) {
497
702
  return [...new Set((labels ?? []).map((label) => label.trim()).filter(Boolean))];
498
703
  }
499
- function decodeCursor(cursor, sourceIds) {
704
+ function canonicalSourceIds(sourceIds) {
705
+ return [...new Set(sourceIds)].sort();
706
+ }
707
+ function linearSourceSetFingerprint(sourceIds, attributionSourceIds) {
708
+ const scope = {
709
+ sourceIds: canonicalSourceIds(sourceIds),
710
+ attributionSourceIds: canonicalSourceIds(attributionSourceIds)
711
+ };
712
+ return createHash("sha256").update(JSON.stringify(scope)).digest("base64url");
713
+ }
714
+ function invalidLinearCursor() {
715
+ return Object.assign(/* @__PURE__ */ new Error("Linear cursor is invalid or stale."), { code: "invalid_cursor" });
716
+ }
717
+ function decodeCursor(cursor, sourceIds, attributionSourceIds) {
500
718
  if (!cursor) return {};
501
- if (sourceIds.length === 1) return { [sourceIds[0]]: cursor };
502
719
  try {
503
- const parsed = JSON.parse(cursor);
504
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
505
- return parsed;
720
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
721
+ const canonical = canonicalSourceIds(sourceIds);
722
+ if (parsed.v !== 1 || parsed.sourceSet !== linearSourceSetFingerprint(sourceIds, attributionSourceIds) || !Array.isArray(parsed.cursors) || parsed.cursors.length !== canonical.length || !parsed.cursors.every((value) => value === null || typeof value === "string")) throw invalidLinearCursor();
723
+ return Object.fromEntries(canonical.map((sourceId, index) => [sourceId, parsed.cursors[index]]));
506
724
  } catch {
507
- throw new Error("Linear cursor is invalid.");
725
+ throw invalidLinearCursor();
508
726
  }
509
727
  }
510
- function encodeCursor(state, sourceIds) {
511
- if (sourceIds.length === 1) return state[sourceIds[0]];
512
- return JSON.stringify(state);
728
+ function encodeCursor(state, sourceIds, attributionSourceIds) {
729
+ const canonical = canonicalSourceIds(sourceIds);
730
+ const cursor = {
731
+ v: 1,
732
+ sourceSet: linearSourceSetFingerprint(sourceIds, attributionSourceIds),
733
+ cursors: canonical.map((sourceId) => state[sourceId] ?? null)
734
+ };
735
+ return Buffer.from(JSON.stringify(cursor)).toString("base64url");
513
736
  }
514
737
  function requireLinearConnection(connection) {
515
738
  if (connection.type !== "oauth") throw new Error("Linear capabilities require an OAuth connection.");
@@ -525,6 +748,6 @@ function optionalPositiveIntegerEnv(name) {
525
748
  return parsed;
526
749
  }
527
750
  //#endregion
528
- export { PlatformLinearIntegration };
751
+ export { PlatformLinearIntegration, decodeTeamSourceId, encodeSourceId, encodeTeamSourceId, parseSourceId };
529
752
 
530
753
  //# sourceMappingURL=integration.js.map