@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.
- package/CHANGELOG.md +10 -0
- package/dist/auth.js +401 -334
- package/dist/auth.js.map +1 -1
- package/dist/capabilities/connection.js +1 -1
- package/dist/capabilities/intake.js +1 -1
- package/dist/capabilities/version-control.js +1 -1
- package/dist/factory.js +441 -39622
- package/dist/factory.js.map +1 -1
- package/dist/index.js +2 -39633
- package/dist/integrations/base.js +1 -1
- package/dist/integrations/github/config.js +36 -22
- package/dist/integrations/github/config.js.map +1 -1
- package/dist/integrations/github/factory-session.js +33 -37
- package/dist/integrations/github/factory-session.js.map +1 -1
- package/dist/integrations/github/integration.js +1037 -3672
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-triage.js +71 -71
- package/dist/integrations/github/issue-triage.js.map +1 -1
- package/dist/integrations/github/pat.js +47 -36
- package/dist/integrations/github/pat.js.map +1 -1
- package/dist/integrations/github/provenance.js +72 -101
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.js +1272 -1881
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +203 -471
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +400 -345
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +165 -223
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.js +43 -51
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.js +15 -18
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/github/webhook.js +359 -343
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/linear/agent-tools.js +77 -100
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js +611 -1309
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.js +227 -230
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.js +126 -346
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/linear/storage.js +1 -1
- package/dist/integrations/platform/api-client.js +131 -135
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +275 -573
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +845 -4022
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js +457 -1309
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/workos/integration.js +80 -85
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +933 -912
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/custom-provider-source.js +110 -93
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.js +401 -349
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +165 -165
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +447 -25166
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +437 -376
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.js +76 -58
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/route.js +12 -8
- package/dist/routes/route.js.map +1 -1
- package/dist/routes/skills.js +160 -173
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/surface.js +250 -28615
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js +141 -166
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js +654 -862
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +23 -35
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.js +200 -309
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.js +438 -683
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.js +5 -599
- package/dist/rules/processor.js +344 -596
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.js +21 -18
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.js +168 -157
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +50 -85
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js +183 -454
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.js +33 -29
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.js +266 -281
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/fleet.js +272 -250
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/sandbox/reattach.js +13 -6
- package/dist/sandbox/reattach.js.map +1 -1
- package/dist/server-error.js +12 -16
- package/dist/server-error.js.map +1 -1
- package/dist/skills/service.js +30 -35
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.js +91 -65
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +73 -44
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +60 -59
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js +169 -112
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js +155 -153
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/credentials/base.js +259 -243
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +138 -143
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js +75 -57
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js +310 -245
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js +117 -102
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-packs/base.js +89 -88
- package/dist/storage/domains/model-packs/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js +97 -92
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js +111 -76
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.js +777 -706
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +322 -348
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.js +1266 -1148
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js +202 -184
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/timing.js +29 -22
- package/dist/timing.js.map +1 -1
- package/dist/workspace.js +236 -477
- package/dist/workspace.js.map +1 -1
- package/package.json +8 -8
- package/dist/capabilities/connection.js.map +0 -1
- package/dist/capabilities/intake.js.map +0 -1
- package/dist/capabilities/version-control.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/base.js.map +0 -1
- package/dist/integrations/linear/storage.js.map +0 -1
- package/dist/rules/index.js.map +0 -1
|
@@ -1,1335 +1,483 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
+
return (ctx.baseUrl || new URL(requestUrl).origin).replace(/\/+$/, "");
|
|
844
17
|
}
|
|
845
18
|
var PlatformLinearIntegration = class {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
-
|
|
437
|
+
return `linear-project:${Buffer.from(JSON.stringify({
|
|
438
|
+
workspaceId,
|
|
439
|
+
projectId
|
|
440
|
+
})).toString("base64url")}`;
|
|
1294
441
|
}
|
|
1295
442
|
function decodeSourceId(sourceId) {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
-
|
|
457
|
+
return [...new Set((labels ?? []).map((label) => label.trim()).filter(Boolean))];
|
|
1308
458
|
}
|
|
1309
459
|
function decodeCursor(cursor, sourceIds) {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
1322
|
-
|
|
471
|
+
if (sourceIds.length === 1) return state[sourceIds[0]];
|
|
472
|
+
return JSON.stringify(state);
|
|
1323
473
|
}
|
|
1324
474
|
function requireLinearConnection(connection) {
|
|
1325
|
-
|
|
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
|
-
|
|
478
|
+
return error instanceof PlatformApiError && error.status === 404;
|
|
1331
479
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
480
|
+
//#endregion
|
|
481
|
+
export { PlatformLinearIntegration };
|
|
482
|
+
|
|
1335
483
|
//# sourceMappingURL=integration.js.map
|