@aaronsb/jira-cloud-mcp 0.1.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.
@@ -0,0 +1,721 @@
1
+ import { Version3Client, AgileClient } from 'jira.js';
2
+ import { TextProcessor } from '../utils/text-processing.js';
3
+ export class JiraClient {
4
+ client;
5
+ agileClient;
6
+ customFields;
7
+ constructor(config) {
8
+ const clientConfig = {
9
+ host: config.host,
10
+ authentication: {
11
+ basic: {
12
+ email: config.email,
13
+ apiToken: config.apiToken,
14
+ },
15
+ },
16
+ };
17
+ this.client = new Version3Client(clientConfig);
18
+ this.agileClient = new AgileClient(clientConfig);
19
+ // Set custom field mappings with defaults
20
+ this.customFields = {
21
+ startDate: config.customFields?.startDate ?? 'customfield_10015',
22
+ storyPoints: config.customFields?.storyPoints ?? 'customfield_10016',
23
+ };
24
+ }
25
+ async getIssue(issueKey, includeComments = false, includeAttachments = false) {
26
+ const fields = [
27
+ 'summary',
28
+ 'description',
29
+ 'parent',
30
+ 'assignee',
31
+ 'reporter',
32
+ 'status',
33
+ 'resolution',
34
+ 'duedate',
35
+ this.customFields.startDate,
36
+ this.customFields.storyPoints,
37
+ 'timeestimate',
38
+ 'issuelinks',
39
+ ];
40
+ if (includeAttachments) {
41
+ fields.push('attachment');
42
+ }
43
+ const params = {
44
+ issueIdOrKey: issueKey,
45
+ fields,
46
+ expand: includeComments ? 'renderedFields,comments' : 'renderedFields'
47
+ };
48
+ const issue = await this.client.issues.getIssue(params);
49
+ const issueDetails = {
50
+ key: issue.key,
51
+ summary: issue.fields.summary,
52
+ description: issue.renderedFields?.description || '',
53
+ parent: issue.fields.parent?.key || null,
54
+ assignee: issue.fields.assignee?.displayName || null,
55
+ reporter: issue.fields.reporter?.displayName || '',
56
+ status: issue.fields.status?.name || '',
57
+ resolution: issue.fields.resolution?.name || null,
58
+ dueDate: issue.fields.duedate || null,
59
+ startDate: issue.fields[this.customFields.startDate] || null,
60
+ storyPoints: issue.fields[this.customFields.storyPoints] || null,
61
+ timeEstimate: issue.fields.timeestimate || null,
62
+ issueLinks: (issue.fields.issuelinks || []).map(link => ({
63
+ type: link.type?.name || '',
64
+ outward: link.outwardIssue?.key || null,
65
+ inward: link.inwardIssue?.key || null,
66
+ })),
67
+ };
68
+ if (includeComments && issue.fields.comment?.comments) {
69
+ issueDetails.comments = issue.fields.comment.comments
70
+ .filter(comment => comment.id &&
71
+ comment.author?.displayName &&
72
+ comment.body &&
73
+ comment.created)
74
+ .map(comment => ({
75
+ id: comment.id,
76
+ author: comment.author.displayName,
77
+ body: comment.body?.content ? TextProcessor.extractTextFromAdf(comment.body) : String(comment.body),
78
+ created: comment.created,
79
+ }));
80
+ }
81
+ if (includeAttachments && issue.fields.attachment) {
82
+ issueDetails.attachments = issue.fields.attachment
83
+ .filter(attachment => attachment.id &&
84
+ attachment.filename &&
85
+ attachment.mimeType &&
86
+ attachment.created &&
87
+ attachment.author?.displayName)
88
+ .map(attachment => ({
89
+ id: attachment.id,
90
+ filename: attachment.filename,
91
+ mimeType: attachment.mimeType,
92
+ size: attachment.size || 0,
93
+ created: attachment.created,
94
+ author: attachment.author.displayName,
95
+ url: attachment.content || '',
96
+ }));
97
+ }
98
+ return issueDetails;
99
+ }
100
+ async getIssueAttachments(issueKey) {
101
+ const issue = await this.client.issues.getIssue({
102
+ issueIdOrKey: issueKey,
103
+ fields: ['attachment'],
104
+ });
105
+ if (!issue.fields.attachment) {
106
+ return [];
107
+ }
108
+ return issue.fields.attachment
109
+ .filter(attachment => attachment.id &&
110
+ attachment.filename &&
111
+ attachment.mimeType &&
112
+ attachment.created &&
113
+ attachment.author?.displayName)
114
+ .map(attachment => ({
115
+ id: attachment.id,
116
+ filename: attachment.filename,
117
+ mimeType: attachment.mimeType,
118
+ size: attachment.size || 0,
119
+ created: attachment.created,
120
+ author: attachment.author.displayName,
121
+ url: attachment.content || '',
122
+ }));
123
+ }
124
+ async getFilterIssues(filterId) {
125
+ const filter = await this.client.filters.getFilter({
126
+ id: parseInt(filterId, 10)
127
+ });
128
+ if (!filter?.jql) {
129
+ throw new Error('Invalid filter or missing JQL');
130
+ }
131
+ const searchResults = await this.client.issueSearch.searchForIssuesUsingJql({
132
+ jql: filter.jql,
133
+ fields: [
134
+ 'summary',
135
+ 'description',
136
+ 'assignee',
137
+ 'reporter',
138
+ 'status',
139
+ 'resolution',
140
+ 'duedate',
141
+ this.customFields.startDate,
142
+ this.customFields.storyPoints,
143
+ 'timeestimate',
144
+ 'issuelinks',
145
+ ],
146
+ expand: 'renderedFields'
147
+ });
148
+ return (searchResults.issues || []).map(issue => ({
149
+ key: issue.key,
150
+ summary: issue.fields.summary,
151
+ description: issue.renderedFields?.description || '',
152
+ parent: issue.fields.parent?.key || null,
153
+ assignee: issue.fields.assignee?.displayName || null,
154
+ reporter: issue.fields.reporter?.displayName || '',
155
+ status: issue.fields.status?.name || '',
156
+ resolution: issue.fields.resolution?.name || null,
157
+ dueDate: issue.fields.duedate || null,
158
+ startDate: issue.fields[this.customFields.startDate] || null,
159
+ storyPoints: issue.fields[this.customFields.storyPoints] || null,
160
+ timeEstimate: issue.fields.timeestimate || null,
161
+ issueLinks: (issue.fields.issuelinks || []).map(link => ({
162
+ type: link.type?.name || '',
163
+ outward: link.outwardIssue?.key || null,
164
+ inward: link.inwardIssue?.key || null,
165
+ })),
166
+ }));
167
+ }
168
+ async updateIssue(issueKey, summary, description, parentKey) {
169
+ const fields = {};
170
+ if (summary)
171
+ fields.summary = summary;
172
+ if (description) {
173
+ fields.description = TextProcessor.markdownToAdf(description);
174
+ }
175
+ if (parentKey !== undefined) {
176
+ fields.parent = parentKey ? { key: parentKey } : null;
177
+ }
178
+ await this.client.issues.editIssue({
179
+ issueIdOrKey: issueKey,
180
+ fields,
181
+ });
182
+ }
183
+ async addComment(issueKey, commentBody) {
184
+ await this.client.issueComments.addComment({
185
+ issueIdOrKey: issueKey,
186
+ comment: TextProcessor.markdownToAdf(commentBody)
187
+ });
188
+ }
189
+ async searchIssues(jql, startAt = 0, maxResults = 25) {
190
+ try {
191
+ // Remove escaped quotes from JQL
192
+ const cleanJql = jql.replace(/\\"/g, '"');
193
+ console.error(`Executing JQL search with query: ${cleanJql}`);
194
+ const searchResults = await this.client.issueSearch.searchForIssuesUsingJql({
195
+ jql: cleanJql,
196
+ startAt,
197
+ maxResults: Math.min(maxResults, 100),
198
+ fields: [
199
+ 'summary',
200
+ 'description',
201
+ 'assignee',
202
+ 'reporter',
203
+ 'status',
204
+ 'resolution',
205
+ 'duedate',
206
+ this.customFields.startDate,
207
+ this.customFields.storyPoints,
208
+ 'timeestimate',
209
+ 'issuelinks',
210
+ ],
211
+ expand: 'renderedFields'
212
+ });
213
+ const issues = (searchResults.issues || []).map(issue => ({
214
+ key: issue.key,
215
+ summary: issue.fields.summary,
216
+ description: issue.renderedFields?.description || '',
217
+ parent: issue.fields.parent?.key || null,
218
+ assignee: issue.fields.assignee?.displayName || null,
219
+ reporter: issue.fields.reporter?.displayName || '',
220
+ status: issue.fields.status?.name || '',
221
+ resolution: issue.fields.resolution?.name || null,
222
+ dueDate: issue.fields.duedate || null,
223
+ startDate: issue.fields[this.customFields.startDate] || null,
224
+ storyPoints: issue.fields[this.customFields.storyPoints] || null,
225
+ timeEstimate: issue.fields.timeestimate || null,
226
+ issueLinks: (issue.fields.issuelinks || []).map(link => ({
227
+ type: link.type?.name || '',
228
+ outward: link.outwardIssue?.key || null,
229
+ inward: link.inwardIssue?.key || null,
230
+ })),
231
+ }));
232
+ return {
233
+ issues,
234
+ pagination: {
235
+ startAt,
236
+ maxResults,
237
+ total: searchResults.total || 0,
238
+ hasMore: (startAt + issues.length) < (searchResults.total || 0)
239
+ }
240
+ };
241
+ }
242
+ catch (error) {
243
+ console.error('Error executing JQL search:', error);
244
+ throw error;
245
+ }
246
+ }
247
+ async getTransitions(issueKey) {
248
+ const transitions = await this.client.issues.getTransitions({
249
+ issueIdOrKey: issueKey,
250
+ });
251
+ return (transitions.transitions || [])
252
+ .filter(transition => transition.id && transition.name && transition.to)
253
+ .map(transition => ({
254
+ id: transition.id,
255
+ name: transition.name,
256
+ to: {
257
+ id: transition.to.id || '',
258
+ name: transition.to.name || '',
259
+ description: transition.to.description,
260
+ },
261
+ }));
262
+ }
263
+ async transitionIssue(issueKey, transitionId, comment) {
264
+ const transitionRequest = {
265
+ issueIdOrKey: issueKey,
266
+ transition: {
267
+ id: transitionId
268
+ }
269
+ };
270
+ if (comment) {
271
+ transitionRequest.update = {
272
+ comment: [{
273
+ add: {
274
+ body: TextProcessor.markdownToAdf(comment)
275
+ }
276
+ }]
277
+ };
278
+ }
279
+ await this.client.issues.doTransition(transitionRequest);
280
+ }
281
+ /**
282
+ * Link two issues together with a specified link type
283
+ * @param sourceIssueKey The source issue key
284
+ * @param targetIssueKey The target issue key
285
+ * @param linkType The type of link to create
286
+ * @param comment Optional comment to add with the link
287
+ */
288
+ async linkIssues(sourceIssueKey, targetIssueKey, linkType, comment) {
289
+ const linkRequest = {
290
+ type: {
291
+ name: linkType
292
+ },
293
+ inwardIssue: {
294
+ key: targetIssueKey
295
+ },
296
+ outwardIssue: {
297
+ key: sourceIssueKey
298
+ }
299
+ };
300
+ if (comment) {
301
+ linkRequest.comment = {
302
+ body: TextProcessor.markdownToAdf(comment)
303
+ };
304
+ }
305
+ // Use the correct method from jira.js library
306
+ await this.client.issueLinks.linkIssues(linkRequest);
307
+ }
308
+ /**
309
+ * Get all available issue link types
310
+ * @returns Array of link types with their names and descriptions
311
+ */
312
+ async getIssueLinkTypes() {
313
+ console.error('Fetching all issue link types...');
314
+ const response = await this.client.issueLinkTypes.getIssueLinkTypes();
315
+ return (response.issueLinkTypes || [])
316
+ .filter(linkType => linkType.id && linkType.name)
317
+ .map(linkType => ({
318
+ id: linkType.id,
319
+ name: linkType.name,
320
+ inward: linkType.inward || '',
321
+ outward: linkType.outward || ''
322
+ }));
323
+ }
324
+ async getPopulatedFields(issueKey) {
325
+ const issue = await this.client.issues.getIssue({
326
+ issueIdOrKey: issueKey,
327
+ expand: 'renderedFields,names',
328
+ });
329
+ const fieldNames = issue.names || {};
330
+ const fields = issue.fields;
331
+ const lines = [];
332
+ // Add issue key and summary at the top
333
+ lines.push(`Issue: ${issue.key}`);
334
+ if (fields.summary) {
335
+ lines.push(`Summary: ${fields.summary}`);
336
+ }
337
+ lines.push('');
338
+ // Process priority fields first
339
+ const priorityFields = [
340
+ 'Description',
341
+ 'Status',
342
+ 'Assignee',
343
+ 'Reporter',
344
+ 'Priority',
345
+ 'Created',
346
+ 'Updated'
347
+ ];
348
+ lines.push('=== Key Details ===');
349
+ for (const priorityField of priorityFields) {
350
+ for (const [fieldId, value] of Object.entries(fields)) {
351
+ const fieldName = fieldNames[fieldId] || fieldId;
352
+ if (fieldName === priorityField) {
353
+ if (TextProcessor.isFieldPopulated(value) && !TextProcessor.shouldExcludeField(fieldId, value)) {
354
+ const formattedValue = TextProcessor.formatFieldValue(value, fieldName);
355
+ if (formattedValue) {
356
+ lines.push(`${fieldName}: ${formattedValue}`);
357
+ }
358
+ }
359
+ break;
360
+ }
361
+ }
362
+ }
363
+ // Group remaining fields by category
364
+ const categories = {
365
+ 'Project Info': ['Project', 'Issue Type', 'Request Type', 'Rank'],
366
+ 'Links': ['Gong Link', 'SalesForce Link'],
367
+ 'Dates & Times': ['Last Viewed', 'Status Category Changed', '[CHART] Date of First Response'],
368
+ 'Request Details': ['Request participants', 'Request language', 'Escalated', 'Next Steps'],
369
+ 'Other Fields': []
370
+ };
371
+ const processedFieldNames = new Set(priorityFields);
372
+ for (const [fieldId, value] of Object.entries(fields)) {
373
+ const fieldName = fieldNames[fieldId] || fieldId;
374
+ if (!processedFieldNames.has(fieldName)) {
375
+ if (TextProcessor.isFieldPopulated(value) && !TextProcessor.shouldExcludeField(fieldId, value)) {
376
+ const formattedValue = TextProcessor.formatFieldValue(value, fieldName);
377
+ if (formattedValue) {
378
+ let categoryFound = false;
379
+ for (const [category, categoryFields] of Object.entries(categories)) {
380
+ if (category !== 'Other Fields') {
381
+ if (categoryFields.some(pattern => fieldName.toLowerCase().includes(pattern.toLowerCase()))) {
382
+ if (!processedFieldNames.has(fieldName)) {
383
+ categories[category].push(fieldName);
384
+ processedFieldNames.add(fieldName);
385
+ categoryFound = true;
386
+ break;
387
+ }
388
+ }
389
+ }
390
+ }
391
+ if (!categoryFound && !processedFieldNames.has(fieldName)) {
392
+ categories['Other Fields'].push(fieldName);
393
+ processedFieldNames.add(fieldName);
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+ for (const [category, categoryFields] of Object.entries(categories)) {
400
+ if (categoryFields.length > 0) {
401
+ lines.push('');
402
+ lines.push(`=== ${category} ===`);
403
+ for (const fieldName of categoryFields) {
404
+ for (const [fieldId, value] of Object.entries(fields)) {
405
+ const currentFieldName = fieldNames[fieldId] || fieldId;
406
+ if (currentFieldName === fieldName) {
407
+ const formattedValue = TextProcessor.formatFieldValue(value, fieldName);
408
+ if (formattedValue) {
409
+ lines.push(`${fieldName}: ${formattedValue}`);
410
+ }
411
+ break;
412
+ }
413
+ }
414
+ }
415
+ }
416
+ }
417
+ if (fields.comment?.comments?.length > 0) {
418
+ lines.push('');
419
+ lines.push('=== Comments ===');
420
+ const comments = TextProcessor.formatFieldValue(fields.comment.comments, 'comments');
421
+ if (comments.trim()) {
422
+ lines.push(comments);
423
+ }
424
+ }
425
+ return lines.join('\n');
426
+ }
427
+ async listBoards() {
428
+ console.error('Fetching all boards...');
429
+ const response = await this.agileClient.board.getAllBoards();
430
+ return (response.values || [])
431
+ .filter(board => board.id && board.name)
432
+ .map(board => ({
433
+ id: board.id,
434
+ name: board.name,
435
+ type: board.type || 'scrum',
436
+ location: board.location ? {
437
+ projectId: board.location.projectId,
438
+ projectName: board.location.projectName || ''
439
+ } : undefined,
440
+ self: board.self || ''
441
+ }));
442
+ }
443
+ async listBoardSprints(boardId) {
444
+ console.error(`Fetching sprints for board ${boardId}...`);
445
+ const response = await this.agileClient.board.getAllSprints({
446
+ boardId: boardId,
447
+ state: 'future,active'
448
+ });
449
+ return (response.values || [])
450
+ .filter(sprint => sprint.id && sprint.name)
451
+ .map(sprint => ({
452
+ id: sprint.id,
453
+ name: sprint.name,
454
+ state: sprint.state || 'unknown',
455
+ startDate: sprint.startDate,
456
+ endDate: sprint.endDate,
457
+ completeDate: sprint.completeDate,
458
+ goal: sprint.goal,
459
+ boardId
460
+ }));
461
+ }
462
+ // Sprint CRUD operations
463
+ /**
464
+ * Create a new sprint
465
+ */
466
+ async createSprint(boardId, name, startDate, endDate, goal) {
467
+ console.error(`Creating sprint "${name}" for board ${boardId}...`);
468
+ const response = await this.agileClient.sprint.createSprint({
469
+ originBoardId: boardId,
470
+ name,
471
+ startDate,
472
+ endDate,
473
+ goal
474
+ });
475
+ return {
476
+ id: response.id,
477
+ name: response.name,
478
+ state: response.state || 'future',
479
+ startDate: response.startDate,
480
+ endDate: response.endDate,
481
+ completeDate: response.completeDate,
482
+ goal: response.goal,
483
+ boardId
484
+ };
485
+ }
486
+ /**
487
+ * Get a sprint by ID
488
+ */
489
+ async getSprint(sprintId) {
490
+ console.error(`Fetching sprint ${sprintId}...`);
491
+ const response = await this.agileClient.sprint.getSprint({
492
+ sprintId
493
+ });
494
+ return {
495
+ id: response.id,
496
+ name: response.name,
497
+ state: response.state || 'unknown',
498
+ startDate: response.startDate,
499
+ endDate: response.endDate,
500
+ completeDate: response.completeDate,
501
+ goal: response.goal,
502
+ boardId: response.originBoardId
503
+ };
504
+ }
505
+ /**
506
+ * List sprints for a board with pagination and filtering
507
+ */
508
+ async listSprints(boardId, state, startAt = 0, maxResults = 50) {
509
+ console.error(`Listing sprints for board ${boardId}...`);
510
+ const stateParam = state || 'future,active,closed';
511
+ const response = await this.agileClient.board.getAllSprints({
512
+ boardId,
513
+ state: stateParam,
514
+ startAt,
515
+ maxResults
516
+ });
517
+ const sprints = (response.values || [])
518
+ .filter(sprint => sprint.id && sprint.name)
519
+ .map(sprint => ({
520
+ id: sprint.id,
521
+ name: sprint.name,
522
+ state: sprint.state || 'unknown',
523
+ startDate: sprint.startDate,
524
+ endDate: sprint.endDate,
525
+ completeDate: sprint.completeDate,
526
+ goal: sprint.goal,
527
+ boardId
528
+ }));
529
+ return {
530
+ sprints,
531
+ total: response.total || sprints.length
532
+ };
533
+ }
534
+ /**
535
+ * Update a sprint
536
+ */
537
+ async updateSprint(sprintId, name, goal, startDate, endDate, state) {
538
+ console.error(`Updating sprint ${sprintId}...`);
539
+ try {
540
+ // First get the current sprint to get its state
541
+ const currentSprint = await this.getSprint(sprintId);
542
+ // Prepare update parameters
543
+ const updateParams = {
544
+ // Always include the current state unless a new state is provided
545
+ state: state || currentSprint.state
546
+ };
547
+ // Add other parameters if provided
548
+ if (name !== undefined)
549
+ updateParams.name = name;
550
+ if (goal !== undefined)
551
+ updateParams.goal = goal;
552
+ if (startDate !== undefined)
553
+ updateParams.startDate = startDate;
554
+ if (endDate !== undefined)
555
+ updateParams.endDate = endDate;
556
+ // If changing to closed state, add completeDate
557
+ if (state === 'closed') {
558
+ updateParams.completeDate = new Date().toISOString();
559
+ }
560
+ // Update the sprint with all parameters in a single call
561
+ await this.agileClient.sprint.updateSprint({
562
+ sprintId,
563
+ ...updateParams
564
+ });
565
+ }
566
+ catch (error) {
567
+ console.error(`Error updating sprint ${sprintId}:`, error);
568
+ throw error;
569
+ }
570
+ }
571
+ /**
572
+ * Delete a sprint
573
+ */
574
+ async deleteSprint(sprintId) {
575
+ console.error(`Deleting sprint ${sprintId}...`);
576
+ await this.agileClient.sprint.deleteSprint({
577
+ sprintId
578
+ });
579
+ }
580
+ /**
581
+ * Get issues in a sprint
582
+ */
583
+ async getSprintIssues(sprintId) {
584
+ console.error(`Fetching issues for sprint ${sprintId}...`);
585
+ const response = await this.agileClient.sprint.getIssuesForSprint({
586
+ sprintId,
587
+ fields: ['summary', 'status', 'assignee']
588
+ });
589
+ return (response.issues || [])
590
+ .filter(issue => issue.key && issue.fields) // Filter out issues with missing key or fields
591
+ .map(issue => ({
592
+ key: issue.key, // Non-null assertion since we filtered
593
+ summary: issue.fields.summary || '',
594
+ status: issue.fields.status?.name || 'Unknown',
595
+ assignee: issue.fields.assignee?.displayName
596
+ }));
597
+ }
598
+ /**
599
+ * Update issues in a sprint (add/remove)
600
+ */
601
+ async updateSprintIssues(sprintId, add, remove) {
602
+ console.error(`Updating issues for sprint ${sprintId}...`);
603
+ try {
604
+ // Add issues to sprint
605
+ if (add && add.length > 0) {
606
+ console.error(`Adding ${add.length} issues to sprint ${sprintId}: ${add.join(', ')}`);
607
+ // Use the correct method from jira.js v4.0.5
608
+ await this.agileClient.sprint.moveIssuesToSprintAndRank({
609
+ sprintId,
610
+ issues: add
611
+ });
612
+ }
613
+ // Remove issues from sprint
614
+ if (remove && remove.length > 0) {
615
+ console.error(`Removing ${remove.length} issues from sprint ${sprintId}: ${remove.join(', ')}`);
616
+ // To remove issues, we move them to the backlog
617
+ await this.agileClient.backlog.moveIssuesToBacklog({
618
+ issues: remove
619
+ });
620
+ }
621
+ }
622
+ catch (error) {
623
+ console.error(`Error updating sprint issues for sprint ${sprintId}:`, error);
624
+ throw new Error(`Failed to update sprint issues: ${error instanceof Error ? error.message : String(error)}`);
625
+ }
626
+ }
627
+ /**
628
+ * Get sprint report
629
+ */
630
+ async getSprintReport(boardId, sprintId) {
631
+ console.error(`Fetching sprint report for sprint ${sprintId} on board ${boardId}...`);
632
+ try {
633
+ // Use type assertion since getSprintReport might not be in the type definitions
634
+ const response = await this.agileClient.board.getSprintReport({
635
+ boardId,
636
+ sprintId
637
+ });
638
+ // Safely extract data from the response
639
+ return {
640
+ completedIssues: response?.contents?.completedIssues?.length || 0,
641
+ incompletedIssues: response?.contents?.incompletedIssues?.length || 0,
642
+ puntedIssues: response?.contents?.puntedIssues?.length || 0,
643
+ addedIssues: response?.contents?.issuesAddedDuringSprint?.length || 0,
644
+ velocityPoints: response?.contents?.completedIssuesEstimateSum?.value || 0
645
+ };
646
+ }
647
+ catch (error) {
648
+ console.error(`Error fetching sprint report for sprint ${sprintId} on board ${boardId}:`, error);
649
+ // Return a default response with zeros to avoid breaking the client
650
+ return {
651
+ completedIssues: 0,
652
+ incompletedIssues: 0,
653
+ puntedIssues: 0,
654
+ addedIssues: 0,
655
+ velocityPoints: 0
656
+ };
657
+ }
658
+ }
659
+ async listProjects() {
660
+ const { values: projects } = await this.client.projects.searchProjects();
661
+ return projects
662
+ .filter(project => Boolean(project?.id && project?.key && project?.name))
663
+ .map(project => ({
664
+ id: project.id,
665
+ key: project.key,
666
+ name: project.name,
667
+ description: project.description || null,
668
+ lead: project.lead?.displayName || null,
669
+ url: project.self || ''
670
+ }));
671
+ }
672
+ async createIssue(params) {
673
+ const fields = {
674
+ project: { key: params.projectKey },
675
+ summary: params.summary,
676
+ issuetype: { name: params.issueType },
677
+ };
678
+ if (params.description) {
679
+ fields.description = TextProcessor.markdownToAdf(params.description);
680
+ }
681
+ if (params.priority)
682
+ fields.priority = { id: params.priority };
683
+ if (params.assignee)
684
+ fields.assignee = { name: params.assignee };
685
+ if (params.labels)
686
+ fields.labels = params.labels;
687
+ if (params.customFields) {
688
+ Object.assign(fields, params.customFields);
689
+ }
690
+ const response = await this.client.issues.createIssue({ fields });
691
+ return { key: response.key };
692
+ }
693
+ async listMyFilters(expand = false) {
694
+ const filters = await this.client.filters.getMyFilters();
695
+ return Promise.all(filters.map(async (filter) => {
696
+ if (!filter.id || !filter.name) {
697
+ throw new Error('Invalid filter response');
698
+ }
699
+ const basic = {
700
+ id: filter.id,
701
+ name: filter.name,
702
+ owner: filter.owner?.displayName || 'Unknown',
703
+ favourite: filter.favourite || false,
704
+ viewUrl: filter.viewUrl || ''
705
+ };
706
+ if (expand) {
707
+ return {
708
+ ...basic,
709
+ description: filter.description || '',
710
+ jql: filter.jql || '',
711
+ sharePermissions: filter.sharePermissions?.map(perm => ({
712
+ type: perm.type,
713
+ group: perm.group?.name,
714
+ project: perm.project?.name
715
+ })) || []
716
+ };
717
+ }
718
+ return basic;
719
+ }));
720
+ }
721
+ }