@enrichlayer/el-linear 1.2.0

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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/claude-skills/linear-operations/SKILL.md +315 -0
  4. package/claude-skills/linear-operations/evals/evals.json +46 -0
  5. package/dist/commands/attachments.d.ts +2 -0
  6. package/dist/commands/attachments.js +57 -0
  7. package/dist/commands/batch.d.ts +2 -0
  8. package/dist/commands/batch.js +309 -0
  9. package/dist/commands/comments.d.ts +2 -0
  10. package/dist/commands/comments.js +272 -0
  11. package/dist/commands/config.d.ts +2 -0
  12. package/dist/commands/config.js +15 -0
  13. package/dist/commands/cycles.d.ts +2 -0
  14. package/dist/commands/cycles.js +63 -0
  15. package/dist/commands/documents.d.ts +2 -0
  16. package/dist/commands/documents.js +175 -0
  17. package/dist/commands/embeds.d.ts +2 -0
  18. package/dist/commands/embeds.js +65 -0
  19. package/dist/commands/gdoc.d.ts +2 -0
  20. package/dist/commands/gdoc.js +37 -0
  21. package/dist/commands/graphql.d.ts +2 -0
  22. package/dist/commands/graphql.js +70 -0
  23. package/dist/commands/init/aliases.d.ts +109 -0
  24. package/dist/commands/init/aliases.js +569 -0
  25. package/dist/commands/init/defaults.d.ts +25 -0
  26. package/dist/commands/init/defaults.js +112 -0
  27. package/dist/commands/init/index.d.ts +18 -0
  28. package/dist/commands/init/index.js +182 -0
  29. package/dist/commands/init/shared.d.ts +88 -0
  30. package/dist/commands/init/shared.js +164 -0
  31. package/dist/commands/init/token.d.ts +50 -0
  32. package/dist/commands/init/token.js +141 -0
  33. package/dist/commands/init/workspace.d.ts +20 -0
  34. package/dist/commands/init/workspace.js +80 -0
  35. package/dist/commands/issue-id.d.ts +28 -0
  36. package/dist/commands/issue-id.js +81 -0
  37. package/dist/commands/issues.d.ts +2 -0
  38. package/dist/commands/issues.js +1145 -0
  39. package/dist/commands/labels.d.ts +2 -0
  40. package/dist/commands/labels.js +100 -0
  41. package/dist/commands/project-milestones.d.ts +2 -0
  42. package/dist/commands/project-milestones.js +143 -0
  43. package/dist/commands/projects.d.ts +2 -0
  44. package/dist/commands/projects.js +336 -0
  45. package/dist/commands/read-shortcut.d.ts +6 -0
  46. package/dist/commands/read-shortcut.js +69 -0
  47. package/dist/commands/releases.d.ts +2 -0
  48. package/dist/commands/releases.js +142 -0
  49. package/dist/commands/search.d.ts +2 -0
  50. package/dist/commands/search.js +171 -0
  51. package/dist/commands/teams.d.ts +2 -0
  52. package/dist/commands/teams.js +19 -0
  53. package/dist/commands/templates.d.ts +2 -0
  54. package/dist/commands/templates.js +58 -0
  55. package/dist/commands/users.d.ts +2 -0
  56. package/dist/commands/users.js +17 -0
  57. package/dist/config/config.d.ts +43 -0
  58. package/dist/config/config.js +81 -0
  59. package/dist/config/issue-validation.d.ts +39 -0
  60. package/dist/config/issue-validation.js +264 -0
  61. package/dist/config/paths.d.ts +20 -0
  62. package/dist/config/paths.js +22 -0
  63. package/dist/config/resolver.d.ts +25 -0
  64. package/dist/config/resolver.js +183 -0
  65. package/dist/config/status-defaults.d.ts +13 -0
  66. package/dist/config/status-defaults.js +20 -0
  67. package/dist/config/term-enforcer.d.ts +31 -0
  68. package/dist/config/term-enforcer.js +69 -0
  69. package/dist/main.d.ts +2 -0
  70. package/dist/main.js +76 -0
  71. package/dist/queries/attachments.d.ts +3 -0
  72. package/dist/queries/attachments.js +35 -0
  73. package/dist/queries/comments.d.ts +3 -0
  74. package/dist/queries/comments.js +64 -0
  75. package/dist/queries/common.d.ts +2 -0
  76. package/dist/queries/common.js +109 -0
  77. package/dist/queries/cycles.d.ts +2 -0
  78. package/dist/queries/cycles.js +40 -0
  79. package/dist/queries/documents.d.ts +5 -0
  80. package/dist/queries/documents.js +67 -0
  81. package/dist/queries/introspect.d.ts +2 -0
  82. package/dist/queries/introspect.js +24 -0
  83. package/dist/queries/issues.d.ts +23 -0
  84. package/dist/queries/issues.js +380 -0
  85. package/dist/queries/labels.d.ts +4 -0
  86. package/dist/queries/labels.js +53 -0
  87. package/dist/queries/project-milestones.d.ts +6 -0
  88. package/dist/queries/project-milestones.js +125 -0
  89. package/dist/queries/projects.d.ts +7 -0
  90. package/dist/queries/projects.js +104 -0
  91. package/dist/queries/releases.d.ts +4 -0
  92. package/dist/queries/releases.js +85 -0
  93. package/dist/queries/search.d.ts +1 -0
  94. package/dist/queries/search.js +35 -0
  95. package/dist/queries/templates.d.ts +2 -0
  96. package/dist/queries/templates.js +30 -0
  97. package/dist/types/linear.d.ts +217 -0
  98. package/dist/types/linear.js +6 -0
  99. package/dist/utils/auth.d.ts +4 -0
  100. package/dist/utils/auth.js +23 -0
  101. package/dist/utils/auto-link-references.d.ts +47 -0
  102. package/dist/utils/auto-link-references.js +188 -0
  103. package/dist/utils/date-format.d.ts +4 -0
  104. package/dist/utils/date-format.js +8 -0
  105. package/dist/utils/download-uploads.d.ts +7 -0
  106. package/dist/utils/download-uploads.js +88 -0
  107. package/dist/utils/embed-parser.d.ts +8 -0
  108. package/dist/utils/embed-parser.js +54 -0
  109. package/dist/utils/error-messages.d.ts +4 -0
  110. package/dist/utils/error-messages.js +17 -0
  111. package/dist/utils/file-service.d.ts +13 -0
  112. package/dist/utils/file-service.js +239 -0
  113. package/dist/utils/gdoc-parser.d.ts +45 -0
  114. package/dist/utils/gdoc-parser.js +107 -0
  115. package/dist/utils/graphql-attachments-service.d.ts +12 -0
  116. package/dist/utils/graphql-attachments-service.js +46 -0
  117. package/dist/utils/graphql-documents-service.d.ts +18 -0
  118. package/dist/utils/graphql-documents-service.js +97 -0
  119. package/dist/utils/graphql-issues-service.d.ts +49 -0
  120. package/dist/utils/graphql-issues-service.js +925 -0
  121. package/dist/utils/graphql-service.d.ts +8 -0
  122. package/dist/utils/graphql-service.js +35 -0
  123. package/dist/utils/identifier-parser.d.ts +7 -0
  124. package/dist/utils/identifier-parser.js +20 -0
  125. package/dist/utils/issue-reference-extractor.d.ts +18 -0
  126. package/dist/utils/issue-reference-extractor.js +95 -0
  127. package/dist/utils/issue-reference-wrapper.d.ts +12 -0
  128. package/dist/utils/issue-reference-wrapper.js +91 -0
  129. package/dist/utils/linear-service.d.ts +26 -0
  130. package/dist/utils/linear-service.js +442 -0
  131. package/dist/utils/logger.d.ts +4 -0
  132. package/dist/utils/logger.js +8 -0
  133. package/dist/utils/markdown-prosemirror.d.ts +24 -0
  134. package/dist/utils/markdown-prosemirror.js +325 -0
  135. package/dist/utils/mention-resolver.d.ts +31 -0
  136. package/dist/utils/mention-resolver.js +234 -0
  137. package/dist/utils/output.d.ts +7 -0
  138. package/dist/utils/output.js +125 -0
  139. package/dist/utils/table-formatter.d.ts +4 -0
  140. package/dist/utils/table-formatter.js +249 -0
  141. package/dist/utils/usage.d.ts +2 -0
  142. package/dist/utils/usage.js +24 -0
  143. package/dist/utils/uuid.d.ts +2 -0
  144. package/dist/utils/uuid.js +8 -0
  145. package/dist/utils/validate-references.d.ts +10 -0
  146. package/dist/utils/validate-references.js +33 -0
  147. package/dist/utils/validators.d.ts +7 -0
  148. package/dist/utils/validators.js +71 -0
  149. package/dist/utils/workspace-url.d.ts +4 -0
  150. package/dist/utils/workspace-url.js +44 -0
  151. package/package.json +71 -0
@@ -0,0 +1,925 @@
1
+ import { resolveUserDisplayName } from "../config/resolver.js";
2
+ import { BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
3
+ import { CREATE_LABEL_MUTATION } from "../queries/labels.js";
4
+ import { toISOStringOrNow } from "./date-format.js";
5
+ import { extractEmbeds } from "./embed-parser.js";
6
+ import { notFoundError } from "./error-messages.js";
7
+ import { parseIssueIdentifier, tryParseIssueIdentifier, } from "./identifier-parser.js";
8
+ import { isUuid } from "./uuid.js";
9
+ const TEAM_KEY_REGEX = /^[A-Z0-9]+$/i;
10
+ function extractSummaryText(summary) {
11
+ if (summary.generationStatus !== "completed" || !summary.content) {
12
+ return undefined;
13
+ }
14
+ const parts = [];
15
+ const walk = (node) => {
16
+ if (node.text) {
17
+ parts.push(node.text);
18
+ }
19
+ if (Array.isArray(node.content)) {
20
+ for (const child of node.content) {
21
+ walk(child);
22
+ }
23
+ }
24
+ };
25
+ walk(summary.content);
26
+ return parts.length > 0 ? parts.join("") : undefined;
27
+ }
28
+ export class GraphQLIssuesService {
29
+ graphQLService;
30
+ linearService;
31
+ constructor(graphQLService, linearService) {
32
+ this.graphQLService = graphQLService;
33
+ this.linearService = linearService;
34
+ }
35
+ async getIssues(limit = 25) {
36
+ const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, {
37
+ first: limit,
38
+ orderBy: "updatedAt",
39
+ });
40
+ const issues = result.issues;
41
+ if (!issues?.nodes) {
42
+ return [];
43
+ }
44
+ return issues.nodes.map((issue) => this.transformIssueData(issue));
45
+ }
46
+ async getIssueById(issueId) {
47
+ let issueData;
48
+ if (isUuid(issueId)) {
49
+ const result = await this.graphQLService.rawRequest(GET_ISSUE_BY_ID_QUERY, { id: issueId });
50
+ if (!result.issue) {
51
+ throw notFoundError("Issue", issueId);
52
+ }
53
+ issueData = result.issue;
54
+ }
55
+ else {
56
+ const { teamKey, issueNumber } = parseIssueIdentifier(issueId);
57
+ const result = await this.graphQLService.rawRequest(GET_ISSUE_BY_IDENTIFIER_QUERY, {
58
+ teamKey,
59
+ number: issueNumber,
60
+ });
61
+ const issues = result.issues;
62
+ const nodes = issues?.nodes;
63
+ if (!nodes?.length) {
64
+ throw notFoundError("Issue", issueId);
65
+ }
66
+ issueData = nodes[0];
67
+ }
68
+ return this.transformIssueData(issueData);
69
+ }
70
+ async updateIssue(args, labelMode = "overwriting") {
71
+ const { resolvedIssueId, issueTeamId, currentIssueLabels, resolveResult } = await this.resolveUpdateContext(args);
72
+ let teamIdForLabels = issueTeamId;
73
+ if (!teamIdForLabels && args.labelIds && resolvedIssueId) {
74
+ teamIdForLabels = await this.fetchIssueTeamId(resolvedIssueId);
75
+ }
76
+ const finalLabelIds = await this.resolveLabelsWithMode(args.labelIds, resolveResult, labelMode, currentIssueLabels, teamIdForLabels, args.teamId);
77
+ const finalProjectId = args.projectId
78
+ ? this.resolveProjectId(args.projectId, resolveResult)
79
+ : args.projectId;
80
+ const { projectMilestoneNodes, issueProjectMilestoneNodes } = this.extractMilestoneNodes(args, resolveResult);
81
+ const finalMilestoneId = this.resolveMilestoneId(args.milestoneId, resolveResult, projectMilestoneNodes, issueProjectMilestoneNodes);
82
+ const finalCycleId = await this.resolveCycleIdForUpdate(args, resolveResult, resolvedIssueId);
83
+ const resolvedStatusId = await this.resolveStatusIdForUpdate(args, resolvedIssueId);
84
+ if (args.assigneeId && !isUuid(args.assigneeId)) {
85
+ args.assigneeId = await this.linearService.resolveUserId(args.assigneeId);
86
+ }
87
+ const updateInput = this.buildUpdateInput(args, {
88
+ statusId: resolvedStatusId,
89
+ projectId: finalProjectId,
90
+ labelIds: finalLabelIds,
91
+ milestoneId: finalMilestoneId,
92
+ cycleId: finalCycleId,
93
+ });
94
+ return this.executeUpdateMutation(resolvedIssueId, args.id, updateInput);
95
+ }
96
+ extractMilestoneNodes(args, resolveResult) {
97
+ const projectNodes = resolveResult.projects?.nodes;
98
+ const projectMilestoneNodes = args.projectId
99
+ ? projectNodes?.[0]?.projectMilestones?.nodes
100
+ : undefined;
101
+ const issueNodes = resolveResult.issues
102
+ ?.nodes;
103
+ const issueProjectMilestoneNodes = issueNodes?.[0]?.project
104
+ ?.projectMilestones?.nodes;
105
+ return { projectMilestoneNodes, issueProjectMilestoneNodes };
106
+ }
107
+ async executeUpdateMutation(resolvedIssueId, originalId, updateInput) {
108
+ let updateResult;
109
+ try {
110
+ updateResult = await this.graphQLService.rawRequest(UPDATE_ISSUE_MUTATION, {
111
+ id: resolvedIssueId,
112
+ input: updateInput,
113
+ });
114
+ }
115
+ catch (error) {
116
+ const msg = error instanceof Error ? error.message : String(error);
117
+ if (msg.includes("Discrepancy between issue team")) {
118
+ const hint = updateInput.projectId
119
+ ? `The project may not be associated with this issue's team. Fix with: el-linear projects add-team "<project>" <team>`
120
+ : "The issue's team doesn't match the assigned project, cycle, or status.";
121
+ throw new Error(`Failed to update issue ${originalId}: ${hint}`);
122
+ }
123
+ throw new Error(`Failed to update issue ${originalId}: ${msg}`);
124
+ }
125
+ const issueUpdate = updateResult.issueUpdate;
126
+ if (!issueUpdate.success) {
127
+ throw new Error(`Failed to update issue ${originalId}`);
128
+ }
129
+ if (!issueUpdate.issue) {
130
+ throw new Error("Failed to retrieve updated issue");
131
+ }
132
+ return this.transformIssueData(issueUpdate.issue);
133
+ }
134
+ async createIssue(args) {
135
+ const resolveVariables = this.buildCreateResolveVariables(args);
136
+ let resolveResult = {};
137
+ if (Object.keys(resolveVariables).length > 0) {
138
+ const { __labelNames, ...queryVars } = resolveVariables;
139
+ const requests = [
140
+ this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_CREATE_QUERY, queryVars),
141
+ ];
142
+ if (__labelNames) {
143
+ const labelQuery = buildResolveLabelsByNameQuery(__labelNames);
144
+ requests.push(this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables));
145
+ }
146
+ const results = await Promise.all(requests);
147
+ resolveResult = results[0];
148
+ if (results[1]) {
149
+ resolveResult.labels = results[1].labels;
150
+ }
151
+ }
152
+ const resolved = await this.resolveCreateFields(args, resolveResult);
153
+ if (args.assigneeId && !isUuid(args.assigneeId)) {
154
+ args.assigneeId = await this.linearService.resolveUserId(args.assigneeId);
155
+ }
156
+ const createInput = this.buildCreateInput(args, resolved);
157
+ return this.executeCreateMutation(createInput);
158
+ }
159
+ async resolveCreateFields(args, resolveResult) {
160
+ let teamId = args.teamId
161
+ ? await this.resolveTeamId(args.teamId, resolveResult)
162
+ : args.teamId;
163
+ const projectId = args.projectId
164
+ ? this.resolveProjectId(args.projectId, resolveResult)
165
+ : args.projectId;
166
+ // Validate team-project compatibility and auto-correct when possible
167
+ if (projectId && teamId) {
168
+ teamId = this.validateProjectTeam(teamId, args.projectId, resolveResult);
169
+ }
170
+ const labelIds = await this.resolveLabels(args.labelIds, resolveResult, teamId, args.teamInput);
171
+ const parentId = this.resolveParentId(args.parentId, resolveResult);
172
+ const milestoneId = this.resolveMilestoneIdForCreate(args.milestoneId, resolveResult, projectId);
173
+ let cycleId = args.cycleId;
174
+ if (args.cycleId &&
175
+ typeof args.cycleId === "string" &&
176
+ !isUuid(args.cycleId)) {
177
+ cycleId = await this.linearService.resolveCycleId(args.cycleId, teamId);
178
+ }
179
+ let statusId = args.statusId;
180
+ if (args.statusId && !isUuid(args.statusId)) {
181
+ statusId = await this.linearService.resolveStatusId(args.statusId, teamId);
182
+ }
183
+ return {
184
+ teamId,
185
+ projectId,
186
+ statusId,
187
+ labelIds,
188
+ parentId,
189
+ milestoneId,
190
+ cycleId,
191
+ };
192
+ }
193
+ async executeCreateMutation(createInput) {
194
+ let createResult;
195
+ try {
196
+ createResult = await this.graphQLService.rawRequest(CREATE_ISSUE_MUTATION, {
197
+ input: createInput,
198
+ });
199
+ }
200
+ catch (error) {
201
+ const msg = error instanceof Error ? error.message : String(error);
202
+ throw new Error(`Failed to create issue: ${msg}`);
203
+ }
204
+ const issueCreate = createResult.issueCreate;
205
+ if (!issueCreate.success) {
206
+ throw new Error("Failed to create issue: the API reported failure.");
207
+ }
208
+ if (!issueCreate.issue) {
209
+ throw new Error("Failed to retrieve created issue");
210
+ }
211
+ return this.transformIssueData(issueCreate.issue);
212
+ }
213
+ async searchIssues(args) {
214
+ const resolveVariables = {};
215
+ if (args.teamId && !isUuid(args.teamId)) {
216
+ Object.assign(resolveVariables, this.buildResolveVariablesForTeam(args.teamId));
217
+ }
218
+ if (args.projectId && !isUuid(args.projectId)) {
219
+ resolveVariables.projectName = args.projectId;
220
+ }
221
+ if (args.assigneeId &&
222
+ !isUuid(args.assigneeId) &&
223
+ args.assigneeId.includes("@")) {
224
+ resolveVariables.assigneeEmail = args.assigneeId;
225
+ }
226
+ let resolveResult = {};
227
+ if (Object.keys(resolveVariables).length > 0) {
228
+ resolveResult = await this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_SEARCH_QUERY, resolveVariables);
229
+ }
230
+ const finalTeamId = args.teamId
231
+ ? await this.resolveTeamId(args.teamId, resolveResult)
232
+ : args.teamId;
233
+ const finalProjectId = args.projectId
234
+ ? this.resolveProjectId(args.projectId, resolveResult)
235
+ : args.projectId;
236
+ const finalAssigneeId = this.resolveAssigneeId(args.assigneeId, resolveResult);
237
+ if (args.query) {
238
+ const searchResult = await this.graphQLService.rawRequest(SEARCH_ISSUES_QUERY, {
239
+ term: args.query,
240
+ first: args.limit || 10,
241
+ });
242
+ const searchIssues = searchResult.searchIssues;
243
+ if (!searchIssues?.nodes) {
244
+ return [];
245
+ }
246
+ const results = searchIssues.nodes.map((issue) => this.transformIssueData(issue));
247
+ return this.applySearchFilters(results, {
248
+ teamId: finalTeamId,
249
+ assigneeId: finalAssigneeId,
250
+ projectId: finalProjectId,
251
+ status: args.status,
252
+ labelNames: args.labelNames,
253
+ priority: args.priority,
254
+ });
255
+ }
256
+ const filter = this.buildSearchFilter({
257
+ teamId: finalTeamId,
258
+ assigneeId: finalAssigneeId,
259
+ projectId: finalProjectId,
260
+ noProject: args.noProject,
261
+ status: args.status,
262
+ labelNames: args.labelNames,
263
+ priority: args.priority,
264
+ });
265
+ const searchResult = await this.graphQLService.rawRequest(FILTERED_SEARCH_ISSUES_QUERY, {
266
+ first: args.limit || 10,
267
+ filter: Object.keys(filter).length > 0 ? filter : undefined,
268
+ orderBy: args.orderBy || "updatedAt",
269
+ });
270
+ const filteredIssues = searchResult.issues;
271
+ if (!filteredIssues?.nodes) {
272
+ return [];
273
+ }
274
+ return filteredIssues.nodes.map((issue) => this.transformIssueData(issue));
275
+ }
276
+ // --- Private helper methods for field resolution ---
277
+ async resolveTeamId(teamId, resolveResult) {
278
+ if (isUuid(teamId)) {
279
+ return teamId;
280
+ }
281
+ const teams = resolveResult.teams;
282
+ const teamNodes = teams?.nodes;
283
+ const resolvedTeam = teamNodes?.[0];
284
+ if (resolvedTeam &&
285
+ (resolvedTeam.key.toUpperCase() === teamId.toUpperCase() ||
286
+ resolvedTeam.name.toLowerCase() === teamId.toLowerCase())) {
287
+ return resolvedTeam.id;
288
+ }
289
+ // Exact GraphQL match failed — fall back to prefix matching via LinearService
290
+ return this.linearService.resolveTeamId(teamId);
291
+ }
292
+ resolveProjectId(projectId, resolveResult) {
293
+ if (isUuid(projectId)) {
294
+ return projectId;
295
+ }
296
+ const projects = resolveResult.projects;
297
+ const projectNodes = projects?.nodes;
298
+ if (!projectNodes?.length) {
299
+ throw notFoundError("Project", projectId);
300
+ }
301
+ return projectNodes[0].id;
302
+ }
303
+ /**
304
+ * Check that the resolved team is associated with the project.
305
+ * If not, auto-switch when the project belongs to exactly one team.
306
+ */
307
+ validateProjectTeam(teamId, projectInput, resolveResult) {
308
+ const projects = resolveResult.projects;
309
+ const projectNode = projects?.nodes?.[0];
310
+ if (!projectNode) {
311
+ return teamId;
312
+ }
313
+ const teamsConn = projectNode.teams;
314
+ const teamNodes = teamsConn?.nodes;
315
+ if (!teamNodes?.length) {
316
+ return teamId;
317
+ }
318
+ const isAssociated = teamNodes.some((t) => t.id === teamId);
319
+ if (isAssociated) {
320
+ return teamId;
321
+ }
322
+ const projectName = projectNode.name || projectInput;
323
+ const teamKeys = teamNodes.map((t) => t.key);
324
+ if (teamNodes.length === 1) {
325
+ const correctTeamId = teamNodes[0].id;
326
+ const correctKey = teamKeys[0];
327
+ process.stderr.write(`Auto-switched team to ${correctKey} (the only team associated with project "${projectName}").\n`);
328
+ return correctTeamId;
329
+ }
330
+ throw new Error(`Project "${projectName}" is not associated with the specified team.\n` +
331
+ `Associated teams: ${teamKeys.join(", ")}\n` +
332
+ `Hint: Use --team ${teamKeys[0]} or run \`el-linear projects add-team "${projectName}" TEAM\` to add your team.`);
333
+ }
334
+ async resolveLabels(labelIds, resolveResult, teamId, teamInput) {
335
+ return this.resolveLabelsWithMode(labelIds, resolveResult, "overwriting", [], teamId, teamInput);
336
+ }
337
+ async resolveLabelsWithMode(labelIds, resolveResult, labelMode, currentIssueLabels, teamId, teamInput) {
338
+ if (!(labelIds && Array.isArray(labelIds))) {
339
+ return labelIds;
340
+ }
341
+ const resolvedLabels = [];
342
+ const labels = resolveResult.labels;
343
+ const labelNodes = labels?.nodes;
344
+ for (const labelIdOrName of labelIds) {
345
+ if (isUuid(labelIdOrName)) {
346
+ resolvedLabels.push(labelIdOrName);
347
+ }
348
+ else {
349
+ const lowerName = labelIdOrName.toLowerCase();
350
+ const candidates = labelNodes?.filter((l) => l.name.toLowerCase() === lowerName);
351
+ let label;
352
+ if (candidates && candidates.length > 1 && teamId) {
353
+ label = candidates.find((l) => {
354
+ const team = l.team;
355
+ return team?.id === teamId;
356
+ });
357
+ }
358
+ if (!label) {
359
+ label = candidates?.[0];
360
+ }
361
+ if (!label) {
362
+ // Auto-create the missing label on the team
363
+ const created = await this.autoCreateLabel(labelIdOrName, teamId);
364
+ if (created) {
365
+ resolvedLabels.push(created);
366
+ continue;
367
+ }
368
+ const teamKey = this.resolveTeamKeyFromResult(resolveResult, teamId, teamInput);
369
+ const hint = teamKey
370
+ ? `— check available labels with: el-linear labels list --team ${teamKey}`
371
+ : undefined;
372
+ throw notFoundError("Label", labelIdOrName, undefined, hint);
373
+ }
374
+ if (label.isGroup) {
375
+ const teamKey = this.resolveTeamKeyFromResult(resolveResult, teamId, teamInput);
376
+ const hint = teamKey
377
+ ? ` Run: el-linear labels list --team ${teamKey}`
378
+ : "";
379
+ throw new Error(`Label "${labelIdOrName}" is a group label. Use a specific child label instead.${hint}`);
380
+ }
381
+ resolvedLabels.push(label.id);
382
+ }
383
+ }
384
+ if (labelMode === "adding") {
385
+ return [...new Set([...currentIssueLabels, ...resolvedLabels])];
386
+ }
387
+ return resolvedLabels;
388
+ }
389
+ /** Auto-create a missing label on the team. Returns the new label ID or null on failure. */
390
+ async autoCreateLabel(name, teamId) {
391
+ try {
392
+ const input = { name };
393
+ if (teamId) {
394
+ input.teamId = teamId;
395
+ }
396
+ const result = await this.graphQLService.rawRequest(CREATE_LABEL_MUTATION, { input });
397
+ const created = result.issueLabelCreate;
398
+ if (created?.success && created.issueLabel) {
399
+ const labelData = created.issueLabel;
400
+ const teamInfo = labelData.team;
401
+ const teamKey = teamInfo?.key ?? "";
402
+ process.stderr.write(`Auto-created label "${name}" on team ${teamKey}\n`);
403
+ return labelData.id;
404
+ }
405
+ return null;
406
+ }
407
+ catch {
408
+ return null;
409
+ }
410
+ }
411
+ resolveTeamKeyFromResult(resolveResult, teamId, teamInput) {
412
+ // Collect team nodes from both the top-level teams query and the project's teams.
413
+ // When the team was pre-resolved to a UUID by config, the top-level teams query
414
+ // runs with null filters and may return an unrelated team. The project's teams
415
+ // always include the correct team when a project is specified.
416
+ const allTeamNodes = this.collectTeamNodes(resolveResult);
417
+ if (teamId) {
418
+ const match = allTeamNodes.find((t) => t.id === teamId);
419
+ if (match) {
420
+ return match.key;
421
+ }
422
+ }
423
+ // If the team was pre-resolved to UUID by config and not found in the result,
424
+ // use the original input (e.g., "DEV") as the hint.
425
+ if (teamInput && !isUuid(teamInput)) {
426
+ return teamInput.toUpperCase();
427
+ }
428
+ // Only fall back to first team if there's exactly one (unambiguous).
429
+ // With multiple teams, returning an arbitrary one produces misleading error hints.
430
+ const topLevelNodes = resolveResult.teams?.nodes;
431
+ return topLevelNodes?.length === 1
432
+ ? topLevelNodes[0].key
433
+ : undefined;
434
+ }
435
+ collectTeamNodes(resolveResult) {
436
+ const nodes = [];
437
+ const seen = new Set();
438
+ const topTeams = resolveResult.teams;
439
+ const topNodes = topTeams?.nodes;
440
+ if (topNodes) {
441
+ for (const t of topNodes) {
442
+ const id = t.id;
443
+ if (!seen.has(id)) {
444
+ seen.add(id);
445
+ nodes.push(t);
446
+ }
447
+ }
448
+ }
449
+ // Also check project teams (which are always fetched when --project is used)
450
+ const projects = resolveResult.projects;
451
+ const projectNode = projects?.nodes?.[0];
452
+ if (projectNode) {
453
+ const projectTeams = projectNode.teams;
454
+ const projectTeamNodes = projectTeams?.nodes;
455
+ if (projectTeamNodes) {
456
+ for (const t of projectTeamNodes) {
457
+ const id = t.id;
458
+ if (!seen.has(id)) {
459
+ seen.add(id);
460
+ nodes.push(t);
461
+ }
462
+ }
463
+ }
464
+ }
465
+ return nodes;
466
+ }
467
+ resolveMilestoneId(milestoneId, resolveResult, projectMilestoneNodes, issueProjectMilestoneNodes) {
468
+ if (!milestoneId || isUuid(milestoneId)) {
469
+ return milestoneId;
470
+ }
471
+ let resolved = milestoneId;
472
+ if (projectMilestoneNodes) {
473
+ const projectMilestone = projectMilestoneNodes.find((m) => m.name === milestoneId);
474
+ if (projectMilestone) {
475
+ resolved = projectMilestone.id;
476
+ }
477
+ }
478
+ if (!isUuid(resolved) && issueProjectMilestoneNodes) {
479
+ const issueMilestone = issueProjectMilestoneNodes.find((m) => m.name === milestoneId);
480
+ if (issueMilestone) {
481
+ resolved = issueMilestone.id;
482
+ }
483
+ }
484
+ const milestones = resolveResult.milestones;
485
+ const milestoneNodes = milestones?.nodes;
486
+ if (!isUuid(resolved) && milestoneNodes?.length) {
487
+ resolved = milestoneNodes[0].id;
488
+ }
489
+ if (!isUuid(resolved)) {
490
+ throw notFoundError("Milestone", milestoneId);
491
+ }
492
+ return resolved;
493
+ }
494
+ resolveMilestoneIdForCreate(milestoneId, resolveResult, finalProjectId) {
495
+ if (!milestoneId || isUuid(milestoneId)) {
496
+ return milestoneId;
497
+ }
498
+ let resolved;
499
+ const createProjects = resolveResult.projects;
500
+ const createProjectNodes = createProjects?.nodes;
501
+ const createProjectMilestones = createProjectNodes?.[0]
502
+ ?.projectMilestones;
503
+ const createProjectMilestoneNodes = createProjectMilestones?.nodes;
504
+ if (createProjectMilestoneNodes) {
505
+ const projectMilestone = createProjectMilestoneNodes.find((m) => m.name === milestoneId);
506
+ if (projectMilestone) {
507
+ resolved = projectMilestone.id;
508
+ }
509
+ }
510
+ const createMilestones = resolveResult.milestones;
511
+ const createMilestoneNodes = createMilestones?.nodes;
512
+ if (!resolved && createMilestoneNodes?.length) {
513
+ resolved = createMilestoneNodes[0].id;
514
+ }
515
+ if (!resolved) {
516
+ if (finalProjectId) {
517
+ throw notFoundError("Milestone", milestoneId, "in project");
518
+ }
519
+ throw notFoundError("Milestone", milestoneId, undefined, "Consider specifying --project.");
520
+ }
521
+ return resolved;
522
+ }
523
+ async fetchIssueTeamId(issueId) {
524
+ const result = await this.graphQLService.rawRequest(GET_ISSUE_TEAM_QUERY, {
525
+ issueId,
526
+ });
527
+ const fetchedIssue = result.issue;
528
+ const fetchedTeam = fetchedIssue?.team;
529
+ return fetchedTeam?.id;
530
+ }
531
+ buildCreateInput(args, resolved) {
532
+ const input = { title: args.title };
533
+ if (resolved.teamId) {
534
+ input.teamId = resolved.teamId;
535
+ }
536
+ if (args.description) {
537
+ input.description = args.description;
538
+ }
539
+ if (args.assigneeId) {
540
+ input.assigneeId = args.assigneeId;
541
+ }
542
+ if (args.priority !== undefined) {
543
+ input.priority = args.priority;
544
+ }
545
+ if (resolved.projectId) {
546
+ input.projectId = resolved.projectId;
547
+ }
548
+ if (resolved.statusId) {
549
+ input.stateId = resolved.statusId;
550
+ }
551
+ if (resolved.labelIds &&
552
+ Array.isArray(resolved.labelIds) &&
553
+ resolved.labelIds.length > 0) {
554
+ input.labelIds = resolved.labelIds;
555
+ }
556
+ if (args.estimate !== undefined) {
557
+ input.estimate = args.estimate;
558
+ }
559
+ if (resolved.parentId) {
560
+ input.parentId = resolved.parentId;
561
+ }
562
+ if (resolved.milestoneId) {
563
+ input.projectMilestoneId = resolved.milestoneId;
564
+ }
565
+ if (resolved.cycleId) {
566
+ input.cycleId = resolved.cycleId;
567
+ }
568
+ if (args.subscriberIds &&
569
+ Array.isArray(args.subscriberIds) &&
570
+ args.subscriberIds.length > 0) {
571
+ input.subscriberIds = args.subscriberIds;
572
+ }
573
+ if (args.dueDate !== undefined) {
574
+ input.dueDate = args.dueDate;
575
+ }
576
+ return input;
577
+ }
578
+ buildUpdateInput(args, resolved) {
579
+ const input = {};
580
+ if (args.title !== undefined) {
581
+ input.title = args.title;
582
+ }
583
+ if (args.description !== undefined) {
584
+ input.description = args.description;
585
+ }
586
+ if (resolved.statusId !== undefined) {
587
+ input.stateId = resolved.statusId;
588
+ }
589
+ if (args.priority !== undefined) {
590
+ input.priority = args.priority;
591
+ }
592
+ if (args.assigneeId !== undefined) {
593
+ input.assigneeId = args.assigneeId;
594
+ }
595
+ if (resolved.projectId !== undefined) {
596
+ input.projectId = resolved.projectId;
597
+ }
598
+ if (resolved.cycleId !== undefined) {
599
+ input.cycleId = resolved.cycleId;
600
+ }
601
+ if (args.estimate !== undefined) {
602
+ input.estimate = args.estimate;
603
+ }
604
+ if (args.parentId !== undefined) {
605
+ input.parentId = args.parentId;
606
+ }
607
+ if (resolved.milestoneId !== undefined) {
608
+ input.projectMilestoneId = resolved.milestoneId;
609
+ }
610
+ if (resolved.labelIds !== undefined) {
611
+ input.labelIds = resolved.labelIds;
612
+ }
613
+ if (args.dueDate !== undefined) {
614
+ input.dueDate = args.dueDate;
615
+ }
616
+ return input;
617
+ }
618
+ buildResolveVariablesForTeam(teamId) {
619
+ const isTeamKey = teamId.length <= 5 && TEAM_KEY_REGEX.test(teamId);
620
+ if (isTeamKey) {
621
+ return { teamKey: teamId, teamName: null };
622
+ }
623
+ return { teamKey: null, teamName: teamId };
624
+ }
625
+ applySearchFilters(results, filters) {
626
+ let filtered = results;
627
+ if (filters.teamId) {
628
+ filtered = filtered.filter((issue) => issue.team?.id === filters.teamId);
629
+ }
630
+ if (filters.assigneeId) {
631
+ filtered = filtered.filter((issue) => issue.assignee?.id === filters.assigneeId);
632
+ }
633
+ if (filters.projectId) {
634
+ filtered = filtered.filter((issue) => issue.project?.id === filters.projectId);
635
+ }
636
+ if (filters.status && filters.status.length > 0) {
637
+ filtered = filtered.filter((issue) => filters.status.includes(issue.state?.name ?? ""));
638
+ }
639
+ if (filters.labelNames && filters.labelNames.length > 0) {
640
+ const lowerNames = filters.labelNames.map((n) => n.toLowerCase());
641
+ filtered = filtered.filter((issue) => {
642
+ const issueLabels = issue.labels.map((l) => l.name.toLowerCase());
643
+ return lowerNames.every((name) => issueLabels.includes(name));
644
+ });
645
+ }
646
+ if (filters.priority && filters.priority.length > 0) {
647
+ filtered = filtered.filter((issue) => filters.priority.includes(issue.priority));
648
+ }
649
+ return filtered;
650
+ }
651
+ buildSearchFilter(filters) {
652
+ const filter = {};
653
+ if (filters.teamId) {
654
+ filter.team = { id: { eq: filters.teamId } };
655
+ }
656
+ if (filters.assigneeId) {
657
+ filter.assignee = { id: { eq: filters.assigneeId } };
658
+ }
659
+ if (filters.noProject) {
660
+ filter.project = { null: true };
661
+ }
662
+ else if (filters.projectId) {
663
+ filter.project = { id: { eq: filters.projectId } };
664
+ }
665
+ if (filters.status && filters.status.length > 0) {
666
+ filter.state = { name: { in: filters.status } };
667
+ }
668
+ if (filters.labelNames && filters.labelNames.length > 0) {
669
+ if (filters.labelNames.length === 1) {
670
+ filter.labels = {
671
+ some: { name: { eqIgnoreCase: filters.labelNames[0] } },
672
+ };
673
+ }
674
+ else {
675
+ filter.labels = {
676
+ and: filters.labelNames.map((name) => ({
677
+ some: { name: { eqIgnoreCase: name } },
678
+ })),
679
+ };
680
+ }
681
+ }
682
+ if (filters.priority && filters.priority.length > 0) {
683
+ filter.priority = { in: filters.priority };
684
+ }
685
+ return filter;
686
+ }
687
+ resolveAssigneeId(assigneeId, resolveResult) {
688
+ if (!assigneeId || isUuid(assigneeId) || !assigneeId.includes("@")) {
689
+ return assigneeId;
690
+ }
691
+ const users = resolveResult.users;
692
+ const userNodes = users?.nodes;
693
+ if (!userNodes?.length) {
694
+ throw notFoundError("User", assigneeId);
695
+ }
696
+ return userNodes[0].id;
697
+ }
698
+ async resolveUpdateContext(args) {
699
+ let resolvedIssueId = args.id;
700
+ let issueTeamId;
701
+ let currentIssueLabels = [];
702
+ const resolveVariables = {};
703
+ if (!isUuid(args.id)) {
704
+ const { teamKey, issueNumber } = parseIssueIdentifier(args.id);
705
+ resolveVariables.teamKey = teamKey;
706
+ resolveVariables.issueNumber = issueNumber;
707
+ }
708
+ if (args.labelIds && Array.isArray(args.labelIds)) {
709
+ const nonUuidLabels = args.labelIds.filter((id) => !isUuid(id));
710
+ if (nonUuidLabels.length > 0) {
711
+ resolveVariables.__labelNames = nonUuidLabels;
712
+ }
713
+ }
714
+ if (args.projectId && !isUuid(args.projectId)) {
715
+ resolveVariables.projectName = args.projectId;
716
+ }
717
+ if (args.milestoneId &&
718
+ typeof args.milestoneId === "string" &&
719
+ !isUuid(args.milestoneId)) {
720
+ resolveVariables.milestoneName = args.milestoneId;
721
+ }
722
+ const { __labelNames, ...updateQueryVars } = resolveVariables;
723
+ const requests = [
724
+ this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_UPDATE_QUERY, updateQueryVars),
725
+ ];
726
+ if (__labelNames) {
727
+ const labelQuery = buildResolveLabelsByNameQuery(__labelNames);
728
+ requests.push(this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables));
729
+ }
730
+ const results = await Promise.all(requests);
731
+ const resolveResult = results[0];
732
+ if (results[1]) {
733
+ resolveResult.labels = results[1].labels;
734
+ }
735
+ if (!isUuid(args.id)) {
736
+ const resolvedIssues = resolveResult.issues;
737
+ const resolvedIssueNodes = resolvedIssues.nodes;
738
+ if (!resolvedIssueNodes.length) {
739
+ throw notFoundError("Issue", args.id);
740
+ }
741
+ resolvedIssueId = resolvedIssueNodes[0].id;
742
+ const team = resolvedIssueNodes[0].team;
743
+ issueTeamId = team?.id;
744
+ const issueLabels = resolvedIssueNodes[0].labels;
745
+ currentIssueLabels = issueLabels.nodes.map((l) => l.id);
746
+ }
747
+ return { resolvedIssueId, issueTeamId, currentIssueLabels, resolveResult };
748
+ }
749
+ async resolveCycleIdForUpdate(args, resolveResult, resolvedIssueId) {
750
+ if (args.cycleId === undefined || args.cycleId === null) {
751
+ return args.cycleId;
752
+ }
753
+ if (typeof args.cycleId !== "string" || isUuid(args.cycleId)) {
754
+ return args.cycleId;
755
+ }
756
+ let teamIdForCycle = resolveResult.issues?.nodes?.[0]?.team?.id;
757
+ if (!teamIdForCycle && resolvedIssueId && isUuid(resolvedIssueId)) {
758
+ teamIdForCycle = await this.fetchIssueTeamId(resolvedIssueId);
759
+ }
760
+ return this.linearService.resolveCycleId(args.cycleId, teamIdForCycle);
761
+ }
762
+ async resolveStatusIdForUpdate(args, resolvedIssueId) {
763
+ if (!args.statusId || isUuid(args.statusId)) {
764
+ return args.statusId;
765
+ }
766
+ let teamId;
767
+ if (resolvedIssueId && isUuid(resolvedIssueId)) {
768
+ teamId = await this.fetchIssueTeamId(resolvedIssueId);
769
+ }
770
+ return this.linearService.resolveStatusId(args.statusId, teamId);
771
+ }
772
+ buildCreateResolveVariables(args) {
773
+ const resolveVariables = {};
774
+ if (args.teamId && !isUuid(args.teamId)) {
775
+ Object.assign(resolveVariables, this.buildResolveVariablesForTeam(args.teamId));
776
+ }
777
+ if (args.projectId && !isUuid(args.projectId)) {
778
+ resolveVariables.projectName = args.projectId;
779
+ }
780
+ if (args.milestoneId && !isUuid(args.milestoneId)) {
781
+ resolveVariables.milestoneName = args.milestoneId;
782
+ }
783
+ if (args.labelIds && Array.isArray(args.labelIds)) {
784
+ const nonUuidLabels = args.labelIds.filter((id) => !isUuid(id));
785
+ if (nonUuidLabels.length > 0) {
786
+ resolveVariables.__labelNames = nonUuidLabels;
787
+ }
788
+ }
789
+ if (args.parentId && !isUuid(args.parentId)) {
790
+ const parentParsed = tryParseIssueIdentifier(args.parentId);
791
+ if (parentParsed) {
792
+ resolveVariables.parentTeamKey = parentParsed.teamKey;
793
+ resolveVariables.parentIssueNumber = parentParsed.issueNumber;
794
+ }
795
+ }
796
+ return resolveVariables;
797
+ }
798
+ resolveParentId(parentId, resolveResult) {
799
+ if (!parentId || isUuid(parentId)) {
800
+ return parentId;
801
+ }
802
+ const parentIssues = resolveResult.parentIssues;
803
+ const parentNodes = parentIssues?.nodes;
804
+ if (!parentNodes?.length) {
805
+ throw notFoundError("Parent issue", parentId);
806
+ }
807
+ return parentNodes[0].id;
808
+ }
809
+ transformIssueData(issue) {
810
+ const labels = issue.labels;
811
+ return {
812
+ ...this.transformIssueCoreFields(issue),
813
+ ...this.transformIssueRelations(issue),
814
+ summary: issue.summary
815
+ ? extractSummaryText(issue.summary)
816
+ : undefined,
817
+ priority: issue.priority,
818
+ estimate: issue.estimate || undefined,
819
+ dueDate: issue.dueDate || undefined,
820
+ labels: labels.nodes.map((label) => ({
821
+ id: label.id,
822
+ name: label.name,
823
+ })),
824
+ ...this.transformIssueHierarchy(issue),
825
+ comments: this.transformIssueComments(issue),
826
+ createdAt: toISOStringOrNow(issue.createdAt),
827
+ updatedAt: toISOStringOrNow(issue.updatedAt),
828
+ };
829
+ }
830
+ transformIssueCoreFields(issue) {
831
+ return {
832
+ id: issue.id,
833
+ identifier: issue.identifier,
834
+ url: issue.url,
835
+ title: issue.title,
836
+ description: issue.description || undefined,
837
+ branchName: issue.branchName || undefined,
838
+ embeds: issue.description
839
+ ? extractEmbeds(issue.description)
840
+ : undefined,
841
+ };
842
+ }
843
+ transformIssueRelations(issue) {
844
+ const state = issue.state;
845
+ const assignee = issue.assignee;
846
+ const team = issue.team;
847
+ const project = issue.project;
848
+ const cycle = issue.cycle;
849
+ const milestone = issue.projectMilestone;
850
+ return {
851
+ state: state
852
+ ? { id: state.id, name: state.name }
853
+ : undefined,
854
+ assignee: assignee
855
+ ? {
856
+ id: assignee.id,
857
+ name: resolveUserDisplayName(assignee.id, assignee.name),
858
+ url: assignee.url || undefined,
859
+ }
860
+ : undefined,
861
+ team: team
862
+ ? {
863
+ id: team.id,
864
+ key: team.key,
865
+ name: team.name,
866
+ }
867
+ : undefined,
868
+ project: project
869
+ ? { id: project.id, name: project.name }
870
+ : undefined,
871
+ cycle: cycle
872
+ ? {
873
+ id: cycle.id,
874
+ name: cycle.name,
875
+ number: cycle.number,
876
+ }
877
+ : undefined,
878
+ projectMilestone: milestone
879
+ ? {
880
+ id: milestone.id,
881
+ name: milestone.name,
882
+ targetDate: milestone.targetDate || undefined,
883
+ }
884
+ : undefined,
885
+ };
886
+ }
887
+ transformIssueHierarchy(issue) {
888
+ const parent = issue.parent;
889
+ const children = issue.children;
890
+ return {
891
+ parentIssue: parent
892
+ ? {
893
+ id: parent.id,
894
+ identifier: parent.identifier,
895
+ title: parent.title,
896
+ }
897
+ : undefined,
898
+ subIssues: children?.nodes?.map((child) => ({
899
+ id: child.id,
900
+ identifier: child.identifier,
901
+ title: child.title,
902
+ })) || undefined,
903
+ };
904
+ }
905
+ transformIssueComments(issue) {
906
+ const comments = issue.comments;
907
+ return (comments?.nodes?.map((comment) => {
908
+ const commentUser = comment.user;
909
+ return {
910
+ id: comment.id,
911
+ body: comment.body,
912
+ embeds: extractEmbeds(comment.body),
913
+ user: commentUser
914
+ ? {
915
+ id: commentUser.id,
916
+ name: resolveUserDisplayName(commentUser.id, commentUser.name),
917
+ url: commentUser.url || undefined,
918
+ }
919
+ : undefined,
920
+ createdAt: toISOStringOrNow(comment.createdAt),
921
+ updatedAt: toISOStringOrNow(comment.updatedAt),
922
+ };
923
+ }) || []);
924
+ }
925
+ }