@ecubelabs/atlassian-mcp 1.4.0-next.1 → 1.4.0-next.6

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.
@@ -274,4 +274,113 @@ export class ConfluenceService extends BaseApiService {
274
274
  status: ['current'], // 현재 상태인 페이지만
275
275
  });
276
276
  }
277
+ /**
278
+ * 페이지 생성
279
+ * @see https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-wiki-api-v2-pages-post
280
+ */
281
+ async createPage(options) {
282
+ const { spaceId, title, body, status = 'current', parentId } = options;
283
+ const requestBody = {
284
+ spaceId,
285
+ title,
286
+ body,
287
+ status,
288
+ };
289
+ if (parentId) {
290
+ requestBody.parentId = parentId;
291
+ }
292
+ return this.makeRequest(() => this.client.post('/pages', requestBody, {
293
+ headers: {
294
+ 'Content-Type': 'application/json',
295
+ },
296
+ }));
297
+ }
298
+ /**
299
+ * 페이지 수정
300
+ * @see https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-wiki-api-v2-pages-id-put
301
+ */
302
+ async updatePage(pageId, options) {
303
+ const { title, body, version, status = 'current' } = options;
304
+ const requestBody = {
305
+ id: pageId,
306
+ status,
307
+ title,
308
+ body,
309
+ version,
310
+ };
311
+ return this.makeRequest(() => this.client.put(`/pages/${pageId}`, requestBody, {
312
+ headers: {
313
+ 'Content-Type': 'application/json',
314
+ },
315
+ }));
316
+ }
317
+ /**
318
+ * 페이지 삭제
319
+ * @see https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-wiki-api-v2-pages-id-delete
320
+ */
321
+ async deletePage(pageId, options) {
322
+ const params = {};
323
+ if (options?.purge !== undefined) {
324
+ params.purge = options.purge;
325
+ }
326
+ if (options?.draft !== undefined) {
327
+ params.draft = options.draft;
328
+ }
329
+ return this.makeRequest(() => this.client.delete(`/pages/${pageId}`, { params }));
330
+ }
331
+ /**
332
+ * 페이지에 라벨 추가
333
+ * @see https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content-labels/#api-wiki-rest-api-content-id-label-post
334
+ * 참고: 라벨 관리는 v1 API를 사용 (/wiki/rest/api/content)
335
+ */
336
+ async addLabelsToPage(pageId, labels) {
337
+ return this.makeRequest(() => this.client.post(`/wiki/rest/api/content/${pageId}/label`, labels, {
338
+ headers: {
339
+ 'Content-Type': 'application/json',
340
+ },
341
+ }));
342
+ }
343
+ /**
344
+ * 페이지에서 라벨 제거
345
+ * @see https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content-labels/#api-wiki-rest-api-content-id-label-delete
346
+ * 참고: 라벨 관리는 v1 API를 사용 (/wiki/rest/api/content)
347
+ */
348
+ async removeLabelFromPage(pageId, labelName) {
349
+ const params = {
350
+ name: labelName,
351
+ };
352
+ return this.makeRequest(() => this.client.delete(`/wiki/rest/api/content/${pageId}/label`, { params }));
353
+ }
354
+ /**
355
+ * 템플릿 목록 조회
356
+ * @see https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-page-get
357
+ * 참고: 템플릿 API는 v1 API를 사용 (/wiki/rest/api/template)
358
+ */
359
+ async getTemplates(options) {
360
+ const params = {};
361
+ if (options) {
362
+ const { spaceKey, start, limit, expand } = options;
363
+ if (spaceKey)
364
+ params.spaceKey = spaceKey;
365
+ if (start !== undefined)
366
+ params.start = start;
367
+ if (limit !== undefined)
368
+ params.limit = limit;
369
+ if (expand)
370
+ params.expand = expand.join(',');
371
+ }
372
+ return this.makeRequest(() => this.client.get('/wiki/rest/api/template/page', { params }));
373
+ }
374
+ /**
375
+ * 특정 템플릿 상세 조회
376
+ * @see https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-contentTemplateId-get
377
+ * 참고: 템플릿 API는 v1 API를 사용 (/wiki/rest/api/template)
378
+ */
379
+ async getTemplateById(contentTemplateId, options) {
380
+ const params = {};
381
+ if (options?.expand) {
382
+ params.expand = options.expand.join(',');
383
+ }
384
+ return this.makeRequest(() => this.client.get(`/wiki/rest/api/template/page/${contentTemplateId}`, { params }));
385
+ }
277
386
  }
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Confluence Storage Format Utilities
3
+ *
4
+ * This module provides helper functions to create Confluence storage format (XHTML-based) content
5
+ * without manually writing XML. It supports common elements like headings, text formatting, lists,
6
+ * tables, macros, images, and links.
7
+ *
8
+ * @see https://confluence.atlassian.com/doc/confluence-storage-format-790796544.html
9
+ */
10
+ /**
11
+ * Confluence Storage Format Builder
12
+ * Provides a fluent API for building Confluence storage format content
13
+ */
14
+ export class ConfluenceStorageFormatBuilder {
15
+ content = [];
16
+ /**
17
+ * Add a heading
18
+ * @param level Heading level (1-6)
19
+ * @param text Heading text
20
+ */
21
+ heading(level, text) {
22
+ this.content.push(`<h${level}>${this.escapeHtml(text)}</h${level}>`);
23
+ return this;
24
+ }
25
+ /**
26
+ * Add a paragraph
27
+ */
28
+ paragraph(text) {
29
+ this.content.push(`<p>${this.escapeHtml(text)}</p>`);
30
+ return this;
31
+ }
32
+ /**
33
+ * Add bold text
34
+ */
35
+ bold(text) {
36
+ return `<strong>${this.escapeHtml(text)}</strong>`;
37
+ }
38
+ /**
39
+ * Add italic text
40
+ */
41
+ italic(text) {
42
+ return `<em>${this.escapeHtml(text)}</em>`;
43
+ }
44
+ /**
45
+ * Add underlined text
46
+ */
47
+ underline(text) {
48
+ return `<u>${this.escapeHtml(text)}</u>`;
49
+ }
50
+ /**
51
+ * Add strikethrough text
52
+ */
53
+ strikethrough(text) {
54
+ return `<s>${this.escapeHtml(text)}</s>`;
55
+ }
56
+ /**
57
+ * Add a line break
58
+ */
59
+ lineBreak() {
60
+ this.content.push('<br />');
61
+ return this;
62
+ }
63
+ /**
64
+ * Add a horizontal rule
65
+ */
66
+ horizontalRule() {
67
+ this.content.push('<hr />');
68
+ return this;
69
+ }
70
+ /**
71
+ * Add an unordered list
72
+ */
73
+ unorderedList(items) {
74
+ const listItems = items.map((item) => `<li>${this.escapeHtml(item)}</li>`).join('');
75
+ this.content.push(`<ul>${listItems}</ul>`);
76
+ return this;
77
+ }
78
+ /**
79
+ * Add an ordered list
80
+ */
81
+ orderedList(items) {
82
+ const listItems = items.map((item) => `<li>${this.escapeHtml(item)}</li>`).join('');
83
+ this.content.push(`<ol>${listItems}</ol>`);
84
+ return this;
85
+ }
86
+ /**
87
+ * Add a blockquote
88
+ */
89
+ blockquote(text) {
90
+ this.content.push(`<blockquote><p>${this.escapeHtml(text)}</p></blockquote>`);
91
+ return this;
92
+ }
93
+ /**
94
+ * Add preformatted text
95
+ */
96
+ preformatted(text) {
97
+ this.content.push(`<pre>${this.escapeHtml(text)}</pre>`);
98
+ return this;
99
+ }
100
+ /**
101
+ * Add a table
102
+ */
103
+ table(rows) {
104
+ const tableRows = rows
105
+ .map((row) => {
106
+ const cells = row.cells
107
+ .map((cell) => {
108
+ const tag = cell.isHeader ? 'th' : 'td';
109
+ const attrs = [];
110
+ if (cell.rowspan && cell.rowspan > 1) {
111
+ attrs.push(`rowspan="${cell.rowspan}"`);
112
+ }
113
+ if (cell.colspan && cell.colspan > 1) {
114
+ attrs.push(`colspan="${cell.colspan}"`);
115
+ }
116
+ const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
117
+ return `<${tag}${attrStr}>${this.escapeHtml(cell.content)}</${tag}>`;
118
+ })
119
+ .join('');
120
+ return `<tr>${cells}</tr>`;
121
+ })
122
+ .join('');
123
+ this.content.push(`<table><tbody>${tableRows}</tbody></table>`);
124
+ return this;
125
+ }
126
+ /**
127
+ * Add a code block macro
128
+ * @param code Code content
129
+ * @param language Programming language for syntax highlighting (e.g., 'java', 'python', 'javascript')
130
+ * @param options Additional options
131
+ */
132
+ codeBlock(code, language, options) {
133
+ const params = [];
134
+ if (language) {
135
+ params.push({ name: 'language', value: language });
136
+ }
137
+ if (options?.title) {
138
+ params.push({ name: 'title', value: options.title });
139
+ }
140
+ if (options?.linenumbers) {
141
+ params.push({ name: 'linenumbers', value: 'true' });
142
+ }
143
+ if (options?.collapse) {
144
+ params.push({ name: 'collapse', value: 'true' });
145
+ }
146
+ if (options?.firstline) {
147
+ params.push({ name: 'firstline', value: String(options.firstline) });
148
+ }
149
+ this.content.push(this.createMacro('code', params, code, true));
150
+ return this;
151
+ }
152
+ /**
153
+ * Add an info panel macro
154
+ */
155
+ infoPanel(content, title, showIcon = true) {
156
+ const params = [];
157
+ if (title) {
158
+ params.push({ name: 'title', value: title });
159
+ }
160
+ params.push({ name: 'icon', value: showIcon ? 'true' : 'false' });
161
+ this.content.push(this.createMacro('info', params, content));
162
+ return this;
163
+ }
164
+ /**
165
+ * Add a note panel macro
166
+ */
167
+ notePanel(content, title, showIcon = true) {
168
+ const params = [];
169
+ if (title) {
170
+ params.push({ name: 'title', value: title });
171
+ }
172
+ params.push({ name: 'icon', value: showIcon ? 'true' : 'false' });
173
+ this.content.push(this.createMacro('note', params, content));
174
+ return this;
175
+ }
176
+ /**
177
+ * Add a warning panel macro
178
+ */
179
+ warningPanel(content, title, showIcon = true) {
180
+ const params = [];
181
+ if (title) {
182
+ params.push({ name: 'title', value: title });
183
+ }
184
+ params.push({ name: 'icon', value: showIcon ? 'true' : 'false' });
185
+ this.content.push(this.createMacro('warning', params, content));
186
+ return this;
187
+ }
188
+ /**
189
+ * Add a tip panel macro
190
+ */
191
+ tipPanel(content, title, showIcon = true) {
192
+ const params = [];
193
+ if (title) {
194
+ params.push({ name: 'title', value: title });
195
+ }
196
+ params.push({ name: 'icon', value: showIcon ? 'true' : 'false' });
197
+ this.content.push(this.createMacro('tip', params, content));
198
+ return this;
199
+ }
200
+ /**
201
+ * Add a custom panel macro
202
+ */
203
+ panel(content, options) {
204
+ const params = [];
205
+ if (options?.title) {
206
+ params.push({ name: 'title', value: options.title });
207
+ }
208
+ if (options?.borderStyle) {
209
+ params.push({ name: 'borderStyle', value: options.borderStyle });
210
+ }
211
+ if (options?.borderColor) {
212
+ params.push({ name: 'borderColor', value: options.borderColor });
213
+ }
214
+ if (options?.borderWidth) {
215
+ params.push({ name: 'borderWidth', value: String(options.borderWidth) });
216
+ }
217
+ if (options?.bgColor) {
218
+ params.push({ name: 'bgColor', value: options.bgColor });
219
+ }
220
+ if (options?.titleBGColor) {
221
+ params.push({ name: 'titleBGColor', value: options.titleBGColor });
222
+ }
223
+ if (options?.titleColor) {
224
+ params.push({ name: 'titleColor', value: options.titleColor });
225
+ }
226
+ this.content.push(this.createMacro('panel', params, content));
227
+ return this;
228
+ }
229
+ /**
230
+ * Add a link to a Confluence page
231
+ */
232
+ pageLink(options) {
233
+ const linkBody = options.linkText
234
+ ? `<ac:plain-text-link-body><![CDATA[${options.linkText}]]></ac:plain-text-link-body>`
235
+ : '';
236
+ const link = `<ac:link><ri:page ri:content-title="${this.escapeAttr(options.pageTitle)}" ri:space-key="${this.escapeAttr(options.spaceKey)}" />${linkBody}</ac:link>`;
237
+ this.content.push(link);
238
+ return this;
239
+ }
240
+ /**
241
+ * Add an external link
242
+ */
243
+ externalLink(url, linkText) {
244
+ const linkBody = linkText ? `<ac:plain-text-link-body><![CDATA[${linkText}]]></ac:plain-text-link-body>` : '';
245
+ const link = `<ac:link><ri:url ri:value="${this.escapeAttr(url)}" />${linkBody}</ac:link>`;
246
+ this.content.push(link);
247
+ return this;
248
+ }
249
+ /**
250
+ * Add a link to an attachment
251
+ */
252
+ attachmentLink(filename, linkText) {
253
+ const linkBody = linkText ? `<ac:plain-text-link-body><![CDATA[${linkText}]]></ac:plain-text-link-body>` : '';
254
+ const link = `<ac:link><ri:attachment ri:filename="${this.escapeAttr(filename)}" />${linkBody}</ac:link>`;
255
+ this.content.push(link);
256
+ return this;
257
+ }
258
+ /**
259
+ * Add an image from attachment
260
+ */
261
+ imageFromAttachment(filename, options) {
262
+ const attrs = [];
263
+ if (options?.width) {
264
+ attrs.push(`ac:width="${options.width}"`);
265
+ }
266
+ if (options?.height) {
267
+ attrs.push(`ac:height="${options.height}"`);
268
+ }
269
+ const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
270
+ const image = `<ac:image${attrStr}><ri:attachment ri:filename="${this.escapeAttr(filename)}" /></ac:image>`;
271
+ this.content.push(image);
272
+ return this;
273
+ }
274
+ /**
275
+ * Add an image from URL
276
+ */
277
+ imageFromUrl(url, options) {
278
+ const attrs = [];
279
+ if (options?.width) {
280
+ attrs.push(`ac:width="${options.width}"`);
281
+ }
282
+ if (options?.height) {
283
+ attrs.push(`ac:height="${options.height}"`);
284
+ }
285
+ const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
286
+ const image = `<ac:image${attrStr}><ri:url ri:value="${this.escapeAttr(url)}" /></ac:image>`;
287
+ this.content.push(image);
288
+ return this;
289
+ }
290
+ /**
291
+ * Add a task list
292
+ */
293
+ taskList(tasks) {
294
+ const taskItems = tasks
295
+ .map((task) => {
296
+ const status = task.completed ? 'complete' : 'incomplete';
297
+ return `<ac:task><ac:task-status>${status}</ac:task-status><ac:task-body>${this.escapeHtml(task.text)}</ac:task-body></ac:task>`;
298
+ })
299
+ .join('');
300
+ this.content.push(`<ac:task-list>${taskItems}</ac:task-list>`);
301
+ return this;
302
+ }
303
+ /**
304
+ * Add a table of contents macro
305
+ */
306
+ tableOfContents(options) {
307
+ const params = [];
308
+ if (options?.maxLevel) {
309
+ params.push({ name: 'maxLevel', value: String(options.maxLevel) });
310
+ }
311
+ if (options?.minLevel) {
312
+ params.push({ name: 'minLevel', value: String(options.minLevel) });
313
+ }
314
+ if (options?.exclude) {
315
+ params.push({ name: 'exclude', value: options.exclude });
316
+ }
317
+ if (options?.type) {
318
+ params.push({ name: 'type', value: options.type });
319
+ }
320
+ if (options?.outline !== undefined) {
321
+ params.push({ name: 'outline', value: String(options.outline) });
322
+ }
323
+ if (options?.style) {
324
+ params.push({ name: 'style', value: options.style });
325
+ }
326
+ this.content.push(this.createMacro('toc', params));
327
+ return this;
328
+ }
329
+ /**
330
+ * Add a layout with sections and cells
331
+ */
332
+ layout(sections) {
333
+ const sectionElements = sections
334
+ .map((section) => {
335
+ const cells = section.cells
336
+ .map((cellContent) => `<ac:layout-cell>${cellContent}</ac:layout-cell>`)
337
+ .join('');
338
+ return `<ac:layout-section ac:type="${this.escapeAttr(section.type)}">${cells}</ac:layout-section>`;
339
+ })
340
+ .join('');
341
+ this.content.push(`<ac:layout>${sectionElements}</ac:layout>`);
342
+ return this;
343
+ }
344
+ /**
345
+ * Add raw storage format content
346
+ */
347
+ raw(storageFormat) {
348
+ this.content.push(storageFormat);
349
+ return this;
350
+ }
351
+ /**
352
+ * Build the final storage format string
353
+ */
354
+ build() {
355
+ return this.content.join('\n');
356
+ }
357
+ /**
358
+ * Create a structured macro
359
+ */
360
+ createMacro(name, parameters, body, isPlainTextBody = false) {
361
+ const params = parameters
362
+ .map((p) => `<ac:parameter ac:name="${this.escapeAttr(p.name)}">${this.escapeHtml(p.value)}</ac:parameter>`)
363
+ .join('');
364
+ let bodyContent = '';
365
+ if (body) {
366
+ if (isPlainTextBody) {
367
+ bodyContent = `<ac:plain-text-body><![CDATA[${body}]]></ac:plain-text-body>`;
368
+ }
369
+ else {
370
+ bodyContent = `<ac:rich-text-body>${this.escapeHtml(body)}</ac:rich-text-body>`;
371
+ }
372
+ }
373
+ return `<ac:structured-macro ac:name="${this.escapeAttr(name)}">${params}${bodyContent}</ac:structured-macro>`;
374
+ }
375
+ /**
376
+ * Escape HTML special characters
377
+ */
378
+ escapeHtml(text) {
379
+ return text
380
+ .replace(/&/g, '&amp;')
381
+ .replace(/</g, '&lt;')
382
+ .replace(/>/g, '&gt;')
383
+ .replace(/"/g, '&quot;')
384
+ .replace(/'/g, '&#039;');
385
+ }
386
+ /**
387
+ * Escape XML attribute values
388
+ */
389
+ escapeAttr(text) {
390
+ return text
391
+ .replace(/&/g, '&amp;')
392
+ .replace(/</g, '&lt;')
393
+ .replace(/>/g, '&gt;')
394
+ .replace(/"/g, '&quot;')
395
+ .replace(/'/g, '&apos;');
396
+ }
397
+ }
398
+ /**
399
+ * Static helper functions for common Confluence storage format patterns
400
+ */
401
+ export class ConfluenceStorageFormat {
402
+ /**
403
+ * Create a new builder instance
404
+ */
405
+ static builder() {
406
+ return new ConfluenceStorageFormatBuilder();
407
+ }
408
+ /**
409
+ * Create a heading
410
+ */
411
+ static heading(level, text) {
412
+ return new ConfluenceStorageFormatBuilder().heading(level, text).build();
413
+ }
414
+ /**
415
+ * Create a paragraph
416
+ */
417
+ static paragraph(text) {
418
+ return new ConfluenceStorageFormatBuilder().paragraph(text).build();
419
+ }
420
+ /**
421
+ * Create a code block
422
+ */
423
+ static codeBlock(code, language, options) {
424
+ return new ConfluenceStorageFormatBuilder().codeBlock(code, language, options).build();
425
+ }
426
+ /**
427
+ * Create an info panel
428
+ */
429
+ static infoPanel(content, title) {
430
+ return new ConfluenceStorageFormatBuilder().infoPanel(content, title).build();
431
+ }
432
+ /**
433
+ * Create a note panel
434
+ */
435
+ static notePanel(content, title) {
436
+ return new ConfluenceStorageFormatBuilder().notePanel(content, title).build();
437
+ }
438
+ /**
439
+ * Create a warning panel
440
+ */
441
+ static warningPanel(content, title) {
442
+ return new ConfluenceStorageFormatBuilder().warningPanel(content, title).build();
443
+ }
444
+ /**
445
+ * Create a tip panel
446
+ */
447
+ static tipPanel(content, title) {
448
+ return new ConfluenceStorageFormatBuilder().tipPanel(content, title).build();
449
+ }
450
+ /**
451
+ * Create a table
452
+ */
453
+ static table(rows) {
454
+ return new ConfluenceStorageFormatBuilder().table(rows).build();
455
+ }
456
+ /**
457
+ * Create an unordered list
458
+ */
459
+ static unorderedList(items) {
460
+ return new ConfluenceStorageFormatBuilder().unorderedList(items).build();
461
+ }
462
+ /**
463
+ * Create an ordered list
464
+ */
465
+ static orderedList(items) {
466
+ return new ConfluenceStorageFormatBuilder().orderedList(items).build();
467
+ }
468
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecubelabs/atlassian-mcp",
3
- "version": "1.4.0-next.1",
3
+ "version": "1.4.0-next.6",
4
4
  "bin": "./dist/index.js",
5
5
  "repository": {
6
6
  "url": "https://github.com/Ecube-Labs/skynet.git"
@@ -37,6 +37,5 @@
37
37
  "semantic-release-yarn": "^3.0.2",
38
38
  "ts-node": "^10.9.2",
39
39
  "typescript": "^5.8.2"
40
- },
41
- "stableVersion": "1.0.0"
40
+ }
42
41
  }