@mastra/factory 0.2.2-alpha.1 → 0.2.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/auth.js +401 -334
  3. package/dist/auth.js.map +1 -1
  4. package/dist/capabilities/connection.js +1 -1
  5. package/dist/capabilities/intake.js +1 -1
  6. package/dist/capabilities/version-control.js +1 -1
  7. package/dist/factory.js +441 -39622
  8. package/dist/factory.js.map +1 -1
  9. package/dist/index.js +2 -39633
  10. package/dist/integrations/base.js +1 -1
  11. package/dist/integrations/github/config.js +36 -22
  12. package/dist/integrations/github/config.js.map +1 -1
  13. package/dist/integrations/github/factory-session.js +33 -37
  14. package/dist/integrations/github/factory-session.js.map +1 -1
  15. package/dist/integrations/github/integration.js +1037 -3672
  16. package/dist/integrations/github/integration.js.map +1 -1
  17. package/dist/integrations/github/issue-triage.js +71 -71
  18. package/dist/integrations/github/issue-triage.js.map +1 -1
  19. package/dist/integrations/github/pat.js +47 -36
  20. package/dist/integrations/github/pat.js.map +1 -1
  21. package/dist/integrations/github/provenance.js +72 -101
  22. package/dist/integrations/github/provenance.js.map +1 -1
  23. package/dist/integrations/github/routes.js +1272 -1881
  24. package/dist/integrations/github/routes.js.map +1 -1
  25. package/dist/integrations/github/rules.js +203 -471
  26. package/dist/integrations/github/rules.js.map +1 -1
  27. package/dist/integrations/github/sandbox.js +400 -345
  28. package/dist/integrations/github/sandbox.js.map +1 -1
  29. package/dist/integrations/github/session-subscriptions.js +165 -223
  30. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  31. package/dist/integrations/github/subscriptions.js +43 -51
  32. package/dist/integrations/github/subscriptions.js.map +1 -1
  33. package/dist/integrations/github/token-refresh.js +15 -18
  34. package/dist/integrations/github/token-refresh.js.map +1 -1
  35. package/dist/integrations/github/webhook.js +359 -343
  36. package/dist/integrations/github/webhook.js.map +1 -1
  37. package/dist/integrations/linear/agent-tools.js +77 -100
  38. package/dist/integrations/linear/agent-tools.js.map +1 -1
  39. package/dist/integrations/linear/integration.js +611 -1309
  40. package/dist/integrations/linear/integration.js.map +1 -1
  41. package/dist/integrations/linear/routes.js +227 -230
  42. package/dist/integrations/linear/routes.js.map +1 -1
  43. package/dist/integrations/linear/rules.js +126 -346
  44. package/dist/integrations/linear/rules.js.map +1 -1
  45. package/dist/integrations/linear/storage.js +1 -1
  46. package/dist/integrations/platform/api-client.js +131 -135
  47. package/dist/integrations/platform/api-client.js.map +1 -1
  48. package/dist/integrations/platform/github/event-worker.js +275 -573
  49. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  50. package/dist/integrations/platform/github/integration.js +845 -4022
  51. package/dist/integrations/platform/github/integration.js.map +1 -1
  52. package/dist/integrations/platform/linear/integration.js +457 -1309
  53. package/dist/integrations/platform/linear/integration.js.map +1 -1
  54. package/dist/integrations/workos/integration.js +80 -85
  55. package/dist/integrations/workos/integration.js.map +1 -1
  56. package/dist/routes/config.d.ts.map +1 -1
  57. package/dist/routes/config.js +933 -912
  58. package/dist/routes/config.js.map +1 -1
  59. package/dist/routes/custom-provider-source.js +110 -93
  60. package/dist/routes/custom-provider-source.js.map +1 -1
  61. package/dist/routes/fs.js +401 -349
  62. package/dist/routes/fs.js.map +1 -1
  63. package/dist/routes/intake.js +165 -165
  64. package/dist/routes/intake.js.map +1 -1
  65. package/dist/routes/oauth.js +447 -25166
  66. package/dist/routes/oauth.js.map +1 -1
  67. package/dist/routes/projects.js +437 -376
  68. package/dist/routes/projects.js.map +1 -1
  69. package/dist/routes/provider-credentials.js +76 -58
  70. package/dist/routes/provider-credentials.js.map +1 -1
  71. package/dist/routes/route.js +12 -8
  72. package/dist/routes/route.js.map +1 -1
  73. package/dist/routes/skills.js +160 -173
  74. package/dist/routes/skills.js.map +1 -1
  75. package/dist/routes/surface.js +250 -28615
  76. package/dist/routes/surface.js.map +1 -1
  77. package/dist/routes/tenant-credentials.js +141 -166
  78. package/dist/routes/tenant-credentials.js.map +1 -1
  79. package/dist/routes/work-items.js +654 -862
  80. package/dist/routes/work-items.js.map +1 -1
  81. package/dist/rules/binding-context.js +23 -35
  82. package/dist/rules/binding-context.js.map +1 -1
  83. package/dist/rules/defaults.js +200 -309
  84. package/dist/rules/defaults.js.map +1 -1
  85. package/dist/rules/dispatcher.js +438 -683
  86. package/dist/rules/dispatcher.js.map +1 -1
  87. package/dist/rules/index.js +5 -599
  88. package/dist/rules/processor.js +344 -596
  89. package/dist/rules/processor.js.map +1 -1
  90. package/dist/rules/resolve.js +21 -18
  91. package/dist/rules/resolve.js.map +1 -1
  92. package/dist/rules/start-coordinator.js +168 -157
  93. package/dist/rules/start-coordinator.js.map +1 -1
  94. package/dist/rules/tools.js +50 -85
  95. package/dist/rules/tools.js.map +1 -1
  96. package/dist/rules/transition-service.js +183 -454
  97. package/dist/rules/transition-service.js.map +1 -1
  98. package/dist/rules/types.js +33 -29
  99. package/dist/rules/types.js.map +1 -1
  100. package/dist/rules/validation.js +266 -281
  101. package/dist/rules/validation.js.map +1 -1
  102. package/dist/sandbox/fleet.js +272 -250
  103. package/dist/sandbox/fleet.js.map +1 -1
  104. package/dist/sandbox/reattach.js +13 -6
  105. package/dist/sandbox/reattach.js.map +1 -1
  106. package/dist/server-error.js +12 -16
  107. package/dist/server-error.js.map +1 -1
  108. package/dist/skills/service.js +30 -35
  109. package/dist/skills/service.js.map +1 -1
  110. package/dist/spa-static.js +91 -65
  111. package/dist/spa-static.js.map +1 -1
  112. package/dist/state-signing.js +73 -44
  113. package/dist/state-signing.js.map +1 -1
  114. package/dist/storage/domains/audit/agent-audit.js +60 -59
  115. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  116. package/dist/storage/domains/audit/base.js +169 -112
  117. package/dist/storage/domains/audit/base.js.map +1 -1
  118. package/dist/storage/domains/audit/domain.js +155 -153
  119. package/dist/storage/domains/audit/domain.js.map +1 -1
  120. package/dist/storage/domains/credentials/base.js +259 -243
  121. package/dist/storage/domains/credentials/base.js.map +1 -1
  122. package/dist/storage/domains/custom-providers/base.js +138 -143
  123. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  124. package/dist/storage/domains/intake/base.js +75 -57
  125. package/dist/storage/domains/intake/base.js.map +1 -1
  126. package/dist/storage/domains/integrations/base.js +310 -245
  127. package/dist/storage/domains/integrations/base.js.map +1 -1
  128. package/dist/storage/domains/memory-settings/base.js +117 -102
  129. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  130. package/dist/storage/domains/model-packs/base.js +89 -88
  131. package/dist/storage/domains/model-packs/base.js.map +1 -1
  132. package/dist/storage/domains/projects/base.js +97 -92
  133. package/dist/storage/domains/projects/base.js.map +1 -1
  134. package/dist/storage/domains/queue-health/base.js +111 -76
  135. package/dist/storage/domains/queue-health/base.js.map +1 -1
  136. package/dist/storage/domains/source-control/base.js +777 -706
  137. package/dist/storage/domains/source-control/base.js.map +1 -1
  138. package/dist/storage/domains/source-control/inmemory.js +322 -348
  139. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  140. package/dist/storage/domains/work-items/base.js +1266 -1148
  141. package/dist/storage/domains/work-items/base.js.map +1 -1
  142. package/dist/storage/domains/work-items/metrics.js +202 -184
  143. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  144. package/dist/timing.js +29 -22
  145. package/dist/timing.js.map +1 -1
  146. package/dist/workspace.js +236 -477
  147. package/dist/workspace.js.map +1 -1
  148. package/package.json +8 -8
  149. package/dist/capabilities/connection.js.map +0 -1
  150. package/dist/capabilities/intake.js.map +0 -1
  151. package/dist/capabilities/version-control.js.map +0 -1
  152. package/dist/index.js.map +0 -1
  153. package/dist/integrations/base.js.map +0 -1
  154. package/dist/integrations/linear/storage.js.map +0 -1
  155. package/dist/rules/index.js.map +0 -1
@@ -1,1335 +1,483 @@
1
- // src/integrations/platform/linear/integration.ts
2
- import { registerApiRoute as registerApiRoute2 } from "@mastra/core/server";
3
-
4
- // src/integrations/linear/agent-tools.ts
5
- import { createTool } from "@mastra/core/tools";
6
- import { z } from "zod";
7
-
8
- // src/integrations/linear/routes.ts
1
+ import { PlatformApiClient, PlatformApiError, logPlatformInfo, logPlatformWarn, platformApiClientConfigFromEnv } from "../api-client.js";
2
+ import { buildLinearRoutes } from "../../linear/routes.js";
3
+ import { attachLinearRules } from "../../linear/rules.js";
4
+ import { buildLinearAgentTools } from "../../linear/agent-tools.js";
9
5
  import { registerApiRoute } from "@mastra/core/server";
10
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
+ //#region src/integrations/platform/linear/integration.ts
7
+ const API_PREFIX = "/v1/server/linear";
8
+ const PAGE_SIZE = 30;
9
+ const MAX_REFERENCE_PAGES = 20;
10
+ const MAX_COMMENT_PAGES = 20;
11
+ const PLATFORM_MANAGED_CONNECTION_TOKEN = "platform-managed";
11
12
  function loose(c) {
12
- return c;
13
- }
14
- async function resolveOrgTenant(c, auth) {
15
- await auth.ensureUser(c);
16
- const tenant = auth.tenant(c);
17
- if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
18
- if (!tenant.orgId) {
19
- return {
20
- response: c.json(
21
- {
22
- error: "organization_required",
23
- message: "Linear intake requires an organization. Personal accounts cannot connect Linear."
24
- },
25
- 403
26
- )
27
- };
28
- }
29
- return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };
30
- }
31
- function parseAfterCursor(raw) {
32
- if (raw === void 0 || raw === "") return void 0;
33
- if (raw.length > 512 || !/^[\w+/=.:-]+$/.test(raw)) return null;
34
- return raw;
35
- }
36
- function linearFetchError(c, err) {
37
- if (err instanceof LinearReauthRequiredError || err.status === 401) {
38
- return c.json({ error: "linear_reauth_required", message: new LinearReauthRequiredError().message }, 409);
39
- }
40
- return c.json({ error: "linear_fetch_failed", message: err instanceof Error ? err.message : String(err) }, 502);
41
- }
42
- function buildLinearRoutes(options) {
43
- const routes = [];
44
- const { linear, auth, stateSigner, intake } = options;
45
- const enabled = Boolean(linear) && auth.enabled();
46
- const diagnostics = () => ({
47
- linearAppConfigured: Boolean(linear),
48
- factoryAuthEnabled: auth.enabled(),
49
- appDbConfigured: true
50
- });
51
- routes.push(
52
- registerApiRoute("/web/linear/status", {
53
- method: "GET",
54
- requiresAuth: false,
55
- handler: async (c) => {
56
- if (!enabled || !linear || !stateSigner) {
57
- return c.json({
58
- enabled: false,
59
- connected: false,
60
- workspace: null,
61
- reason: "missing_config",
62
- diagnostics: diagnostics()
63
- });
64
- }
65
- await auth.ensureUser(loose(c));
66
- const tenant = auth.tenant(loose(c));
67
- if (!tenant) return c.json({ error: "unauthorized", reason: "auth_required" }, 401);
68
- if (!tenant.orgId) {
69
- return c.json({
70
- enabled: true,
71
- organizationRequired: true,
72
- connected: false,
73
- workspace: null,
74
- reason: "organization_required",
75
- diagnostics: diagnostics()
76
- });
77
- }
78
- const connection = await linear.loadConnection(tenant.orgId);
79
- return c.json({
80
- enabled: true,
81
- connected: Boolean(connection),
82
- workspace: connection ? { name: connection.workspaceName, urlKey: connection.workspaceUrlKey } : null,
83
- reason: connection ? "ready" : "not_connected",
84
- diagnostics: diagnostics()
85
- });
86
- }
87
- })
88
- );
89
- if (!enabled || !linear || !stateSigner || !intake) {
90
- return routes;
91
- }
92
- const redirectUri = options.redirectUri ?? `${(options.baseUrl ?? "").replace(/\/$/, "")}/auth/linear/callback`;
93
- routes.push(
94
- registerApiRoute("/auth/linear/connect", {
95
- method: "GET",
96
- requiresAuth: false,
97
- handler: async (c) => {
98
- const resolved = await resolveOrgTenant(loose(c), auth);
99
- if ("response" in resolved) return resolved.response;
100
- const state = stateSigner.sign(resolved.tenant.orgId, resolved.tenant.userId);
101
- return c.redirect(linear.buildAuthorizeUrl(state, redirectUri));
102
- }
103
- })
104
- );
105
- routes.push(
106
- registerApiRoute("/auth/linear/callback", {
107
- method: "GET",
108
- requiresAuth: false,
109
- handler: async (c) => {
110
- const resolved = await resolveOrgTenant(loose(c), auth);
111
- if ("response" in resolved) return resolved.response;
112
- const { orgId, userId } = resolved.tenant;
113
- const stateTenant = stateSigner.verify(c.req.query("state"));
114
- if (!stateTenant || stateTenant.userId !== userId || stateTenant.orgId !== orgId) {
115
- console.warn("[Linear] OAuth callback rejected: state/tenant mismatch.");
116
- return c.redirect("/?linear=error");
117
- }
118
- const code = c.req.query("code");
119
- if (!code) {
120
- return c.redirect("/?linear=error");
121
- }
122
- try {
123
- const tokens = await linear.exchangeOAuthCode(code, redirectUri);
124
- const workspace = await linear.fetchWorkspace(tokens.accessToken);
125
- await linear.upsertConnection({
126
- orgId,
127
- userId,
128
- accessToken: tokens.accessToken,
129
- refreshToken: tokens.refreshToken,
130
- expiresAt: tokens.expiresAt,
131
- scope: tokens.scope,
132
- workspaceName: workspace.name,
133
- workspaceUrlKey: workspace.urlKey
134
- });
135
- } catch (error) {
136
- console.warn(`[Linear] OAuth callback failed to persist connection for org ${orgId}.`, error);
137
- return c.redirect("/?linear=error");
138
- }
139
- return c.redirect("/?linear=connected");
140
- }
141
- })
142
- );
143
- routes.push(
144
- registerApiRoute("/web/linear/projects", {
145
- method: "GET",
146
- requiresAuth: false,
147
- handler: async (c) => {
148
- const resolved = await resolveOrgTenant(loose(c), auth);
149
- if ("response" in resolved) return resolved.response;
150
- const connection = await linear.loadConnection(resolved.tenant.orgId);
151
- if (!connection) {
152
- return c.json({ error: "linear_not_connected", message: "Connect Linear to list Linear projects." }, 409);
153
- }
154
- try {
155
- const accessToken = await linear.getFreshAccessToken(connection);
156
- const projects = await linear.listProjects(accessToken);
157
- return c.json({ projects });
158
- } catch (err) {
159
- return linearFetchError(loose(c), err);
160
- }
161
- }
162
- })
163
- );
164
- routes.push(
165
- registerApiRoute("/web/linear/issues", {
166
- method: "GET",
167
- requiresAuth: false,
168
- handler: async (c) => {
169
- const resolved = await resolveOrgTenant(loose(c), auth);
170
- if ("response" in resolved) return resolved.response;
171
- const after = parseAfterCursor(c.req.query("after"));
172
- if (after === null) return c.json({ error: "invalid_cursor" }, 400);
173
- const factoryProjectId = c.req.query("factoryProjectId");
174
- if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {
175
- return c.json({ error: "invalid_factory_project_id" }, 400);
176
- }
177
- const connection = await linear.loadConnection(resolved.tenant.orgId);
178
- if (!connection) {
179
- return c.json({ error: "linear_not_connected", message: "Connect Linear to see intake issues." }, 409);
180
- }
181
- await intake.ensureReady();
182
- const config = await intake.getConfig({
183
- orgId: resolved.tenant.orgId,
184
- userId: resolved.tenant.userId,
185
- integrationIds: ["linear"]
186
- });
187
- const selection = config.linear;
188
- if (!selection.enabled) {
189
- return c.json({ error: "linear_intake_disabled", message: "Linear intake is turned off in Settings." }, 404);
190
- }
191
- const projectIds = selection.sourceIds ?? [];
192
- if (projectIds.length === 0) {
193
- return c.json({ issues: [], nextCursor: null });
194
- }
195
- try {
196
- const accessToken = await linear.getFreshAccessToken(connection);
197
- const { issues, nextCursor } = await linear.intake.listIssues({
198
- connection: { type: "oauth", accessToken },
199
- sourceIds: projectIds,
200
- cursor: after
201
- });
202
- const issuePayload = issues.map((issue) => ({
203
- id: issue.id,
204
- identifier: issue.identifier,
205
- title: issue.title,
206
- url: issue.url,
207
- state: issue.state ?? "",
208
- stateType: issue.stateType ?? "",
209
- priorityLabel: issue.priority ?? "",
210
- assignee: issue.assignee,
211
- team: issue.source,
212
- labels: issue.labels,
213
- createdAt: issue.createdAt,
214
- updatedAt: issue.updatedAt
215
- }));
216
- if (factoryProjectId && options.ingestFactoryIssues) {
217
- await options.ingestFactoryIssues({
218
- orgId: resolved.tenant.orgId,
219
- userId: resolved.tenant.userId,
220
- factoryProjectId,
221
- issues: issuePayload
222
- });
223
- }
224
- return c.json({ issues: issuePayload, nextCursor });
225
- } catch (err) {
226
- return linearFetchError(loose(c), err);
227
- }
228
- }
229
- })
230
- );
231
- return routes;
232
- }
233
-
234
- // src/rules/resolve.ts
235
- function resolveFactoryLinearRule(rules, event) {
236
- return rules.linear[event]?.onEvent;
237
- }
238
-
239
- // src/rules/types.ts
240
- var FACTORY_RULE_STAGES = ["intake", "triage", "planning", "execute", "review", "done", "canceled"];
241
- var FACTORY_RULE_BOARDS = ["work", "review"];
242
-
243
- // src/rules/validation.ts
244
- var MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;
245
- var MAX_IDEMPOTENCY_KEY_LENGTH = 256;
246
- var MAX_REASON_LENGTH = 512;
247
- var MAX_TITLE_LENGTH = 512;
248
- var MAX_MESSAGE_LENGTH = 8192;
249
- var MAX_ARGUMENTS_LENGTH = 4096;
250
- var MAX_ROLE_LENGTH = 32;
251
- var MAX_SKILL_NAME_LENGTH = 128;
252
- var MAX_SOURCE_KEY_LENGTH = 256;
253
- var MAX_URL_LENGTH = 2048;
254
- var MAX_METADATA_JSON_LENGTH = 16384;
255
- var MAX_JSON_DEPTH = 8;
256
- var MAX_JSON_COLLECTION_SIZE = 100;
257
- var IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;
258
- var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
259
- var SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;
260
- var WORK_ITEM_SOURCES = ["github-issue", "github-pr", "linear-issue", "manual"];
261
- var REJECTION_CODES = [
262
- "forbidden",
263
- "invalid_transition",
264
- "missing_binding",
265
- "stale",
266
- "timeout",
267
- "rule_error",
268
- "causal_depth_exceeded",
269
- "repeated_transition"
270
- ];
271
- var FactoryRuleValidationError = class extends Error {
272
- code = "invalid_factory_rule";
273
- constructor(message) {
274
- super(message);
275
- this.name = "FactoryRuleValidationError";
276
- }
277
- };
278
- function isPlainObject(value) {
279
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
280
- const prototype = Object.getPrototypeOf(value);
281
- return prototype === Object.prototype || prototype === null;
282
- }
283
- function assertExactKeys(value, keys, label) {
284
- const allowed = new Set(keys);
285
- if (Object.keys(value).some((key) => !allowed.has(key))) {
286
- throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);
287
- }
288
- }
289
- function boundedString(value, label, max, pattern) {
290
- if (typeof value !== "string") throw new FactoryRuleValidationError(`${label} must be a string.`);
291
- const normalized = value.trim();
292
- if (normalized.length === 0 || normalized.length > max || pattern && !pattern.test(normalized)) {
293
- throw new FactoryRuleValidationError(`${label} is invalid.`);
294
- }
295
- return normalized;
296
- }
297
- function optionalBoundedString(value, label, max) {
298
- if (value === void 0) return void 0;
299
- return boundedString(value, label, max);
300
- }
301
- function enumValue(value, allowed, label) {
302
- if (typeof value !== "string" || !allowed.includes(value)) {
303
- throw new FactoryRuleValidationError(`${label} is invalid.`);
304
- }
305
- return value;
306
- }
307
- function normalizeFactoryRuleJsonValue(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
308
- if (value === null || typeof value === "boolean" || typeof value === "string") return value;
309
- if (typeof value === "number") {
310
- if (!Number.isFinite(value)) throw new FactoryRuleValidationError("Rule metadata must contain finite numbers.");
311
- return value;
312
- }
313
- if (depth >= MAX_JSON_DEPTH || typeof value !== "object" && !Array.isArray(value)) {
314
- throw new FactoryRuleValidationError("Rule metadata is not bounded JSON.");
315
- }
316
- if (seen.has(value)) throw new FactoryRuleValidationError("Rule metadata must not contain cycles.");
317
- seen.add(value);
318
- try {
319
- if (Array.isArray(value)) {
320
- if (value.length > MAX_JSON_COLLECTION_SIZE) {
321
- throw new FactoryRuleValidationError("Rule metadata contains too many entries.");
322
- }
323
- return value.map((entry) => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));
324
- }
325
- if (!isPlainObject(value)) throw new FactoryRuleValidationError("Rule metadata must use plain objects.");
326
- const entries = Object.entries(value);
327
- if (entries.length > MAX_JSON_COLLECTION_SIZE) {
328
- throw new FactoryRuleValidationError("Rule metadata contains too many fields.");
329
- }
330
- const sanitized = {};
331
- for (const [key, entry] of entries) {
332
- const normalizedKey = boundedString(key, "Rule metadata key", 128, IDENTIFIER_RE);
333
- sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey) ? "[REDACTED]" : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);
334
- }
335
- return sanitized;
336
- } finally {
337
- seen.delete(value);
338
- }
339
- }
340
- function sanitizeMetadata(value) {
341
- if (value === void 0) return void 0;
342
- const sanitized = normalizeFactoryRuleJsonValue(value);
343
- if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError("Rule metadata must be an object.");
344
- if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {
345
- throw new FactoryRuleValidationError("Rule metadata is too large.");
346
- }
347
- return sanitized;
348
- }
349
- function commonCommitFields(value) {
350
- return {
351
- idempotencyKey: boundedString(value.idempotencyKey, "Factory decision idempotencyKey", MAX_IDEMPOTENCY_KEY_LENGTH)
352
- };
353
- }
354
- function validateFactoryRuleDecision(value, causalDepth = 0) {
355
- if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {
356
- throw new FactoryRuleValidationError("Factory rule causal depth exceeded.");
357
- }
358
- if (!isPlainObject(value)) throw new FactoryRuleValidationError("Factory rule decision must be an object.");
359
- const type = value.type;
360
- if (typeof type !== "string") throw new FactoryRuleValidationError("Factory rule decision type is required.");
361
- switch (type) {
362
- case "reject": {
363
- assertExactKeys(value, ["type", "code", "reason"], "Factory reject decision");
364
- return {
365
- type,
366
- code: enumValue(value.code, REJECTION_CODES, "Factory rejection code"),
367
- reason: boundedString(value.reason, "Factory rejection reason", MAX_REASON_LENGTH)
368
- };
369
- }
370
- case "transition": {
371
- assertExactKeys(value, ["type", "idempotencyKey", "board", "stage"], "Factory transition decision");
372
- return {
373
- type,
374
- ...commonCommitFields(value),
375
- board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory transition board"),
376
- stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory transition stage")
377
- };
378
- }
379
- case "upsertLinkedWorkItem": {
380
- assertExactKeys(
381
- value,
382
- ["type", "idempotencyKey", "board", "source", "sourceKey", "title", "url", "stage", "metadata"],
383
- "Factory linked work item decision"
384
- );
385
- const url = value.url;
386
- if (url !== null && (typeof url !== "string" || url.length > MAX_URL_LENGTH || !/^https?:\/\//.test(url))) {
387
- throw new FactoryRuleValidationError("Factory linked work item URL is invalid.");
388
- }
389
- const metadata = sanitizeMetadata(value.metadata);
390
- return {
391
- type,
392
- ...commonCommitFields(value),
393
- board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory linked work item board"),
394
- source: enumValue(value.source, WORK_ITEM_SOURCES, "Factory linked work item source"),
395
- sourceKey: boundedString(value.sourceKey, "Factory linked work item sourceKey", MAX_SOURCE_KEY_LENGTH),
396
- title: boundedString(value.title, "Factory linked work item title", MAX_TITLE_LENGTH),
397
- url,
398
- stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory linked work item stage"),
399
- ...metadata ? { metadata } : {}
400
- };
401
- }
402
- case "invokeSkill": {
403
- assertExactKeys(
404
- value,
405
- ["type", "idempotencyKey", "role", "skillName", "arguments", "precedingMessage"],
406
- "Factory invoke skill decision"
407
- );
408
- const args = optionalBoundedString(value.arguments, "Factory skill arguments", MAX_ARGUMENTS_LENGTH);
409
- const precedingMessage = optionalBoundedString(
410
- value.precedingMessage,
411
- "Factory skill preceding message",
412
- MAX_MESSAGE_LENGTH
413
- );
414
- return {
415
- type,
416
- ...commonCommitFields(value),
417
- role: boundedString(value.role, "Factory skill role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
418
- skillName: boundedString(value.skillName, "Factory skill name", MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE),
419
- ...args ? { arguments: args } : {},
420
- ...precedingMessage ? { precedingMessage } : {}
421
- };
422
- }
423
- case "sendMessage": {
424
- assertExactKeys(
425
- value,
426
- ["type", "idempotencyKey", "role", "message", "priority", "idleBehavior", "prepareBinding"],
427
- "Factory send message decision"
428
- );
429
- const priority = value.priority === void 0 ? void 0 : enumValue(value.priority, ["medium", "high", "urgent"], "Factory message priority");
430
- const idleBehavior = value.idleBehavior === void 0 ? void 0 : enumValue(value.idleBehavior, ["persist", "wake"], "Factory message idle behavior");
431
- if (value.prepareBinding !== void 0 && typeof value.prepareBinding !== "boolean") {
432
- throw new FactoryRuleValidationError("Factory message prepareBinding must be a boolean.");
433
- }
434
- return {
435
- type,
436
- ...commonCommitFields(value),
437
- role: boundedString(value.role, "Factory message role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
438
- message: boundedString(value.message, "Factory message", MAX_MESSAGE_LENGTH),
439
- ...priority ? { priority } : {},
440
- ...idleBehavior ? { idleBehavior } : {},
441
- ...value.prepareBinding === true ? { prepareBinding: true } : {}
442
- };
443
- }
444
- case "notify": {
445
- assertExactKeys(value, ["type", "idempotencyKey", "title", "body", "level"], "Factory notify decision");
446
- const body = optionalBoundedString(value.body, "Factory notification body", MAX_MESSAGE_LENGTH);
447
- const level = value.level === void 0 ? void 0 : enumValue(value.level, ["info", "warning", "error"], "Factory notification level");
448
- return {
449
- type,
450
- ...commonCommitFields(value),
451
- title: boundedString(value.title, "Factory notification title", MAX_TITLE_LENGTH),
452
- ...body ? { body } : {},
453
- ...level ? { level } : {}
454
- };
455
- }
456
- default:
457
- throw new FactoryRuleValidationError("Factory rule decision type is unsupported.");
458
- }
459
- }
460
- function validateFactoryRuleDecisions(values, causalDepth = 0) {
461
- if (values.length > MAX_JSON_COLLECTION_SIZE) {
462
- throw new FactoryRuleValidationError("Factory rule produced too many decisions.");
463
- }
464
- const decisions = [];
465
- for (const value of values) {
466
- const decision = validateFactoryRuleDecision(value, causalDepth);
467
- if (decision.type === "reject") {
468
- throw new FactoryRuleValidationError("A rejection cannot be persisted with commit decisions.");
469
- }
470
- decisions.push(decision);
471
- }
472
- const keys = decisions.map((decision) => decision.idempotencyKey);
473
- if (new Set(keys).size !== keys.length) {
474
- throw new FactoryRuleValidationError("Factory decisions require unique idempotency keys.");
475
- }
476
- return decisions;
477
- }
478
-
479
- // src/integrations/linear/rules.ts
480
- var RULE_TIMEOUT_MS = 5e3;
481
- async function withRuleTimeout(promise) {
482
- let timeout;
483
- try {
484
- return await Promise.race([
485
- promise,
486
- new Promise((_, reject) => {
487
- timeout = setTimeout(() => reject(new Error("FACTORY_RULE_TIMEOUT")), RULE_TIMEOUT_MS);
488
- })
489
- ]);
490
- } finally {
491
- if (timeout) clearTimeout(timeout);
492
- }
493
- }
494
- var LinearRules = class {
495
- constructor(options) {
496
- this.options = options;
497
- }
498
- options;
499
- async ingest(input) {
500
- const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });
501
- if (!project) return { status: "missing", ingested: 0 };
502
- const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });
503
- const itemsBySourceKey = new Map(items.map((item) => [item.externalSource?.externalId, item]));
504
- const statuses = [];
505
- for (const issue of input.issues) {
506
- statuses.push(await this.#ingestIssue(input, issue, itemsBySourceKey.get(`linear:${issue.identifier}`)));
507
- }
508
- if (statuses.some((status) => status === "committed")) return { status: "committed", ingested: statuses.length };
509
- if (statuses.some((status) => status === "replayed")) return { status: "replayed", ingested: statuses.length };
510
- return { status: "missing", ingested: statuses.length };
511
- }
512
- async #ingestIssue(input, issue, relatedItem) {
513
- const ingressId = `linear:${issue.id}:${issue.updatedAt}`;
514
- const actor = { type: "human", id: input.userId };
515
- const context = {
516
- tenant: { orgId: input.orgId, projectId: input.factoryProjectId },
517
- actor,
518
- ingress: { type: "linear", id: ingressId },
519
- cause: "linear.issueObserved",
520
- causalChain: [],
521
- ruleSetVersion: this.options.rules.version,
522
- ...relatedItem ? {
523
- item: {
524
- id: relatedItem.id,
525
- source: "linear-issue",
526
- sourceKey: relatedItem.externalSource?.externalId ?? null,
527
- parentWorkItemId: relatedItem.parentWorkItemId,
528
- title: relatedItem.title,
529
- url: relatedItem.externalSource?.url ?? null,
530
- stages: relatedItem.stages
531
- },
532
- board: "work",
533
- itemRevision: relatedItem.revision
534
- } : {},
535
- event: "issueObserved",
536
- issue
537
- };
538
- const rule = resolveFactoryLinearRule(this.options.rules, context.event);
539
- let decision;
540
- let decisions = [];
541
- let outcome = { status: "accepted" };
542
- try {
543
- decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : void 0;
544
- if (decision?.type === "reject") {
545
- outcome = { status: "rejected", code: decision.code, reason: decision.reason };
546
- } else if (decision) {
547
- decisions = validateFactoryRuleDecisions([decision]).map((entry) => ({ ...entry }));
548
- }
549
- } catch (error) {
550
- const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
551
- outcome = {
552
- status: "rejected",
553
- code: timedOut ? "timeout" : "rule_error",
554
- reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory Linear rule failed."
555
- };
556
- }
557
- const committed = await this.options.storage.commitRuleEvaluation({
558
- orgId: input.orgId,
559
- factoryProjectId: input.factoryProjectId,
560
- workItemId: relatedItem?.id ?? null,
561
- ingress: { identity: ingressId, triggerType: "linear.issueObserved" },
562
- ruleSetVersion: this.options.rules.version,
563
- expectedRevision: relatedItem?.revision ?? null,
564
- actor,
565
- outcome,
566
- decisions,
567
- causalChain: [],
568
- now: /* @__PURE__ */ new Date()
569
- });
570
- return committed.status;
571
- }
572
- };
573
- function attachLinearRules(context) {
574
- if (!context.rules) return void 0;
575
- const rules = new LinearRules({
576
- projects: context.storage.projects,
577
- storage: context.rules.workItems,
578
- rules: context.rules.config
579
- });
580
- return (input) => rules.ingest(input);
581
- }
582
-
583
- // src/integrations/linear/integration.ts
584
- var LinearReauthRequiredError = class extends Error {
585
- constructor() {
586
- super("Linear authorization expired. Reconnect Linear to keep syncing intake issues.");
587
- }
588
- };
589
-
590
- // src/integrations/linear/agent-tools.ts
591
- function createLinearGetIssueTool(linear, orgId) {
592
- return createTool({
593
- id: "linear_get_issue",
594
- 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.",
595
- inputSchema: z.object({
596
- issue: z.string().min(1).describe('The Linear issue identifier (e.g. "ENG-123") or issue UUID.')
597
- }),
598
- execute: async ({ issue }) => {
599
- const connection = await linear.loadConnection(orgId);
600
- if (!connection) {
601
- return { error: "Linear is not connected for this repository. Connect Linear in Settings to fetch issues." };
602
- }
603
- try {
604
- const accessToken = await linear.getFreshAccessToken(connection);
605
- const detail = await linear.intake.getIssue({
606
- connection: { type: "oauth", accessToken },
607
- issueId: issue.trim()
608
- });
609
- if (!detail) {
610
- return { error: `Linear issue "${issue}" was not found in this workspace.` };
611
- }
612
- return detail;
613
- } catch (err) {
614
- if (err instanceof LinearReauthRequiredError) {
615
- return { error: err.message };
616
- }
617
- return { error: `Failed to fetch Linear issue: ${err instanceof Error ? err.message : String(err)}` };
618
- }
619
- }
620
- });
621
- }
622
- function createLinearCommentTool(linear, orgId) {
623
- return createTool({
624
- id: "linear_create_comment",
625
- 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.",
626
- inputSchema: z.object({
627
- issue: z.string().min(1).describe('The Linear issue identifier (e.g. "ENG-123") or issue UUID.'),
628
- body: z.string().min(1).describe("The comment body, as Linear-flavored markdown.")
629
- }),
630
- execute: async ({ issue, body }) => {
631
- const connection = await linear.loadConnection(orgId);
632
- if (!connection) {
633
- return { error: "Linear is not connected for this repository. Connect Linear in Settings to post comments." };
634
- }
635
- if (!linear.canPostComments(connection)) {
636
- return {
637
- error: "The Linear connection does not have comment permissions. Reconnect Linear in Settings to grant them."
638
- };
639
- }
640
- try {
641
- const accessToken = await linear.getFreshAccessToken(connection);
642
- const comment = await linear.intake.createComment({
643
- connection: { type: "oauth", accessToken },
644
- issueId: issue.trim(),
645
- body
646
- });
647
- if (!comment) {
648
- return { error: `Linear issue "${issue}" was not found in this workspace.` };
649
- }
650
- return { posted: true, url: comment.url };
651
- } catch (err) {
652
- if (err instanceof LinearReauthRequiredError) {
653
- return { error: err.message };
654
- }
655
- return { error: `Failed to post Linear comment: ${err instanceof Error ? err.message : String(err)}` };
656
- }
657
- }
658
- });
659
- }
660
- async function buildLinearAgentTools({
661
- requestContext,
662
- linear
663
- }) {
664
- if (!linear.authEnabled) return {};
665
- const ctx = requestContext.get("controller");
666
- const resourceId = ctx?.resourceId;
667
- if (!resourceId) return {};
668
- const orgId = await linear.resolveOrgId(resourceId);
669
- if (!orgId) return {};
670
- const check = await linear.checkConnection(orgId);
671
- if (!check.connected) return {};
672
- return {
673
- linear_get_issue: createLinearGetIssueTool(linear, orgId),
674
- // Only offered when the granted OAuth scope allows posting comments —
675
- // connections made before `comments:create` was requested are read-only
676
- // until the org reconnects Linear.
677
- ...check.canComment ? { linear_create_comment: createLinearCommentTool(linear, orgId) } : {}
678
- };
679
- }
680
-
681
- // src/integrations/platform/api-client.ts
682
- function platformApiClientConfigFromEnv() {
683
- const sharedApiUrl = process.env.MASTRA_SHARED_API_URL?.trim() || "https://platform.mastra.ai/v1";
684
- const accessToken = process.env.MASTRA_PLATFORM_SECRET_KEY?.trim();
685
- if (!accessToken) {
686
- throw new Error("Platform integration: missing required environment variable MASTRA_PLATFORM_SECRET_KEY.");
687
- }
688
- return { baseUrl: normalizeSharedApiUrl(sharedApiUrl), accessToken };
689
- }
690
- function normalizeSharedApiUrl(sharedApiUrl) {
691
- return sharedApiUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
692
- }
693
- var PlatformApiError = class extends Error {
694
- status;
695
- retryAfterSeconds;
696
- constructor(message, status, retryAfterSeconds = null) {
697
- super(message);
698
- this.name = "PlatformApiError";
699
- this.status = status;
700
- this.retryAfterSeconds = retryAfterSeconds;
701
- }
702
- };
703
- var PlatformApiClient = class {
704
- #baseUrl;
705
- #accessToken;
706
- #fetch;
707
- constructor(config) {
708
- const missing = ["baseUrl", "accessToken"].filter((field) => !config[field]);
709
- if (missing.length > 0) {
710
- throw new Error(`Platform integration: missing required config field(s): ${missing.join(", ")}.`);
711
- }
712
- this.#baseUrl = config.baseUrl.replace(/\/+$/, "");
713
- this.#accessToken = config.accessToken;
714
- this.#fetch = config.fetchImpl ?? globalThis.fetch;
715
- }
716
- async request(method, path, body, options) {
717
- const response = await this.#send(method, path, body, options);
718
- if (!response.ok) {
719
- const message = redact(await extractError(response), this.#accessToken);
720
- const retryAfterSeconds = parseRetryAfter(response.headers.get("retry-after"));
721
- logPlatformError("Platform API request failed", {
722
- method,
723
- path,
724
- status: response.status,
725
- retryAfterSeconds,
726
- message
727
- });
728
- throw new PlatformApiError(message, response.status, retryAfterSeconds);
729
- }
730
- if (response.status === 204) return void 0;
731
- return await response.json();
732
- }
733
- async requestRedirect(method, path, options) {
734
- const response = await this.#send(method, path, void 0, options, "manual");
735
- if (response.status >= 300 && response.status < 400) {
736
- const location = response.headers.get("location");
737
- if (location) return location;
738
- }
739
- if (!response.ok) {
740
- const message = redact(await extractError(response), this.#accessToken);
741
- const retryAfterSeconds = parseRetryAfter(response.headers.get("retry-after"));
742
- logPlatformError("Platform API redirect request failed", {
743
- method,
744
- path,
745
- status: response.status,
746
- retryAfterSeconds,
747
- message
748
- });
749
- throw new PlatformApiError(message, response.status, retryAfterSeconds);
750
- }
751
- logPlatformError("Platform API request did not return a redirect", { method, path, status: response.status });
752
- throw new PlatformApiError("Platform API request did not return a redirect.", response.status);
753
- }
754
- async #send(method, path, body, options, redirect) {
755
- const headers = {
756
- accept: "application/json",
757
- authorization: `Bearer ${this.#accessToken}`
758
- };
759
- if (options?.actingUserId) {
760
- headers["x-acting-user-id"] = options.actingUserId;
761
- }
762
- const timeoutSignal = AbortSignal.timeout(15e3);
763
- const init = {
764
- method,
765
- headers,
766
- redirect,
767
- signal: options?.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal
768
- };
769
- if (body !== void 0) {
770
- headers["content-type"] = "application/json";
771
- init.body = JSON.stringify(body);
772
- }
773
- try {
774
- return await this.#fetch(`${this.#baseUrl}${path}`, init);
775
- } catch (error) {
776
- if (error instanceof Error && error.message.includes(this.#accessToken)) {
777
- const redacted = new Error(redact(error.message, this.#accessToken));
778
- redacted.name = error.name;
779
- logPlatformError("Platform API transport error", {
780
- method,
781
- path,
782
- name: redacted.name,
783
- message: redacted.message
784
- });
785
- throw redacted;
786
- }
787
- logPlatformError("Platform API transport error", {
788
- method,
789
- path,
790
- name: error instanceof Error ? error.name : void 0,
791
- message: error instanceof Error ? error.message : String(error)
792
- });
793
- throw error;
794
- }
795
- }
796
- };
797
- async function extractError(response) {
798
- try {
799
- const data = await response.clone().json();
800
- for (const field of ["detail", "error", "title"]) {
801
- if (typeof data[field] === "string" && data[field]) return data[field];
802
- }
803
- } catch {
804
- }
805
- return `Platform API request failed (${response.status})`;
806
- }
807
- function redact(message, accessToken) {
808
- return message.split(accessToken).join("[REDACTED]");
809
- }
810
- function logPlatformInfo(message, fields) {
811
- writePlatformLog("info", message, fields);
812
- }
813
- function logPlatformWarn(message, fields) {
814
- writePlatformLog("warn", message, fields);
815
- }
816
- function logPlatformError(message, fields) {
817
- writePlatformLog("error", message, fields);
818
- }
819
- function writePlatformLog(level, message, fields) {
820
- const metadata = fields ? ` ${JSON.stringify(stripUndefined(fields))}` : "";
821
- process.stderr.write(`[Mastra Factory] ${level.toUpperCase()} ${message}${metadata}
822
- `);
823
- }
824
- function stripUndefined(fields) {
825
- return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== void 0));
826
- }
827
- function parseRetryAfter(value) {
828
- if (!value) return null;
829
- const seconds = Number.parseInt(value, 10);
830
- return Number.isSafeInteger(seconds) && seconds >= 0 ? seconds : null;
831
- }
832
-
833
- // src/integrations/platform/linear/integration.ts
834
- var API_PREFIX = "/v1/server/linear";
835
- var PAGE_SIZE = 30;
836
- var MAX_REFERENCE_PAGES = 20;
837
- var MAX_COMMENT_PAGES = 20;
838
- var PLATFORM_MANAGED_CONNECTION_TOKEN = "platform-managed";
839
- function loose2(c) {
840
- return c;
13
+ return c;
841
14
  }
842
15
  function routeBaseUrl(ctx, requestUrl) {
843
- return (ctx.baseUrl || new URL(requestUrl).origin).replace(/\/+$/, "");
16
+ return (ctx.baseUrl || new URL(requestUrl).origin).replace(/\/+$/, "");
844
17
  }
845
18
  var PlatformLinearIntegration = class {
846
- id = "linear";
847
- #client;
848
- #endpointHost;
849
- #projects;
850
- #auth;
851
- /**
852
- * Per-instance workflow-state cache keyed by `${workspaceId}:${teamId}`. Scoped to the process
853
- * lifetime; not shared across requests intentionally to keep failure modes simple (stale states
854
- * clear on process restart). Populated lazily on first `updateIssue` per team.
855
- */
856
- #workflowStatesByTeam = /* @__PURE__ */ new Map();
857
- intake = {
858
- resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
859
- listSources: async () => {
860
- const sources = await this.#listProjectSources();
861
- return sources.map(({ workspace, project }) => ({
862
- id: encodeSourceId(workspace.linearWorkspaceId, project.id),
863
- name: project.name,
864
- type: "project",
865
- metadata: {
866
- workspaceId: workspace.linearWorkspaceId,
867
- workspaceName: workspace.linearWorkspaceName,
868
- workspaceUrlKey: workspace.urlKey,
869
- state: project.state,
870
- teams: project.teams
871
- }
872
- }));
873
- },
874
- listItems: async ({ sourceIds, cursor }) => {
875
- const result = await this.#listIssues(sourceIds, cursor);
876
- return {
877
- items: result.issues.map(({ issue, source }) => ({
878
- source: { type: "issue", externalId: issue.id, url: issue.url },
879
- sourceId: encodeSourceId(source.workspace.linearWorkspaceId, source.project.id),
880
- title: issue.title,
881
- status: issue.state.name,
882
- labels: issue.labels.map((label) => label.name),
883
- assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,
884
- createdAt: issue.createdAt,
885
- updatedAt: issue.updatedAt,
886
- metadata: {
887
- identifier: issue.identifier,
888
- workspaceId: source.workspace.linearWorkspaceId,
889
- workspaceName: source.workspace.linearWorkspaceName,
890
- projectId: source.project.id,
891
- projectName: source.project.name,
892
- team: issue.team.key,
893
- priority: issue.priorityLabel
894
- }
895
- })),
896
- nextCursor: result.nextCursor
897
- };
898
- },
899
- listIssues: async ({ connection, sourceIds, labels, cursor }) => {
900
- requireLinearConnection(connection);
901
- const result = await this.#listIssues(sourceIds, cursor, labels);
902
- return {
903
- issues: result.issues.map(({ issue }) => parseIssue(issue)),
904
- nextCursor: result.nextCursor
905
- };
906
- },
907
- getIssue: async ({ connection, sourceId, issueId }) => {
908
- requireLinearConnection(connection);
909
- const located = await this.#findIssue(sourceId, issueId);
910
- if (!located) return null;
911
- const comments = await this.#loadComments(located.workspaceId, issueId, located.issue.comments);
912
- return parseIssueDetail(located.issue, comments);
913
- },
914
- createComment: async ({ connection, sourceId, issueId, body }) => {
915
- requireLinearConnection(connection);
916
- const workspaceId = await this.#resolveWorkspaceForIssue(sourceId, issueId);
917
- if (!workspaceId) return null;
918
- try {
919
- const comment = await this.#client.request(
920
- "POST",
921
- `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments`,
922
- { body }
923
- );
924
- return { id: comment.id, url: comment.url };
925
- } catch (error) {
926
- if (isNotFound(error)) return null;
927
- throw error;
928
- }
929
- },
930
- updateIssue: async (input) => {
931
- requireLinearConnection(input.connection);
932
- const located = await this.#findIssue(input.sourceId, input.issueId);
933
- if (!located) return null;
934
- const { workspaceId, issue } = located;
935
- const target = input.state;
936
- if (target.kind === "byType" && issue.state.type === target.stateType) {
937
- return parseIssue(issue);
938
- }
939
- if (target.kind === "byName" && issue.state.name.toLowerCase() === target.name.toLowerCase()) {
940
- return parseIssue(issue);
941
- }
942
- let states;
943
- try {
944
- states = await this.#listWorkflowStates(workspaceId, issue.team.id);
945
- } catch (error) {
946
- if (isNotFound(error)) {
947
- logPlatformWarn("Platform Linear: workflow-states endpoint not available; skipping updateIssue.", {
948
- workspaceId,
949
- teamId: issue.team.id
950
- });
951
- return null;
952
- }
953
- throw error;
954
- }
955
- let matched;
956
- if (target.kind === "byType") {
957
- matched = states.slice().sort((a, b) => a.position - b.position).find((s) => s.type === target.stateType);
958
- } else {
959
- const wanted = target.name.toLowerCase();
960
- matched = states.find((s) => s.name.toLowerCase() === wanted);
961
- }
962
- if (!matched) {
963
- logPlatformWarn("Platform Linear: no workflow state matched target; skipping updateIssue.", {
964
- workspaceId,
965
- teamId: issue.team.id,
966
- target
967
- });
968
- return null;
969
- }
970
- try {
971
- await this.#client.request(
972
- "PATCH",
973
- `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(input.issueId)}`,
974
- { stateId: matched.id }
975
- );
976
- } catch (error) {
977
- if (isNotFound(error)) return null;
978
- throw error;
979
- }
980
- const refreshed = await this.#findIssue(input.sourceId, input.issueId);
981
- return refreshed ? parseIssue(refreshed.issue) : null;
982
- }
983
- };
984
- constructor() {
985
- const config = platformApiClientConfigFromEnv();
986
- this.#client = new PlatformApiClient(config);
987
- this.#endpointHost = new URL(config.baseUrl).host;
988
- }
989
- get storage() {
990
- const now = /* @__PURE__ */ new Date();
991
- return {
992
- integrationId: this.id,
993
- connections: {
994
- get: async (orgId) => ({
995
- id: `platform-linear:${orgId}`,
996
- orgId,
997
- userId: null,
998
- data: {
999
- accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN,
1000
- refreshToken: null,
1001
- expiresAtMs: null,
1002
- scope: "read,comments:create",
1003
- workspaceName: null,
1004
- workspaceUrlKey: null
1005
- },
1006
- metadata: {},
1007
- createdAt: now,
1008
- updatedAt: now
1009
- })
1010
- }
1011
- };
1012
- }
1013
- get projects() {
1014
- if (!this.#projects) throw new Error("PlatformLinearIntegration projects storage has not been initialized.");
1015
- return this.#projects;
1016
- }
1017
- initialize({ projects, auth }) {
1018
- this.#projects = projects;
1019
- this.#auth = auth;
1020
- logPlatformInfo("Platform Linear integration initialized", { endpointHost: this.#endpointHost });
1021
- }
1022
- get authEnabled() {
1023
- return this.#auth?.enabled() ?? false;
1024
- }
1025
- async resolveOrgId(resourceId) {
1026
- try {
1027
- const project = await this.projects.getById({ id: resourceId });
1028
- return project?.orgId ?? null;
1029
- } catch {
1030
- return null;
1031
- }
1032
- }
1033
- async loadConnection(orgId) {
1034
- const workspace = (await this.#listWorkspaces())[0];
1035
- if (!workspace) return null;
1036
- const now = /* @__PURE__ */ new Date();
1037
- return {
1038
- id: `platform-linear:${orgId}`,
1039
- orgId,
1040
- userId: null,
1041
- accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN,
1042
- scope: "read,comments:create",
1043
- refreshToken: null,
1044
- expiresAt: null,
1045
- workspaceName: workspace.linearWorkspaceName,
1046
- workspaceUrlKey: workspace.urlKey,
1047
- createdAt: now,
1048
- updatedAt: now
1049
- };
1050
- }
1051
- async getFreshAccessToken(_connection) {
1052
- return PLATFORM_MANAGED_CONNECTION_TOKEN;
1053
- }
1054
- /**
1055
- * Background-dispatch context: platform-managed connection token when the
1056
- * org has a connected workspace. Linear work items store the issue UUID
1057
- * directly as `externalId`.
1058
- */
1059
- async #resolveIntakeDispatch({
1060
- orgId,
1061
- externalSource
1062
- }) {
1063
- if (externalSource.type !== "issue") return null;
1064
- const connection = await this.loadConnection(orgId);
1065
- if (!connection) return null;
1066
- return {
1067
- connection: { type: "oauth", accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN },
1068
- issueId: externalSource.externalId
1069
- };
1070
- }
1071
- canPostComments(connection) {
1072
- const scopes = (connection.scope ?? "").split(/[\s,]+/).filter(Boolean);
1073
- return scopes.some((scope) => scope === "comments:create" || scope === "write" || scope === "admin");
1074
- }
1075
- async checkConnection(orgId) {
1076
- const connection = await this.loadConnection(orgId);
1077
- return {
1078
- connected: connection !== null,
1079
- canComment: connection !== null && this.canPostComments(connection),
1080
- checkedAt: Date.now()
1081
- };
1082
- }
1083
- routes(ctx) {
1084
- return [
1085
- this.#connectRoute(ctx),
1086
- ...buildLinearRoutes({
1087
- auth: ctx.auth,
1088
- linear: this,
1089
- stateSigner: ctx.stateSigner,
1090
- baseUrl: ctx.baseUrl,
1091
- intake: ctx.storage.intake,
1092
- ingestFactoryIssues: attachLinearRules(ctx)
1093
- }).filter((route) => !route.path.startsWith("/auth/linear/"))
1094
- ];
1095
- }
1096
- #connectRoute(ctx) {
1097
- return registerApiRoute2("/auth/linear/connect", {
1098
- method: "GET",
1099
- requiresAuth: false,
1100
- handler: async (c) => {
1101
- await ctx.auth.ensureUser(loose2(c));
1102
- const tenant = ctx.auth.tenant(loose2(c));
1103
- if (!tenant?.orgId) return c.json({ error: "unauthorized" }, 401);
1104
- const returnTo = c.req.query("return_to") || "/";
1105
- const originator = routeBaseUrl(ctx, c.req.url);
1106
- logPlatformInfo("Starting Platform Linear connect flow", {
1107
- orgId: tenant.orgId,
1108
- returnTo,
1109
- originator
1110
- });
1111
- const query = new URLSearchParams({ return_to: returnTo, originator });
1112
- const location = await this.#client.requestRedirect("GET", `${API_PREFIX}/authorize?${query}`);
1113
- return c.redirect(location);
1114
- }
1115
- });
1116
- }
1117
- async agentTools({ requestContext }) {
1118
- return buildLinearAgentTools({ requestContext, linear: this });
1119
- }
1120
- diagnostics() {
1121
- return { mode: "platform", endpointHost: this.#endpointHost };
1122
- }
1123
- async listProjects() {
1124
- return (await this.#listProjectSources()).map(({ workspace, project }) => ({
1125
- ...project,
1126
- id: encodeSourceId(workspace.linearWorkspaceId, project.id),
1127
- workspaceId: workspace.linearWorkspaceId
1128
- }));
1129
- }
1130
- async #listProjectSources() {
1131
- const workspaces = await this.#listWorkspaces();
1132
- const projectGroups = await Promise.all(
1133
- workspaces.map(async (workspace) => {
1134
- const projects = [];
1135
- let after;
1136
- for (let page = 0; page < MAX_REFERENCE_PAGES; page += 1) {
1137
- const query = new URLSearchParams({ first: "200" });
1138
- if (after) query.set("after", after);
1139
- const result = await this.#client.request(
1140
- "GET",
1141
- `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/projects?${query}`
1142
- );
1143
- projects.push(...result.projects);
1144
- if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor) break;
1145
- after = result.pageInfo.endCursor;
1146
- }
1147
- return projects.map((project) => ({ workspace, project }));
1148
- })
1149
- );
1150
- return projectGroups.flat();
1151
- }
1152
- async #listWorkspaces() {
1153
- const result = await this.#client.request("GET", `${API_PREFIX}/workspaces`);
1154
- return result.workspaces.filter((workspace) => workspace.connected);
1155
- }
1156
- async #listIssues(sourceIds, cursor, labels) {
1157
- if (sourceIds.length === 0)
1158
- return { issues: [], nextCursor: null };
1159
- const sources = await this.#listProjectSources();
1160
- const sourceMap = new Map(
1161
- sources.map((source) => [encodeSourceId(source.workspace.linearWorkspaceId, source.project.id), source])
1162
- );
1163
- const selected = sourceIds.map((sourceId) => sourceMap.get(sourceId)).filter((source) => !!source);
1164
- const cursors = decodeCursor(cursor, sourceIds);
1165
- const normalizedLabels = normalizeLabels(labels);
1166
- const nextState = {};
1167
- let hasNextPage = false;
1168
- const pages = await Promise.all(
1169
- selected.map(async (source) => {
1170
- const sourceId = encodeSourceId(source.workspace.linearWorkspaceId, source.project.id);
1171
- if (cursors[sourceId] === null) {
1172
- nextState[sourceId] = null;
1173
- return [];
1174
- }
1175
- const query = new URLSearchParams({
1176
- first: String(PAGE_SIZE),
1177
- projectIds: source.project.id,
1178
- stateType: "triage,backlog,unstarted,started",
1179
- orderBy: "updatedAt"
1180
- });
1181
- const after = cursors[sourceId];
1182
- if (after) query.set("after", after);
1183
- const result = await this.#client.request(
1184
- "GET",
1185
- `${API_PREFIX}/workspaces/${encodeURIComponent(source.workspace.linearWorkspaceId)}/issues?${query}`
1186
- );
1187
- const next = result.pageInfo.hasNextPage ? result.pageInfo.endCursor : null;
1188
- nextState[sourceId] = next;
1189
- hasNextPage ||= next !== null;
1190
- return result.issues.filter(
1191
- (issue) => normalizedLabels.length === 0 || issue.labels.some((label) => normalizedLabels.includes(label.name))
1192
- ).map((issue) => ({ issue, source }));
1193
- })
1194
- );
1195
- return {
1196
- issues: pages.flat(),
1197
- nextCursor: hasNextPage ? encodeCursor(nextState, sourceIds) : null
1198
- };
1199
- }
1200
- async #findIssue(sourceId, issueId) {
1201
- const workspaceIds = await this.#candidateWorkspaceIds(sourceId);
1202
- for (const workspaceId of workspaceIds) {
1203
- try {
1204
- const issue = await this.#client.request(
1205
- "GET",
1206
- `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}?include=comments`
1207
- );
1208
- return { workspaceId, issue };
1209
- } catch (error) {
1210
- if (!isNotFound(error)) throw error;
1211
- }
1212
- }
1213
- return null;
1214
- }
1215
- async #resolveWorkspaceForIssue(sourceId, issueId) {
1216
- const workspaceIds = await this.#candidateWorkspaceIds(sourceId);
1217
- if (workspaceIds.length === 1) return workspaceIds[0];
1218
- for (const workspaceId of workspaceIds) {
1219
- try {
1220
- await this.#client.request(
1221
- "GET",
1222
- `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}`
1223
- );
1224
- return workspaceId;
1225
- } catch (error) {
1226
- if (!isNotFound(error)) throw error;
1227
- }
1228
- }
1229
- return null;
1230
- }
1231
- async #listWorkflowStates(workspaceId, teamId) {
1232
- const cacheKey = `${workspaceId}:${teamId}`;
1233
- const cached = this.#workflowStatesByTeam.get(cacheKey);
1234
- if (cached) return cached;
1235
- const result = await this.#client.request(
1236
- "GET",
1237
- `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/workflow-states?teamId=${encodeURIComponent(teamId)}&first=100`
1238
- );
1239
- this.#workflowStatesByTeam.set(cacheKey, result.workflowStates);
1240
- return result.workflowStates;
1241
- }
1242
- async #candidateWorkspaceIds(sourceId) {
1243
- if (sourceId) return [decodeSourceId(sourceId).workspaceId];
1244
- return (await this.#listWorkspaces()).map((workspace) => workspace.linearWorkspaceId);
1245
- }
1246
- async #loadComments(workspaceId, issueId, embedded) {
1247
- const comments = [...embedded?.nodes ?? []];
1248
- let pageInfo = embedded?.pageInfo;
1249
- let page = 0;
1250
- while (pageInfo?.hasNextPage && pageInfo.endCursor && page < MAX_COMMENT_PAGES) {
1251
- const result = await this.#client.request(
1252
- "GET",
1253
- `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments?first=200&after=${encodeURIComponent(pageInfo.endCursor)}`
1254
- );
1255
- comments.push(...result.comments);
1256
- pageInfo = result.pageInfo;
1257
- page += 1;
1258
- }
1259
- return comments;
1260
- }
19
+ id = "linear";
20
+ #client;
21
+ #endpointHost;
22
+ #projects;
23
+ #auth;
24
+ /**
25
+ * Per-instance workflow-state cache keyed by `${workspaceId}:${teamId}`. Scoped to the process
26
+ * lifetime; not shared across requests intentionally to keep failure modes simple (stale states
27
+ * clear on process restart). Populated lazily on first `updateIssue` per team.
28
+ */
29
+ #workflowStatesByTeam = /* @__PURE__ */ new Map();
30
+ intake = {
31
+ resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
32
+ listSources: async () => {
33
+ return (await this.#listProjectSources()).map(({ workspace, project }) => ({
34
+ id: encodeSourceId(workspace.linearWorkspaceId, project.id),
35
+ name: project.name,
36
+ type: "project",
37
+ metadata: {
38
+ workspaceId: workspace.linearWorkspaceId,
39
+ workspaceName: workspace.linearWorkspaceName,
40
+ workspaceUrlKey: workspace.urlKey,
41
+ state: project.state,
42
+ teams: project.teams
43
+ }
44
+ }));
45
+ },
46
+ listItems: async ({ sourceIds, cursor }) => {
47
+ const result = await this.#listIssues(sourceIds, cursor);
48
+ return {
49
+ items: result.issues.map(({ issue, source }) => ({
50
+ source: {
51
+ type: "issue",
52
+ externalId: issue.id,
53
+ url: issue.url
54
+ },
55
+ sourceId: encodeSourceId(source.workspace.linearWorkspaceId, source.project.id),
56
+ title: issue.title,
57
+ status: issue.state.name,
58
+ labels: issue.labels.map((label) => label.name),
59
+ assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,
60
+ createdAt: issue.createdAt,
61
+ updatedAt: issue.updatedAt,
62
+ metadata: {
63
+ identifier: issue.identifier,
64
+ workspaceId: source.workspace.linearWorkspaceId,
65
+ workspaceName: source.workspace.linearWorkspaceName,
66
+ projectId: source.project.id,
67
+ projectName: source.project.name,
68
+ team: issue.team.key,
69
+ priority: issue.priorityLabel
70
+ }
71
+ })),
72
+ nextCursor: result.nextCursor
73
+ };
74
+ },
75
+ listIssues: async ({ connection, sourceIds, labels, cursor }) => {
76
+ requireLinearConnection(connection);
77
+ const result = await this.#listIssues(sourceIds, cursor, labels);
78
+ return {
79
+ issues: result.issues.map(({ issue }) => parseIssue(issue)),
80
+ nextCursor: result.nextCursor
81
+ };
82
+ },
83
+ getIssue: async ({ connection, sourceId, issueId }) => {
84
+ requireLinearConnection(connection);
85
+ const located = await this.#findIssue(sourceId, issueId);
86
+ if (!located) return null;
87
+ const comments = await this.#loadComments(located.workspaceId, issueId, located.issue.comments);
88
+ return parseIssueDetail(located.issue, comments);
89
+ },
90
+ createComment: async ({ connection, sourceId, issueId, body }) => {
91
+ requireLinearConnection(connection);
92
+ const workspaceId = await this.#resolveWorkspaceForIssue(sourceId, issueId);
93
+ if (!workspaceId) return null;
94
+ try {
95
+ const comment = await this.#client.request("POST", `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments`, { body });
96
+ return {
97
+ id: comment.id,
98
+ url: comment.url
99
+ };
100
+ } catch (error) {
101
+ if (isNotFound(error)) return null;
102
+ throw error;
103
+ }
104
+ },
105
+ updateIssue: async (input) => {
106
+ requireLinearConnection(input.connection);
107
+ const located = await this.#findIssue(input.sourceId, input.issueId);
108
+ if (!located) return null;
109
+ const { workspaceId, issue } = located;
110
+ const target = input.state;
111
+ if (target.kind === "byType" && issue.state.type === target.stateType) return parseIssue(issue);
112
+ if (target.kind === "byName" && issue.state.name.toLowerCase() === target.name.toLowerCase()) return parseIssue(issue);
113
+ let states;
114
+ try {
115
+ states = await this.#listWorkflowStates(workspaceId, issue.team.id);
116
+ } catch (error) {
117
+ if (isNotFound(error)) {
118
+ logPlatformWarn("Platform Linear: workflow-states endpoint not available; skipping updateIssue.", {
119
+ workspaceId,
120
+ teamId: issue.team.id
121
+ });
122
+ return null;
123
+ }
124
+ throw error;
125
+ }
126
+ let matched;
127
+ if (target.kind === "byType") matched = states.slice().sort((a, b) => a.position - b.position).find((s) => s.type === target.stateType);
128
+ else {
129
+ const wanted = target.name.toLowerCase();
130
+ matched = states.find((s) => s.name.toLowerCase() === wanted);
131
+ }
132
+ if (!matched) {
133
+ logPlatformWarn("Platform Linear: no workflow state matched target; skipping updateIssue.", {
134
+ workspaceId,
135
+ teamId: issue.team.id,
136
+ target
137
+ });
138
+ return null;
139
+ }
140
+ try {
141
+ await this.#client.request("PATCH", `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(input.issueId)}`, { stateId: matched.id });
142
+ } catch (error) {
143
+ if (isNotFound(error)) return null;
144
+ throw error;
145
+ }
146
+ const refreshed = await this.#findIssue(input.sourceId, input.issueId);
147
+ return refreshed ? parseIssue(refreshed.issue) : null;
148
+ }
149
+ };
150
+ constructor() {
151
+ const config = platformApiClientConfigFromEnv();
152
+ this.#client = new PlatformApiClient(config);
153
+ this.#endpointHost = new URL(config.baseUrl).host;
154
+ }
155
+ get storage() {
156
+ const now = /* @__PURE__ */ new Date();
157
+ return {
158
+ integrationId: this.id,
159
+ connections: { get: async (orgId) => ({
160
+ id: `platform-linear:${orgId}`,
161
+ orgId,
162
+ userId: null,
163
+ data: {
164
+ accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN,
165
+ refreshToken: null,
166
+ expiresAtMs: null,
167
+ scope: "read,comments:create",
168
+ workspaceName: null,
169
+ workspaceUrlKey: null
170
+ },
171
+ metadata: {},
172
+ createdAt: now,
173
+ updatedAt: now
174
+ }) }
175
+ };
176
+ }
177
+ get projects() {
178
+ if (!this.#projects) throw new Error("PlatformLinearIntegration projects storage has not been initialized.");
179
+ return this.#projects;
180
+ }
181
+ initialize({ projects, auth }) {
182
+ this.#projects = projects;
183
+ this.#auth = auth;
184
+ logPlatformInfo("Platform Linear integration initialized", { endpointHost: this.#endpointHost });
185
+ }
186
+ get authEnabled() {
187
+ return this.#auth?.enabled() ?? false;
188
+ }
189
+ async resolveOrgId(resourceId) {
190
+ try {
191
+ return (await this.projects.getById({ id: resourceId }))?.orgId ?? null;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+ async loadConnection(orgId) {
197
+ const workspace = (await this.#listWorkspaces())[0];
198
+ if (!workspace) return null;
199
+ const now = /* @__PURE__ */ new Date();
200
+ return {
201
+ id: `platform-linear:${orgId}`,
202
+ orgId,
203
+ userId: null,
204
+ accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN,
205
+ scope: "read,comments:create",
206
+ refreshToken: null,
207
+ expiresAt: null,
208
+ workspaceName: workspace.linearWorkspaceName,
209
+ workspaceUrlKey: workspace.urlKey,
210
+ createdAt: now,
211
+ updatedAt: now
212
+ };
213
+ }
214
+ async getFreshAccessToken(_connection) {
215
+ return PLATFORM_MANAGED_CONNECTION_TOKEN;
216
+ }
217
+ /**
218
+ * Background-dispatch context: platform-managed connection token when the
219
+ * org has a connected workspace. Linear work items store the issue UUID
220
+ * directly as `externalId`.
221
+ */
222
+ async #resolveIntakeDispatch({ orgId, externalSource }) {
223
+ if (externalSource.type !== "issue") return null;
224
+ if (!await this.loadConnection(orgId)) return null;
225
+ return {
226
+ connection: {
227
+ type: "oauth",
228
+ accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN
229
+ },
230
+ issueId: externalSource.externalId
231
+ };
232
+ }
233
+ canPostComments(connection) {
234
+ return (connection.scope ?? "").split(/[\s,]+/).filter(Boolean).some((scope) => scope === "comments:create" || scope === "write" || scope === "admin");
235
+ }
236
+ async checkConnection(orgId) {
237
+ const connection = await this.loadConnection(orgId);
238
+ return {
239
+ connected: connection !== null,
240
+ canComment: connection !== null && this.canPostComments(connection),
241
+ checkedAt: Date.now()
242
+ };
243
+ }
244
+ routes(ctx) {
245
+ return [this.#connectRoute(ctx), ...buildLinearRoutes({
246
+ auth: ctx.auth,
247
+ linear: this,
248
+ stateSigner: ctx.stateSigner,
249
+ baseUrl: ctx.baseUrl,
250
+ intake: ctx.storage.intake,
251
+ ingestFactoryIssues: attachLinearRules(ctx)
252
+ }).filter((route) => !route.path.startsWith("/auth/linear/"))];
253
+ }
254
+ #connectRoute(ctx) {
255
+ return registerApiRoute("/auth/linear/connect", {
256
+ method: "GET",
257
+ requiresAuth: false,
258
+ handler: async (c) => {
259
+ await ctx.auth.ensureUser(loose(c));
260
+ const tenant = ctx.auth.tenant(loose(c));
261
+ if (!tenant?.orgId) return c.json({ error: "unauthorized" }, 401);
262
+ const returnTo = c.req.query("return_to") || "/";
263
+ const originator = routeBaseUrl(ctx, c.req.url);
264
+ logPlatformInfo("Starting Platform Linear connect flow", {
265
+ orgId: tenant.orgId,
266
+ returnTo,
267
+ originator
268
+ });
269
+ const query = new URLSearchParams({
270
+ return_to: returnTo,
271
+ originator
272
+ });
273
+ const location = await this.#client.requestRedirect("GET", `${API_PREFIX}/authorize?${query}`);
274
+ return c.redirect(location);
275
+ }
276
+ });
277
+ }
278
+ async agentTools({ requestContext }) {
279
+ return buildLinearAgentTools({
280
+ requestContext,
281
+ linear: this
282
+ });
283
+ }
284
+ diagnostics() {
285
+ return {
286
+ mode: "platform",
287
+ endpointHost: this.#endpointHost
288
+ };
289
+ }
290
+ async listProjects() {
291
+ return (await this.#listProjectSources()).map(({ workspace, project }) => ({
292
+ ...project,
293
+ id: encodeSourceId(workspace.linearWorkspaceId, project.id),
294
+ workspaceId: workspace.linearWorkspaceId
295
+ }));
296
+ }
297
+ async #listProjectSources() {
298
+ const workspaces = await this.#listWorkspaces();
299
+ return (await Promise.all(workspaces.map(async (workspace) => {
300
+ const projects = [];
301
+ let after;
302
+ for (let page = 0; page < MAX_REFERENCE_PAGES; page += 1) {
303
+ const query = new URLSearchParams({ first: "200" });
304
+ if (after) query.set("after", after);
305
+ const result = await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/projects?${query}`);
306
+ projects.push(...result.projects);
307
+ if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor) break;
308
+ after = result.pageInfo.endCursor;
309
+ }
310
+ return projects.map((project) => ({
311
+ workspace,
312
+ project
313
+ }));
314
+ }))).flat();
315
+ }
316
+ async #listWorkspaces() {
317
+ return (await this.#client.request("GET", `${API_PREFIX}/workspaces`)).workspaces.filter((workspace) => workspace.connected);
318
+ }
319
+ async #listIssues(sourceIds, cursor, labels) {
320
+ if (sourceIds.length === 0) return {
321
+ issues: [],
322
+ nextCursor: null
323
+ };
324
+ const sources = await this.#listProjectSources();
325
+ const sourceMap = new Map(sources.map((source) => [encodeSourceId(source.workspace.linearWorkspaceId, source.project.id), source]));
326
+ const selected = sourceIds.map((sourceId) => sourceMap.get(sourceId)).filter((source) => !!source);
327
+ const cursors = decodeCursor(cursor, sourceIds);
328
+ const normalizedLabels = normalizeLabels(labels);
329
+ const nextState = {};
330
+ let hasNextPage = false;
331
+ return {
332
+ issues: (await Promise.all(selected.map(async (source) => {
333
+ const sourceId = encodeSourceId(source.workspace.linearWorkspaceId, source.project.id);
334
+ if (cursors[sourceId] === null) {
335
+ nextState[sourceId] = null;
336
+ return [];
337
+ }
338
+ const query = new URLSearchParams({
339
+ first: String(PAGE_SIZE),
340
+ projectIds: source.project.id,
341
+ stateType: "triage,backlog,unstarted,started",
342
+ orderBy: "updatedAt"
343
+ });
344
+ const after = cursors[sourceId];
345
+ if (after) query.set("after", after);
346
+ const result = await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(source.workspace.linearWorkspaceId)}/issues?${query}`);
347
+ const next = result.pageInfo.hasNextPage ? result.pageInfo.endCursor : null;
348
+ nextState[sourceId] = next;
349
+ hasNextPage ||= next !== null;
350
+ return result.issues.filter((issue) => normalizedLabels.length === 0 || issue.labels.some((label) => normalizedLabels.includes(label.name))).map((issue) => ({
351
+ issue,
352
+ source
353
+ }));
354
+ }))).flat(),
355
+ nextCursor: hasNextPage ? encodeCursor(nextState, sourceIds) : null
356
+ };
357
+ }
358
+ async #findIssue(sourceId, issueId) {
359
+ const workspaceIds = await this.#candidateWorkspaceIds(sourceId);
360
+ for (const workspaceId of workspaceIds) try {
361
+ return {
362
+ workspaceId,
363
+ issue: await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}?include=comments`)
364
+ };
365
+ } catch (error) {
366
+ if (!isNotFound(error)) throw error;
367
+ }
368
+ return null;
369
+ }
370
+ async #resolveWorkspaceForIssue(sourceId, issueId) {
371
+ const workspaceIds = await this.#candidateWorkspaceIds(sourceId);
372
+ if (workspaceIds.length === 1) return workspaceIds[0];
373
+ for (const workspaceId of workspaceIds) try {
374
+ await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}`);
375
+ return workspaceId;
376
+ } catch (error) {
377
+ if (!isNotFound(error)) throw error;
378
+ }
379
+ return null;
380
+ }
381
+ async #listWorkflowStates(workspaceId, teamId) {
382
+ const cacheKey = `${workspaceId}:${teamId}`;
383
+ const cached = this.#workflowStatesByTeam.get(cacheKey);
384
+ if (cached) return cached;
385
+ const result = await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/workflow-states?teamId=${encodeURIComponent(teamId)}&first=100`);
386
+ this.#workflowStatesByTeam.set(cacheKey, result.workflowStates);
387
+ return result.workflowStates;
388
+ }
389
+ async #candidateWorkspaceIds(sourceId) {
390
+ if (sourceId) return [decodeSourceId(sourceId).workspaceId];
391
+ return (await this.#listWorkspaces()).map((workspace) => workspace.linearWorkspaceId);
392
+ }
393
+ async #loadComments(workspaceId, issueId, embedded) {
394
+ const comments = [...embedded?.nodes ?? []];
395
+ let pageInfo = embedded?.pageInfo;
396
+ let page = 0;
397
+ while (pageInfo?.hasNextPage && pageInfo.endCursor && page < MAX_COMMENT_PAGES) {
398
+ const result = await this.#client.request("GET", `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments?first=200&after=${encodeURIComponent(pageInfo.endCursor)}`);
399
+ comments.push(...result.comments);
400
+ pageInfo = result.pageInfo;
401
+ page += 1;
402
+ }
403
+ return comments;
404
+ }
1261
405
  };
1262
406
  function parseIssue(issue) {
1263
- return {
1264
- id: issue.id,
1265
- identifier: issue.identifier,
1266
- title: issue.title,
1267
- url: issue.url,
1268
- author: issue.creator?.displayName ?? issue.creator?.name ?? null,
1269
- state: issue.state.name,
1270
- stateType: issue.state.type,
1271
- priority: issue.priorityLabel,
1272
- assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,
1273
- source: issue.team.key,
1274
- labels: issue.labels.map((label) => label.name),
1275
- commentCount: null,
1276
- createdAt: issue.createdAt,
1277
- updatedAt: issue.updatedAt
1278
- };
407
+ return {
408
+ id: issue.id,
409
+ identifier: issue.identifier,
410
+ title: issue.title,
411
+ url: issue.url,
412
+ author: issue.creator?.displayName ?? issue.creator?.name ?? null,
413
+ state: issue.state.name,
414
+ stateType: issue.state.type,
415
+ priority: issue.priorityLabel,
416
+ assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,
417
+ source: issue.team.key,
418
+ labels: issue.labels.map((label) => label.name),
419
+ commentCount: null,
420
+ createdAt: issue.createdAt,
421
+ updatedAt: issue.updatedAt
422
+ };
1279
423
  }
1280
424
  function parseIssueDetail(issue, comments) {
1281
- return {
1282
- ...parseIssue(issue),
1283
- commentCount: comments.length,
1284
- description: issue.description?.trim() ? issue.description : null,
1285
- comments: comments.map((comment) => ({
1286
- author: comment.user?.displayName ?? comment.user?.name ?? null,
1287
- body: comment.body,
1288
- createdAt: comment.createdAt
1289
- }))
1290
- };
425
+ return {
426
+ ...parseIssue(issue),
427
+ commentCount: comments.length,
428
+ description: issue.description?.trim() ? issue.description : null,
429
+ comments: comments.map((comment) => ({
430
+ author: comment.user?.displayName ?? comment.user?.name ?? null,
431
+ body: comment.body,
432
+ createdAt: comment.createdAt
433
+ }))
434
+ };
1291
435
  }
1292
436
  function encodeSourceId(workspaceId, projectId) {
1293
- return `linear-project:${Buffer.from(JSON.stringify({ workspaceId, projectId })).toString("base64url")}`;
437
+ return `linear-project:${Buffer.from(JSON.stringify({
438
+ workspaceId,
439
+ projectId
440
+ })).toString("base64url")}`;
1294
441
  }
1295
442
  function decodeSourceId(sourceId) {
1296
- if (!sourceId.startsWith("linear-project:")) throw new Error("Linear project source id is invalid.");
1297
- try {
1298
- const parsed = JSON.parse(Buffer.from(sourceId.slice("linear-project:".length), "base64url").toString("utf8"));
1299
- if (typeof parsed.workspaceId !== "string" || !parsed.workspaceId) throw new Error();
1300
- if (typeof parsed.projectId !== "string" || !parsed.projectId) throw new Error();
1301
- return { workspaceId: parsed.workspaceId, projectId: parsed.projectId };
1302
- } catch {
1303
- throw new Error("Linear project source id is invalid.");
1304
- }
443
+ if (!sourceId.startsWith("linear-project:")) throw new Error("Linear project source id is invalid.");
444
+ try {
445
+ const parsed = JSON.parse(Buffer.from(sourceId.slice(15), "base64url").toString("utf8"));
446
+ if (typeof parsed.workspaceId !== "string" || !parsed.workspaceId) throw new Error();
447
+ if (typeof parsed.projectId !== "string" || !parsed.projectId) throw new Error();
448
+ return {
449
+ workspaceId: parsed.workspaceId,
450
+ projectId: parsed.projectId
451
+ };
452
+ } catch {
453
+ throw new Error("Linear project source id is invalid.");
454
+ }
1305
455
  }
1306
456
  function normalizeLabels(labels) {
1307
- return [...new Set((labels ?? []).map((label) => label.trim()).filter(Boolean))];
457
+ return [...new Set((labels ?? []).map((label) => label.trim()).filter(Boolean))];
1308
458
  }
1309
459
  function decodeCursor(cursor, sourceIds) {
1310
- if (!cursor) return {};
1311
- if (sourceIds.length === 1) return { [sourceIds[0]]: cursor };
1312
- try {
1313
- const parsed = JSON.parse(cursor);
1314
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
1315
- return parsed;
1316
- } catch {
1317
- throw new Error("Linear cursor is invalid.");
1318
- }
460
+ if (!cursor) return {};
461
+ if (sourceIds.length === 1) return { [sourceIds[0]]: cursor };
462
+ try {
463
+ const parsed = JSON.parse(cursor);
464
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
465
+ return parsed;
466
+ } catch {
467
+ throw new Error("Linear cursor is invalid.");
468
+ }
1319
469
  }
1320
470
  function encodeCursor(state, sourceIds) {
1321
- if (sourceIds.length === 1) return state[sourceIds[0]];
1322
- return JSON.stringify(state);
471
+ if (sourceIds.length === 1) return state[sourceIds[0]];
472
+ return JSON.stringify(state);
1323
473
  }
1324
474
  function requireLinearConnection(connection) {
1325
- if (connection.type !== "oauth") {
1326
- throw new Error("Linear capabilities require an OAuth connection.");
1327
- }
475
+ if (connection.type !== "oauth") throw new Error("Linear capabilities require an OAuth connection.");
1328
476
  }
1329
477
  function isNotFound(error) {
1330
- return error instanceof PlatformApiError && error.status === 404;
478
+ return error instanceof PlatformApiError && error.status === 404;
1331
479
  }
1332
- export {
1333
- PlatformLinearIntegration
1334
- };
480
+ //#endregion
481
+ export { PlatformLinearIntegration };
482
+
1335
483
  //# sourceMappingURL=integration.js.map