@jagadeesh52423/atlassian-mcp-server 1.1.1 → 1.2.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.
Files changed (151) hide show
  1. package/.env.example +6 -1
  2. package/README.md +26 -14
  3. package/dist/config.d.ts +2 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +7 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/constants.d.ts +0 -4
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/constants.js +0 -5
  10. package/dist/constants.js.map +1 -1
  11. package/dist/formatters/adf-formatter.d.ts +35 -4
  12. package/dist/formatters/adf-formatter.d.ts.map +1 -1
  13. package/dist/formatters/adf-formatter.js +691 -156
  14. package/dist/formatters/adf-formatter.js.map +1 -1
  15. package/dist/formatters/adf-to-text.d.ts +9 -0
  16. package/dist/formatters/adf-to-text.d.ts.map +1 -0
  17. package/dist/formatters/adf-to-text.js +25 -0
  18. package/dist/formatters/adf-to-text.js.map +1 -0
  19. package/dist/formatters/index.d.ts +7 -1
  20. package/dist/formatters/index.d.ts.map +1 -1
  21. package/dist/formatters/index.js +7 -1
  22. package/dist/formatters/index.js.map +1 -1
  23. package/dist/formatters/markdown-to-adf.d.ts +6 -0
  24. package/dist/formatters/markdown-to-adf.d.ts.map +1 -0
  25. package/dist/formatters/markdown-to-adf.js +27 -0
  26. package/dist/formatters/markdown-to-adf.js.map +1 -0
  27. package/dist/handlers/base-handler.d.ts +5 -0
  28. package/dist/handlers/base-handler.d.ts.map +1 -0
  29. package/dist/handlers/base-handler.js +7 -0
  30. package/dist/handlers/base-handler.js.map +1 -0
  31. package/dist/handlers/bitbucket-handlers.d.ts +9 -1
  32. package/dist/handlers/bitbucket-handlers.d.ts.map +1 -1
  33. package/dist/handlers/bitbucket-handlers.js +51 -191
  34. package/dist/handlers/bitbucket-handlers.js.map +1 -1
  35. package/dist/handlers/confluence-handlers.d.ts +2 -1
  36. package/dist/handlers/confluence-handlers.d.ts.map +1 -1
  37. package/dist/handlers/confluence-handlers.js +46 -243
  38. package/dist/handlers/confluence-handlers.js.map +1 -1
  39. package/dist/handlers/jira-handlers.d.ts +9 -1
  40. package/dist/handlers/jira-handlers.d.ts.map +1 -1
  41. package/dist/handlers/jira-handlers.js +144 -575
  42. package/dist/handlers/jira-handlers.js.map +1 -1
  43. package/dist/index.js +123 -194
  44. package/dist/index.js.map +1 -1
  45. package/dist/registry/tool-registry.d.ts +11 -0
  46. package/dist/registry/tool-registry.d.ts.map +1 -0
  47. package/dist/registry/tool-registry.js +24 -0
  48. package/dist/registry/tool-registry.js.map +1 -0
  49. package/dist/services/bitbucket.d.ts +7 -0
  50. package/dist/services/bitbucket.d.ts.map +1 -1
  51. package/dist/services/bitbucket.js +36 -74
  52. package/dist/services/bitbucket.js.map +1 -1
  53. package/dist/services/confluence.d.ts +52 -10
  54. package/dist/services/confluence.d.ts.map +1 -1
  55. package/dist/services/confluence.js +247 -311
  56. package/dist/services/confluence.js.map +1 -1
  57. package/dist/services/jira/agile.d.ts +33 -0
  58. package/dist/services/jira/agile.d.ts.map +1 -0
  59. package/dist/services/jira/agile.js +118 -0
  60. package/dist/services/jira/agile.js.map +1 -0
  61. package/dist/services/jira/attachments.d.ts +7 -0
  62. package/dist/services/jira/attachments.d.ts.map +1 -0
  63. package/dist/services/jira/attachments.js +26 -0
  64. package/dist/services/jira/attachments.js.map +1 -0
  65. package/dist/services/jira/base.d.ts +8 -0
  66. package/dist/services/jira/base.d.ts.map +1 -0
  67. package/dist/services/jira/base.js +10 -0
  68. package/dist/services/jira/base.js.map +1 -0
  69. package/dist/services/jira/comments.d.ts +8 -0
  70. package/dist/services/jira/comments.d.ts.map +1 -0
  71. package/dist/services/jira/comments.js +33 -0
  72. package/dist/services/jira/comments.js.map +1 -0
  73. package/dist/services/jira/issues.d.ts +32 -0
  74. package/dist/services/jira/issues.d.ts.map +1 -0
  75. package/dist/services/jira/issues.js +177 -0
  76. package/dist/services/jira/issues.js.map +1 -0
  77. package/dist/services/jira/labels.d.ts +7 -0
  78. package/dist/services/jira/labels.d.ts.map +1 -0
  79. package/dist/services/jira/labels.js +21 -0
  80. package/dist/services/jira/labels.js.map +1 -0
  81. package/dist/services/jira/links.d.ts +9 -0
  82. package/dist/services/jira/links.d.ts.map +1 -0
  83. package/dist/services/jira/links.js +55 -0
  84. package/dist/services/jira/links.js.map +1 -0
  85. package/dist/services/jira/metadata.d.ts +8 -0
  86. package/dist/services/jira/metadata.d.ts.map +1 -0
  87. package/dist/services/jira/metadata.js +32 -0
  88. package/dist/services/jira/metadata.js.map +1 -0
  89. package/dist/services/jira/subtasks.d.ts +9 -0
  90. package/dist/services/jira/subtasks.d.ts.map +1 -0
  91. package/dist/services/jira/subtasks.js +32 -0
  92. package/dist/services/jira/subtasks.js.map +1 -0
  93. package/dist/services/jira/transitions.d.ts +16 -0
  94. package/dist/services/jira/transitions.d.ts.map +1 -0
  95. package/dist/services/jira/transitions.js +203 -0
  96. package/dist/services/jira/transitions.js.map +1 -0
  97. package/dist/services/jira/versions.d.ts +19 -0
  98. package/dist/services/jira/versions.d.ts.map +1 -0
  99. package/dist/services/jira/versions.js +62 -0
  100. package/dist/services/jira/versions.js.map +1 -0
  101. package/dist/services/jira/watchers.d.ts +7 -0
  102. package/dist/services/jira/watchers.d.ts.map +1 -0
  103. package/dist/services/jira/watchers.js +23 -0
  104. package/dist/services/jira/watchers.js.map +1 -0
  105. package/dist/services/jira/worklogs.d.ts +8 -0
  106. package/dist/services/jira/worklogs.d.ts.map +1 -0
  107. package/dist/services/jira/worklogs.js +37 -0
  108. package/dist/services/jira/worklogs.js.map +1 -0
  109. package/dist/services/jira.d.ts +58 -103
  110. package/dist/services/jira.d.ts.map +1 -1
  111. package/dist/services/jira.js +100 -1481
  112. package/dist/services/jira.js.map +1 -1
  113. package/dist/snapshot/decorators/bitbucket-snapshots.d.ts +4 -0
  114. package/dist/snapshot/decorators/bitbucket-snapshots.d.ts.map +1 -0
  115. package/dist/snapshot/decorators/bitbucket-snapshots.js +65 -0
  116. package/dist/snapshot/decorators/bitbucket-snapshots.js.map +1 -0
  117. package/dist/snapshot/decorators/confluence-snapshots.d.ts +4 -0
  118. package/dist/snapshot/decorators/confluence-snapshots.d.ts.map +1 -0
  119. package/dist/snapshot/decorators/confluence-snapshots.js +71 -0
  120. package/dist/snapshot/decorators/confluence-snapshots.js.map +1 -0
  121. package/dist/snapshot/decorators/jira-snapshots.d.ts +4 -0
  122. package/dist/snapshot/decorators/jira-snapshots.d.ts.map +1 -0
  123. package/dist/snapshot/decorators/jira-snapshots.js +171 -0
  124. package/dist/snapshot/decorators/jira-snapshots.js.map +1 -0
  125. package/dist/snapshot/index.d.ts +7 -0
  126. package/dist/snapshot/index.d.ts.map +1 -0
  127. package/dist/snapshot/index.js +7 -0
  128. package/dist/snapshot/index.js.map +1 -0
  129. package/dist/snapshot/snapshot-decorator.d.ts +4 -0
  130. package/dist/snapshot/snapshot-decorator.d.ts.map +1 -0
  131. package/dist/snapshot/snapshot-decorator.js +74 -0
  132. package/dist/snapshot/snapshot-decorator.js.map +1 -0
  133. package/dist/snapshot/snapshot-manager.d.ts +9 -0
  134. package/dist/snapshot/snapshot-manager.d.ts.map +1 -0
  135. package/dist/snapshot/snapshot-manager.js +56 -0
  136. package/dist/snapshot/snapshot-manager.js.map +1 -0
  137. package/dist/snapshot/snapshot-types.d.ts +26 -0
  138. package/dist/snapshot/snapshot-types.d.ts.map +1 -0
  139. package/dist/snapshot/snapshot-types.js +2 -0
  140. package/dist/snapshot/snapshot-types.js.map +1 -0
  141. package/dist/tools/bitbucket-tools.d.ts.map +1 -1
  142. package/dist/tools/bitbucket-tools.js +30 -0
  143. package/dist/tools/bitbucket-tools.js.map +1 -1
  144. package/dist/tools/confluence-tools.js +12 -12
  145. package/dist/tools/confluence-tools.js.map +1 -1
  146. package/dist/tools/jira-tools.d.ts.map +1 -1
  147. package/dist/tools/jira-tools.js +35 -3
  148. package/dist/tools/jira-tools.js.map +1 -1
  149. package/dist/types.d.ts +17 -2
  150. package/dist/types.d.ts.map +1 -1
  151. package/package.json +6 -2
@@ -1,403 +1,339 @@
1
1
  import { ConfluenceClient } from '../clients/confluence-client.js';
2
+ import { markdownToADF } from '../formatters/index.js';
2
3
  import * as fs from 'fs';
3
4
  import * as path from 'path';
5
+ /**
6
+ * Confluence service.
7
+ *
8
+ * Endpoint policy (Phase 3):
9
+ * - Pages, spaces, page children, versions, footer-comments, attachment
10
+ * reads/deletes use the v2 API (`/wiki/api/v2/...`) with bodies in ADF
11
+ * (`representation: "atlas_doc_format"`, value = JSON-stringified ADF).
12
+ * - CQL search stays on v1 (`/wiki/rest/api/search`) — v2 has no equivalent.
13
+ * - Attachment upload stays on v1 (`/wiki/rest/api/content/{id}/child/attachment`)
14
+ * — v2 has no upload endpoint; only read/delete.
15
+ * - Label *writes* stay on v1 (`/wiki/rest/api/content/{id}/label[/...]`) — v2
16
+ * exposes only label *reads* on pages. Reads use v2.
17
+ *
18
+ * Body format:
19
+ * - Submit: body.atlas_doc_format.value = JSON.stringify(adf), representation
20
+ * = "atlas_doc_format".
21
+ * - Read: append `?body-format=atlas_doc_format` to GETs that need a body and
22
+ * parse `body.atlas_doc_format.value` (a JSON string) back into an object.
23
+ */
4
24
  export class ConfluenceService {
25
+ static V2 = '/wiki/api/v2';
26
+ static V1 = '/wiki/rest/api';
27
+ static REPRESENTATION = 'atlas_doc_format';
28
+ static BODY_FORMAT_QS = 'body-format=atlas_doc_format';
5
29
  client;
30
+ // Cache of spaceKey → spaceId for the duration of a single service instance.
31
+ // v2 page write endpoints take spaceId, but the public MCP tools still
32
+ // accept spaceKey for backwards compatibility.
33
+ spaceIdByKey = new Map();
6
34
  constructor() {
7
35
  this.client = new ConfluenceClient();
8
36
  }
37
+ // ---------- Internal helpers ----------
9
38
  /**
10
39
  * Get file buffer from either base64 content or file path
11
40
  */
12
41
  async getFileBuffer(fileContent, filePath) {
13
42
  if (filePath) {
14
- // Read from file path
15
43
  if (!fs.existsSync(filePath)) {
16
44
  throw new Error(`File not found: ${filePath}`);
17
45
  }
18
46
  return fs.readFileSync(filePath);
19
47
  }
20
48
  else if (fileContent) {
21
- // Decode from base64
22
49
  return Buffer.from(fileContent, 'base64');
23
50
  }
24
51
  else {
25
52
  throw new Error('Either fileContent (base64) or filePath must be provided');
26
53
  }
27
54
  }
28
- /**
29
- * Extract filename from file path
30
- */
31
55
  getFilenameFromPath(filePath) {
32
56
  return path.basename(filePath);
33
57
  }
58
+ /**
59
+ * Resolve a Confluence space key (e.g. "ENG") to its v2 numeric space id.
60
+ * v2 endpoints accept space `id`, not `key`. Cached per service instance.
61
+ */
62
+ async resolveSpaceId(spaceKey) {
63
+ const cached = this.spaceIdByKey.get(spaceKey);
64
+ if (cached)
65
+ return cached;
66
+ const response = await this.client.get(`${ConfluenceService.V2}/spaces`, { keys: spaceKey });
67
+ const space = response?.results?.[0];
68
+ if (!space?.id) {
69
+ throw new Error(`Confluence space not found for key "${spaceKey}"`);
70
+ }
71
+ const id = String(space.id);
72
+ this.spaceIdByKey.set(spaceKey, id);
73
+ return id;
74
+ }
75
+ /**
76
+ * Parse the ADF JSON string from a v2 page/comment body envelope.
77
+ * Returns the parsed ADF object, or null if no body was returned.
78
+ */
79
+ parseAdfBody(body) {
80
+ const value = body?.atlas_doc_format?.value;
81
+ if (!value || typeof value !== 'string')
82
+ return null;
83
+ try {
84
+ return JSON.parse(value);
85
+ }
86
+ catch {
87
+ // Should not happen — Confluence always returns valid JSON here — but
88
+ // surfacing the raw string is more useful than throwing.
89
+ return value;
90
+ }
91
+ }
92
+ /**
93
+ * Build a write body envelope with ADF representation.
94
+ */
95
+ adfBodyEnvelope(markdown) {
96
+ return {
97
+ representation: ConfluenceService.REPRESENTATION,
98
+ value: JSON.stringify(markdownToADF(markdown)),
99
+ };
100
+ }
101
+ /**
102
+ * Map a v2 page response to our public ConfluencePage shape.
103
+ * v2 returns `spaceId` (not `space.key`) and timestamps under different
104
+ * paths than v1 — the caller may pass a known `spaceKey` to fill that
105
+ * field; otherwise it's left empty.
106
+ */
107
+ mapV2Page(page, spaceKey = '') {
108
+ return {
109
+ id: String(page?.id ?? ''),
110
+ title: page?.title ?? '',
111
+ content: this.parseAdfBody(page?.body),
112
+ spaceKey,
113
+ version: page?.version?.number ?? 1,
114
+ created: page?.createdAt ?? page?.version?.createdAt ?? '',
115
+ updated: page?.version?.createdAt ?? '',
116
+ };
117
+ }
118
+ // ---------- Pages ----------
34
119
  async searchPages(options = {}) {
120
+ // CQL search stays on v1 — v2 has no equivalent. Body returned by v1 is
121
+ // storage-format HTML (a string); ConfluencePage.content is `any` so this
122
+ // is allowed but consumers should treat search results as opaque.
35
123
  const { query, limit = 25, startAt = 0, expand = ['body.storage', 'version', 'space'] } = options;
36
- let cql = '';
37
- if (query) {
38
- cql = `text ~ "${query}"`;
39
- }
124
+ const cql = query ? `text ~ "${query}"` : '';
40
125
  const params = {
41
126
  cql,
42
127
  limit,
43
128
  start: startAt,
44
- expand: expand.join(',')
129
+ expand: expand.join(','),
45
130
  };
46
- const response = await this.client.get('/wiki/rest/api/content/search', params);
47
- return response.results.map((page) => ({
131
+ const response = await this.client.get(`${ConfluenceService.V1}/search`, params);
132
+ return (response.results ?? []).map((page) => ({
48
133
  id: page.id,
49
134
  title: page.title,
50
- content: page.body?.storage?.value || '',
51
- spaceKey: page.space?.key || '',
52
- version: page.version?.number || 1,
53
- created: page.created || '',
54
- updated: page.updated || ''
135
+ content: page.body?.storage?.value ?? '',
136
+ spaceKey: page.space?.key ?? '',
137
+ version: page.version?.number ?? 1,
138
+ created: page.created ?? '',
139
+ updated: page.updated ?? '',
55
140
  }));
56
141
  }
57
- async getPage(pageId, expand = ['body.storage', 'version', 'space']) {
58
- const params = {
59
- expand: expand.join(',')
60
- };
61
- const page = await this.client.get(`/wiki/rest/api/content/${pageId}`, params);
62
- return {
63
- id: page.id,
64
- title: page.title,
65
- content: page.body?.storage?.value || '',
66
- spaceKey: page.space?.key || '',
67
- version: page.version?.number || 1,
68
- created: page.created || '',
69
- updated: page.updated || ''
70
- };
142
+ async getPage(pageId) {
143
+ const page = await this.client.get(`${ConfluenceService.V2}/pages/${pageId}?${ConfluenceService.BODY_FORMAT_QS}`);
144
+ return this.mapV2Page(page);
71
145
  }
72
146
  async createPage(spaceKey, title, content, parentId) {
147
+ const spaceId = await this.resolveSpaceId(spaceKey);
73
148
  const data = {
74
- type: 'page',
149
+ spaceId,
150
+ status: 'current',
75
151
  title,
76
- space: { key: spaceKey },
77
- body: {
78
- storage: {
79
- value: content,
80
- representation: 'storage'
81
- }
82
- }
83
- };
84
- if (parentId) {
85
- data.ancestors = [{ id: parentId }];
86
- }
87
- const page = await this.client.post('/wiki/rest/api/content?expand=body.storage,version,space', data);
88
- return {
89
- id: page.id,
90
- title: page.title,
91
- content: page.body?.storage?.value || '',
92
- spaceKey: page.space?.key || '',
93
- version: page.version?.number || 1,
94
- created: page.created || '',
95
- updated: page.updated || ''
152
+ body: this.adfBodyEnvelope(content),
96
153
  };
154
+ if (parentId)
155
+ data.parentId = parentId;
156
+ const page = await this.client.post(`${ConfluenceService.V2}/pages?${ConfluenceService.BODY_FORMAT_QS}`, data);
157
+ return this.mapV2Page(page, spaceKey);
97
158
  }
98
159
  async updatePage(pageId, title, content, version) {
99
160
  const data = {
100
- version: { number: version + 1 },
161
+ id: pageId,
162
+ status: 'current',
101
163
  title,
102
- type: 'page',
103
- body: {
104
- storage: {
105
- value: content,
106
- representation: 'storage'
107
- }
108
- }
109
- };
110
- const page = await this.client.put(`/wiki/rest/api/content/${pageId}`, data);
111
- return {
112
- id: page.id,
113
- title: page.title,
114
- content: page.body?.storage?.value || '',
115
- spaceKey: page.space?.key || '',
116
- version: page.version?.number || 1,
117
- created: page.created || '',
118
- updated: page.updated || ''
164
+ body: this.adfBodyEnvelope(content),
165
+ version: { number: version + 1, message: '' },
119
166
  };
167
+ const page = await this.client.put(`${ConfluenceService.V2}/pages/${pageId}?${ConfluenceService.BODY_FORMAT_QS}`, data);
168
+ return this.mapV2Page(page);
120
169
  }
121
170
  async deletePage(pageId) {
122
- await this.client.delete(`/wiki/rest/api/content/${pageId}`);
171
+ await this.client.delete(`${ConfluenceService.V2}/pages/${pageId}`);
123
172
  }
173
+ // ---------- Spaces ----------
124
174
  async getSpaces(limit = 25) {
125
- const params = {
126
- limit,
127
- expand: 'description.plain'
128
- };
129
- const response = await this.client.get('/wiki/rest/api/space', params);
130
- return response.results;
175
+ const response = await this.client.get(`${ConfluenceService.V2}/spaces`, { limit });
176
+ return response?.results ?? [];
131
177
  }
132
178
  async getPagesBySpace(spaceKey, limit = 25) {
133
- const params = {
134
- spaceKey,
135
- limit,
136
- expand: 'body.storage,version'
137
- };
138
- const response = await this.client.get('/wiki/rest/api/content', params);
139
- return response.results.map((page) => ({
140
- id: page.id,
141
- title: page.title,
142
- content: page.body?.storage?.value || '',
143
- spaceKey: page.space?.key || spaceKey,
144
- version: page.version?.number || 1,
145
- created: page.created || '',
146
- updated: page.updated || ''
179
+ const spaceId = await this.resolveSpaceId(spaceKey);
180
+ const response = await this.client.get(`${ConfluenceService.V2}/spaces/${spaceId}/pages`, { limit, 'body-format': ConfluenceService.REPRESENTATION });
181
+ return (response?.results ?? []).map((page) => this.mapV2Page(page, spaceKey));
182
+ }
183
+ // ---------- Page children ----------
184
+ async getPageChildren(pageId, limit = 25) {
185
+ // v2 children endpoint does not return body or version — only id, title,
186
+ // status, spaceId, parentId, position, lastOwnerId, childPosition.
187
+ const response = await this.client.get(`${ConfluenceService.V2}/pages/${pageId}/children`, { limit });
188
+ return (response?.results ?? []).map((child) => ({
189
+ id: String(child?.id ?? ''),
190
+ title: child?.title ?? '',
191
+ content: null,
192
+ spaceKey: '',
193
+ version: 1,
194
+ created: '',
195
+ updated: '',
147
196
  }));
148
197
  }
198
+ // ---------- Page version history ----------
199
+ async getPageHistory(pageId, limit = 25) {
200
+ // v2 versions response: { results: [{ createdAt, message, number,
201
+ // minorEdit, authorId }], _links }. v2 does NOT include the author's
202
+ // displayName — only authorId — so `by` falls back to the id.
203
+ const response = await this.client.get(`${ConfluenceService.V2}/pages/${pageId}/versions`, { limit });
204
+ return (response?.results ?? []).map((version) => ({
205
+ number: version.number,
206
+ by: version.authorId ?? 'Unknown',
207
+ byAccountId: version.authorId,
208
+ when: version.createdAt ?? '',
209
+ message: version.message ?? '',
210
+ minorEdit: version.minorEdit ?? false,
211
+ }));
212
+ }
213
+ // ---------- Labels ----------
214
+ // v2 supports reading labels on pages but does NOT expose write endpoints.
215
+ // Adding/removing labels therefore stays on v1.
216
+ async getLabels(pageId) {
217
+ const response = await this.client.get(`${ConfluenceService.V2}/pages/${pageId}/labels`);
218
+ return (response?.results ?? []).map((label) => ({
219
+ prefix: label.prefix,
220
+ name: label.name,
221
+ id: label.id,
222
+ }));
223
+ }
224
+ async addLabels(pageId, labels) {
225
+ // v1 — v2 has no equivalent write endpoint for page labels.
226
+ const data = labels.map(name => ({ prefix: 'global', name }));
227
+ const response = await this.client.post(`${ConfluenceService.V1}/content/${pageId}/label`, data);
228
+ return (response?.results ?? []).map((label) => ({
229
+ prefix: label.prefix,
230
+ name: label.name,
231
+ id: label.id,
232
+ }));
233
+ }
234
+ async removeLabel(pageId, label) {
235
+ // v1 — v2 has no equivalent.
236
+ await this.client.delete(`${ConfluenceService.V1}/content/${pageId}/label/${encodeURIComponent(label)}`);
237
+ }
238
+ // ---------- Attachments ----------
149
239
  async getAttachments(pageId) {
150
- const response = await this.client.get(`/wiki/rest/api/content/${pageId}/child/attachment`, {
151
- expand: 'version'
152
- });
153
- return response.results?.map((att) => ({
154
- id: att.id,
155
- title: att.title,
156
- filename: att.title,
157
- mediaType: att.metadata?.mediaType || '',
158
- fileSize: att.extensions?.fileSize || 0,
159
- created: att.history?.created?.when || '',
160
- downloadUrl: att._links?.download || ''
161
- })) || [];
240
+ const response = await this.client.get(`${ConfluenceService.V2}/pages/${pageId}/attachments`);
241
+ return (response?.results ?? []).map((att) => ({
242
+ id: String(att?.id ?? ''),
243
+ title: att?.title ?? '',
244
+ filename: att?.title ?? '',
245
+ mediaType: att?.mediaType ?? '',
246
+ fileSize: att?.fileSize ?? 0,
247
+ created: att?.createdAt ?? '',
248
+ downloadUrl: att?.downloadLink ?? '',
249
+ }));
162
250
  }
163
251
  async addAttachment(pageId, filename, fileContent, filePath) {
252
+ // v2 has no upload endpoint — multipart upload stays on v1.
164
253
  const FormData = (await import('form-data')).default;
165
254
  const form = new FormData();
166
255
  const buffer = await this.getFileBuffer(fileContent, filePath);
167
256
  const finalFilename = filename || (filePath ? this.getFilenameFromPath(filePath) : 'attachment');
168
257
  form.append('file', buffer, { filename: finalFilename });
169
- const response = await this.client.postFormData(`/wiki/rest/api/content/${pageId}/child/attachment`, form);
258
+ const response = await this.client.postFormData(`${ConfluenceService.V1}/content/${pageId}/child/attachment`, form);
170
259
  return response;
171
260
  }
172
261
  async deleteAttachment(attachmentId) {
173
- await this.client.delete(`/wiki/rest/api/content/${attachmentId}`);
262
+ await this.client.delete(`${ConfluenceService.V2}/attachments/${attachmentId}`);
174
263
  }
175
- async embedImage(pageId, filename, fileContent, filePath, options = {}) {
176
- // Determine filename
177
- const finalFilename = filename || (filePath ? this.getFilenameFromPath(filePath) : 'image');
178
- // Step 1: Attach the image
179
- const attachment = await this.addAttachment(pageId, finalFilename, fileContent, filePath);
180
- // Step 2: Get current page content and version
181
- const currentPage = await this.getPage(pageId);
182
- // Step 3: Build image markup
183
- const alt = options.alt || finalFilename;
184
- const alignAttr = options.align ? ` ac:align="${options.align}" ac:layout="${options.align}"` : '';
185
- const widthAttr = options.width ? ` ac:custom-width="true" ac:width="${options.width}"` : '';
186
- let imageMarkup = `<ac:image${alignAttr}${widthAttr} ac:alt="${alt}"><ri:attachment ri:filename="${finalFilename}" />`;
187
- if (options.caption) {
188
- imageMarkup += `<ac:caption><p>${options.caption}</p></ac:caption>`;
189
- }
190
- imageMarkup += '</ac:image>';
191
- // Step 4: Insert image at specified position
192
- let updatedContent = currentPage.content;
193
- const position = options.position || 'bottom';
194
- if (position === 'top') {
195
- updatedContent = imageMarkup + '\n' + updatedContent;
196
- }
197
- else if (position === 'after-heading' && options.headingText) {
198
- // Find the heading and insert after it
199
- const headingRegex = new RegExp(`(<h[1-6][^>]*>.*?${options.headingText}.*?</h[1-6]>)`, 'i');
200
- const match = updatedContent.match(headingRegex);
201
- if (match) {
202
- updatedContent = updatedContent.replace(headingRegex, `$1\n${imageMarkup}`);
203
- }
204
- else {
205
- // Fallback to bottom if heading not found
206
- updatedContent = updatedContent + '\n' + imageMarkup;
207
- }
208
- }
209
- else {
210
- // Default: bottom
211
- updatedContent = updatedContent + '\n' + imageMarkup;
212
- }
213
- // Step 5: Update the page
214
- const updatedPage = await this.updatePage(pageId, currentPage.title, updatedContent, currentPage.version);
215
- return { page: updatedPage, attachment };
264
+ // ---------- Image embedding (NOT supported in ADF mode) ----------
265
+ //
266
+ // The pre-Phase-3 implementation built Confluence storage-format XHTML
267
+ // (`<ac:image><ri:attachment .../></ac:image>`) and string-spliced it into
268
+ // page bodies. With ADF as the only supported body format, that path would
269
+ // corrupt page bodies (storage HTML is not valid ADF). Inserting a
270
+ // first-class ADF `media`/`mediaSingle` node requires a Media API token
271
+ // (collection + occurrenceKey + media id), which is out of scope for the
272
+ // formatter swap.
273
+ //
274
+ // Behaviour: surface an explicit, actionable error rather than silently
275
+ // emitting broken markup. The `addAttachment` path still works — callers
276
+ // can upload the file and reference it in subsequent UI edits.
277
+ static EMBED_NOT_SUPPORTED_MSG = 'Image embedding is not supported in ADF mode. Use confluence_add_attachment to upload the file; ' +
278
+ 'embedding inline requires the Atlassian Media API and is not implemented in this version.';
279
+ async embedImage(_pageId, _filename, _fileContent, _filePath, _options = {}) {
280
+ throw new Error(ConfluenceService.EMBED_NOT_SUPPORTED_MSG);
216
281
  }
217
282
  async createPageWithImages(spaceKey, title, content, parentId, images) {
218
- // First create the page
219
- const page = await this.createPage(spaceKey, title, content, parentId);
220
283
  if (images && images.length > 0) {
221
- // Upload each image and collect their attachment info
222
- const uploadedImages = [];
223
- for (const image of images) {
224
- try {
225
- const resolvedFilename = image.filename || (image.filePath ? this.getFilenameFromPath(image.filePath) : 'image');
226
- const attachment = await this.addAttachment(page.id, resolvedFilename, image.fileContent, image.filePath);
227
- uploadedImages.push({
228
- ...image,
229
- filename: resolvedFilename,
230
- resolvedFilename,
231
- attachmentId: attachment.results?.[0]?.id
232
- });
233
- }
234
- catch (error) {
235
- console.warn(`Failed to upload image ${image.filename || image.filePath}:`, error);
236
- }
237
- }
238
- // If we have uploaded images, update the page content to include image references
239
- if (uploadedImages.length > 0) {
240
- let updatedContent = content;
241
- const imagesWithoutPlaceholder = [];
242
- for (const img of uploadedImages) {
243
- const imageMarkup = this.buildImageMarkup(img);
244
- const fname = img.resolvedFilename || img.filename || 'image';
245
- // Check for placeholder {{IMAGE:filename}}
246
- const placeholder = new RegExp(`\\{\\{IMAGE:${fname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\}\\}`, 'gi');
247
- if (placeholder.test(updatedContent)) {
248
- // Replace placeholder with image markup
249
- updatedContent = updatedContent.replace(placeholder, imageMarkup);
250
- }
251
- else {
252
- // No placeholder found, add to list for appending at end
253
- imagesWithoutPlaceholder.push(img);
254
- }
255
- }
256
- // Append images without placeholders at the end
257
- if (imagesWithoutPlaceholder.length > 0) {
258
- const appendedMarkup = imagesWithoutPlaceholder
259
- .map(img => this.buildImageMarkup(img))
260
- .join('\n');
261
- updatedContent += `\n\n${appendedMarkup}`;
262
- }
263
- // Update the page with the new content and return the updated version
264
- const updatedPage = await this.updatePage(page.id, title, updatedContent, page.version);
265
- return updatedPage;
266
- }
267
- }
268
- return page;
269
- }
270
- buildImageMarkup(image) {
271
- const filename = image.resolvedFilename || image.filename || 'image';
272
- const alt = image.alt || filename;
273
- const alignAttr = image.align ? ` ac:align="${image.align}" ac:layout="${image.align}"` : '';
274
- const widthAttr = image.width ? ` ac:custom-width="true" ac:width="${image.width}"` : '';
275
- let markup = `<ac:image${alignAttr}${widthAttr} ac:alt="${alt}"><ri:attachment ri:filename="${filename}" />`;
276
- if (image.caption) {
277
- markup += `<ac:caption><p>${image.caption}</p></ac:caption>`;
284
+ throw new Error(ConfluenceService.EMBED_NOT_SUPPORTED_MSG);
278
285
  }
279
- markup += '</ac:image>';
280
- return markup;
281
- }
282
- // Page History Methods
283
- async getPageHistory(pageId, limit = 25) {
284
- const params = {
285
- limit,
286
- expand: 'content'
287
- };
288
- const response = await this.client.get(`/wiki/rest/api/content/${pageId}/version`, params);
289
- return response.results?.map((version) => ({
290
- number: version.number,
291
- by: version.by?.displayName || 'Unknown',
292
- byAccountId: version.by?.accountId,
293
- when: version.when || '',
294
- message: version.message || '',
295
- minorEdit: version.minorEdit || false,
296
- })) || [];
297
- }
298
- // Page Children Methods
299
- async getPageChildren(pageId, limit = 25) {
300
- const params = {
301
- limit,
302
- expand: 'version,space'
303
- };
304
- const response = await this.client.get(`/wiki/rest/api/content/${pageId}/child/page`, params);
305
- return response.results?.map((page) => ({
306
- id: page.id,
307
- title: page.title,
308
- content: '',
309
- spaceKey: page.space?.key || '',
310
- version: page.version?.number || 1,
311
- created: page.history?.createdDate || '',
312
- updated: page.version?.when || '',
313
- })) || [];
314
- }
315
- // Label Methods
316
- async getLabels(pageId) {
317
- const response = await this.client.get(`/wiki/rest/api/content/${pageId}/label`);
318
- return response.results?.map((label) => ({
319
- prefix: label.prefix,
320
- name: label.name,
321
- id: label.id,
322
- })) || [];
323
- }
324
- async addLabels(pageId, labels) {
325
- const data = labels.map(name => ({ prefix: 'global', name }));
326
- const response = await this.client.post(`/wiki/rest/api/content/${pageId}/label`, data);
327
- return response.results?.map((label) => ({
328
- prefix: label.prefix,
329
- name: label.name,
330
- id: label.id,
331
- })) || [];
286
+ return this.createPage(spaceKey, title, content, parentId);
332
287
  }
333
- async removeLabel(pageId, label) {
334
- await this.client.delete(`/wiki/rest/api/content/${pageId}/label/${encodeURIComponent(label)}`);
335
- }
336
- // Comment Methods
288
+ // ---------- Comments (footer comments) ----------
337
289
  async getComments(pageId) {
338
- const params = {
339
- expand: 'body.storage,version,history'
340
- };
341
- const response = await this.client.get(`/wiki/rest/api/content/${pageId}/child/comment`, params);
342
- return response.results?.map((comment) => ({
343
- id: comment.id,
344
- body: comment.body?.storage?.value || '',
345
- author: comment.history?.createdBy?.displayName || 'Unknown',
346
- authorAccountId: comment.history?.createdBy?.accountId,
347
- created: comment.history?.createdDate || '',
348
- updated: comment.version?.when || '',
349
- version: comment.version?.number || 1
350
- })) || [];
290
+ const response = await this.client.get(`${ConfluenceService.V2}/pages/${pageId}/footer-comments`, { 'body-format': ConfluenceService.REPRESENTATION });
291
+ return (response?.results ?? []).map((comment) => ({
292
+ id: String(comment?.id ?? ''),
293
+ body: this.parseAdfBody(comment?.body),
294
+ // v2 does not return author displayName — only authorId — so we fall
295
+ // back to the id (or "Unknown") to preserve the public shape.
296
+ author: comment?.version?.authorId ?? 'Unknown',
297
+ authorAccountId: comment?.version?.authorId,
298
+ created: comment?.version?.createdAt ?? '',
299
+ updated: comment?.version?.createdAt ?? '',
300
+ version: comment?.version?.number ?? 1,
301
+ }));
351
302
  }
352
303
  async addComment(pageId, body) {
353
304
  const data = {
354
- type: 'comment',
355
- container: {
356
- id: pageId,
357
- type: 'page'
358
- },
359
- body: {
360
- storage: {
361
- value: body,
362
- representation: 'storage'
363
- }
364
- }
305
+ pageId,
306
+ body: this.adfBodyEnvelope(body),
365
307
  };
366
- const comment = await this.client.post('/wiki/rest/api/content', data);
308
+ const comment = await this.client.post(`${ConfluenceService.V2}/footer-comments?${ConfluenceService.BODY_FORMAT_QS}`, data);
367
309
  return {
368
- id: comment.id,
369
- body: comment.body?.storage?.value || body,
370
- author: comment.history?.createdBy?.displayName || 'Unknown',
371
- authorAccountId: comment.history?.createdBy?.accountId,
372
- created: comment.history?.createdDate || '',
373
- updated: comment.version?.when || '',
374
- version: comment.version?.number || 1
310
+ id: String(comment?.id ?? ''),
311
+ body: this.parseAdfBody(comment?.body) ?? body,
312
+ author: comment?.version?.authorId ?? 'Unknown',
313
+ authorAccountId: comment?.version?.authorId,
314
+ created: comment?.version?.createdAt ?? '',
315
+ updated: comment?.version?.createdAt ?? '',
316
+ version: comment?.version?.number ?? 1,
375
317
  };
376
318
  }
377
319
  async updateComment(commentId, body, version) {
378
320
  const data = {
379
- type: 'comment',
380
- version: { number: version + 1 },
381
- body: {
382
- storage: {
383
- value: body,
384
- representation: 'storage'
385
- }
386
- }
321
+ version: { number: version + 1, message: '' },
322
+ body: this.adfBodyEnvelope(body),
387
323
  };
388
- const comment = await this.client.put(`/wiki/rest/api/content/${commentId}`, data);
324
+ const comment = await this.client.put(`${ConfluenceService.V2}/footer-comments/${commentId}?${ConfluenceService.BODY_FORMAT_QS}`, data);
389
325
  return {
390
- id: comment.id,
391
- body: comment.body?.storage?.value || body,
392
- author: comment.history?.createdBy?.displayName || 'Unknown',
393
- authorAccountId: comment.history?.createdBy?.accountId,
394
- created: comment.history?.createdDate || '',
395
- updated: comment.version?.when || '',
396
- version: comment.version?.number || 1
326
+ id: String(comment?.id ?? ''),
327
+ body: this.parseAdfBody(comment?.body) ?? body,
328
+ author: comment?.version?.authorId ?? 'Unknown',
329
+ authorAccountId: comment?.version?.authorId,
330
+ created: comment?.version?.createdAt ?? '',
331
+ updated: comment?.version?.createdAt ?? '',
332
+ version: comment?.version?.number ?? 1,
397
333
  };
398
334
  }
399
335
  async deleteComment(commentId) {
400
- await this.client.delete(`/wiki/rest/api/content/${commentId}`);
336
+ await this.client.delete(`${ConfluenceService.V2}/footer-comments/${commentId}`);
401
337
  }
402
338
  }
403
339
  //# sourceMappingURL=confluence.js.map