@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,1259 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ previewIssueCommentBody
5
+ } from "./chunk-OYRN356W.js";
6
+ import {
7
+ downloadAuthenticatedFile,
8
+ extractHttpUrls,
9
+ hostMatches,
10
+ issueAttachmentWorktreePath,
11
+ looksLikeHttpUrl,
12
+ mergeIssueAttachments
13
+ } from "./chunk-F6GE7V3Q.js";
14
+ import {
15
+ httpFetch
16
+ } from "./chunk-2GPQZZLY.js";
17
+ import {
18
+ disconnectLinearConnection,
19
+ loadAppSettings,
20
+ saveLinearOAuth
21
+ } from "./chunk-D744HOI5.js";
22
+
23
+ // src/integrations/linear-oauth.ts
24
+ import { createHash, randomBytes } from "crypto";
25
+ import { createServer } from "http";
26
+
27
+ // src/integrations/linear-app.ts
28
+ var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
29
+
30
+ // src/integrations/linear-oauth.ts
31
+ var LINEAR_OAUTH_PORT = 19848;
32
+ var LINEAR_OAUTH_REDIRECT = `http://127.0.0.1:${LINEAR_OAUTH_PORT}/callback`;
33
+ var LINEAR_TOKEN = "https://api.linear.app/oauth/token";
34
+ var REFRESH_SKEW_MS = 5 * 6e4;
35
+ function linearOAuthCredentials() {
36
+ const settings = loadAppSettings();
37
+ const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
38
+ const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || "";
39
+ if (!clientId) {
40
+ throw new Error(
41
+ `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.`
42
+ );
43
+ }
44
+ return { clientId, clientSecret };
45
+ }
46
+ function linearAuthorizationHeader(token) {
47
+ const t = token.trim();
48
+ if (!t) return t;
49
+ if (/^bearer\s+/i.test(t)) return t;
50
+ if (t.startsWith("lin_api_")) return t;
51
+ return `Bearer ${t}`;
52
+ }
53
+ async function exchangeLinearToken(body) {
54
+ const res = await httpFetch(LINEAR_TOKEN, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
57
+ body
58
+ });
59
+ const data = await res.json().catch(() => ({}));
60
+ if (!res.ok || data.error || !data.access_token?.trim()) {
61
+ const detail = data.error_description || data.error || `HTTP ${res.status}`;
62
+ throw new Error(`Linear OAuth token exchange failed: ${detail}`);
63
+ }
64
+ return data;
65
+ }
66
+ async function persistLinearTokens(data, viewer) {
67
+ return saveLinearOAuth({
68
+ accessToken: data.access_token.trim(),
69
+ refreshToken: data.refresh_token?.trim(),
70
+ expiresIn: data.expires_in,
71
+ viewerName: viewer?.name,
72
+ organizationName: viewer?.organizationName
73
+ });
74
+ }
75
+ var refreshInFlight = null;
76
+ async function refreshLinearAccessToken(refreshToken) {
77
+ const { clientId, clientSecret } = linearOAuthCredentials();
78
+ const body = new URLSearchParams({
79
+ grant_type: "refresh_token",
80
+ refresh_token: refreshToken,
81
+ client_id: clientId
82
+ });
83
+ if (clientSecret) body.set("client_secret", clientSecret);
84
+ try {
85
+ const data = await exchangeLinearToken(body);
86
+ await persistLinearTokens(data);
87
+ return data.access_token.trim();
88
+ } catch (err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
91
+ disconnectLinearConnection();
92
+ throw new Error("Linear sign-in expired \u2014 connect again in Account settings");
93
+ }
94
+ throw err;
95
+ }
96
+ }
97
+ async function getLinearAuthToken(settings = loadAppSettings()) {
98
+ const access = settings.integrations.linearAccessToken?.trim() || "";
99
+ const refresh = settings.integrations.linearRefreshToken?.trim() || "";
100
+ const expiresAt = settings.integrations.linearTokenExpiresAt ?? 0;
101
+ const apiKey = settings.integrations.linearApiKey?.trim() || "";
102
+ if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS)) {
103
+ return access;
104
+ }
105
+ if (refresh) {
106
+ if (!refreshInFlight) {
107
+ refreshInFlight = refreshLinearAccessToken(refresh).finally(() => {
108
+ refreshInFlight = null;
109
+ });
110
+ }
111
+ return refreshInFlight.catch((err) => {
112
+ if (apiKey) return apiKey;
113
+ throw err;
114
+ });
115
+ }
116
+ return apiKey || null;
117
+ }
118
+
119
+ // src/integrations/linear-attachments.ts
120
+ var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
121
+ var LINEAR_FILE_HOSTS = ["linear.app", "uploads.linear.app", "linearusercontent.com"];
122
+ var LINEAR_LINK_SOURCE_TYPES = /* @__PURE__ */ new Set([
123
+ "github",
124
+ "githubPullRequest",
125
+ "gitlab",
126
+ "figma",
127
+ "slack",
128
+ "sentry",
129
+ "zendesk",
130
+ "intercom",
131
+ "front"
132
+ ]);
133
+ var ATTACHMENT_QUERY = `
134
+ query SideboardAttachment($id: String!) {
135
+ attachment(id: $id) {
136
+ id
137
+ title
138
+ url
139
+ subtitle
140
+ sourceType
141
+ }
142
+ }
143
+ `;
144
+ function mapLinearAttachments(node) {
145
+ const listed = [];
146
+ for (const attachment of node.attachments?.nodes ?? []) {
147
+ const id = String(attachment.id ?? "");
148
+ const url = String(attachment.url ?? "").trim();
149
+ if (!id && !url) continue;
150
+ listed.push({
151
+ id,
152
+ title: String(attachment.title ?? ""),
153
+ url,
154
+ subtitle: attachment.subtitle?.trim() || void 0,
155
+ sourceType: attachment.sourceType?.trim() || void 0
156
+ });
157
+ }
158
+ const embedded = [
159
+ ...extractHttpUrls(node.description).filter((url) => hostMatches(url, LINEAR_FILE_HOSTS)),
160
+ ...(node.comments?.nodes ?? []).flatMap(
161
+ (comment) => extractHttpUrls(comment.body).filter((url) => hostMatches(url, LINEAR_FILE_HOSTS))
162
+ )
163
+ ];
164
+ const merged = mergeIssueAttachments([
165
+ ...listed.map((item) => ({
166
+ id: item.id,
167
+ name: item.title || item.subtitle || "attachment",
168
+ url: item.url,
169
+ sourceType: item.sourceType
170
+ })),
171
+ ...embedded.map((url) => ({
172
+ id: url,
173
+ name: url.split("/").pop() || "image",
174
+ url,
175
+ sourceType: "upload"
176
+ }))
177
+ ]);
178
+ const byUrl = new Map(listed.map((item) => [item.url, item]));
179
+ return merged.map((item) => {
180
+ const prior = byUrl.get(item.url);
181
+ return {
182
+ id: prior?.id || item.id,
183
+ title: prior?.title || item.name,
184
+ url: item.url,
185
+ subtitle: prior?.subtitle,
186
+ sourceType: prior?.sourceType || item.sourceType
187
+ };
188
+ });
189
+ }
190
+ function assertLinearFileUrl(url, sourceType) {
191
+ const kind = sourceType?.trim();
192
+ if (kind && LINEAR_LINK_SOURCE_TYPES.has(kind)) {
193
+ throw new Error(
194
+ `Linear attachment is a ${kind} link, not a file. Open the URL in the browser instead of downloading it.`
195
+ );
196
+ }
197
+ if (!hostMatches(url, LINEAR_FILE_HOSTS)) {
198
+ throw new Error(
199
+ `Linear attachment URL is not a Linear upload (${url}). Only uploads.linear.app / linear.app file hosts can be downloaded with the Linear token.`
200
+ );
201
+ }
202
+ }
203
+ async function requireLinearToken(apiKey) {
204
+ const token = (apiKey ?? await getLinearAuthToken())?.trim();
205
+ if (!token) {
206
+ throw new Error("Linear is not connected \u2014 sign in from Account settings");
207
+ }
208
+ return token;
209
+ }
210
+ async function downloadLinearAttachment(input, opts) {
211
+ const id = input.id.trim();
212
+ if (!id) throw new Error("Linear attachment id or URL is required");
213
+ const apiKey = await requireLinearToken(opts?.apiKey);
214
+ let url = id;
215
+ let name = input.name?.trim();
216
+ let sourceType;
217
+ if (!looksLikeHttpUrl(id)) {
218
+ const res = await httpFetch(LINEAR_GRAPHQL, {
219
+ method: "POST",
220
+ headers: {
221
+ "Content-Type": "application/json",
222
+ Authorization: linearAuthorizationHeader(apiKey)
223
+ },
224
+ body: JSON.stringify({ query: ATTACHMENT_QUERY, variables: { id } })
225
+ });
226
+ if (!res.ok) {
227
+ throw new Error(`Linear API error ${res.status} looking up attachment ${id}`);
228
+ }
229
+ const json = await res.json();
230
+ if (json.errors?.length) {
231
+ throw new Error(json.errors.map((e) => e.message ?? "Linear error").join("; "));
232
+ }
233
+ const attachment = json.data?.attachment;
234
+ if (!attachment?.url?.trim()) {
235
+ throw new Error(`Linear attachment not found: ${id}`);
236
+ }
237
+ url = attachment.url.trim();
238
+ name = name || attachment.title?.trim() || attachment.subtitle?.trim() || void 0;
239
+ sourceType = attachment.sourceType?.trim() || void 0;
240
+ }
241
+ assertLinearFileUrl(url, sourceType);
242
+ return downloadAuthenticatedFile({
243
+ url,
244
+ headers: { Authorization: linearAuthorizationHeader(apiKey) },
245
+ worktreePath: issueAttachmentWorktreePath(opts?.destPath),
246
+ name
247
+ });
248
+ }
249
+
250
+ // src/integrations/linear.ts
251
+ var LINEAR_GRAPHQL2 = "https://api.linear.app/graphql";
252
+ var LIST_ISSUE_FIELDS = `
253
+ id
254
+ identifier
255
+ title
256
+ url
257
+ createdAt
258
+ updatedAt
259
+ assignee { id name }
260
+ team { id key }
261
+ labels(first: 10) { nodes { name } }
262
+ cycle { id name number startsAt endsAt completedAt }
263
+ project { id name }
264
+ `;
265
+ var ISSUE_REF_FIELDS = `
266
+ id
267
+ identifier
268
+ title
269
+ url
270
+ `;
271
+ var ISSUE_FIELDS = `
272
+ id
273
+ identifier
274
+ title
275
+ description
276
+ url
277
+ priority
278
+ estimate
279
+ dueDate
280
+ branchName
281
+ createdAt
282
+ updatedAt
283
+ completedAt
284
+ state { id name type }
285
+ assignee { id name }
286
+ creator { id name }
287
+ team { id key name }
288
+ labels(first: 50) { nodes { name } }
289
+ cycle { id name number startsAt endsAt completedAt }
290
+ project { id name }
291
+ parent { ${ISSUE_REF_FIELDS} }
292
+ children(first: 25) { nodes { ${ISSUE_REF_FIELDS} } }
293
+ relations(first: 25) {
294
+ nodes {
295
+ id
296
+ type
297
+ relatedIssue { ${ISSUE_REF_FIELDS} }
298
+ }
299
+ }
300
+ inverseRelations(first: 25) {
301
+ nodes {
302
+ id
303
+ type
304
+ issue { ${ISSUE_REF_FIELDS} }
305
+ }
306
+ }
307
+ comments(first: 50) {
308
+ nodes {
309
+ id
310
+ body
311
+ url
312
+ createdAt
313
+ updatedAt
314
+ user { id name }
315
+ }
316
+ }
317
+ attachments(first: 20) {
318
+ nodes { id title url subtitle sourceType }
319
+ }
320
+ `;
321
+ var ASSIGNED_ISSUES_QUERY = `
322
+ query SideboardAssignedIssues($first: Int!, $filter: IssueFilter) {
323
+ viewer {
324
+ id
325
+ name
326
+ assignedIssues(
327
+ first: $first
328
+ orderBy: updatedAt
329
+ filter: $filter
330
+ ) {
331
+ nodes { ${LIST_ISSUE_FIELDS} }
332
+ }
333
+ }
334
+ }
335
+ `;
336
+ var COMMENTS_SINCE_QUERY = `
337
+ query SideboardCommentsSince($first: Int!, $filter: CommentFilter) {
338
+ viewer { id name }
339
+ comments(first: $first, orderBy: createdAt, filter: $filter) {
340
+ nodes {
341
+ id
342
+ body
343
+ url
344
+ createdAt
345
+ user { id name }
346
+ issue { identifier title url }
347
+ }
348
+ }
349
+ }
350
+ `;
351
+ var ISSUES_QUERY = `
352
+ query SideboardIssues($first: Int!, $filter: IssueFilter) {
353
+ viewer { id name }
354
+ issues(first: $first, orderBy: updatedAt, filter: $filter) {
355
+ nodes { ${LIST_ISSUE_FIELDS} }
356
+ }
357
+ }
358
+ `;
359
+ var SEARCH_ISSUES_QUERY = `
360
+ query SideboardSearchIssues($term: String!, $first: Int!, $filter: IssueFilter) {
361
+ viewer { id name }
362
+ searchIssues(term: $term, first: $first, includeArchived: false, filter: $filter) {
363
+ nodes { ${LIST_ISSUE_FIELDS} }
364
+ }
365
+ }
366
+ `;
367
+ var TEAMS_QUERY = `
368
+ query SideboardTeams {
369
+ viewer { id name }
370
+ teams(first: 50) {
371
+ nodes {
372
+ id
373
+ key
374
+ name
375
+ states(first: 50) { nodes { id name type } }
376
+ activeCycle { id name number startsAt endsAt completedAt }
377
+ }
378
+ }
379
+ }
380
+ `;
381
+ var TEAM_CYCLES_QUERY = `
382
+ query SideboardTeamCycles($id: String!) {
383
+ team(id: $id) {
384
+ id
385
+ key
386
+ activeCycle { id name number startsAt endsAt completedAt }
387
+ cycles(first: 40) {
388
+ nodes { id name number startsAt endsAt completedAt }
389
+ }
390
+ }
391
+ }
392
+ `;
393
+ var ISSUE_QUERY = `
394
+ query SideboardIssue($id: String!) {
395
+ issue(id: $id) { ${ISSUE_FIELDS} }
396
+ }
397
+ `;
398
+ var ISSUE_CREATE = `
399
+ mutation SideboardIssueCreate($input: IssueCreateInput!) {
400
+ issueCreate(input: $input) {
401
+ success
402
+ issue { ${ISSUE_FIELDS} }
403
+ }
404
+ }
405
+ `;
406
+ var ISSUE_UPDATE = `
407
+ mutation SideboardIssueUpdate($id: String!, $input: IssueUpdateInput!) {
408
+ issueUpdate(id: $id, input: $input) {
409
+ success
410
+ issue { ${ISSUE_FIELDS} }
411
+ }
412
+ }
413
+ `;
414
+ var COMMENT_CREATE = `
415
+ mutation SideboardCommentCreate($input: CommentCreateInput!) {
416
+ commentCreate(input: $input) {
417
+ success
418
+ comment { id body url }
419
+ }
420
+ }
421
+ `;
422
+ var LABELS_QUERY = `
423
+ query SideboardLabels($first: Int!) {
424
+ issueLabels(first: $first) {
425
+ nodes { id name }
426
+ }
427
+ }
428
+ `;
429
+ var TEAM_LABELS_QUERY = `
430
+ query SideboardTeamLabels($id: String!, $first: Int!, $after: String) {
431
+ team(id: $id) {
432
+ labels(first: $first, after: $after) {
433
+ nodes { id name }
434
+ pageInfo { hasNextPage endCursor }
435
+ }
436
+ }
437
+ }
438
+ `;
439
+ var PROJECTS_QUERY = `
440
+ query SideboardProjects($first: Int!, $filter: ProjectFilter, $after: String) {
441
+ projects(first: $first, filter: $filter, after: $after) {
442
+ nodes { id name slugId }
443
+ pageInfo { hasNextPage endCursor }
444
+ }
445
+ }
446
+ `;
447
+ var ISSUE_RELATION_CREATE = `
448
+ mutation SideboardIssueRelationCreate($input: IssueRelationCreateInput!) {
449
+ issueRelationCreate(input: $input) {
450
+ success
451
+ issueRelation { id type }
452
+ }
453
+ }
454
+ `;
455
+ var ISSUE_RELATION_DELETE = `
456
+ mutation SideboardIssueRelationDelete($id: String!) {
457
+ issueRelationDelete(id: $id) {
458
+ success
459
+ }
460
+ }
461
+ `;
462
+ function rewriteLinearError(message) {
463
+ if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
464
+ return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
465
+ }
466
+ return message;
467
+ }
468
+ async function requireLinearToken2(apiKey) {
469
+ const token = (apiKey ?? await getLinearAuthToken())?.trim();
470
+ if (!token) {
471
+ throw new Error("Linear is not connected \u2014 sign in from Account settings");
472
+ }
473
+ return token;
474
+ }
475
+ async function linearGraphql(query, variables, opts) {
476
+ const apiKey = await requireLinearToken2(opts?.apiKey);
477
+ const res = await httpFetch(LINEAR_GRAPHQL2, {
478
+ method: "POST",
479
+ headers: {
480
+ "Content-Type": "application/json",
481
+ Authorization: linearAuthorizationHeader(apiKey)
482
+ },
483
+ body: JSON.stringify({ query, variables })
484
+ });
485
+ if (!res.ok) {
486
+ const body = await res.text().catch(() => "");
487
+ throw new Error(
488
+ rewriteLinearError(
489
+ `Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
490
+ )
491
+ );
492
+ }
493
+ const json = await res.json();
494
+ if (json.errors?.length) {
495
+ throw new Error(
496
+ rewriteLinearError(json.errors.map((e) => e.message ?? "Linear error").join("; "))
497
+ );
498
+ }
499
+ if (json.data == null) {
500
+ throw new Error("Linear API returned no data");
501
+ }
502
+ return json.data;
503
+ }
504
+ function mapState(node) {
505
+ if (!node?.id) return void 0;
506
+ return {
507
+ id: String(node.id),
508
+ name: String(node.name ?? ""),
509
+ type: String(node.type ?? "")
510
+ };
511
+ }
512
+ function linearCycleIsActive(cycle, now = Date.now()) {
513
+ if (!cycle) return false;
514
+ if (cycle.completedAt) return false;
515
+ const start = cycle.startsAt ? Date.parse(cycle.startsAt) : Number.NaN;
516
+ const end = cycle.endsAt ? Date.parse(cycle.endsAt) : Number.NaN;
517
+ if (Number.isFinite(start) && now < start) return false;
518
+ if (Number.isFinite(end) && now > end) return false;
519
+ return true;
520
+ }
521
+ function cycleDisplayName(node) {
522
+ return String(node?.name ?? (node?.number != null ? `Cycle ${node.number}` : "")).trim();
523
+ }
524
+ function mapNamedCycle(node) {
525
+ if (!node?.id) return null;
526
+ const name = cycleDisplayName(node);
527
+ if (!name) return null;
528
+ return {
529
+ id: String(node.id),
530
+ name,
531
+ number: typeof node.number === "number" ? node.number : void 0,
532
+ isActive: linearCycleIsActive(node)
533
+ };
534
+ }
535
+ function mapCycle(node) {
536
+ if (!node?.name && node?.number == null) return null;
537
+ const name = cycleDisplayName(node);
538
+ if (!name) return null;
539
+ return {
540
+ ...node.id ? { id: String(node.id) } : {},
541
+ name,
542
+ number: typeof node.number === "number" ? node.number : void 0,
543
+ isActive: linearCycleIsActive(node)
544
+ };
545
+ }
546
+ function mapUser(node) {
547
+ if (!node?.id && !node?.name) return void 0;
548
+ return {
549
+ id: String(node.id ?? ""),
550
+ name: String(node.name ?? "")
551
+ };
552
+ }
553
+ function mapIssueRef(node) {
554
+ if (!node?.id && !node?.identifier) return void 0;
555
+ return {
556
+ id: String(node.id ?? node.identifier ?? ""),
557
+ identifier: String(node.identifier ?? node.id ?? ""),
558
+ title: String(node.title ?? ""),
559
+ url: String(node.url ?? "")
560
+ };
561
+ }
562
+ function flipLinearRelationType(type) {
563
+ switch (type) {
564
+ case "blocks":
565
+ return "blockedBy";
566
+ case "blockedBy":
567
+ return "blocks";
568
+ case "duplicate":
569
+ return "duplicateOf";
570
+ case "duplicateOf":
571
+ return "duplicate";
572
+ default:
573
+ return type;
574
+ }
575
+ }
576
+ function mapRelations(node) {
577
+ const out = [];
578
+ for (const rel of node.relations?.nodes ?? []) {
579
+ const issue = mapIssueRef(rel.relatedIssue);
580
+ if (!issue) continue;
581
+ const id = rel.id?.trim();
582
+ out.push({ type: String(rel.type ?? "related"), issue, ...id ? { id } : {} });
583
+ }
584
+ for (const rel of node.inverseRelations?.nodes ?? []) {
585
+ const issue = mapIssueRef(rel.issue);
586
+ if (!issue) continue;
587
+ const id = rel.id?.trim();
588
+ out.push({
589
+ type: flipLinearRelationType(String(rel.type ?? "related")),
590
+ issue,
591
+ ...id ? { id } : {}
592
+ });
593
+ }
594
+ return out;
595
+ }
596
+ function mapComments(node) {
597
+ const out = [];
598
+ for (const comment of node.comments?.nodes ?? []) {
599
+ const id = String(comment.id ?? "");
600
+ const body = String(comment.body ?? "");
601
+ if (!id && !body) continue;
602
+ out.push({
603
+ id,
604
+ body,
605
+ url: comment.url?.trim() || void 0,
606
+ createdAt: comment.createdAt?.trim() || void 0,
607
+ updatedAt: comment.updatedAt?.trim() || void 0,
608
+ user: mapUser(comment.user)
609
+ });
610
+ }
611
+ return out;
612
+ }
613
+ function mapIssue(node) {
614
+ const team = node.team;
615
+ return {
616
+ id: String(node.id ?? node.identifier ?? ""),
617
+ identifier: String(node.identifier ?? node.id ?? ""),
618
+ title: String(node.title ?? ""),
619
+ url: String(node.url ?? ""),
620
+ description: node.description?.trim() || void 0,
621
+ priority: typeof node.priority === "number" ? node.priority : void 0,
622
+ estimate: typeof node.estimate === "number" ? node.estimate : void 0,
623
+ dueDate: node.dueDate?.trim() || void 0,
624
+ branchName: node.branchName?.trim() || void 0,
625
+ createdAt: node.createdAt?.trim() || void 0,
626
+ updatedAt: node.updatedAt?.trim() || void 0,
627
+ completedAt: node.completedAt?.trim() || void 0,
628
+ state: mapState(node.state),
629
+ assignee: mapUser(node.assignee),
630
+ creator: mapUser(node.creator),
631
+ team: team?.id ? {
632
+ id: String(team.id),
633
+ key: String(team.key ?? ""),
634
+ name: String(team.name ?? ""),
635
+ states: (team.states?.nodes ?? []).flatMap((s) => {
636
+ const state = mapState(s);
637
+ return state ? [state] : [];
638
+ })
639
+ } : void 0,
640
+ labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n)),
641
+ cycle: mapCycle(node.cycle),
642
+ project: node.project?.id ? { id: String(node.project.id), name: String(node.project.name ?? "") } : void 0,
643
+ parent: mapIssueRef(node.parent),
644
+ children: (node.children?.nodes ?? []).flatMap((child) => {
645
+ const ref = mapIssueRef(child);
646
+ return ref ? [ref] : [];
647
+ }),
648
+ relations: mapRelations(node),
649
+ comments: mapComments(node),
650
+ attachments: mapLinearAttachments(node)
651
+ };
652
+ }
653
+ function toIssueInfo(issue) {
654
+ return {
655
+ id: issue.id,
656
+ identifier: issue.identifier,
657
+ title: issue.title,
658
+ url: issue.url,
659
+ labels: issue.labels,
660
+ provider: "linear",
661
+ assignee: issue.assignee?.name,
662
+ assignees: issue.assignee?.name ? [issue.assignee.name] : void 0,
663
+ cycle: issue.cycle ?? null,
664
+ teamKey: issue.team?.key || void 0,
665
+ ...issue.project ? { projects: [issue.project] } : {},
666
+ ...issue.createdAt ? { createdAt: issue.createdAt } : {},
667
+ ...issue.updatedAt ? { updatedAt: issue.updatedAt } : {}
668
+ };
669
+ }
670
+ function mapTeam(node) {
671
+ const cycles = (node.cycles?.nodes ?? []).flatMap((c) => {
672
+ const cycle = mapNamedCycle(c);
673
+ return cycle ? [cycle] : [];
674
+ });
675
+ return {
676
+ id: String(node.id ?? ""),
677
+ key: String(node.key ?? ""),
678
+ name: String(node.name ?? ""),
679
+ states: (node.states?.nodes ?? []).flatMap((s) => {
680
+ const state = mapState(s);
681
+ return state ? [state] : [];
682
+ }),
683
+ activeCycle: mapNamedCycle(node.activeCycle),
684
+ ...cycles.length ? { cycles } : {}
685
+ };
686
+ }
687
+ function resolveLinearTeam(teams, team) {
688
+ const t = team.trim();
689
+ if (!t) throw new Error("Linear team is required (id, key, or name from linear_list_teams)");
690
+ const lower = t.toLowerCase();
691
+ const found = teams.find(
692
+ (x) => x.id === t || x.key.toLowerCase() === lower || x.name.toLowerCase() === lower
693
+ );
694
+ if (!found) {
695
+ const keys = teams.map((x) => x.key).filter(Boolean).join(", ") || "(none)";
696
+ throw new Error(`Linear team not found: ${team}. Known keys: ${keys}`);
697
+ }
698
+ return found;
699
+ }
700
+ function resolveLinearState(team, state) {
701
+ const s = state.trim();
702
+ if (!s) throw new Error("Linear state is empty");
703
+ const lower = s.toLowerCase();
704
+ 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);
705
+ if (!found) {
706
+ const available = team.states.map((x) => `${x.name} (${x.type})`).join(", ") || "(none)";
707
+ throw new Error(`Linear state not found on ${team.key}: ${state}. Available: ${available}`);
708
+ }
709
+ return found;
710
+ }
711
+ function resolveLinearCycle(team, cycle) {
712
+ if (cycle == null) return null;
713
+ const s = cycle.trim();
714
+ if (!s || /^(none|clear|unschedule|null)$/i.test(s)) return null;
715
+ if (/^(current|active|this)$/i.test(s)) {
716
+ const active = team.activeCycle ?? team.cycles?.find((c) => c.isActive);
717
+ if (!active?.id) {
718
+ throw new Error(`Linear team ${team.key} has no active cycle`);
719
+ }
720
+ return active.id;
721
+ }
722
+ const lower = s.toLowerCase();
723
+ const asNumber = Number.parseInt(s, 10);
724
+ const seen = /* @__PURE__ */ new Set();
725
+ const cycles = [team.activeCycle, ...team.cycles ?? []].filter(
726
+ (c) => {
727
+ if (!c?.id || seen.has(c.id)) return false;
728
+ seen.add(c.id);
729
+ return true;
730
+ }
731
+ );
732
+ 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);
733
+ if (!found) {
734
+ const available = cycles.map((c) => c.number != null ? `${c.name} (${c.number})` : c.name).join(", ") || "(none)";
735
+ throw new Error(`Linear cycle not found on ${team.key}: ${cycle}. Available: ${available}`);
736
+ }
737
+ return found.id;
738
+ }
739
+ var LINEAR_NONE_RE = /^(none|clear|unschedule|null)$/i;
740
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
741
+ function isLinearNoneToken(value) {
742
+ if (value == null) return true;
743
+ const s = value.trim();
744
+ return !s || LINEAR_NONE_RE.test(s);
745
+ }
746
+ function resolveLinearLabel(labels, name) {
747
+ const s = name.trim();
748
+ if (!s) throw new Error("Linear label is empty");
749
+ const lower = s.toLowerCase();
750
+ const found = labels.find((x) => x.id === s) || labels.find((x) => x.name.toLowerCase() === lower);
751
+ if (!found) {
752
+ const available = labels.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
753
+ throw new Error(`Linear label not found: ${name}. Available: ${available}`);
754
+ }
755
+ return found;
756
+ }
757
+ function resolveLinearProject(projects, project) {
758
+ if (isLinearNoneToken(project)) return null;
759
+ const s = (project ?? "").trim();
760
+ const lower = s.toLowerCase();
761
+ const found = projects.find((x) => x.id === s) || projects.find((x) => x.name.toLowerCase() === lower) || projects.find((x) => x.slugId?.toLowerCase() === lower);
762
+ if (!found) {
763
+ const available = projects.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
764
+ throw new Error(`Linear project not found: ${project}. Available: ${available}`);
765
+ }
766
+ return found.id;
767
+ }
768
+ function normalizeLinearRelationType(type) {
769
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
770
+ if (s === "blocks" || s === "block") return "blocks";
771
+ if (s === "blockedby" || s === "blocked") return "blockedBy";
772
+ if (s === "related" || s === "relates" || s === "relatesto") return "related";
773
+ if (s === "duplicate" || s === "dup") return "duplicate";
774
+ if (s === "duplicateof" || s === "original") return "duplicateOf";
775
+ if (s === "similar") return "similar";
776
+ throw new Error(
777
+ `Linear relation type must be blocks, blockedBy, related, duplicate, or duplicateOf (got ${type})`
778
+ );
779
+ }
780
+ function linearRelationCreateInput(thisIssueId, type, relatedIssueId) {
781
+ const view = normalizeLinearRelationType(type);
782
+ if (view === "blockedBy") {
783
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "blocks" };
784
+ }
785
+ if (view === "duplicateOf") {
786
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "duplicate" };
787
+ }
788
+ return { issueId: thisIssueId, relatedIssueId, type: view };
789
+ }
790
+ function relationMatches(rel, type, issueRef) {
791
+ if (rel.type.toLowerCase() !== type.toLowerCase()) return false;
792
+ const ref = issueRef.trim().toLowerCase();
793
+ return rel.issue.id.toLowerCase() === ref || rel.issue.identifier.toLowerCase() === ref;
794
+ }
795
+ function formatRelationList(relations) {
796
+ if (!relations.length) return "(none)";
797
+ return relations.map((r) => `${r.type} ${r.issue.identifier || r.issue.id}`).join(", ");
798
+ }
799
+ function mapLabelNodes(nodes) {
800
+ return (nodes ?? []).flatMap((node) => {
801
+ const id = String(node.id ?? "").trim();
802
+ const name = String(node.name ?? "").trim();
803
+ if (!id) return [];
804
+ return [{ id, name }];
805
+ });
806
+ }
807
+ async function listLinearWorkspaceLabels(opts) {
808
+ const json = await linearGraphql(LABELS_QUERY, { first: 250 }, opts);
809
+ return mapLabelNodes(json.issueLabels?.nodes);
810
+ }
811
+ async function listLinearLabels(opts) {
812
+ const teamId = opts?.teamId?.trim();
813
+ if (teamId) {
814
+ const out = [];
815
+ let after;
816
+ for (let page = 0; page < 4; page++) {
817
+ const json = await linearGraphql(
818
+ TEAM_LABELS_QUERY,
819
+ after ? { id: teamId, first: 250, after } : { id: teamId, first: 250 },
820
+ opts
821
+ );
822
+ const conn = json.team?.labels;
823
+ out.push(...mapLabelNodes(conn?.nodes));
824
+ if (!conn?.pageInfo?.hasNextPage || !conn.pageInfo.endCursor) break;
825
+ after = conn.pageInfo.endCursor;
826
+ }
827
+ if (out.length) return out;
828
+ }
829
+ return listLinearWorkspaceLabels(opts);
830
+ }
831
+ async function queryLinearProjects(filter, opts) {
832
+ const out = [];
833
+ let after;
834
+ for (let page = 0; page < 4; page++) {
835
+ const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {}, ...after ? { after } : {} }, opts);
836
+ for (const node of json.projects?.nodes ?? []) {
837
+ const id = String(node.id ?? "").trim();
838
+ if (!id) continue;
839
+ out.push({ id, name: String(node.name ?? ""), ...node.slugId ? { slugId: String(node.slugId) } : {} });
840
+ }
841
+ const pageInfo = json.projects?.pageInfo;
842
+ if (!pageInfo?.hasNextPage || !pageInfo.endCursor) break;
843
+ after = pageInfo.endCursor;
844
+ }
845
+ return out;
846
+ }
847
+ function listLinearProjects(opts) {
848
+ return queryLinearProjects(void 0, opts);
849
+ }
850
+ async function resolveLinearProjectId(project, opts) {
851
+ if (isLinearNoneToken(project)) return null;
852
+ const s = (project ?? "").trim();
853
+ const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
854
+ const matched = await queryLinearProjects(filter, opts);
855
+ try {
856
+ return resolveLinearProject(matched, s);
857
+ } catch {
858
+ if (UUID_RE.test(s)) return s;
859
+ const listed = await queryLinearProjects(void 0, opts);
860
+ return resolveLinearProject(listed, s);
861
+ }
862
+ }
863
+ async function resolveLinearLabelIds(names, opts) {
864
+ if (names.length === 0) return [];
865
+ const labels = await listLinearLabels(opts);
866
+ return names.map((name) => {
867
+ const s = name.trim();
868
+ try {
869
+ return resolveLinearLabel(labels, s).id;
870
+ } catch (error) {
871
+ if (UUID_RE.test(s)) return s;
872
+ throw error;
873
+ }
874
+ });
875
+ }
876
+ async function applyLinearIssueRelations(issue, add, remove, opts) {
877
+ const current = [...issue.relations];
878
+ for (const rel of remove ?? []) {
879
+ const type = normalizeLinearRelationType(rel.type);
880
+ const issueRef = rel.issue.trim();
881
+ if (!issueRef) throw new Error("Linear removeRelations.issue is required (uuid or ENG-123)");
882
+ const match = current.find((r) => relationMatches(r, type, issueRef));
883
+ if (!match?.id) {
884
+ throw new Error(
885
+ `Linear relation not found: ${type} ${issueRef}. Existing: ${formatRelationList(current)}`
886
+ );
887
+ }
888
+ const json = await linearGraphql(ISSUE_RELATION_DELETE, { id: match.id }, opts);
889
+ if (!json.issueRelationDelete?.success) {
890
+ throw new Error(`Linear issueRelationDelete failed (${type} ${issueRef})`);
891
+ }
892
+ const idx = current.indexOf(match);
893
+ if (idx >= 0) current.splice(idx, 1);
894
+ }
895
+ for (const rel of add ?? []) {
896
+ const type = normalizeLinearRelationType(rel.type);
897
+ const issueRef = rel.issue.trim();
898
+ if (!issueRef) throw new Error("Linear relations.issue is required (uuid or ENG-123)");
899
+ if (current.some((r) => relationMatches(r, type, issueRef))) continue;
900
+ const related = await getLinearIssue(issueRef, opts);
901
+ const input = linearRelationCreateInput(issue.id, type, related.id);
902
+ const json = await linearGraphql(ISSUE_RELATION_CREATE, { input }, opts);
903
+ if (!json.issueRelationCreate?.success) {
904
+ throw new Error(`Linear issueRelationCreate failed (${type} ${related.identifier})`);
905
+ }
906
+ current.push({ type, issue: related, id: void 0 });
907
+ }
908
+ }
909
+ function normalizePriority(priority) {
910
+ if (priority == null) return void 0;
911
+ if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
912
+ throw new Error("Linear priority must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low)");
913
+ }
914
+ return priority;
915
+ }
916
+ function assigneeFilterKey(assignee) {
917
+ return (assignee ?? "").trim();
918
+ }
919
+ function applyLinearIssueQueryFilter(filter, query) {
920
+ const term = query?.trim();
921
+ if (!term) return;
922
+ const or = [
923
+ { title: { containsIgnoreCase: term } },
924
+ { description: { containsIgnoreCase: term } }
925
+ ];
926
+ const identifierMatch = term.match(/^[A-Za-z][\w]*-(\d+)$/);
927
+ const asNumber = identifierMatch ? Number(identifierMatch[1]) : /^\d+$/.test(term) ? Number(term) : NaN;
928
+ if (Number.isFinite(asNumber) && asNumber > 0) {
929
+ or.push({ number: { eq: asNumber } });
930
+ }
931
+ filter.or = or;
932
+ }
933
+ function buildLinearIssueFilter(input) {
934
+ const filter = {
935
+ state: { type: { nin: ["completed", "canceled"] } }
936
+ };
937
+ const since = input?.updatedSince?.trim();
938
+ if (since) filter.updatedAt = { gte: since };
939
+ applyLinearIssueQueryFilter(filter, input?.query);
940
+ const raw = assigneeFilterKey(input?.assignee) || "me";
941
+ const key = raw.toLowerCase();
942
+ if (key === "all" || key === "*") return filter;
943
+ if (key === "unassigned" || key === "none" || key === "null") {
944
+ filter.assignee = { null: true };
945
+ return filter;
946
+ }
947
+ if (key === "me" || key === "@me") {
948
+ filter.assignee = { isMe: { eq: true } };
949
+ return filter;
950
+ }
951
+ if (UUID_RE.test(raw)) {
952
+ filter.assignee = { id: { eq: raw } };
953
+ return filter;
954
+ }
955
+ filter.assignee = { name: { eqIgnoreCase: raw } };
956
+ return filter;
957
+ }
958
+ function isAssignedToMe(assignee) {
959
+ const key = assignee.toLowerCase();
960
+ return key === "me" || key === "@me";
961
+ }
962
+ function listedLinearIssues(json, nodes) {
963
+ return {
964
+ viewer: {
965
+ id: String(json.viewer?.id ?? ""),
966
+ name: String(json.viewer?.name ?? "")
967
+ },
968
+ issues: (nodes ?? []).map((node) => toIssueInfo(mapIssue(node)))
969
+ };
970
+ }
971
+ async function listLinearIssuesFiltered(opts) {
972
+ const first = Math.max(1, Math.min(250, opts?.limit ?? 200));
973
+ const query = opts?.query?.trim() ?? "";
974
+ const assignee = assigneeFilterKey(opts?.assignee) || (query ? "all" : "me");
975
+ const filter = buildLinearIssueFilter({
976
+ assignee,
977
+ updatedSince: opts?.updatedSince
978
+ });
979
+ if (query) {
980
+ const json2 = await linearGraphql(
981
+ SEARCH_ISSUES_QUERY,
982
+ { term: query, first, filter },
983
+ opts
984
+ );
985
+ return listedLinearIssues(json2, json2.searchIssues?.nodes);
986
+ }
987
+ if (isAssignedToMe(assignee)) {
988
+ const json2 = await linearGraphql(
989
+ ASSIGNED_ISSUES_QUERY,
990
+ { first, filter },
991
+ opts
992
+ );
993
+ return listedLinearIssues(json2, json2.viewer?.assignedIssues?.nodes);
994
+ }
995
+ const json = await linearGraphql(
996
+ ISSUES_QUERY,
997
+ { first, filter },
998
+ opts
999
+ );
1000
+ return listedLinearIssues(json, json.issues?.nodes);
1001
+ }
1002
+ async function listLinearCommentsSince(opts) {
1003
+ const first = Math.max(1, Math.min(250, opts.limit ?? 200));
1004
+ const query = opts.query?.trim() ?? "";
1005
+ const restrictToIssues = opts.issueIdentifiers != null;
1006
+ const allowed = new Set(
1007
+ (opts.issueIdentifiers ?? []).map((id) => id.trim()).filter(Boolean)
1008
+ );
1009
+ if (restrictToIssues && allowed.size === 0) return [];
1010
+ const assignee = assigneeFilterKey(opts.assignee) || (query ? "all" : "me");
1011
+ const json = await linearGraphql(
1012
+ COMMENTS_SINCE_QUERY,
1013
+ {
1014
+ first,
1015
+ filter: {
1016
+ createdAt: { gte: opts.since },
1017
+ issue: buildLinearIssueFilter({
1018
+ assignee,
1019
+ // Search hits may match identifier/comments — do not also require
1020
+ // title/description contains when an allowlist is already applied.
1021
+ query: restrictToIssues ? void 0 : query || void 0
1022
+ })
1023
+ }
1024
+ },
1025
+ opts
1026
+ );
1027
+ const out = [];
1028
+ for (const node of json.comments?.nodes ?? []) {
1029
+ const identifier = String(node.issue?.identifier ?? "").trim();
1030
+ if (restrictToIssues && !allowed.has(identifier)) continue;
1031
+ const body = previewIssueCommentBody(String(node.body ?? ""));
1032
+ if (!identifier && !body) continue;
1033
+ out.push({
1034
+ identifier: identifier || String(node.id ?? ""),
1035
+ ...node.issue?.title ? { title: String(node.issue.title) } : {},
1036
+ ...node.user?.name ? { author: String(node.user.name) } : {},
1037
+ ...node.createdAt?.trim() ? { createdAt: node.createdAt.trim() } : {},
1038
+ body,
1039
+ ...node.url?.trim() ? { url: node.url.trim() } : {}
1040
+ });
1041
+ }
1042
+ return out;
1043
+ }
1044
+ async function listLinearAssignedIssues(opts) {
1045
+ return listLinearIssuesFiltered({ ...opts, assignee: "me" });
1046
+ }
1047
+ async function listLinearIssuesDirect(opts) {
1048
+ return (await listLinearAssignedIssues(opts)).issues;
1049
+ }
1050
+ async function listLinearTeams(opts) {
1051
+ const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
1052
+ return {
1053
+ viewer: {
1054
+ id: String(json.viewer?.id ?? ""),
1055
+ name: String(json.viewer?.name ?? "")
1056
+ },
1057
+ teams: (json.teams?.nodes ?? []).map(mapTeam)
1058
+ };
1059
+ }
1060
+ async function listLinearTeamCycles(teamId, opts) {
1061
+ const json = await linearGraphql(TEAM_CYCLES_QUERY, { id: teamId }, opts);
1062
+ if (!json.team?.id) {
1063
+ throw new Error(`Linear team not found: ${teamId}`);
1064
+ }
1065
+ const mapped = mapTeam(json.team);
1066
+ return {
1067
+ id: mapped.id,
1068
+ key: mapped.key,
1069
+ activeCycle: mapped.activeCycle,
1070
+ cycles: mapped.cycles ?? []
1071
+ };
1072
+ }
1073
+ async function getLinearIssue(id, opts) {
1074
+ const issueId = id.trim();
1075
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
1076
+ const json = await linearGraphql(
1077
+ ISSUE_QUERY,
1078
+ { id: issueId },
1079
+ opts
1080
+ );
1081
+ if (!json.issue) {
1082
+ throw new Error(`Linear issue not found: ${issueId}`);
1083
+ }
1084
+ return mapIssue(json.issue);
1085
+ }
1086
+ async function createLinearIssue(input, opts) {
1087
+ const title = input.title.trim();
1088
+ if (!title) throw new Error("Linear issue title is required");
1089
+ const { viewer, teams } = await listLinearTeams(opts);
1090
+ const team = resolveLinearTeam(teams, input.team);
1091
+ const mutationInput = {
1092
+ teamId: team.id,
1093
+ title
1094
+ };
1095
+ const description = input.description?.trim();
1096
+ if (description) mutationInput.description = description;
1097
+ if (input.state?.trim()) {
1098
+ mutationInput.stateId = resolveLinearState(team, input.state).id;
1099
+ }
1100
+ const parentRef = input.parent?.trim();
1101
+ if (parentRef) {
1102
+ const parent = await getLinearIssue(parentRef, opts);
1103
+ mutationInput.parentId = parent.id;
1104
+ }
1105
+ const assignee = input.assignee === void 0 ? void 0 : input.assignee?.trim() || null;
1106
+ if (assignee === "me") mutationInput.assigneeId = viewer.id;
1107
+ else if (assignee) mutationInput.assigneeId = assignee;
1108
+ const priority = normalizePriority(input.priority);
1109
+ if (priority != null) mutationInput.priority = priority;
1110
+ const json = await linearGraphql(ISSUE_CREATE, { input: mutationInput }, opts);
1111
+ if (!json.issueCreate?.success || !json.issueCreate.issue) {
1112
+ throw new Error("Linear issueCreate failed");
1113
+ }
1114
+ return mapIssue(json.issueCreate.issue);
1115
+ }
1116
+ async function updateLinearIssue(input, opts) {
1117
+ const issueId = input.id.trim();
1118
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
1119
+ const mutationInput = {};
1120
+ if (input.title !== void 0) {
1121
+ const title = input.title.trim();
1122
+ if (!title) throw new Error("Linear issue title cannot be empty");
1123
+ mutationInput.title = title;
1124
+ }
1125
+ if (input.description !== void 0) {
1126
+ mutationInput.description = input.description;
1127
+ }
1128
+ const addRelations = input.relations?.length ? input.relations : void 0;
1129
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
1130
+ const needsIssue = Boolean(input.state?.trim()) || input.cycle !== void 0 || input.labels !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
1131
+ const existing = needsIssue ? await getLinearIssue(issueId, opts) : void 0;
1132
+ if (input.state?.trim()) {
1133
+ const { teams } = await listLinearTeams(opts);
1134
+ const teamRef = existing?.team?.key || existing?.team?.id;
1135
+ const team = teamRef ? resolveLinearTeam(teams, teamRef) : void 0;
1136
+ if (!team?.states.length) {
1137
+ throw new Error(`Linear issue ${existing?.identifier ?? issueId} has no workflow states to resolve "${input.state}"`);
1138
+ }
1139
+ mutationInput.stateId = resolveLinearState(team, input.state).id;
1140
+ }
1141
+ if (input.cycle !== void 0) {
1142
+ const teamId = existing?.team?.id;
1143
+ if (!teamId) {
1144
+ throw new Error(`Linear issue ${existing?.identifier ?? issueId} has no team to resolve a cycle`);
1145
+ }
1146
+ const cycles = await listLinearTeamCycles(teamId, opts);
1147
+ mutationInput.cycleId = resolveLinearCycle(
1148
+ { ...cycles, key: cycles.key || existing?.team?.key || teamId },
1149
+ input.cycle
1150
+ );
1151
+ }
1152
+ if (input.assignee !== void 0) {
1153
+ const assignee = input.assignee?.trim() || null;
1154
+ if (assignee === "me") {
1155
+ const { viewer } = await listLinearTeams(opts);
1156
+ mutationInput.assigneeId = viewer.id;
1157
+ } else {
1158
+ mutationInput.assigneeId = assignee;
1159
+ }
1160
+ }
1161
+ if (input.priority !== void 0) {
1162
+ mutationInput.priority = normalizePriority(input.priority);
1163
+ }
1164
+ if (input.labels !== void 0) {
1165
+ mutationInput.labelIds = await resolveLinearLabelIds(input.labels, {
1166
+ ...opts,
1167
+ teamId: existing?.team?.id
1168
+ });
1169
+ }
1170
+ if (input.project !== void 0) {
1171
+ mutationInput.projectId = await resolveLinearProjectId(input.project, opts);
1172
+ }
1173
+ if (input.parent !== void 0) {
1174
+ if (isLinearNoneToken(input.parent)) {
1175
+ mutationInput.parentId = null;
1176
+ } else {
1177
+ const parent = await getLinearIssue((input.parent ?? "").trim(), opts);
1178
+ mutationInput.parentId = parent.id;
1179
+ }
1180
+ }
1181
+ const hasIssueFields = Object.keys(mutationInput).length > 0;
1182
+ const hasRelationWrites = Boolean(addRelations || removeRelations);
1183
+ if (!hasIssueFields && !hasRelationWrites) {
1184
+ throw new Error(
1185
+ "linear_update_issue needs at least one of title, description, state, assignee, priority, cycle, labels, project, parent, relations, removeRelations"
1186
+ );
1187
+ }
1188
+ let updated;
1189
+ if (hasIssueFields) {
1190
+ const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
1191
+ if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
1192
+ throw new Error("Linear issueUpdate failed");
1193
+ }
1194
+ updated = mapIssue(json.issueUpdate.issue);
1195
+ }
1196
+ if (hasRelationWrites) {
1197
+ const base = updated ?? existing;
1198
+ if (!base) {
1199
+ throw new Error(`Linear issue not found: ${issueId}`);
1200
+ }
1201
+ await applyLinearIssueRelations(base, addRelations, removeRelations, opts);
1202
+ return getLinearIssue(base.id || issueId, opts);
1203
+ }
1204
+ return updated;
1205
+ }
1206
+ async function commentLinearIssue(input, opts) {
1207
+ const issueId = input.id.trim();
1208
+ const body = input.body.trim();
1209
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
1210
+ if (!body) throw new Error("Linear comment body is required");
1211
+ const json = await linearGraphql(COMMENT_CREATE, { input: { issueId, body } }, opts);
1212
+ if (!json.commentCreate?.success || !json.commentCreate.comment?.id) {
1213
+ throw new Error("Linear commentCreate failed");
1214
+ }
1215
+ return {
1216
+ id: String(json.commentCreate.comment.id),
1217
+ body: String(json.commentCreate.comment.body ?? body),
1218
+ url: json.commentCreate.comment.url?.trim() || void 0
1219
+ };
1220
+ }
1221
+ async function validateLinearApiKey(apiKey) {
1222
+ const key = apiKey.trim();
1223
+ if (!key) return false;
1224
+ try {
1225
+ await listLinearIssuesDirect({ limit: 1, apiKey: key });
1226
+ return true;
1227
+ } catch {
1228
+ return false;
1229
+ }
1230
+ }
1231
+
1232
+ export {
1233
+ downloadLinearAttachment,
1234
+ rewriteLinearError,
1235
+ linearGraphql,
1236
+ linearCycleIsActive,
1237
+ flipLinearRelationType,
1238
+ resolveLinearTeam,
1239
+ resolveLinearState,
1240
+ resolveLinearCycle,
1241
+ isLinearNoneToken,
1242
+ resolveLinearLabel,
1243
+ resolveLinearProject,
1244
+ normalizeLinearRelationType,
1245
+ linearRelationCreateInput,
1246
+ listLinearProjects,
1247
+ buildLinearIssueFilter,
1248
+ listLinearIssuesFiltered,
1249
+ listLinearCommentsSince,
1250
+ listLinearAssignedIssues,
1251
+ listLinearIssuesDirect,
1252
+ listLinearTeams,
1253
+ listLinearTeamCycles,
1254
+ getLinearIssue,
1255
+ createLinearIssue,
1256
+ updateLinearIssue,
1257
+ commentLinearIssue,
1258
+ validateLinearApiKey
1259
+ };