@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.
- package/README.md +70 -0
- package/build/client/jira-client.js +721 -0
- package/build/handlers/board-handlers.js +326 -0
- package/build/handlers/filter-handlers.js +427 -0
- package/build/handlers/issue-handlers.js +311 -0
- package/build/handlers/project-handlers.js +368 -0
- package/build/handlers/resource-handlers.js +320 -0
- package/build/handlers/search-handlers.js +103 -0
- package/build/handlers/sprint-handlers.js +433 -0
- package/build/handlers/tool-resource-handlers.js +1185 -0
- package/build/health-check.js +67 -0
- package/build/index.js +141 -0
- package/build/schemas/request-schemas.js +187 -0
- package/build/schemas/tool-schemas.js +450 -0
- package/build/types/index.js +1 -0
- package/build/utils/formatters/base-formatter.js +58 -0
- package/build/utils/formatters/board-formatter.js +63 -0
- package/build/utils/formatters/filter-formatter.js +66 -0
- package/build/utils/formatters/index.js +7 -0
- package/build/utils/formatters/issue-formatter.js +84 -0
- package/build/utils/formatters/project-formatter.js +55 -0
- package/build/utils/formatters/search-formatter.js +62 -0
- package/build/utils/formatters/sprint-formatter.js +111 -0
- package/build/utils/text-processing.js +343 -0
- package/package.json +65 -0
|
@@ -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
|
+
}
|