@sideboard-ai/core 0.1.324 → 0.1.326

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 (83) hide show
  1. package/dist/{abletime-CU2UPMJF.js → abletime-RDHYSZL2.js} +5 -4
  2. package/dist/{abletime-5ZY3ZUUO.js → abletime-RYYFGKMU.js} +6 -5
  3. package/dist/{abletime-oauth-7EOSBEFM.js → abletime-oauth-OYGS2DAU.js} +3 -3
  4. package/dist/{abletime-oauth-4TTCR3CT.js → abletime-oauth-QR2QDVMK.js} +3 -3
  5. package/dist/{abletime-rest-KOKE7V4B.js → abletime-rest-BKYKNPTC.js} +3 -3
  6. package/dist/{abletime-rest-3TG4G7LW.js → abletime-rest-QDPTWNS4.js} +3 -3
  7. package/dist/agents/claude-runner.cjs +59 -22
  8. package/dist/agents/claude-runner.js +39 -16
  9. package/dist/agents/cursor-runner.cjs +97 -32
  10. package/dist/agents/cursor-runner.js +74 -24
  11. package/dist/{agents-YK65UDLT.js → agents-HANWZNI6.js} +9 -8
  12. package/dist/{agents-LUSTCMDN.js → agents-QO7GKBYT.js} +12 -11
  13. package/dist/{app-settings-XIF4EC4K.js → app-settings-RXBH7KY4.js} +1 -1
  14. package/dist/{app-settings-I5HM5RMP.js → app-settings-TUVLZNDE.js} +1 -1
  15. package/dist/{chunk-7MQSYPAY.js → chunk-2EOLWHXH.js} +206 -188
  16. package/dist/{chunk-QOS4465Y.js → chunk-3Q4XNYHY.js} +1 -1
  17. package/dist/{chunk-RR4WCGNB.js → chunk-46FECFZU.js} +18 -15
  18. package/dist/{chunk-5GDJSFCT.js → chunk-4H5MWZ6J.js} +2 -2
  19. package/dist/{chunk-EQ52JR3Q.js → chunk-5MXLDDYF.js} +1 -1
  20. package/dist/{chunk-NCHOTN3C.js → chunk-5YCKATG6.js} +4 -3
  21. package/dist/{chunk-ROOZDDFA.js → chunk-75RPUXJS.js} +7 -8
  22. package/dist/{chunk-INBUV7HY.js → chunk-B244TJWC.js} +2 -2
  23. package/dist/{chunk-JH5SMVM5.js → chunk-BBNWKL4I.js} +268 -235
  24. package/dist/{chunk-ODH3XHMI.js → chunk-C7HUCBX7.js} +5 -5
  25. package/dist/{chunk-OIHRC2ZJ.js → chunk-D744HOI5.js} +25 -3
  26. package/dist/chunk-DBKHL7NC.js +214 -0
  27. package/dist/chunk-F6GE7V3Q.js +216 -0
  28. package/dist/{chunk-JE3JLHMO.js → chunk-FWVUGLBG.js} +3 -3
  29. package/dist/{chunk-DF5BM5OY.js → chunk-GP3RLZ6A.js} +207 -186
  30. package/dist/{chunk-FR3SAYUL.js → chunk-JM2TGOHP.js} +5 -5
  31. package/dist/{chunk-Y3WREIPQ.js → chunk-K7M7MIWL.js} +12 -210
  32. package/dist/{chunk-FUFNMUNR.js → chunk-KECHK3ZT.js} +7 -8
  33. package/dist/{chunk-EKU7FQVG.js → chunk-KXRF6RNL.js} +2 -2
  34. package/dist/chunk-LRHUBGEM.js +1259 -0
  35. package/dist/{chunk-MZIZTSR2.js → chunk-MAW7DIG7.js} +2 -2
  36. package/dist/{chunk-ZHGMRBSC.js → chunk-NURZOXGN.js} +4 -4
  37. package/dist/{chunk-QIC2JXHW.js → chunk-OOXRMTU6.js} +1 -1
  38. package/dist/{chunk-5XNT6BCG.js → chunk-OPXEJJBM.js} +1 -1
  39. package/dist/chunk-OYRN356W.js +80 -0
  40. package/dist/{chunk-7PVT73H6.js → chunk-PB6HW7SA.js} +12 -210
  41. package/dist/{chunk-B4CDSSRN.js → chunk-Q3FHTGIW.js} +2 -0
  42. package/dist/{chunk-7D5QIU3N.js → chunk-QG4CYRA3.js} +301 -268
  43. package/dist/{chunk-EJ66LEKH.js → chunk-QPSKQP6E.js} +24 -3
  44. package/dist/{chunk-Q3YJZPQI.js → chunk-RZVOX2MT.js} +3 -3
  45. package/dist/chunk-SO7G6O24.js +1445 -0
  46. package/dist/{chunk-YDUHLWI5.js → chunk-SUZOPJZC.js} +21 -7
  47. package/dist/chunk-TMCK5TLA.js +25 -0
  48. package/dist/chunk-W3ZLE4PO.js +77 -0
  49. package/dist/chunk-WUCAVIIP.js +28 -0
  50. package/dist/{chunk-BYUCNIXK.js → chunk-XLVLMQKN.js} +13 -9
  51. package/dist/{chunk-ZIRQZUZY.js → chunk-YGVGXQA7.js} +3 -3
  52. package/dist/{chunk-NXUN4U5P.js → chunk-YWQF7WAG.js} +2 -2
  53. package/dist/{chunk-IFWGG2M2.js → chunk-ZSHS46V5.js} +2 -0
  54. package/dist/{chunk-74OPNF7U.js → chunk-ZUAYA57W.js} +2 -2
  55. package/dist/{conductor-SPZOTC7W.js → conductor-4GVTHUUB.js} +7 -7
  56. package/dist/{conductor-JFABMXDA.js → conductor-TGWI6ETZ.js} +6 -6
  57. package/dist/{coordinator-prompt-K66RGNWK.js → coordinator-prompt-3E6SIDTM.js} +4 -4
  58. package/dist/{coordinator-prompt-Z6UFHFDH.js → coordinator-prompt-WEYJREV6.js} +4 -4
  59. package/dist/{cursor-recover-TEBFKAZ4.js → cursor-recover-7F46PAJ5.js} +1 -1
  60. package/dist/{cursor-recover-OWPQUHFH.js → cursor-recover-BDIAZIE3.js} +1 -1
  61. package/dist/{global-workspace-A6UO6PJV.js → global-workspace-FPDZKUPF.js} +4 -4
  62. package/dist/{global-workspace-RHEO55B4.js → global-workspace-UIEXAG2B.js} +4 -4
  63. package/dist/index.cjs +21094 -20840
  64. package/dist/index.d.cts +36 -17
  65. package/dist/index.d.ts +36 -17
  66. package/dist/index.js +343 -1678
  67. package/dist/linear-6S7MK5YK.js +69 -0
  68. package/dist/linear-Z2T4KUAQ.js +67 -0
  69. package/dist/linear-list-A37WLUBO.js +6 -0
  70. package/dist/linear-list-Q4M7AZO2.js +8 -0
  71. package/dist/mcp/run-stdio.cjs +15903 -15640
  72. package/dist/mcp/run-stdio.js +67 -1247
  73. package/dist/{optional-cli-RMMS5L6F.js → optional-cli-AANBL4U5.js} +3 -3
  74. package/dist/{optional-cli-Y3Z3GIPZ.js → optional-cli-CA4LL322.js} +4 -4
  75. package/dist/{orchestrator-7VTAUY24.js → orchestrator-7FVDBE7Y.js} +15 -13
  76. package/dist/{orchestrator-2O6OJQLX.js → orchestrator-RQXRZW7Z.js} +14 -12
  77. package/dist/{thread-store-3F7GR64A.js → thread-store-4ZN3DS2W.js} +1 -1
  78. package/dist/{thread-store-JRSL4HQY.js → thread-store-IC4BQXDW.js} +1 -1
  79. package/dist/{workspaces-NRKEXOYR.js → workspaces-LEW4UP56.js} +4 -4
  80. package/dist/{workspaces-42E6AGXD.js → workspaces-QONNLHNR.js} +4 -4
  81. package/dist/{worktree-ZDA2UJ7C.js → worktree-6GIONDQO.js} +3 -3
  82. package/dist/{worktree-4NGXCCUO.js → worktree-KFEJAQ2H.js} +3 -3
  83. package/package.json +1 -1
@@ -0,0 +1,1445 @@
1
+ import {
2
+ downloadAuthenticatedFile,
3
+ extractHttpUrls,
4
+ hostMatches,
5
+ issueAttachmentWorktreePath,
6
+ looksLikeHttpUrl,
7
+ mergeIssueAttachments
8
+ } from "./chunk-DBKHL7NC.js";
9
+ import {
10
+ previewIssueCommentBody
11
+ } from "./chunk-W3ZLE4PO.js";
12
+ import {
13
+ httpFetch
14
+ } from "./chunk-JAT3JFOS.js";
15
+ import {
16
+ disconnectLinearConnection,
17
+ loadAppSettings,
18
+ saveLinearOAuth
19
+ } from "./chunk-QPSKQP6E.js";
20
+
21
+ // src/integrations/linear-oauth.ts
22
+ import { createHash, randomBytes } from "crypto";
23
+ import { createServer } from "http";
24
+
25
+ // src/integrations/linear-app.ts
26
+ var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
27
+ function hasBakedLinearOAuth() {
28
+ return Boolean(BAKED_LINEAR_CLIENT_ID.trim());
29
+ }
30
+
31
+ // src/integrations/linear-oauth.ts
32
+ var LINEAR_OAUTH_PORT = 19848;
33
+ var LINEAR_OAUTH_REDIRECT = `http://127.0.0.1:${LINEAR_OAUTH_PORT}/callback`;
34
+ var LINEAR_AUTHORIZE = "https://linear.app/oauth/authorize";
35
+ var LINEAR_TOKEN = "https://api.linear.app/oauth/token";
36
+ var LINEAR_REVOKE = "https://api.linear.app/oauth/revoke";
37
+ var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
38
+ var LINEAR_OAUTH_SCOPES = "read,write";
39
+ var REFRESH_SKEW_MS = 5 * 6e4;
40
+ function linearOAuthCredentials() {
41
+ const settings = loadAppSettings();
42
+ const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
43
+ const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || "";
44
+ if (!clientId) {
45
+ throw new Error(
46
+ `Linear browser sign-in needs a Linear OAuth app Client ID. Create one at linear.app/settings/api/applications/new with callback ${LINEAR_OAUTH_REDIRECT}, then set SIDEBOARD_LINEAR_CLIENT_ID (PKCE does not require a secret). You can still paste a personal API key.`
47
+ );
48
+ }
49
+ return { clientId, clientSecret };
50
+ }
51
+ function createLinearPkce() {
52
+ const verifier = randomBytes(32).toString("base64url");
53
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
54
+ return { verifier, challenge };
55
+ }
56
+ function linearOAuthAuthorizeUrl(clientId, state, codeChallenge) {
57
+ const params = new URLSearchParams({
58
+ client_id: clientId,
59
+ redirect_uri: LINEAR_OAUTH_REDIRECT,
60
+ response_type: "code",
61
+ scope: LINEAR_OAUTH_SCOPES,
62
+ state,
63
+ prompt: "consent",
64
+ actor: "user",
65
+ code_challenge: codeChallenge,
66
+ code_challenge_method: "S256"
67
+ });
68
+ return `${LINEAR_AUTHORIZE}?${params.toString()}`;
69
+ }
70
+ function linearAuthorizationHeader(token) {
71
+ const t = token.trim();
72
+ if (!t) return t;
73
+ if (/^bearer\s+/i.test(t)) return t;
74
+ if (t.startsWith("lin_api_")) return t;
75
+ return `Bearer ${t}`;
76
+ }
77
+ function htmlPage(title, body) {
78
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
79
+ <style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
80
+ h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
81
+ }
82
+ async function exchangeLinearToken(body) {
83
+ const res = await httpFetch(LINEAR_TOKEN, {
84
+ method: "POST",
85
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
86
+ body
87
+ });
88
+ const data = await res.json().catch(() => ({}));
89
+ if (!res.ok || data.error || !data.access_token?.trim()) {
90
+ const detail = data.error_description || data.error || `HTTP ${res.status}`;
91
+ throw new Error(`Linear OAuth token exchange failed: ${detail}`);
92
+ }
93
+ return data;
94
+ }
95
+ async function persistLinearTokens(data, viewer) {
96
+ return saveLinearOAuth({
97
+ accessToken: data.access_token.trim(),
98
+ refreshToken: data.refresh_token?.trim(),
99
+ expiresIn: data.expires_in,
100
+ viewerName: viewer?.name,
101
+ organizationName: viewer?.organizationName
102
+ });
103
+ }
104
+ async function fetchLinearViewer(accessToken) {
105
+ const res = await httpFetch(LINEAR_GRAPHQL, {
106
+ method: "POST",
107
+ headers: {
108
+ "Content-Type": "application/json",
109
+ Authorization: linearAuthorizationHeader(accessToken)
110
+ },
111
+ body: JSON.stringify({
112
+ query: "{ viewer { name organization { name } } }"
113
+ })
114
+ });
115
+ if (!res.ok) return {};
116
+ const json = await res.json().catch(() => ({}));
117
+ const viewer = json.data?.viewer;
118
+ return {
119
+ name: viewer?.name?.trim() || void 0,
120
+ organizationName: viewer?.organization?.name?.trim() || void 0
121
+ };
122
+ }
123
+ var refreshInFlight = null;
124
+ async function refreshLinearAccessToken(refreshToken) {
125
+ const { clientId, clientSecret } = linearOAuthCredentials();
126
+ const body = new URLSearchParams({
127
+ grant_type: "refresh_token",
128
+ refresh_token: refreshToken,
129
+ client_id: clientId
130
+ });
131
+ if (clientSecret) body.set("client_secret", clientSecret);
132
+ try {
133
+ const data = await exchangeLinearToken(body);
134
+ await persistLinearTokens(data);
135
+ return data.access_token.trim();
136
+ } catch (err) {
137
+ const message = err instanceof Error ? err.message : String(err);
138
+ if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
139
+ disconnectLinearConnection();
140
+ throw new Error("Linear sign-in expired \u2014 connect again in Account settings");
141
+ }
142
+ throw err;
143
+ }
144
+ }
145
+ async function getLinearAuthToken(settings = loadAppSettings()) {
146
+ const access = settings.integrations.linearAccessToken?.trim() || "";
147
+ const refresh = settings.integrations.linearRefreshToken?.trim() || "";
148
+ const expiresAt = settings.integrations.linearTokenExpiresAt ?? 0;
149
+ const apiKey = settings.integrations.linearApiKey?.trim() || "";
150
+ if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS)) {
151
+ return access;
152
+ }
153
+ if (refresh) {
154
+ if (!refreshInFlight) {
155
+ refreshInFlight = refreshLinearAccessToken(refresh).finally(() => {
156
+ refreshInFlight = null;
157
+ });
158
+ }
159
+ return refreshInFlight.catch((err) => {
160
+ if (apiKey) return apiKey;
161
+ throw err;
162
+ });
163
+ }
164
+ return apiKey || null;
165
+ }
166
+ var LINEAR_OAUTH_CANCELLED = "Linear sign-in cancelled";
167
+ var LinearOAuthCancelledError = class extends Error {
168
+ constructor(message = LINEAR_OAUTH_CANCELLED) {
169
+ super(message);
170
+ this.name = "LinearOAuthCancelledError";
171
+ }
172
+ };
173
+ function isLinearOAuthCancelled(err) {
174
+ if (err instanceof LinearOAuthCancelledError) return true;
175
+ const message = err instanceof Error ? err.message : String(err);
176
+ if (message.includes(LINEAR_OAUTH_CANCELLED)) return true;
177
+ return err instanceof Error && err.name === "LinearOAuthCancelledError";
178
+ }
179
+ async function startLinearOAuth(opts) {
180
+ if (opts?.signal?.aborted) {
181
+ throw new LinearOAuthCancelledError();
182
+ }
183
+ const { clientId, clientSecret } = linearOAuthCredentials();
184
+ const state = randomBytes(16).toString("hex");
185
+ const pkce = createLinearPkce();
186
+ const authorizeUrl = linearOAuthAuthorizeUrl(clientId, state, pkce.challenge);
187
+ const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
188
+ const code = await new Promise((resolve, reject) => {
189
+ let settled = false;
190
+ const finish = (err, value) => {
191
+ if (settled) return;
192
+ settled = true;
193
+ cleanup();
194
+ if (err) reject(err);
195
+ else resolve(value);
196
+ };
197
+ const server = createServer((req, res) => {
198
+ try {
199
+ const url = new URL(req.url || "/", `http://127.0.0.1:${LINEAR_OAUTH_PORT}`);
200
+ if (url.pathname !== "/callback") {
201
+ res.writeHead(404);
202
+ res.end("Not found");
203
+ return;
204
+ }
205
+ const err = url.searchParams.get("error");
206
+ const gotState = url.searchParams.get("state");
207
+ const gotCode = url.searchParams.get("code");
208
+ if (err) {
209
+ res.writeHead(400, { "Content-Type": "text/html" });
210
+ res.end(htmlPage("Linear", `<h1>Authorization cancelled</h1><p>${err}</p>`));
211
+ finish(new LinearOAuthCancelledError(`Linear OAuth: ${err}`));
212
+ return;
213
+ }
214
+ if (gotState !== state || !gotCode) {
215
+ res.writeHead(400, { "Content-Type": "text/html" });
216
+ res.end(htmlPage("Linear", "<h1>Invalid callback</h1><p>State or code missing.</p>"));
217
+ finish(new Error("Linear OAuth callback was invalid"));
218
+ return;
219
+ }
220
+ res.writeHead(200, { "Content-Type": "text/html" });
221
+ res.end(
222
+ htmlPage(
223
+ "Linear connected",
224
+ "<h1>Linear connected</h1><p>You can close this tab and return to Sideboard.</p>"
225
+ )
226
+ );
227
+ finish(void 0, gotCode);
228
+ } catch (e) {
229
+ finish(e instanceof Error ? e : new Error(String(e)));
230
+ }
231
+ });
232
+ const timer = setTimeout(() => {
233
+ finish(new Error("Linear sign-in timed out \u2014 try again"));
234
+ }, timeoutMs);
235
+ const onAbort = () => finish(new LinearOAuthCancelledError());
236
+ const cleanup = () => {
237
+ clearTimeout(timer);
238
+ opts?.signal?.removeEventListener("abort", onAbort);
239
+ server.close();
240
+ };
241
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
242
+ if (opts?.signal?.aborted) {
243
+ onAbort();
244
+ return;
245
+ }
246
+ server.on("error", (e) => {
247
+ finish(
248
+ e instanceof Error && e.code === "EADDRINUSE" ? new Error(
249
+ `Port ${LINEAR_OAUTH_PORT} is in use. Close whatever is bound there, or paste a Linear API key instead.`
250
+ ) : e instanceof Error ? e : new Error(String(e))
251
+ );
252
+ });
253
+ server.listen(LINEAR_OAUTH_PORT, "127.0.0.1", () => {
254
+ void Promise.resolve(opts?.openUrl?.(authorizeUrl)).catch((e) => {
255
+ finish(e instanceof Error ? e : new Error(String(e)));
256
+ });
257
+ });
258
+ });
259
+ const body = new URLSearchParams({
260
+ grant_type: "authorization_code",
261
+ code,
262
+ redirect_uri: LINEAR_OAUTH_REDIRECT,
263
+ client_id: clientId,
264
+ code_verifier: pkce.verifier
265
+ });
266
+ if (clientSecret) body.set("client_secret", clientSecret);
267
+ const data = await exchangeLinearToken(body);
268
+ const viewer = await fetchLinearViewer(data.access_token.trim()).catch(() => ({}));
269
+ return persistLinearTokens(data, viewer);
270
+ }
271
+ async function revokeLinearToken(token, tokenTypeHint) {
272
+ const body = new URLSearchParams({
273
+ token,
274
+ token_type_hint: tokenTypeHint
275
+ });
276
+ await httpFetch(LINEAR_REVOKE, {
277
+ method: "POST",
278
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
279
+ body
280
+ }).catch(() => void 0);
281
+ }
282
+ async function disconnectLinear() {
283
+ const settings = loadAppSettings();
284
+ const access = settings.integrations.linearAccessToken?.trim();
285
+ const refresh = settings.integrations.linearRefreshToken?.trim();
286
+ if (access) await revokeLinearToken(access, "access_token");
287
+ if (refresh) await revokeLinearToken(refresh, "refresh_token");
288
+ return disconnectLinearConnection();
289
+ }
290
+
291
+ // src/integrations/linear-attachments.ts
292
+ var LINEAR_GRAPHQL2 = "https://api.linear.app/graphql";
293
+ var LINEAR_FILE_HOSTS = ["linear.app", "uploads.linear.app", "linearusercontent.com"];
294
+ var LINEAR_LINK_SOURCE_TYPES = /* @__PURE__ */ new Set([
295
+ "github",
296
+ "githubPullRequest",
297
+ "gitlab",
298
+ "figma",
299
+ "slack",
300
+ "sentry",
301
+ "zendesk",
302
+ "intercom",
303
+ "front"
304
+ ]);
305
+ var ATTACHMENT_QUERY = `
306
+ query SideboardAttachment($id: String!) {
307
+ attachment(id: $id) {
308
+ id
309
+ title
310
+ url
311
+ subtitle
312
+ sourceType
313
+ }
314
+ }
315
+ `;
316
+ function mapLinearAttachments(node) {
317
+ const listed = [];
318
+ for (const attachment of node.attachments?.nodes ?? []) {
319
+ const id = String(attachment.id ?? "");
320
+ const url = String(attachment.url ?? "").trim();
321
+ if (!id && !url) continue;
322
+ listed.push({
323
+ id,
324
+ title: String(attachment.title ?? ""),
325
+ url,
326
+ subtitle: attachment.subtitle?.trim() || void 0,
327
+ sourceType: attachment.sourceType?.trim() || void 0
328
+ });
329
+ }
330
+ const embedded = [
331
+ ...extractHttpUrls(node.description).filter((url) => hostMatches(url, LINEAR_FILE_HOSTS)),
332
+ ...(node.comments?.nodes ?? []).flatMap(
333
+ (comment) => extractHttpUrls(comment.body).filter((url) => hostMatches(url, LINEAR_FILE_HOSTS))
334
+ )
335
+ ];
336
+ const merged = mergeIssueAttachments([
337
+ ...listed.map((item) => ({
338
+ id: item.id,
339
+ name: item.title || item.subtitle || "attachment",
340
+ url: item.url,
341
+ sourceType: item.sourceType
342
+ })),
343
+ ...embedded.map((url) => ({
344
+ id: url,
345
+ name: url.split("/").pop() || "image",
346
+ url,
347
+ sourceType: "upload"
348
+ }))
349
+ ]);
350
+ const byUrl = new Map(listed.map((item) => [item.url, item]));
351
+ return merged.map((item) => {
352
+ const prior = byUrl.get(item.url);
353
+ return {
354
+ id: prior?.id || item.id,
355
+ title: prior?.title || item.name,
356
+ url: item.url,
357
+ subtitle: prior?.subtitle,
358
+ sourceType: prior?.sourceType || item.sourceType
359
+ };
360
+ });
361
+ }
362
+ function assertLinearFileUrl(url, sourceType) {
363
+ const kind = sourceType?.trim();
364
+ if (kind && LINEAR_LINK_SOURCE_TYPES.has(kind)) {
365
+ throw new Error(
366
+ `Linear attachment is a ${kind} link, not a file. Open the URL in the browser instead of downloading it.`
367
+ );
368
+ }
369
+ if (!hostMatches(url, LINEAR_FILE_HOSTS)) {
370
+ throw new Error(
371
+ `Linear attachment URL is not a Linear upload (${url}). Only uploads.linear.app / linear.app file hosts can be downloaded with the Linear token.`
372
+ );
373
+ }
374
+ }
375
+ async function requireLinearToken(apiKey) {
376
+ const token = (apiKey ?? await getLinearAuthToken())?.trim();
377
+ if (!token) {
378
+ throw new Error("Linear is not connected \u2014 sign in from Account settings");
379
+ }
380
+ return token;
381
+ }
382
+ async function downloadLinearAttachment(input, opts) {
383
+ const id = input.id.trim();
384
+ if (!id) throw new Error("Linear attachment id or URL is required");
385
+ const apiKey = await requireLinearToken(opts?.apiKey);
386
+ let url = id;
387
+ let name = input.name?.trim();
388
+ let sourceType;
389
+ if (!looksLikeHttpUrl(id)) {
390
+ const res = await httpFetch(LINEAR_GRAPHQL2, {
391
+ method: "POST",
392
+ headers: {
393
+ "Content-Type": "application/json",
394
+ Authorization: linearAuthorizationHeader(apiKey)
395
+ },
396
+ body: JSON.stringify({ query: ATTACHMENT_QUERY, variables: { id } })
397
+ });
398
+ if (!res.ok) {
399
+ throw new Error(`Linear API error ${res.status} looking up attachment ${id}`);
400
+ }
401
+ const json = await res.json();
402
+ if (json.errors?.length) {
403
+ throw new Error(json.errors.map((e) => e.message ?? "Linear error").join("; "));
404
+ }
405
+ const attachment = json.data?.attachment;
406
+ if (!attachment?.url?.trim()) {
407
+ throw new Error(`Linear attachment not found: ${id}`);
408
+ }
409
+ url = attachment.url.trim();
410
+ name = name || attachment.title?.trim() || attachment.subtitle?.trim() || void 0;
411
+ sourceType = attachment.sourceType?.trim() || void 0;
412
+ }
413
+ assertLinearFileUrl(url, sourceType);
414
+ return downloadAuthenticatedFile({
415
+ url,
416
+ headers: { Authorization: linearAuthorizationHeader(apiKey) },
417
+ worktreePath: issueAttachmentWorktreePath(opts?.destPath),
418
+ name
419
+ });
420
+ }
421
+
422
+ // src/integrations/linear.ts
423
+ var LINEAR_GRAPHQL3 = "https://api.linear.app/graphql";
424
+ var LIST_ISSUE_FIELDS = `
425
+ id
426
+ identifier
427
+ title
428
+ url
429
+ createdAt
430
+ updatedAt
431
+ assignee { id name }
432
+ team { id key }
433
+ labels(first: 10) { nodes { name } }
434
+ cycle { id name number startsAt endsAt completedAt }
435
+ project { id name }
436
+ `;
437
+ var ISSUE_REF_FIELDS = `
438
+ id
439
+ identifier
440
+ title
441
+ url
442
+ `;
443
+ var ISSUE_FIELDS = `
444
+ id
445
+ identifier
446
+ title
447
+ description
448
+ url
449
+ priority
450
+ estimate
451
+ dueDate
452
+ branchName
453
+ createdAt
454
+ updatedAt
455
+ completedAt
456
+ state { id name type }
457
+ assignee { id name }
458
+ creator { id name }
459
+ team { id key name }
460
+ labels(first: 50) { nodes { name } }
461
+ cycle { id name number startsAt endsAt completedAt }
462
+ project { id name }
463
+ parent { ${ISSUE_REF_FIELDS} }
464
+ children(first: 25) { nodes { ${ISSUE_REF_FIELDS} } }
465
+ relations(first: 25) {
466
+ nodes {
467
+ id
468
+ type
469
+ relatedIssue { ${ISSUE_REF_FIELDS} }
470
+ }
471
+ }
472
+ inverseRelations(first: 25) {
473
+ nodes {
474
+ id
475
+ type
476
+ issue { ${ISSUE_REF_FIELDS} }
477
+ }
478
+ }
479
+ comments(first: 50) {
480
+ nodes {
481
+ id
482
+ body
483
+ url
484
+ createdAt
485
+ updatedAt
486
+ user { id name }
487
+ }
488
+ }
489
+ attachments(first: 20) {
490
+ nodes { id title url subtitle sourceType }
491
+ }
492
+ `;
493
+ var ASSIGNED_ISSUES_QUERY = `
494
+ query SideboardAssignedIssues($first: Int!, $filter: IssueFilter) {
495
+ viewer {
496
+ id
497
+ name
498
+ assignedIssues(
499
+ first: $first
500
+ orderBy: updatedAt
501
+ filter: $filter
502
+ ) {
503
+ nodes { ${LIST_ISSUE_FIELDS} }
504
+ }
505
+ }
506
+ }
507
+ `;
508
+ var COMMENTS_SINCE_QUERY = `
509
+ query SideboardCommentsSince($first: Int!, $filter: CommentFilter) {
510
+ viewer { id name }
511
+ comments(first: $first, orderBy: createdAt, filter: $filter) {
512
+ nodes {
513
+ id
514
+ body
515
+ url
516
+ createdAt
517
+ user { id name }
518
+ issue { identifier title url }
519
+ }
520
+ }
521
+ }
522
+ `;
523
+ var ISSUES_QUERY = `
524
+ query SideboardIssues($first: Int!, $filter: IssueFilter) {
525
+ viewer { id name }
526
+ issues(first: $first, orderBy: updatedAt, filter: $filter) {
527
+ nodes { ${LIST_ISSUE_FIELDS} }
528
+ }
529
+ }
530
+ `;
531
+ var SEARCH_ISSUES_QUERY = `
532
+ query SideboardSearchIssues($term: String!, $first: Int!, $filter: IssueFilter) {
533
+ viewer { id name }
534
+ searchIssues(term: $term, first: $first, includeArchived: false, filter: $filter) {
535
+ nodes { ${LIST_ISSUE_FIELDS} }
536
+ }
537
+ }
538
+ `;
539
+ var TEAMS_QUERY = `
540
+ query SideboardTeams {
541
+ viewer { id name }
542
+ teams(first: 50) {
543
+ nodes {
544
+ id
545
+ key
546
+ name
547
+ states(first: 50) { nodes { id name type } }
548
+ activeCycle { id name number startsAt endsAt completedAt }
549
+ }
550
+ }
551
+ }
552
+ `;
553
+ var TEAM_CYCLES_QUERY = `
554
+ query SideboardTeamCycles($id: String!) {
555
+ team(id: $id) {
556
+ id
557
+ key
558
+ activeCycle { id name number startsAt endsAt completedAt }
559
+ cycles(first: 40) {
560
+ nodes { id name number startsAt endsAt completedAt }
561
+ }
562
+ }
563
+ }
564
+ `;
565
+ var ISSUE_QUERY = `
566
+ query SideboardIssue($id: String!) {
567
+ issue(id: $id) { ${ISSUE_FIELDS} }
568
+ }
569
+ `;
570
+ var ISSUE_CREATE = `
571
+ mutation SideboardIssueCreate($input: IssueCreateInput!) {
572
+ issueCreate(input: $input) {
573
+ success
574
+ issue { ${ISSUE_FIELDS} }
575
+ }
576
+ }
577
+ `;
578
+ var ISSUE_UPDATE = `
579
+ mutation SideboardIssueUpdate($id: String!, $input: IssueUpdateInput!) {
580
+ issueUpdate(id: $id, input: $input) {
581
+ success
582
+ issue { ${ISSUE_FIELDS} }
583
+ }
584
+ }
585
+ `;
586
+ var COMMENT_CREATE = `
587
+ mutation SideboardCommentCreate($input: CommentCreateInput!) {
588
+ commentCreate(input: $input) {
589
+ success
590
+ comment { id body url }
591
+ }
592
+ }
593
+ `;
594
+ var LABELS_QUERY = `
595
+ query SideboardLabels($first: Int!) {
596
+ issueLabels(first: $first) {
597
+ nodes { id name }
598
+ }
599
+ }
600
+ `;
601
+ var TEAM_LABELS_QUERY = `
602
+ query SideboardTeamLabels($id: String!, $first: Int!, $after: String) {
603
+ team(id: $id) {
604
+ labels(first: $first, after: $after) {
605
+ nodes { id name }
606
+ pageInfo { hasNextPage endCursor }
607
+ }
608
+ }
609
+ }
610
+ `;
611
+ var PROJECTS_QUERY = `
612
+ query SideboardProjects($first: Int!, $filter: ProjectFilter, $after: String) {
613
+ projects(first: $first, filter: $filter, after: $after) {
614
+ nodes { id name slugId }
615
+ pageInfo { hasNextPage endCursor }
616
+ }
617
+ }
618
+ `;
619
+ var ISSUE_RELATION_CREATE = `
620
+ mutation SideboardIssueRelationCreate($input: IssueRelationCreateInput!) {
621
+ issueRelationCreate(input: $input) {
622
+ success
623
+ issueRelation { id type }
624
+ }
625
+ }
626
+ `;
627
+ var ISSUE_RELATION_DELETE = `
628
+ mutation SideboardIssueRelationDelete($id: String!) {
629
+ issueRelationDelete(id: $id) {
630
+ success
631
+ }
632
+ }
633
+ `;
634
+ function rewriteLinearError(message) {
635
+ if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
636
+ return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
637
+ }
638
+ return message;
639
+ }
640
+ async function requireLinearToken2(apiKey) {
641
+ const token = (apiKey ?? await getLinearAuthToken())?.trim();
642
+ if (!token) {
643
+ throw new Error("Linear is not connected \u2014 sign in from Account settings");
644
+ }
645
+ return token;
646
+ }
647
+ async function linearGraphql(query, variables, opts) {
648
+ const apiKey = await requireLinearToken2(opts?.apiKey);
649
+ const res = await httpFetch(LINEAR_GRAPHQL3, {
650
+ method: "POST",
651
+ headers: {
652
+ "Content-Type": "application/json",
653
+ Authorization: linearAuthorizationHeader(apiKey)
654
+ },
655
+ body: JSON.stringify({ query, variables })
656
+ });
657
+ if (!res.ok) {
658
+ const body = await res.text().catch(() => "");
659
+ throw new Error(
660
+ rewriteLinearError(
661
+ `Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
662
+ )
663
+ );
664
+ }
665
+ const json = await res.json();
666
+ if (json.errors?.length) {
667
+ throw new Error(
668
+ rewriteLinearError(json.errors.map((e) => e.message ?? "Linear error").join("; "))
669
+ );
670
+ }
671
+ if (json.data == null) {
672
+ throw new Error("Linear API returned no data");
673
+ }
674
+ return json.data;
675
+ }
676
+ function mapState(node) {
677
+ if (!node?.id) return void 0;
678
+ return {
679
+ id: String(node.id),
680
+ name: String(node.name ?? ""),
681
+ type: String(node.type ?? "")
682
+ };
683
+ }
684
+ function linearCycleIsActive(cycle, now = Date.now()) {
685
+ if (!cycle) return false;
686
+ if (cycle.completedAt) return false;
687
+ const start = cycle.startsAt ? Date.parse(cycle.startsAt) : Number.NaN;
688
+ const end = cycle.endsAt ? Date.parse(cycle.endsAt) : Number.NaN;
689
+ if (Number.isFinite(start) && now < start) return false;
690
+ if (Number.isFinite(end) && now > end) return false;
691
+ return true;
692
+ }
693
+ function cycleDisplayName(node) {
694
+ return String(node?.name ?? (node?.number != null ? `Cycle ${node.number}` : "")).trim();
695
+ }
696
+ function mapNamedCycle(node) {
697
+ if (!node?.id) return null;
698
+ const name = cycleDisplayName(node);
699
+ if (!name) return null;
700
+ return {
701
+ id: String(node.id),
702
+ name,
703
+ number: typeof node.number === "number" ? node.number : void 0,
704
+ isActive: linearCycleIsActive(node)
705
+ };
706
+ }
707
+ function mapCycle(node) {
708
+ if (!node?.name && node?.number == null) return null;
709
+ const name = cycleDisplayName(node);
710
+ if (!name) return null;
711
+ return {
712
+ ...node.id ? { id: String(node.id) } : {},
713
+ name,
714
+ number: typeof node.number === "number" ? node.number : void 0,
715
+ isActive: linearCycleIsActive(node)
716
+ };
717
+ }
718
+ function mapUser(node) {
719
+ if (!node?.id && !node?.name) return void 0;
720
+ return {
721
+ id: String(node.id ?? ""),
722
+ name: String(node.name ?? "")
723
+ };
724
+ }
725
+ function mapIssueRef(node) {
726
+ if (!node?.id && !node?.identifier) return void 0;
727
+ return {
728
+ id: String(node.id ?? node.identifier ?? ""),
729
+ identifier: String(node.identifier ?? node.id ?? ""),
730
+ title: String(node.title ?? ""),
731
+ url: String(node.url ?? "")
732
+ };
733
+ }
734
+ function flipLinearRelationType(type) {
735
+ switch (type) {
736
+ case "blocks":
737
+ return "blockedBy";
738
+ case "blockedBy":
739
+ return "blocks";
740
+ case "duplicate":
741
+ return "duplicateOf";
742
+ case "duplicateOf":
743
+ return "duplicate";
744
+ default:
745
+ return type;
746
+ }
747
+ }
748
+ function mapRelations(node) {
749
+ const out = [];
750
+ for (const rel of node.relations?.nodes ?? []) {
751
+ const issue = mapIssueRef(rel.relatedIssue);
752
+ if (!issue) continue;
753
+ const id = rel.id?.trim();
754
+ out.push({ type: String(rel.type ?? "related"), issue, ...id ? { id } : {} });
755
+ }
756
+ for (const rel of node.inverseRelations?.nodes ?? []) {
757
+ const issue = mapIssueRef(rel.issue);
758
+ if (!issue) continue;
759
+ const id = rel.id?.trim();
760
+ out.push({
761
+ type: flipLinearRelationType(String(rel.type ?? "related")),
762
+ issue,
763
+ ...id ? { id } : {}
764
+ });
765
+ }
766
+ return out;
767
+ }
768
+ function mapComments(node) {
769
+ const out = [];
770
+ for (const comment of node.comments?.nodes ?? []) {
771
+ const id = String(comment.id ?? "");
772
+ const body = String(comment.body ?? "");
773
+ if (!id && !body) continue;
774
+ out.push({
775
+ id,
776
+ body,
777
+ url: comment.url?.trim() || void 0,
778
+ createdAt: comment.createdAt?.trim() || void 0,
779
+ updatedAt: comment.updatedAt?.trim() || void 0,
780
+ user: mapUser(comment.user)
781
+ });
782
+ }
783
+ return out;
784
+ }
785
+ function mapIssue(node) {
786
+ const team = node.team;
787
+ return {
788
+ id: String(node.id ?? node.identifier ?? ""),
789
+ identifier: String(node.identifier ?? node.id ?? ""),
790
+ title: String(node.title ?? ""),
791
+ url: String(node.url ?? ""),
792
+ description: node.description?.trim() || void 0,
793
+ priority: typeof node.priority === "number" ? node.priority : void 0,
794
+ estimate: typeof node.estimate === "number" ? node.estimate : void 0,
795
+ dueDate: node.dueDate?.trim() || void 0,
796
+ branchName: node.branchName?.trim() || void 0,
797
+ createdAt: node.createdAt?.trim() || void 0,
798
+ updatedAt: node.updatedAt?.trim() || void 0,
799
+ completedAt: node.completedAt?.trim() || void 0,
800
+ state: mapState(node.state),
801
+ assignee: mapUser(node.assignee),
802
+ creator: mapUser(node.creator),
803
+ team: team?.id ? {
804
+ id: String(team.id),
805
+ key: String(team.key ?? ""),
806
+ name: String(team.name ?? ""),
807
+ states: (team.states?.nodes ?? []).flatMap((s) => {
808
+ const state = mapState(s);
809
+ return state ? [state] : [];
810
+ })
811
+ } : void 0,
812
+ labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n)),
813
+ cycle: mapCycle(node.cycle),
814
+ project: node.project?.id ? { id: String(node.project.id), name: String(node.project.name ?? "") } : void 0,
815
+ parent: mapIssueRef(node.parent),
816
+ children: (node.children?.nodes ?? []).flatMap((child) => {
817
+ const ref = mapIssueRef(child);
818
+ return ref ? [ref] : [];
819
+ }),
820
+ relations: mapRelations(node),
821
+ comments: mapComments(node),
822
+ attachments: mapLinearAttachments(node)
823
+ };
824
+ }
825
+ function toIssueInfo(issue) {
826
+ return {
827
+ id: issue.id,
828
+ identifier: issue.identifier,
829
+ title: issue.title,
830
+ url: issue.url,
831
+ labels: issue.labels,
832
+ provider: "linear",
833
+ assignee: issue.assignee?.name,
834
+ assignees: issue.assignee?.name ? [issue.assignee.name] : void 0,
835
+ cycle: issue.cycle ?? null,
836
+ teamKey: issue.team?.key || void 0,
837
+ ...issue.project ? { projects: [issue.project] } : {},
838
+ ...issue.createdAt ? { createdAt: issue.createdAt } : {},
839
+ ...issue.updatedAt ? { updatedAt: issue.updatedAt } : {}
840
+ };
841
+ }
842
+ function mapTeam(node) {
843
+ const cycles = (node.cycles?.nodes ?? []).flatMap((c) => {
844
+ const cycle = mapNamedCycle(c);
845
+ return cycle ? [cycle] : [];
846
+ });
847
+ return {
848
+ id: String(node.id ?? ""),
849
+ key: String(node.key ?? ""),
850
+ name: String(node.name ?? ""),
851
+ states: (node.states?.nodes ?? []).flatMap((s) => {
852
+ const state = mapState(s);
853
+ return state ? [state] : [];
854
+ }),
855
+ activeCycle: mapNamedCycle(node.activeCycle),
856
+ ...cycles.length ? { cycles } : {}
857
+ };
858
+ }
859
+ function resolveLinearTeam(teams, team) {
860
+ const t = team.trim();
861
+ if (!t) throw new Error("Linear team is required (id, key, or name from linear_list_teams)");
862
+ const lower = t.toLowerCase();
863
+ const found = teams.find(
864
+ (x) => x.id === t || x.key.toLowerCase() === lower || x.name.toLowerCase() === lower
865
+ );
866
+ if (!found) {
867
+ const keys = teams.map((x) => x.key).filter(Boolean).join(", ") || "(none)";
868
+ throw new Error(`Linear team not found: ${team}. Known keys: ${keys}`);
869
+ }
870
+ return found;
871
+ }
872
+ function resolveLinearState(team, state) {
873
+ const s = state.trim();
874
+ if (!s) throw new Error("Linear state is empty");
875
+ const lower = s.toLowerCase();
876
+ const found = team.states.find((x) => x.id === s) || team.states.find((x) => x.name.toLowerCase() === lower) || team.states.find((x) => x.type.toLowerCase() === lower);
877
+ if (!found) {
878
+ const available = team.states.map((x) => `${x.name} (${x.type})`).join(", ") || "(none)";
879
+ throw new Error(`Linear state not found on ${team.key}: ${state}. Available: ${available}`);
880
+ }
881
+ return found;
882
+ }
883
+ function resolveLinearCycle(team, cycle) {
884
+ if (cycle == null) return null;
885
+ const s = cycle.trim();
886
+ if (!s || /^(none|clear|unschedule|null)$/i.test(s)) return null;
887
+ if (/^(current|active|this)$/i.test(s)) {
888
+ const active = team.activeCycle ?? team.cycles?.find((c) => c.isActive);
889
+ if (!active?.id) {
890
+ throw new Error(`Linear team ${team.key} has no active cycle`);
891
+ }
892
+ return active.id;
893
+ }
894
+ const lower = s.toLowerCase();
895
+ const asNumber = Number.parseInt(s, 10);
896
+ const seen = /* @__PURE__ */ new Set();
897
+ const cycles = [team.activeCycle, ...team.cycles ?? []].filter(
898
+ (c) => {
899
+ if (!c?.id || seen.has(c.id)) return false;
900
+ seen.add(c.id);
901
+ return true;
902
+ }
903
+ );
904
+ const found = cycles.find((c) => c.id === s) || cycles.find((c) => c.name.toLowerCase() === lower) || (Number.isInteger(asNumber) ? cycles.find((c) => c.number === asNumber) : void 0);
905
+ if (!found) {
906
+ const available = cycles.map((c) => c.number != null ? `${c.name} (${c.number})` : c.name).join(", ") || "(none)";
907
+ throw new Error(`Linear cycle not found on ${team.key}: ${cycle}. Available: ${available}`);
908
+ }
909
+ return found.id;
910
+ }
911
+ var LINEAR_NONE_RE = /^(none|clear|unschedule|null)$/i;
912
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
913
+ function isLinearNoneToken(value) {
914
+ if (value == null) return true;
915
+ const s = value.trim();
916
+ return !s || LINEAR_NONE_RE.test(s);
917
+ }
918
+ function resolveLinearLabel(labels, name) {
919
+ const s = name.trim();
920
+ if (!s) throw new Error("Linear label is empty");
921
+ const lower = s.toLowerCase();
922
+ const found = labels.find((x) => x.id === s) || labels.find((x) => x.name.toLowerCase() === lower);
923
+ if (!found) {
924
+ const available = labels.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
925
+ throw new Error(`Linear label not found: ${name}. Available: ${available}`);
926
+ }
927
+ return found;
928
+ }
929
+ function resolveLinearProject(projects, project) {
930
+ if (isLinearNoneToken(project)) return null;
931
+ const s = (project ?? "").trim();
932
+ const lower = s.toLowerCase();
933
+ const found = projects.find((x) => x.id === s) || projects.find((x) => x.name.toLowerCase() === lower) || projects.find((x) => x.slugId?.toLowerCase() === lower);
934
+ if (!found) {
935
+ const available = projects.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
936
+ throw new Error(`Linear project not found: ${project}. Available: ${available}`);
937
+ }
938
+ return found.id;
939
+ }
940
+ function normalizeLinearRelationType(type) {
941
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
942
+ if (s === "blocks" || s === "block") return "blocks";
943
+ if (s === "blockedby" || s === "blocked") return "blockedBy";
944
+ if (s === "related" || s === "relates" || s === "relatesto") return "related";
945
+ if (s === "duplicate" || s === "dup") return "duplicate";
946
+ if (s === "duplicateof" || s === "original") return "duplicateOf";
947
+ if (s === "similar") return "similar";
948
+ throw new Error(
949
+ `Linear relation type must be blocks, blockedBy, related, duplicate, or duplicateOf (got ${type})`
950
+ );
951
+ }
952
+ function linearRelationCreateInput(thisIssueId, type, relatedIssueId) {
953
+ const view = normalizeLinearRelationType(type);
954
+ if (view === "blockedBy") {
955
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "blocks" };
956
+ }
957
+ if (view === "duplicateOf") {
958
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "duplicate" };
959
+ }
960
+ return { issueId: thisIssueId, relatedIssueId, type: view };
961
+ }
962
+ function relationMatches(rel, type, issueRef) {
963
+ if (rel.type.toLowerCase() !== type.toLowerCase()) return false;
964
+ const ref = issueRef.trim().toLowerCase();
965
+ return rel.issue.id.toLowerCase() === ref || rel.issue.identifier.toLowerCase() === ref;
966
+ }
967
+ function formatRelationList(relations) {
968
+ if (!relations.length) return "(none)";
969
+ return relations.map((r) => `${r.type} ${r.issue.identifier || r.issue.id}`).join(", ");
970
+ }
971
+ function mapLabelNodes(nodes) {
972
+ return (nodes ?? []).flatMap((node) => {
973
+ const id = String(node.id ?? "").trim();
974
+ const name = String(node.name ?? "").trim();
975
+ if (!id) return [];
976
+ return [{ id, name }];
977
+ });
978
+ }
979
+ async function listLinearWorkspaceLabels(opts) {
980
+ const json = await linearGraphql(LABELS_QUERY, { first: 250 }, opts);
981
+ return mapLabelNodes(json.issueLabels?.nodes);
982
+ }
983
+ async function listLinearLabels(opts) {
984
+ const teamId = opts?.teamId?.trim();
985
+ if (teamId) {
986
+ const out = [];
987
+ let after;
988
+ for (let page = 0; page < 4; page++) {
989
+ const json = await linearGraphql(
990
+ TEAM_LABELS_QUERY,
991
+ after ? { id: teamId, first: 250, after } : { id: teamId, first: 250 },
992
+ opts
993
+ );
994
+ const conn = json.team?.labels;
995
+ out.push(...mapLabelNodes(conn?.nodes));
996
+ if (!conn?.pageInfo?.hasNextPage || !conn.pageInfo.endCursor) break;
997
+ after = conn.pageInfo.endCursor;
998
+ }
999
+ if (out.length) return out;
1000
+ }
1001
+ return listLinearWorkspaceLabels(opts);
1002
+ }
1003
+ async function queryLinearProjects(filter, opts) {
1004
+ const out = [];
1005
+ let after;
1006
+ for (let page = 0; page < 4; page++) {
1007
+ const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {}, ...after ? { after } : {} }, opts);
1008
+ for (const node of json.projects?.nodes ?? []) {
1009
+ const id = String(node.id ?? "").trim();
1010
+ if (!id) continue;
1011
+ out.push({ id, name: String(node.name ?? ""), ...node.slugId ? { slugId: String(node.slugId) } : {} });
1012
+ }
1013
+ const pageInfo = json.projects?.pageInfo;
1014
+ if (!pageInfo?.hasNextPage || !pageInfo.endCursor) break;
1015
+ after = pageInfo.endCursor;
1016
+ }
1017
+ return out;
1018
+ }
1019
+ function listLinearProjects(opts) {
1020
+ return queryLinearProjects(void 0, opts);
1021
+ }
1022
+ async function resolveLinearProjectId(project, opts) {
1023
+ if (isLinearNoneToken(project)) return null;
1024
+ const s = (project ?? "").trim();
1025
+ const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
1026
+ const matched = await queryLinearProjects(filter, opts);
1027
+ try {
1028
+ return resolveLinearProject(matched, s);
1029
+ } catch {
1030
+ if (UUID_RE.test(s)) return s;
1031
+ const listed = await queryLinearProjects(void 0, opts);
1032
+ return resolveLinearProject(listed, s);
1033
+ }
1034
+ }
1035
+ async function resolveLinearLabelIds(names, opts) {
1036
+ if (names.length === 0) return [];
1037
+ const labels = await listLinearLabels(opts);
1038
+ return names.map((name) => {
1039
+ const s = name.trim();
1040
+ try {
1041
+ return resolveLinearLabel(labels, s).id;
1042
+ } catch (error) {
1043
+ if (UUID_RE.test(s)) return s;
1044
+ throw error;
1045
+ }
1046
+ });
1047
+ }
1048
+ async function applyLinearIssueRelations(issue, add, remove, opts) {
1049
+ const current = [...issue.relations];
1050
+ for (const rel of remove ?? []) {
1051
+ const type = normalizeLinearRelationType(rel.type);
1052
+ const issueRef = rel.issue.trim();
1053
+ if (!issueRef) throw new Error("Linear removeRelations.issue is required (uuid or ENG-123)");
1054
+ const match = current.find((r) => relationMatches(r, type, issueRef));
1055
+ if (!match?.id) {
1056
+ throw new Error(
1057
+ `Linear relation not found: ${type} ${issueRef}. Existing: ${formatRelationList(current)}`
1058
+ );
1059
+ }
1060
+ const json = await linearGraphql(ISSUE_RELATION_DELETE, { id: match.id }, opts);
1061
+ if (!json.issueRelationDelete?.success) {
1062
+ throw new Error(`Linear issueRelationDelete failed (${type} ${issueRef})`);
1063
+ }
1064
+ const idx = current.indexOf(match);
1065
+ if (idx >= 0) current.splice(idx, 1);
1066
+ }
1067
+ for (const rel of add ?? []) {
1068
+ const type = normalizeLinearRelationType(rel.type);
1069
+ const issueRef = rel.issue.trim();
1070
+ if (!issueRef) throw new Error("Linear relations.issue is required (uuid or ENG-123)");
1071
+ if (current.some((r) => relationMatches(r, type, issueRef))) continue;
1072
+ const related = await getLinearIssue(issueRef, opts);
1073
+ const input = linearRelationCreateInput(issue.id, type, related.id);
1074
+ const json = await linearGraphql(ISSUE_RELATION_CREATE, { input }, opts);
1075
+ if (!json.issueRelationCreate?.success) {
1076
+ throw new Error(`Linear issueRelationCreate failed (${type} ${related.identifier})`);
1077
+ }
1078
+ current.push({ type, issue: related, id: void 0 });
1079
+ }
1080
+ }
1081
+ function normalizePriority(priority) {
1082
+ if (priority == null) return void 0;
1083
+ if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
1084
+ throw new Error("Linear priority must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low)");
1085
+ }
1086
+ return priority;
1087
+ }
1088
+ function assigneeFilterKey(assignee) {
1089
+ return (assignee ?? "").trim();
1090
+ }
1091
+ function applyLinearIssueQueryFilter(filter, query) {
1092
+ const term = query?.trim();
1093
+ if (!term) return;
1094
+ const or = [
1095
+ { title: { containsIgnoreCase: term } },
1096
+ { description: { containsIgnoreCase: term } }
1097
+ ];
1098
+ const identifierMatch = term.match(/^[A-Za-z][\w]*-(\d+)$/);
1099
+ const asNumber = identifierMatch ? Number(identifierMatch[1]) : /^\d+$/.test(term) ? Number(term) : NaN;
1100
+ if (Number.isFinite(asNumber) && asNumber > 0) {
1101
+ or.push({ number: { eq: asNumber } });
1102
+ }
1103
+ filter.or = or;
1104
+ }
1105
+ function buildLinearIssueFilter(input) {
1106
+ const filter = {
1107
+ state: { type: { nin: ["completed", "canceled"] } }
1108
+ };
1109
+ const since = input?.updatedSince?.trim();
1110
+ if (since) filter.updatedAt = { gte: since };
1111
+ applyLinearIssueQueryFilter(filter, input?.query);
1112
+ const raw = assigneeFilterKey(input?.assignee) || "me";
1113
+ const key = raw.toLowerCase();
1114
+ if (key === "all" || key === "*") return filter;
1115
+ if (key === "unassigned" || key === "none" || key === "null") {
1116
+ filter.assignee = { null: true };
1117
+ return filter;
1118
+ }
1119
+ if (key === "me" || key === "@me") {
1120
+ filter.assignee = { isMe: { eq: true } };
1121
+ return filter;
1122
+ }
1123
+ if (UUID_RE.test(raw)) {
1124
+ filter.assignee = { id: { eq: raw } };
1125
+ return filter;
1126
+ }
1127
+ filter.assignee = { name: { eqIgnoreCase: raw } };
1128
+ return filter;
1129
+ }
1130
+ function isAssignedToMe(assignee) {
1131
+ const key = assignee.toLowerCase();
1132
+ return key === "me" || key === "@me";
1133
+ }
1134
+ function listedLinearIssues(json, nodes) {
1135
+ return {
1136
+ viewer: {
1137
+ id: String(json.viewer?.id ?? ""),
1138
+ name: String(json.viewer?.name ?? "")
1139
+ },
1140
+ issues: (nodes ?? []).map((node) => toIssueInfo(mapIssue(node)))
1141
+ };
1142
+ }
1143
+ async function listLinearIssuesFiltered(opts) {
1144
+ const first = Math.max(1, Math.min(250, opts?.limit ?? 200));
1145
+ const query = opts?.query?.trim() ?? "";
1146
+ const assignee = assigneeFilterKey(opts?.assignee) || (query ? "all" : "me");
1147
+ const filter = buildLinearIssueFilter({
1148
+ assignee,
1149
+ updatedSince: opts?.updatedSince
1150
+ });
1151
+ if (query) {
1152
+ const json2 = await linearGraphql(
1153
+ SEARCH_ISSUES_QUERY,
1154
+ { term: query, first, filter },
1155
+ opts
1156
+ );
1157
+ return listedLinearIssues(json2, json2.searchIssues?.nodes);
1158
+ }
1159
+ if (isAssignedToMe(assignee)) {
1160
+ const json2 = await linearGraphql(
1161
+ ASSIGNED_ISSUES_QUERY,
1162
+ { first, filter },
1163
+ opts
1164
+ );
1165
+ return listedLinearIssues(json2, json2.viewer?.assignedIssues?.nodes);
1166
+ }
1167
+ const json = await linearGraphql(
1168
+ ISSUES_QUERY,
1169
+ { first, filter },
1170
+ opts
1171
+ );
1172
+ return listedLinearIssues(json, json.issues?.nodes);
1173
+ }
1174
+ async function listLinearCommentsSince(opts) {
1175
+ const first = Math.max(1, Math.min(250, opts.limit ?? 200));
1176
+ const query = opts.query?.trim() ?? "";
1177
+ const restrictToIssues = opts.issueIdentifiers != null;
1178
+ const allowed = new Set(
1179
+ (opts.issueIdentifiers ?? []).map((id) => id.trim()).filter(Boolean)
1180
+ );
1181
+ if (restrictToIssues && allowed.size === 0) return [];
1182
+ const assignee = assigneeFilterKey(opts.assignee) || (query ? "all" : "me");
1183
+ const json = await linearGraphql(
1184
+ COMMENTS_SINCE_QUERY,
1185
+ {
1186
+ first,
1187
+ filter: {
1188
+ createdAt: { gte: opts.since },
1189
+ issue: buildLinearIssueFilter({
1190
+ assignee,
1191
+ // Search hits may match identifier/comments — do not also require
1192
+ // title/description contains when an allowlist is already applied.
1193
+ query: restrictToIssues ? void 0 : query || void 0
1194
+ })
1195
+ }
1196
+ },
1197
+ opts
1198
+ );
1199
+ const out = [];
1200
+ for (const node of json.comments?.nodes ?? []) {
1201
+ const identifier = String(node.issue?.identifier ?? "").trim();
1202
+ if (restrictToIssues && !allowed.has(identifier)) continue;
1203
+ const body = previewIssueCommentBody(String(node.body ?? ""));
1204
+ if (!identifier && !body) continue;
1205
+ out.push({
1206
+ identifier: identifier || String(node.id ?? ""),
1207
+ ...node.issue?.title ? { title: String(node.issue.title) } : {},
1208
+ ...node.user?.name ? { author: String(node.user.name) } : {},
1209
+ ...node.createdAt?.trim() ? { createdAt: node.createdAt.trim() } : {},
1210
+ body,
1211
+ ...node.url?.trim() ? { url: node.url.trim() } : {}
1212
+ });
1213
+ }
1214
+ return out;
1215
+ }
1216
+ async function listLinearAssignedIssues(opts) {
1217
+ return listLinearIssuesFiltered({ ...opts, assignee: "me" });
1218
+ }
1219
+ async function listLinearIssuesDirect(opts) {
1220
+ return (await listLinearAssignedIssues(opts)).issues;
1221
+ }
1222
+ async function listLinearTeams(opts) {
1223
+ const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
1224
+ return {
1225
+ viewer: {
1226
+ id: String(json.viewer?.id ?? ""),
1227
+ name: String(json.viewer?.name ?? "")
1228
+ },
1229
+ teams: (json.teams?.nodes ?? []).map(mapTeam)
1230
+ };
1231
+ }
1232
+ async function listLinearTeamCycles(teamId, opts) {
1233
+ const json = await linearGraphql(TEAM_CYCLES_QUERY, { id: teamId }, opts);
1234
+ if (!json.team?.id) {
1235
+ throw new Error(`Linear team not found: ${teamId}`);
1236
+ }
1237
+ const mapped = mapTeam(json.team);
1238
+ return {
1239
+ id: mapped.id,
1240
+ key: mapped.key,
1241
+ activeCycle: mapped.activeCycle,
1242
+ cycles: mapped.cycles ?? []
1243
+ };
1244
+ }
1245
+ async function getLinearIssue(id, opts) {
1246
+ const issueId = id.trim();
1247
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
1248
+ const json = await linearGraphql(
1249
+ ISSUE_QUERY,
1250
+ { id: issueId },
1251
+ opts
1252
+ );
1253
+ if (!json.issue) {
1254
+ throw new Error(`Linear issue not found: ${issueId}`);
1255
+ }
1256
+ return mapIssue(json.issue);
1257
+ }
1258
+ async function createLinearIssue(input, opts) {
1259
+ const title = input.title.trim();
1260
+ if (!title) throw new Error("Linear issue title is required");
1261
+ const { viewer, teams } = await listLinearTeams(opts);
1262
+ const team = resolveLinearTeam(teams, input.team);
1263
+ const mutationInput = {
1264
+ teamId: team.id,
1265
+ title
1266
+ };
1267
+ const description = input.description?.trim();
1268
+ if (description) mutationInput.description = description;
1269
+ if (input.state?.trim()) {
1270
+ mutationInput.stateId = resolveLinearState(team, input.state).id;
1271
+ }
1272
+ const parentRef = input.parent?.trim();
1273
+ if (parentRef) {
1274
+ const parent = await getLinearIssue(parentRef, opts);
1275
+ mutationInput.parentId = parent.id;
1276
+ }
1277
+ const assignee = input.assignee === void 0 ? void 0 : input.assignee?.trim() || null;
1278
+ if (assignee === "me") mutationInput.assigneeId = viewer.id;
1279
+ else if (assignee) mutationInput.assigneeId = assignee;
1280
+ const priority = normalizePriority(input.priority);
1281
+ if (priority != null) mutationInput.priority = priority;
1282
+ const json = await linearGraphql(ISSUE_CREATE, { input: mutationInput }, opts);
1283
+ if (!json.issueCreate?.success || !json.issueCreate.issue) {
1284
+ throw new Error("Linear issueCreate failed");
1285
+ }
1286
+ return mapIssue(json.issueCreate.issue);
1287
+ }
1288
+ async function updateLinearIssue(input, opts) {
1289
+ const issueId = input.id.trim();
1290
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
1291
+ const mutationInput = {};
1292
+ if (input.title !== void 0) {
1293
+ const title = input.title.trim();
1294
+ if (!title) throw new Error("Linear issue title cannot be empty");
1295
+ mutationInput.title = title;
1296
+ }
1297
+ if (input.description !== void 0) {
1298
+ mutationInput.description = input.description;
1299
+ }
1300
+ const addRelations = input.relations?.length ? input.relations : void 0;
1301
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
1302
+ const needsIssue = Boolean(input.state?.trim()) || input.cycle !== void 0 || input.labels !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
1303
+ const existing = needsIssue ? await getLinearIssue(issueId, opts) : void 0;
1304
+ if (input.state?.trim()) {
1305
+ const { teams } = await listLinearTeams(opts);
1306
+ const teamRef = existing?.team?.key || existing?.team?.id;
1307
+ const team = teamRef ? resolveLinearTeam(teams, teamRef) : void 0;
1308
+ if (!team?.states.length) {
1309
+ throw new Error(`Linear issue ${existing?.identifier ?? issueId} has no workflow states to resolve "${input.state}"`);
1310
+ }
1311
+ mutationInput.stateId = resolveLinearState(team, input.state).id;
1312
+ }
1313
+ if (input.cycle !== void 0) {
1314
+ const teamId = existing?.team?.id;
1315
+ if (!teamId) {
1316
+ throw new Error(`Linear issue ${existing?.identifier ?? issueId} has no team to resolve a cycle`);
1317
+ }
1318
+ const cycles = await listLinearTeamCycles(teamId, opts);
1319
+ mutationInput.cycleId = resolveLinearCycle(
1320
+ { ...cycles, key: cycles.key || existing?.team?.key || teamId },
1321
+ input.cycle
1322
+ );
1323
+ }
1324
+ if (input.assignee !== void 0) {
1325
+ const assignee = input.assignee?.trim() || null;
1326
+ if (assignee === "me") {
1327
+ const { viewer } = await listLinearTeams(opts);
1328
+ mutationInput.assigneeId = viewer.id;
1329
+ } else {
1330
+ mutationInput.assigneeId = assignee;
1331
+ }
1332
+ }
1333
+ if (input.priority !== void 0) {
1334
+ mutationInput.priority = normalizePriority(input.priority);
1335
+ }
1336
+ if (input.labels !== void 0) {
1337
+ mutationInput.labelIds = await resolveLinearLabelIds(input.labels, {
1338
+ ...opts,
1339
+ teamId: existing?.team?.id
1340
+ });
1341
+ }
1342
+ if (input.project !== void 0) {
1343
+ mutationInput.projectId = await resolveLinearProjectId(input.project, opts);
1344
+ }
1345
+ if (input.parent !== void 0) {
1346
+ if (isLinearNoneToken(input.parent)) {
1347
+ mutationInput.parentId = null;
1348
+ } else {
1349
+ const parent = await getLinearIssue((input.parent ?? "").trim(), opts);
1350
+ mutationInput.parentId = parent.id;
1351
+ }
1352
+ }
1353
+ const hasIssueFields = Object.keys(mutationInput).length > 0;
1354
+ const hasRelationWrites = Boolean(addRelations || removeRelations);
1355
+ if (!hasIssueFields && !hasRelationWrites) {
1356
+ throw new Error(
1357
+ "linear_update_issue needs at least one of title, description, state, assignee, priority, cycle, labels, project, parent, relations, removeRelations"
1358
+ );
1359
+ }
1360
+ let updated;
1361
+ if (hasIssueFields) {
1362
+ const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
1363
+ if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
1364
+ throw new Error("Linear issueUpdate failed");
1365
+ }
1366
+ updated = mapIssue(json.issueUpdate.issue);
1367
+ }
1368
+ if (hasRelationWrites) {
1369
+ const base = updated ?? existing;
1370
+ if (!base) {
1371
+ throw new Error(`Linear issue not found: ${issueId}`);
1372
+ }
1373
+ await applyLinearIssueRelations(base, addRelations, removeRelations, opts);
1374
+ return getLinearIssue(base.id || issueId, opts);
1375
+ }
1376
+ return updated;
1377
+ }
1378
+ async function commentLinearIssue(input, opts) {
1379
+ const issueId = input.id.trim();
1380
+ const body = input.body.trim();
1381
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
1382
+ if (!body) throw new Error("Linear comment body is required");
1383
+ const json = await linearGraphql(COMMENT_CREATE, { input: { issueId, body } }, opts);
1384
+ if (!json.commentCreate?.success || !json.commentCreate.comment?.id) {
1385
+ throw new Error("Linear commentCreate failed");
1386
+ }
1387
+ return {
1388
+ id: String(json.commentCreate.comment.id),
1389
+ body: String(json.commentCreate.comment.body ?? body),
1390
+ url: json.commentCreate.comment.url?.trim() || void 0
1391
+ };
1392
+ }
1393
+ async function validateLinearApiKey(apiKey) {
1394
+ const key = apiKey.trim();
1395
+ if (!key) return false;
1396
+ try {
1397
+ await listLinearIssuesDirect({ limit: 1, apiKey: key });
1398
+ return true;
1399
+ } catch {
1400
+ return false;
1401
+ }
1402
+ }
1403
+
1404
+ export {
1405
+ hasBakedLinearOAuth,
1406
+ LINEAR_OAUTH_PORT,
1407
+ LINEAR_OAUTH_REDIRECT,
1408
+ LINEAR_OAUTH_SCOPES,
1409
+ linearOAuthCredentials,
1410
+ createLinearPkce,
1411
+ linearOAuthAuthorizeUrl,
1412
+ linearAuthorizationHeader,
1413
+ getLinearAuthToken,
1414
+ LINEAR_OAUTH_CANCELLED,
1415
+ LinearOAuthCancelledError,
1416
+ isLinearOAuthCancelled,
1417
+ startLinearOAuth,
1418
+ disconnectLinear,
1419
+ downloadLinearAttachment,
1420
+ rewriteLinearError,
1421
+ linearGraphql,
1422
+ linearCycleIsActive,
1423
+ flipLinearRelationType,
1424
+ resolveLinearTeam,
1425
+ resolveLinearState,
1426
+ resolveLinearCycle,
1427
+ isLinearNoneToken,
1428
+ resolveLinearLabel,
1429
+ resolveLinearProject,
1430
+ normalizeLinearRelationType,
1431
+ linearRelationCreateInput,
1432
+ listLinearProjects,
1433
+ buildLinearIssueFilter,
1434
+ listLinearIssuesFiltered,
1435
+ listLinearCommentsSince,
1436
+ listLinearAssignedIssues,
1437
+ listLinearIssuesDirect,
1438
+ listLinearTeams,
1439
+ listLinearTeamCycles,
1440
+ getLinearIssue,
1441
+ createLinearIssue,
1442
+ updateLinearIssue,
1443
+ commentLinearIssue,
1444
+ validateLinearApiKey
1445
+ };