@zierocode/mcp-atlassian-cloud 2.6.1

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,1384 @@
1
+ import axios, { AxiosError } from "axios";
2
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
3
+ import { createRequire } from "module";
4
+ // CommonJS require for @atlaskit packages (they don't have proper ESM exports)
5
+ const require = createRequire(import.meta.url);
6
+ const { defaultSchema } = require("@atlaskit/adf-schema/schema-default");
7
+ const { MarkdownTransformer } = require("@atlaskit/editor-markdown-transformer");
8
+ const { JSONTransformer } = require("@atlaskit/editor-json-transformer");
9
+ class MetadataCache {
10
+ cache = new Map();
11
+ TTL_MS = 30 * 60 * 1000; // 30 minutes
12
+ async get(key, fetchFn) {
13
+ const cached = this.cache.get(key);
14
+ const now = Date.now();
15
+ if (cached && now < cached.expiresAt) {
16
+ return cached.data;
17
+ }
18
+ const data = await fetchFn();
19
+ this.cache.set(key, { data, expiresAt: now + this.TTL_MS });
20
+ return data;
21
+ }
22
+ clear() {
23
+ this.cache.clear();
24
+ }
25
+ getStats() {
26
+ return {
27
+ size: this.cache.size,
28
+ keys: Array.from(this.cache.keys()),
29
+ };
30
+ }
31
+ }
32
+ // Singleton cache instance (shared across all sessions)
33
+ const metadataCache = new MetadataCache();
34
+ // ============================================================
35
+ // RETRY WITH EXPONENTIAL BACKOFF
36
+ // ============================================================
37
+ const RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];
38
+ const MAX_RETRIES = 3;
39
+ const BASE_DELAY_MS = 1000;
40
+ function sleep(ms) {
41
+ return new Promise((resolve) => setTimeout(resolve, ms));
42
+ }
43
+ async function axiosWithRetry(config, context) {
44
+ let lastError = null;
45
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
46
+ try {
47
+ const response = await axios.request(config);
48
+ return response.data;
49
+ }
50
+ catch (error) {
51
+ lastError = error;
52
+ if (error instanceof AxiosError) {
53
+ const status = error.response?.status;
54
+ // Don't retry client errors (4xx except 408, 429)
55
+ if (status && status >= 400 && status < 500 && !RETRYABLE_STATUS_CODES.includes(status)) {
56
+ throw error;
57
+ }
58
+ // Retry for network errors and retryable status codes
59
+ if (attempt < MAX_RETRIES - 1) {
60
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt); // 1s, 2s, 4s
61
+ console.log(`[Retry] ${context} - attempt ${attempt + 1}/${MAX_RETRIES}, waiting ${delay}ms`);
62
+ await sleep(delay);
63
+ continue;
64
+ }
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+ throw lastError;
70
+ }
71
+ // ============================================================
72
+ // HELPER FUNCTIONS
73
+ // ============================================================
74
+ function getRequiredEnv(name) {
75
+ const value = process.env[name];
76
+ if (!value) {
77
+ throw new Error(`Missing required environment variable: ${name}\n` +
78
+ "Please set this variable before starting the server.");
79
+ }
80
+ return value;
81
+ }
82
+ function validateConfig() {
83
+ return {
84
+ domain: getRequiredEnv("ATLASSIAN_DOMAIN"),
85
+ email: getRequiredEnv("ATLASSIAN_EMAIL"),
86
+ apiToken: getRequiredEnv("ATLASSIAN_API_TOKEN"),
87
+ };
88
+ }
89
+ // ============================================================
90
+ // MARKDOWN TO ADF CONVERTER (Official Atlaskit Library)
91
+ // ============================================================
92
+ // Initialize transformers with Jira-compatible schema
93
+ const markdownTransformer = new MarkdownTransformer(defaultSchema);
94
+ const jsonTransformer = new JSONTransformer();
95
+ /**
96
+ * Convert Markdown text to Atlassian Document Format (ADF)
97
+ * Uses official @atlaskit/editor-markdown-transformer library
98
+ *
99
+ * Supported Markdown:
100
+ * - **bold** → strong mark
101
+ * - *italic* → em mark
102
+ * - ~~strikethrough~~ → strike mark
103
+ * - `inline code` → code mark
104
+ * - [link](url) → link mark
105
+ * - # Heading 1-6 → heading node
106
+ * - - bullet list → bulletList node
107
+ * - 1. ordered list → orderedList node
108
+ * - > blockquote → blockquote node
109
+ * - ```code block``` → codeBlock node
110
+ */
111
+ function textToAdf(text) {
112
+ try {
113
+ // Parse markdown to ProseMirror document
114
+ const pmDoc = markdownTransformer.parse(text);
115
+ // Convert to ADF JSON
116
+ const adf = jsonTransformer.encode(pmDoc);
117
+ return adf;
118
+ }
119
+ catch (error) {
120
+ // Fallback to plain text if parsing fails
121
+ console.warn("[textToAdf] Markdown parse failed, using plain text:", error);
122
+ return {
123
+ type: "doc",
124
+ version: 1,
125
+ content: [
126
+ {
127
+ type: "paragraph",
128
+ content: [{ type: "text", text }],
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ }
134
+ /**
135
+ * Convert an array of DescriptionBlocks to a single ADF document
136
+ * Supports mixing Markdown blocks with raw ADF nodes
137
+ *
138
+ * Example:
139
+ * [
140
+ * { markdown: "# Title\nSome **bold** text" },
141
+ * { adf: { type: "panel", attrs: { panelType: "info" }, content: [...] } },
142
+ * { markdown: "More text\n- List item 1\n- List item 2" }
143
+ * ]
144
+ */
145
+ function blocksToAdf(blocks) {
146
+ const allContent = [];
147
+ for (const block of blocks) {
148
+ if ("markdown" in block) {
149
+ // Convert Markdown to ADF and extract content nodes
150
+ const adf = textToAdf(block.markdown);
151
+ if (adf.content) {
152
+ allContent.push(...adf.content);
153
+ }
154
+ }
155
+ else if ("adf" in block) {
156
+ // Raw ADF node - add directly
157
+ allContent.push(block.adf);
158
+ }
159
+ }
160
+ return {
161
+ type: "doc",
162
+ version: 1,
163
+ content: allContent,
164
+ };
165
+ }
166
+ /**
167
+ * Resolve description from multiple possible formats
168
+ * Priority: description_adf > description_blocks > description (markdown)
169
+ */
170
+ function resolveDescription(params) {
171
+ // Raw ADF document takes highest priority
172
+ if (params.description_adf) {
173
+ return params.description_adf;
174
+ }
175
+ // Hybrid blocks (mix of Markdown + ADF)
176
+ if (params.description_blocks && params.description_blocks.length > 0) {
177
+ return blocksToAdf(params.description_blocks);
178
+ }
179
+ // Simple Markdown string
180
+ if (params.description) {
181
+ return textToAdf(params.description);
182
+ }
183
+ return undefined;
184
+ }
185
+ // ============================================================
186
+ // ATLASSIAN MANAGER CLASS
187
+ // ============================================================
188
+ export class AtlassianManager {
189
+ config;
190
+ baseUrl;
191
+ agileBaseUrl;
192
+ constructor() {
193
+ this.config = validateConfig();
194
+ this.baseUrl = `https://${this.config.domain}/rest/api/3`;
195
+ this.agileBaseUrl = `https://${this.config.domain}/rest/agile/1.0`;
196
+ }
197
+ get authHeader() {
198
+ const auth = Buffer.from(`${this.config.email}:${this.config.apiToken}`).toString("base64");
199
+ return { Authorization: `Basic ${auth}` };
200
+ }
201
+ get defaultHeaders() {
202
+ return {
203
+ ...this.authHeader,
204
+ Accept: "application/json",
205
+ "Content-Type": "application/json",
206
+ };
207
+ }
208
+ handleError(error, context) {
209
+ if (error instanceof AxiosError) {
210
+ const status = error.response?.status;
211
+ const errorData = error.response?.data;
212
+ const messages = errorData?.errorMessages ?? [];
213
+ const fieldErrors = errorData?.errors
214
+ ? Object.entries(errorData.errors).map(([k, v]) => `${k}: ${v}`)
215
+ : [];
216
+ const allErrors = [...messages, ...fieldErrors];
217
+ const message = allErrors.length > 0
218
+ ? allErrors.join("; ")
219
+ : error.message ?? "Unknown error";
220
+ if (status === 404) {
221
+ throw new McpError(ErrorCode.InvalidRequest, `${context}: Not found`);
222
+ }
223
+ if (status === 400) {
224
+ throw new McpError(ErrorCode.InvalidRequest, `${context}: ${message}`);
225
+ }
226
+ if (status === 401 || status === 403) {
227
+ throw new McpError(ErrorCode.InvalidRequest, "Authentication failed. Check your Atlassian credentials.");
228
+ }
229
+ throw new McpError(ErrorCode.InternalError, `${context}: ${message}`);
230
+ }
231
+ throw new McpError(ErrorCode.InternalError, `${context}: ${error instanceof Error ? error.message : String(error)}`);
232
+ }
233
+ // ============================================================
234
+ // ISSUES - CRUD
235
+ // ============================================================
236
+ async getJiraIssue(issueKey) {
237
+ try {
238
+ const response = await axios.get(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}`, {
239
+ headers: this.defaultHeaders,
240
+ timeout: 30000,
241
+ });
242
+ return response.data;
243
+ }
244
+ catch (error) {
245
+ if (error instanceof AxiosError && error.response?.status === 404) {
246
+ throw new McpError(ErrorCode.InvalidRequest, `Jira issue not found: ${issueKey}`);
247
+ }
248
+ this.handleError(error, `Failed to get issue ${issueKey}`);
249
+ }
250
+ }
251
+ async searchIssues(jql, maxResults = 20, fields) {
252
+ try {
253
+ const response = await axios.post(`${this.baseUrl}/search`, {
254
+ jql,
255
+ maxResults,
256
+ fields: fields ?? ["key", "summary", "status", "assignee", "priority", "issuetype", "created", "updated"],
257
+ }, {
258
+ headers: this.defaultHeaders,
259
+ timeout: 30000,
260
+ });
261
+ return response.data;
262
+ }
263
+ catch (error) {
264
+ this.handleError(error, "Failed to search issues");
265
+ }
266
+ }
267
+ async createIssue(params) {
268
+ try {
269
+ const fields = {
270
+ project: { key: params.projectKey },
271
+ issuetype: { name: params.issueType },
272
+ summary: params.summary,
273
+ };
274
+ // Resolve description from multiple formats (adf > blocks > markdown)
275
+ const description = resolveDescription(params);
276
+ if (description) {
277
+ fields.description = description;
278
+ }
279
+ if (params.assignee) {
280
+ fields.assignee = { accountId: params.assignee };
281
+ }
282
+ if (params.priority) {
283
+ fields.priority = { name: params.priority };
284
+ }
285
+ if (params.labels && params.labels.length > 0) {
286
+ fields.labels = params.labels;
287
+ }
288
+ if (params.components && params.components.length > 0) {
289
+ fields.components = params.components.map((name) => ({ name }));
290
+ }
291
+ if (params.parentKey) {
292
+ fields.parent = { key: params.parentKey };
293
+ }
294
+ const response = await axios.post(`${this.baseUrl}/issue`, { fields }, {
295
+ headers: this.defaultHeaders,
296
+ timeout: 30000,
297
+ });
298
+ return response.data;
299
+ }
300
+ catch (error) {
301
+ this.handleError(error, "Failed to create issue");
302
+ }
303
+ }
304
+ async updateIssue(issueKey, params) {
305
+ try {
306
+ const fields = {};
307
+ if (params.summary !== undefined) {
308
+ fields.summary = params.summary;
309
+ }
310
+ // Resolve description from multiple formats (adf > blocks > markdown)
311
+ const description = resolveDescription(params);
312
+ if (description) {
313
+ fields.description = description;
314
+ }
315
+ if (params.assignee !== undefined) {
316
+ fields.assignee = params.assignee ? { accountId: params.assignee } : null;
317
+ }
318
+ if (params.priority !== undefined) {
319
+ fields.priority = { name: params.priority };
320
+ }
321
+ if (params.labels !== undefined) {
322
+ fields.labels = params.labels;
323
+ }
324
+ if (params.components !== undefined) {
325
+ fields.components = params.components.map((name) => ({ name }));
326
+ }
327
+ await axios.put(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}`, { fields }, {
328
+ headers: this.defaultHeaders,
329
+ timeout: 30000,
330
+ });
331
+ }
332
+ catch (error) {
333
+ this.handleError(error, `Failed to update issue ${issueKey}`);
334
+ }
335
+ }
336
+ async deleteIssue(issueKey, deleteSubtasks = false) {
337
+ try {
338
+ await axios.delete(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}`, {
339
+ headers: this.defaultHeaders,
340
+ params: { deleteSubtasks },
341
+ timeout: 30000,
342
+ });
343
+ }
344
+ catch (error) {
345
+ this.handleError(error, `Failed to delete issue ${issueKey}`);
346
+ }
347
+ }
348
+ // ============================================================
349
+ // ISSUES - TRANSITIONS & ASSIGNMENT
350
+ // ============================================================
351
+ async getIssueTransitions(issueKey) {
352
+ try {
353
+ const response = await axios.get(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/transitions`, {
354
+ headers: this.defaultHeaders,
355
+ timeout: 30000,
356
+ });
357
+ return response.data.transitions;
358
+ }
359
+ catch (error) {
360
+ this.handleError(error, `Failed to get transitions for ${issueKey}`);
361
+ }
362
+ }
363
+ async transitionIssue(issueKey, transitionId, comment) {
364
+ try {
365
+ const body = {
366
+ transition: { id: transitionId },
367
+ };
368
+ if (comment) {
369
+ body.update = {
370
+ comment: [{ add: { body: textToAdf(comment) } }],
371
+ };
372
+ }
373
+ await axios.post(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/transitions`, body, {
374
+ headers: this.defaultHeaders,
375
+ timeout: 30000,
376
+ });
377
+ }
378
+ catch (error) {
379
+ this.handleError(error, `Failed to transition issue ${issueKey}`);
380
+ }
381
+ }
382
+ async assignIssue(issueKey, accountId) {
383
+ try {
384
+ await axios.put(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/assignee`, { accountId }, {
385
+ headers: this.defaultHeaders,
386
+ timeout: 30000,
387
+ });
388
+ }
389
+ catch (error) {
390
+ this.handleError(error, `Failed to assign issue ${issueKey}`);
391
+ }
392
+ }
393
+ async bulkCreateIssues(issues) {
394
+ try {
395
+ const issueUpdates = issues.map((params) => {
396
+ const fields = {
397
+ project: { key: params.projectKey },
398
+ issuetype: { name: params.issueType },
399
+ summary: params.summary,
400
+ };
401
+ if (params.description) {
402
+ fields.description = textToAdf(params.description);
403
+ }
404
+ if (params.assignee) {
405
+ fields.assignee = { accountId: params.assignee };
406
+ }
407
+ if (params.priority) {
408
+ fields.priority = { name: params.priority };
409
+ }
410
+ if (params.labels && params.labels.length > 0) {
411
+ fields.labels = params.labels;
412
+ }
413
+ if (params.parentKey) {
414
+ fields.parent = { key: params.parentKey };
415
+ }
416
+ return { fields };
417
+ });
418
+ const response = await axios.post(`${this.baseUrl}/issue/bulk`, { issueUpdates }, {
419
+ headers: this.defaultHeaders,
420
+ timeout: 60000,
421
+ });
422
+ return response.data;
423
+ }
424
+ catch (error) {
425
+ this.handleError(error, "Failed to bulk create issues");
426
+ }
427
+ }
428
+ // ============================================================
429
+ // COMMENTS
430
+ // ============================================================
431
+ async getIssueComments(issueKey, maxResults = 50) {
432
+ try {
433
+ const response = await axios.get(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/comment`, {
434
+ headers: this.defaultHeaders,
435
+ params: { maxResults },
436
+ timeout: 30000,
437
+ });
438
+ return response.data;
439
+ }
440
+ catch (error) {
441
+ this.handleError(error, `Failed to get comments for ${issueKey}`);
442
+ }
443
+ }
444
+ async addIssueComment(issueKey, body) {
445
+ try {
446
+ const response = await axios.post(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/comment`, { body: textToAdf(body) }, {
447
+ headers: this.defaultHeaders,
448
+ timeout: 30000,
449
+ });
450
+ return response.data;
451
+ }
452
+ catch (error) {
453
+ this.handleError(error, `Failed to add comment to ${issueKey}`);
454
+ }
455
+ }
456
+ async updateIssueComment(issueKey, commentId, body) {
457
+ try {
458
+ const response = await axios.put(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/comment/${commentId}`, { body: textToAdf(body) }, {
459
+ headers: this.defaultHeaders,
460
+ timeout: 30000,
461
+ });
462
+ return response.data;
463
+ }
464
+ catch (error) {
465
+ this.handleError(error, `Failed to update comment ${commentId}`);
466
+ }
467
+ }
468
+ async deleteIssueComment(issueKey, commentId) {
469
+ try {
470
+ await axios.delete(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/comment/${commentId}`, {
471
+ headers: this.defaultHeaders,
472
+ timeout: 30000,
473
+ });
474
+ }
475
+ catch (error) {
476
+ this.handleError(error, `Failed to delete comment ${commentId}`);
477
+ }
478
+ }
479
+ // ============================================================
480
+ // WORKLOGS
481
+ // ============================================================
482
+ async getIssueWorklogs(issueKey, maxResults = 50) {
483
+ try {
484
+ const response = await axios.get(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/worklog`, {
485
+ headers: this.defaultHeaders,
486
+ params: { maxResults },
487
+ timeout: 30000,
488
+ });
489
+ return response.data;
490
+ }
491
+ catch (error) {
492
+ this.handleError(error, `Failed to get worklogs for ${issueKey}`);
493
+ }
494
+ }
495
+ async addWorklog(issueKey, timeSpent, comment, started) {
496
+ try {
497
+ const body = { timeSpent };
498
+ if (comment) {
499
+ body.comment = textToAdf(comment);
500
+ }
501
+ if (started) {
502
+ body.started = started;
503
+ }
504
+ const response = await axios.post(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/worklog`, body, {
505
+ headers: this.defaultHeaders,
506
+ timeout: 30000,
507
+ });
508
+ return response.data;
509
+ }
510
+ catch (error) {
511
+ this.handleError(error, `Failed to add worklog to ${issueKey}`);
512
+ }
513
+ }
514
+ async updateWorklog(issueKey, worklogId, timeSpent, comment) {
515
+ try {
516
+ const body = { timeSpent };
517
+ if (comment) {
518
+ body.comment = textToAdf(comment);
519
+ }
520
+ const response = await axios.put(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/worklog/${worklogId}`, body, {
521
+ headers: this.defaultHeaders,
522
+ timeout: 30000,
523
+ });
524
+ return response.data;
525
+ }
526
+ catch (error) {
527
+ this.handleError(error, `Failed to update worklog ${worklogId}`);
528
+ }
529
+ }
530
+ async deleteWorklog(issueKey, worklogId) {
531
+ try {
532
+ await axios.delete(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/worklog/${worklogId}`, {
533
+ headers: this.defaultHeaders,
534
+ timeout: 30000,
535
+ });
536
+ }
537
+ catch (error) {
538
+ this.handleError(error, `Failed to delete worklog ${worklogId}`);
539
+ }
540
+ }
541
+ // ============================================================
542
+ // ATTACHMENTS
543
+ // ============================================================
544
+ async addAttachment(issueKey, filename, content) {
545
+ try {
546
+ const FormData = (await import("form-data")).default;
547
+ const form = new FormData();
548
+ form.append("file", content, { filename });
549
+ const response = await axios.post(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/attachments`, form, {
550
+ headers: {
551
+ ...this.authHeader,
552
+ ...form.getHeaders(),
553
+ "X-Atlassian-Token": "no-check",
554
+ },
555
+ timeout: 60000,
556
+ });
557
+ return response.data;
558
+ }
559
+ catch (error) {
560
+ this.handleError(error, `Failed to add attachment to ${issueKey}`);
561
+ }
562
+ }
563
+ async deleteAttachment(attachmentId) {
564
+ try {
565
+ await axios.delete(`${this.baseUrl}/attachment/${attachmentId}`, {
566
+ headers: this.defaultHeaders,
567
+ timeout: 30000,
568
+ });
569
+ }
570
+ catch (error) {
571
+ this.handleError(error, `Failed to delete attachment ${attachmentId}`);
572
+ }
573
+ }
574
+ async getAttachmentContent(attachmentId) {
575
+ try {
576
+ // First get attachment metadata
577
+ const metaResponse = await axios.get(`${this.baseUrl}/attachment/${attachmentId}`, {
578
+ headers: this.defaultHeaders,
579
+ timeout: 30000,
580
+ });
581
+ // Then get content
582
+ const contentResponse = await axios.get(`${this.baseUrl}/attachment/content/${attachmentId}`, {
583
+ headers: this.authHeader,
584
+ responseType: "arraybuffer",
585
+ timeout: 60000,
586
+ });
587
+ return {
588
+ content: contentResponse.data.toString("base64"),
589
+ filename: metaResponse.data.filename,
590
+ };
591
+ }
592
+ catch (error) {
593
+ this.handleError(error, `Failed to get attachment ${attachmentId}`);
594
+ }
595
+ }
596
+ // ============================================================
597
+ // ISSUE LINKS
598
+ // ============================================================
599
+ async linkIssues(inwardIssueKey, outwardIssueKey, linkType) {
600
+ try {
601
+ await axios.post(`${this.baseUrl}/issueLink`, {
602
+ type: { name: linkType },
603
+ inwardIssue: { key: inwardIssueKey },
604
+ outwardIssue: { key: outwardIssueKey },
605
+ }, {
606
+ headers: this.defaultHeaders,
607
+ timeout: 30000,
608
+ });
609
+ }
610
+ catch (error) {
611
+ this.handleError(error, `Failed to link issues ${inwardIssueKey} and ${outwardIssueKey}`);
612
+ }
613
+ }
614
+ async deleteIssueLink(linkId) {
615
+ try {
616
+ await axios.delete(`${this.baseUrl}/issueLink/${linkId}`, {
617
+ headers: this.defaultHeaders,
618
+ timeout: 30000,
619
+ });
620
+ }
621
+ catch (error) {
622
+ this.handleError(error, `Failed to delete issue link ${linkId}`);
623
+ }
624
+ }
625
+ async getIssueLinkTypes() {
626
+ try {
627
+ const response = await axios.get(`${this.baseUrl}/issueLinkType`, {
628
+ headers: this.defaultHeaders,
629
+ timeout: 30000,
630
+ });
631
+ return response.data.issueLinkTypes;
632
+ }
633
+ catch (error) {
634
+ this.handleError(error, "Failed to get issue link types");
635
+ }
636
+ }
637
+ // ============================================================
638
+ // WATCHERS
639
+ // ============================================================
640
+ async getIssueWatchers(issueKey) {
641
+ try {
642
+ const response = await axios.get(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/watchers`, {
643
+ headers: this.defaultHeaders,
644
+ timeout: 30000,
645
+ });
646
+ return { watchers: response.data.watchers };
647
+ }
648
+ catch (error) {
649
+ this.handleError(error, `Failed to get watchers for ${issueKey}`);
650
+ }
651
+ }
652
+ async addWatcher(issueKey, accountId) {
653
+ try {
654
+ await axios.post(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/watchers`, JSON.stringify(accountId), {
655
+ headers: this.defaultHeaders,
656
+ timeout: 30000,
657
+ });
658
+ }
659
+ catch (error) {
660
+ this.handleError(error, `Failed to add watcher to ${issueKey}`);
661
+ }
662
+ }
663
+ async removeWatcher(issueKey, accountId) {
664
+ try {
665
+ await axios.delete(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/watchers`, {
666
+ headers: this.defaultHeaders,
667
+ params: { accountId },
668
+ timeout: 30000,
669
+ });
670
+ }
671
+ catch (error) {
672
+ this.handleError(error, `Failed to remove watcher from ${issueKey}`);
673
+ }
674
+ }
675
+ // ============================================================
676
+ // PROJECTS
677
+ // ============================================================
678
+ async listProjects(maxResults = 50) {
679
+ try {
680
+ const response = await axios.get(`${this.baseUrl}/project/search`, {
681
+ headers: this.defaultHeaders,
682
+ params: { maxResults },
683
+ timeout: 30000,
684
+ });
685
+ return response.data.values;
686
+ }
687
+ catch (error) {
688
+ this.handleError(error, "Failed to list projects");
689
+ }
690
+ }
691
+ async getProject(projectKey) {
692
+ try {
693
+ const response = await axios.get(`${this.baseUrl}/project/${encodeURIComponent(projectKey)}`, {
694
+ headers: this.defaultHeaders,
695
+ timeout: 30000,
696
+ });
697
+ return response.data;
698
+ }
699
+ catch (error) {
700
+ this.handleError(error, `Failed to get project ${projectKey}`);
701
+ }
702
+ }
703
+ async getProjectComponents(projectKey) {
704
+ try {
705
+ const response = await axios.get(`${this.baseUrl}/project/${encodeURIComponent(projectKey)}/components`, {
706
+ headers: this.defaultHeaders,
707
+ timeout: 30000,
708
+ });
709
+ return response.data;
710
+ }
711
+ catch (error) {
712
+ this.handleError(error, `Failed to get components for ${projectKey}`);
713
+ }
714
+ }
715
+ async getProjectVersions(projectKey) {
716
+ try {
717
+ const response = await axios.get(`${this.baseUrl}/project/${encodeURIComponent(projectKey)}/versions`, {
718
+ headers: this.defaultHeaders,
719
+ timeout: 30000,
720
+ });
721
+ return response.data;
722
+ }
723
+ catch (error) {
724
+ this.handleError(error, `Failed to get versions for ${projectKey}`);
725
+ }
726
+ }
727
+ async getProjectStatuses(projectKey) {
728
+ try {
729
+ const response = await axios.get(`${this.baseUrl}/project/${encodeURIComponent(projectKey)}/statuses`, {
730
+ headers: this.defaultHeaders,
731
+ timeout: 30000,
732
+ });
733
+ // Flatten statuses from all issue types
734
+ const allStatuses = response.data.flatMap((it) => it.statuses);
735
+ // Remove duplicates by id
736
+ const uniqueStatuses = [...new Map(allStatuses.map((s) => [s.id, s])).values()];
737
+ return uniqueStatuses;
738
+ }
739
+ catch (error) {
740
+ this.handleError(error, `Failed to get statuses for ${projectKey}`);
741
+ }
742
+ }
743
+ async createProjectVersion(projectKey, name, description, releaseDate, startDate) {
744
+ try {
745
+ // First get project ID
746
+ const project = await this.getProject(projectKey);
747
+ const response = await axios.post(`${this.baseUrl}/version`, {
748
+ projectId: project.id,
749
+ name,
750
+ description,
751
+ releaseDate,
752
+ startDate,
753
+ }, {
754
+ headers: this.defaultHeaders,
755
+ timeout: 30000,
756
+ });
757
+ return response.data;
758
+ }
759
+ catch (error) {
760
+ this.handleError(error, `Failed to create version in ${projectKey}`);
761
+ }
762
+ }
763
+ async batchCreateVersions(projectKey, versions) {
764
+ const created = [];
765
+ const errors = [];
766
+ // Get project ID once
767
+ const project = await this.getProject(projectKey);
768
+ for (const version of versions) {
769
+ try {
770
+ const response = await axios.post(`${this.baseUrl}/version`, {
771
+ projectId: project.id,
772
+ name: version.name,
773
+ description: version.description,
774
+ releaseDate: version.releaseDate,
775
+ startDate: version.startDate,
776
+ }, {
777
+ headers: this.defaultHeaders,
778
+ timeout: 30000,
779
+ });
780
+ created.push(response.data);
781
+ }
782
+ catch (error) {
783
+ const axiosError = error;
784
+ errors.push({
785
+ name: version.name,
786
+ error: axiosError.message || "Unknown error",
787
+ });
788
+ }
789
+ }
790
+ return { created, errors };
791
+ }
792
+ async createProjectComponent(projectKey, name, description, leadAccountId) {
793
+ try {
794
+ const response = await axios.post(`${this.baseUrl}/component`, {
795
+ project: projectKey,
796
+ name,
797
+ description,
798
+ leadAccountId,
799
+ }, {
800
+ headers: this.defaultHeaders,
801
+ timeout: 30000,
802
+ });
803
+ return response.data;
804
+ }
805
+ catch (error) {
806
+ this.handleError(error, `Failed to create component in ${projectKey}`);
807
+ }
808
+ }
809
+ // ============================================================
810
+ // USERS
811
+ // ============================================================
812
+ async searchUsers(query, maxResults = 20) {
813
+ try {
814
+ const response = await axios.get(`${this.baseUrl}/user/search`, {
815
+ headers: this.defaultHeaders,
816
+ params: { query, maxResults },
817
+ timeout: 30000,
818
+ });
819
+ return response.data;
820
+ }
821
+ catch (error) {
822
+ this.handleError(error, "Failed to search users");
823
+ }
824
+ }
825
+ async getCurrentUser() {
826
+ try {
827
+ const response = await axios.get(`${this.baseUrl}/myself`, {
828
+ headers: this.defaultHeaders,
829
+ timeout: 30000,
830
+ });
831
+ return response.data;
832
+ }
833
+ catch (error) {
834
+ this.handleError(error, "Failed to get current user");
835
+ }
836
+ }
837
+ async getUser(accountId) {
838
+ try {
839
+ const response = await axios.get(`${this.baseUrl}/user`, {
840
+ headers: this.defaultHeaders,
841
+ params: { accountId },
842
+ timeout: 30000,
843
+ });
844
+ return response.data;
845
+ }
846
+ catch (error) {
847
+ this.handleError(error, `Failed to get user ${accountId}`);
848
+ }
849
+ }
850
+ async getAssignableUsers(projectKey, issueKey, maxResults = 50) {
851
+ try {
852
+ const params = { project: projectKey, maxResults };
853
+ if (issueKey) {
854
+ params.issueKey = issueKey;
855
+ }
856
+ const response = await axios.get(`${this.baseUrl}/user/assignable/search`, {
857
+ headers: this.defaultHeaders,
858
+ params,
859
+ timeout: 30000,
860
+ });
861
+ return response.data;
862
+ }
863
+ catch (error) {
864
+ this.handleError(error, "Failed to get assignable users");
865
+ }
866
+ }
867
+ // ============================================================
868
+ // AGILE - BOARDS
869
+ // ============================================================
870
+ async listBoards(projectKeyOrId, type, maxResults = 50) {
871
+ try {
872
+ const params = { maxResults };
873
+ if (projectKeyOrId) {
874
+ params.projectKeyOrId = projectKeyOrId;
875
+ }
876
+ if (type) {
877
+ params.type = type;
878
+ }
879
+ const response = await axios.get(`${this.agileBaseUrl}/board`, {
880
+ headers: this.defaultHeaders,
881
+ params,
882
+ timeout: 30000,
883
+ });
884
+ return response.data.values;
885
+ }
886
+ catch (error) {
887
+ this.handleError(error, "Failed to list boards");
888
+ }
889
+ }
890
+ async getBoard(boardId) {
891
+ try {
892
+ const response = await axios.get(`${this.agileBaseUrl}/board/${boardId}`, {
893
+ headers: this.defaultHeaders,
894
+ timeout: 30000,
895
+ });
896
+ return response.data;
897
+ }
898
+ catch (error) {
899
+ this.handleError(error, `Failed to get board ${boardId}`);
900
+ }
901
+ }
902
+ async getBoardIssues(boardId, jql, maxResults = 50) {
903
+ try {
904
+ const params = { maxResults };
905
+ if (jql) {
906
+ params.jql = jql;
907
+ }
908
+ const response = await axios.get(`${this.agileBaseUrl}/board/${boardId}/issue`, {
909
+ headers: this.defaultHeaders,
910
+ params,
911
+ timeout: 30000,
912
+ });
913
+ return response.data;
914
+ }
915
+ catch (error) {
916
+ this.handleError(error, `Failed to get issues for board ${boardId}`);
917
+ }
918
+ }
919
+ async getBacklogIssues(boardId, maxResults = 50) {
920
+ try {
921
+ const response = await axios.get(`${this.agileBaseUrl}/board/${boardId}/backlog`, {
922
+ headers: this.defaultHeaders,
923
+ params: { maxResults },
924
+ timeout: 30000,
925
+ });
926
+ return response.data;
927
+ }
928
+ catch (error) {
929
+ this.handleError(error, `Failed to get backlog for board ${boardId}`);
930
+ }
931
+ }
932
+ // ============================================================
933
+ // AGILE - SPRINTS
934
+ // ============================================================
935
+ async listSprints(boardId, state, maxResults = 50) {
936
+ try {
937
+ const params = { maxResults };
938
+ if (state) {
939
+ params.state = state;
940
+ }
941
+ const response = await axios.get(`${this.agileBaseUrl}/board/${boardId}/sprint`, {
942
+ headers: this.defaultHeaders,
943
+ params,
944
+ timeout: 30000,
945
+ });
946
+ return response.data.values;
947
+ }
948
+ catch (error) {
949
+ this.handleError(error, `Failed to list sprints for board ${boardId}`);
950
+ }
951
+ }
952
+ async getSprint(sprintId) {
953
+ try {
954
+ const response = await axios.get(`${this.agileBaseUrl}/sprint/${sprintId}`, {
955
+ headers: this.defaultHeaders,
956
+ timeout: 30000,
957
+ });
958
+ return response.data;
959
+ }
960
+ catch (error) {
961
+ this.handleError(error, `Failed to get sprint ${sprintId}`);
962
+ }
963
+ }
964
+ async getSprintIssues(sprintId, maxResults = 50) {
965
+ try {
966
+ const response = await axios.get(`${this.agileBaseUrl}/sprint/${sprintId}/issue`, {
967
+ headers: this.defaultHeaders,
968
+ params: { maxResults },
969
+ timeout: 30000,
970
+ });
971
+ return response.data;
972
+ }
973
+ catch (error) {
974
+ this.handleError(error, `Failed to get issues for sprint ${sprintId}`);
975
+ }
976
+ }
977
+ async createSprint(boardId, name, goal, startDate, endDate) {
978
+ try {
979
+ const response = await axios.post(`${this.agileBaseUrl}/sprint`, {
980
+ originBoardId: boardId,
981
+ name,
982
+ goal,
983
+ startDate,
984
+ endDate,
985
+ }, {
986
+ headers: this.defaultHeaders,
987
+ timeout: 30000,
988
+ });
989
+ return response.data;
990
+ }
991
+ catch (error) {
992
+ this.handleError(error, "Failed to create sprint");
993
+ }
994
+ }
995
+ async updateSprint(sprintId, updates) {
996
+ try {
997
+ const response = await axios.put(`${this.agileBaseUrl}/sprint/${sprintId}`, updates, {
998
+ headers: this.defaultHeaders,
999
+ timeout: 30000,
1000
+ });
1001
+ return response.data;
1002
+ }
1003
+ catch (error) {
1004
+ this.handleError(error, `Failed to update sprint ${sprintId}`);
1005
+ }
1006
+ }
1007
+ async moveIssuesToSprint(sprintId, issueKeys) {
1008
+ try {
1009
+ await axios.post(`${this.agileBaseUrl}/sprint/${sprintId}/issue`, { issues: issueKeys }, {
1010
+ headers: this.defaultHeaders,
1011
+ timeout: 30000,
1012
+ });
1013
+ }
1014
+ catch (error) {
1015
+ this.handleError(error, `Failed to move issues to sprint ${sprintId}`);
1016
+ }
1017
+ }
1018
+ // ============================================================
1019
+ // AGILE - EPICS
1020
+ // ============================================================
1021
+ async getEpic(epicIdOrKey) {
1022
+ try {
1023
+ const response = await axios.get(`${this.agileBaseUrl}/epic/${encodeURIComponent(epicIdOrKey)}`, {
1024
+ headers: this.defaultHeaders,
1025
+ timeout: 30000,
1026
+ });
1027
+ return response.data;
1028
+ }
1029
+ catch (error) {
1030
+ this.handleError(error, `Failed to get epic ${epicIdOrKey}`);
1031
+ }
1032
+ }
1033
+ async getEpicIssues(epicIdOrKey, maxResults = 50) {
1034
+ try {
1035
+ const response = await axios.get(`${this.agileBaseUrl}/epic/${encodeURIComponent(epicIdOrKey)}/issue`, {
1036
+ headers: this.defaultHeaders,
1037
+ params: { maxResults },
1038
+ timeout: 30000,
1039
+ });
1040
+ return response.data;
1041
+ }
1042
+ catch (error) {
1043
+ this.handleError(error, `Failed to get issues for epic ${epicIdOrKey}`);
1044
+ }
1045
+ }
1046
+ async moveIssuesToEpic(epicIdOrKey, issueKeys) {
1047
+ try {
1048
+ await axios.post(`${this.agileBaseUrl}/epic/${encodeURIComponent(epicIdOrKey)}/issue`, { issues: issueKeys }, {
1049
+ headers: this.defaultHeaders,
1050
+ timeout: 30000,
1051
+ });
1052
+ }
1053
+ catch (error) {
1054
+ this.handleError(error, `Failed to move issues to epic ${epicIdOrKey}`);
1055
+ }
1056
+ }
1057
+ async removeIssuesFromEpic(issueKeys) {
1058
+ try {
1059
+ await axios.post(`${this.agileBaseUrl}/epic/none/issue`, { issues: issueKeys }, {
1060
+ headers: this.defaultHeaders,
1061
+ timeout: 30000,
1062
+ });
1063
+ }
1064
+ catch (error) {
1065
+ this.handleError(error, "Failed to remove issues from epic");
1066
+ }
1067
+ }
1068
+ // ============================================================
1069
+ // FILTERS
1070
+ // ============================================================
1071
+ async getMyFilters() {
1072
+ try {
1073
+ const response = await axios.get(`${this.baseUrl}/filter/my`, {
1074
+ headers: this.defaultHeaders,
1075
+ timeout: 30000,
1076
+ });
1077
+ return response.data;
1078
+ }
1079
+ catch (error) {
1080
+ this.handleError(error, "Failed to get my filters");
1081
+ }
1082
+ }
1083
+ async getFavoriteFilters() {
1084
+ try {
1085
+ const response = await axios.get(`${this.baseUrl}/filter/favourite`, {
1086
+ headers: this.defaultHeaders,
1087
+ timeout: 30000,
1088
+ });
1089
+ return response.data;
1090
+ }
1091
+ catch (error) {
1092
+ this.handleError(error, "Failed to get favorite filters");
1093
+ }
1094
+ }
1095
+ async getFilter(filterId) {
1096
+ try {
1097
+ const response = await axios.get(`${this.baseUrl}/filter/${filterId}`, {
1098
+ headers: this.defaultHeaders,
1099
+ timeout: 30000,
1100
+ });
1101
+ return response.data;
1102
+ }
1103
+ catch (error) {
1104
+ this.handleError(error, `Failed to get filter ${filterId}`);
1105
+ }
1106
+ }
1107
+ async createFilter(name, jql, description, favourite) {
1108
+ try {
1109
+ const response = await axios.post(`${this.baseUrl}/filter`, { name, jql, description, favourite }, {
1110
+ headers: this.defaultHeaders,
1111
+ timeout: 30000,
1112
+ });
1113
+ return response.data;
1114
+ }
1115
+ catch (error) {
1116
+ this.handleError(error, "Failed to create filter");
1117
+ }
1118
+ }
1119
+ // ============================================================
1120
+ // METADATA (with caching - 30 min TTL)
1121
+ // ============================================================
1122
+ async getFields() {
1123
+ return metadataCache.get("fields", async () => {
1124
+ try {
1125
+ return await axiosWithRetry({
1126
+ method: "GET",
1127
+ url: `${this.baseUrl}/field`,
1128
+ headers: this.defaultHeaders,
1129
+ timeout: 30000,
1130
+ }, "getFields");
1131
+ }
1132
+ catch (error) {
1133
+ this.handleError(error, "Failed to get fields");
1134
+ }
1135
+ });
1136
+ }
1137
+ async getIssueTypes() {
1138
+ return metadataCache.get("issueTypes", async () => {
1139
+ try {
1140
+ return await axiosWithRetry({
1141
+ method: "GET",
1142
+ url: `${this.baseUrl}/issuetype`,
1143
+ headers: this.defaultHeaders,
1144
+ timeout: 30000,
1145
+ }, "getIssueTypes");
1146
+ }
1147
+ catch (error) {
1148
+ this.handleError(error, "Failed to get issue types");
1149
+ }
1150
+ });
1151
+ }
1152
+ async getPriorities() {
1153
+ return metadataCache.get("priorities", async () => {
1154
+ try {
1155
+ return await axiosWithRetry({
1156
+ method: "GET",
1157
+ url: `${this.baseUrl}/priority`,
1158
+ headers: this.defaultHeaders,
1159
+ timeout: 30000,
1160
+ }, "getPriorities");
1161
+ }
1162
+ catch (error) {
1163
+ this.handleError(error, "Failed to get priorities");
1164
+ }
1165
+ });
1166
+ }
1167
+ async getStatuses() {
1168
+ return metadataCache.get("statuses", async () => {
1169
+ try {
1170
+ return await axiosWithRetry({
1171
+ method: "GET",
1172
+ url: `${this.baseUrl}/status`,
1173
+ headers: this.defaultHeaders,
1174
+ timeout: 30000,
1175
+ }, "getStatuses");
1176
+ }
1177
+ catch (error) {
1178
+ this.handleError(error, "Failed to get statuses");
1179
+ }
1180
+ });
1181
+ }
1182
+ async getResolutions() {
1183
+ return metadataCache.get("resolutions", async () => {
1184
+ try {
1185
+ return await axiosWithRetry({
1186
+ method: "GET",
1187
+ url: `${this.baseUrl}/resolution`,
1188
+ headers: this.defaultHeaders,
1189
+ timeout: 30000,
1190
+ }, "getResolutions");
1191
+ }
1192
+ catch (error) {
1193
+ this.handleError(error, "Failed to get resolutions");
1194
+ }
1195
+ });
1196
+ }
1197
+ // Cache management
1198
+ clearMetadataCache() {
1199
+ const stats = metadataCache.getStats();
1200
+ metadataCache.clear();
1201
+ return { cleared: true, stats };
1202
+ }
1203
+ getMetadataCacheStats() {
1204
+ return metadataCache.getStats();
1205
+ }
1206
+ async getLabels(maxResults = 1000) {
1207
+ try {
1208
+ const response = await axios.get(`${this.baseUrl}/label`, {
1209
+ headers: this.defaultHeaders,
1210
+ params: { maxResults },
1211
+ timeout: 30000,
1212
+ });
1213
+ return response.data.values;
1214
+ }
1215
+ catch (error) {
1216
+ this.handleError(error, "Failed to get labels");
1217
+ }
1218
+ }
1219
+ async getServerInfo() {
1220
+ try {
1221
+ const response = await axios.get(`${this.baseUrl}/serverInfo`, {
1222
+ headers: this.defaultHeaders,
1223
+ timeout: 30000,
1224
+ });
1225
+ return response.data;
1226
+ }
1227
+ catch (error) {
1228
+ this.handleError(error, "Failed to get server info");
1229
+ }
1230
+ }
1231
+ // ============================================================
1232
+ // CHANGELOG METHODS (Cloud only)
1233
+ // ============================================================
1234
+ async getIssueChangelog(issueKey, maxResults = 100) {
1235
+ try {
1236
+ const response = await axios.get(`${this.baseUrl}/issue/${issueKey}/changelog`, {
1237
+ headers: this.defaultHeaders,
1238
+ params: { maxResults },
1239
+ timeout: 30000,
1240
+ });
1241
+ return {
1242
+ changelogs: response.data.values,
1243
+ total: response.data.total,
1244
+ };
1245
+ }
1246
+ catch (error) {
1247
+ this.handleError(error, `Failed to get changelog for ${issueKey}`);
1248
+ }
1249
+ }
1250
+ async batchGetChangelogs(issueKeys, maxResults = 50) {
1251
+ const results = [];
1252
+ // Process in parallel with limit of 5 concurrent requests
1253
+ const batchSize = 5;
1254
+ for (let i = 0; i < issueKeys.length; i += batchSize) {
1255
+ const batch = issueKeys.slice(i, i + batchSize);
1256
+ const batchResults = await Promise.all(batch.map(async (issueKey) => {
1257
+ try {
1258
+ // First get issue ID
1259
+ const issue = await this.getJiraIssue(issueKey);
1260
+ const changelog = await this.getIssueChangelog(issueKey, maxResults);
1261
+ return {
1262
+ issueKey,
1263
+ issueId: issue.id,
1264
+ changelogs: changelog.changelogs,
1265
+ };
1266
+ }
1267
+ catch {
1268
+ return {
1269
+ issueKey,
1270
+ issueId: "",
1271
+ changelogs: [],
1272
+ };
1273
+ }
1274
+ }));
1275
+ results.push(...batchResults);
1276
+ }
1277
+ return results;
1278
+ }
1279
+ // ============================================================
1280
+ // SEARCH FIELDS (fuzzy search)
1281
+ // ============================================================
1282
+ async searchFields(keyword, limit = 10) {
1283
+ const allFields = await this.getFields();
1284
+ const keywordLower = keyword.toLowerCase();
1285
+ // Fuzzy match: name or id contains keyword
1286
+ const matches = allFields.filter((field) => {
1287
+ const nameLower = field.name.toLowerCase();
1288
+ const idLower = field.id.toLowerCase();
1289
+ return nameLower.includes(keywordLower) || idLower.includes(keywordLower);
1290
+ });
1291
+ // Sort by relevance: exact match first, then startsWith, then contains
1292
+ matches.sort((a, b) => {
1293
+ const aName = a.name.toLowerCase();
1294
+ const bName = b.name.toLowerCase();
1295
+ // Exact match
1296
+ if (aName === keywordLower && bName !== keywordLower)
1297
+ return -1;
1298
+ if (bName === keywordLower && aName !== keywordLower)
1299
+ return 1;
1300
+ // Starts with
1301
+ if (aName.startsWith(keywordLower) && !bName.startsWith(keywordLower))
1302
+ return -1;
1303
+ if (bName.startsWith(keywordLower) && !aName.startsWith(keywordLower))
1304
+ return 1;
1305
+ // Alphabetical
1306
+ return aName.localeCompare(bName);
1307
+ });
1308
+ return matches.slice(0, limit);
1309
+ }
1310
+ // ============================================================
1311
+ // REMOTE ISSUE LINKS (web links, Confluence)
1312
+ // ============================================================
1313
+ async createRemoteIssueLink(issueKey, url, title, summary, iconUrl, relationship) {
1314
+ try {
1315
+ const response = await axios.post(`${this.baseUrl}/issue/${encodeURIComponent(issueKey)}/remotelink`, {
1316
+ object: {
1317
+ url,
1318
+ title,
1319
+ summary,
1320
+ icon: iconUrl ? { url16x16: iconUrl } : undefined,
1321
+ },
1322
+ relationship: relationship || "relates to",
1323
+ }, {
1324
+ headers: this.defaultHeaders,
1325
+ timeout: 30000,
1326
+ });
1327
+ return response.data;
1328
+ }
1329
+ catch (error) {
1330
+ this.handleError(error, `Failed to create remote link for ${issueKey}`);
1331
+ }
1332
+ }
1333
+ // ============================================================
1334
+ // DOWNLOAD ATTACHMENTS
1335
+ // ============================================================
1336
+ async getIssueAttachments(issueKey) {
1337
+ try {
1338
+ const issue = await this.getJiraIssue(issueKey);
1339
+ const attachments = issue.fields.attachment;
1340
+ return attachments || [];
1341
+ }
1342
+ catch (error) {
1343
+ this.handleError(error, `Failed to get attachments for ${issueKey}`);
1344
+ }
1345
+ }
1346
+ async downloadAttachment(attachmentId) {
1347
+ try {
1348
+ // Get metadata
1349
+ const metaResponse = await axios.get(`${this.baseUrl}/attachment/${attachmentId}`, {
1350
+ headers: this.defaultHeaders,
1351
+ timeout: 30000,
1352
+ });
1353
+ // Download content
1354
+ const contentResponse = await axios.get(metaResponse.data.content, {
1355
+ headers: this.authHeader,
1356
+ responseType: "arraybuffer",
1357
+ timeout: 120000, // 2 min for large files
1358
+ });
1359
+ return {
1360
+ content: contentResponse.data.toString("base64"),
1361
+ filename: metaResponse.data.filename,
1362
+ mimeType: metaResponse.data.mimeType,
1363
+ size: metaResponse.data.size,
1364
+ };
1365
+ }
1366
+ catch (error) {
1367
+ this.handleError(error, `Failed to download attachment ${attachmentId}`);
1368
+ }
1369
+ }
1370
+ // ============================================================
1371
+ // PROJECT ISSUES (convenience wrapper)
1372
+ // ============================================================
1373
+ async getProjectIssues(projectKey, maxResults = 50, fields) {
1374
+ const jql = `project = "${projectKey}" ORDER BY created DESC`;
1375
+ return this.searchIssues(jql, maxResults, fields);
1376
+ }
1377
+ // ============================================================
1378
+ // LINK ISSUE TO EPIC (convenience wrapper)
1379
+ // ============================================================
1380
+ async linkIssueToEpic(issueKey, epicKey) {
1381
+ await this.moveIssuesToEpic(epicKey, [issueKey]);
1382
+ }
1383
+ }
1384
+ //# sourceMappingURL=atlassian.js.map