@jagadeesh52423/atlassian-mcp-server 1.1.2 → 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.
- package/.env.example +6 -1
- package/README.md +26 -14
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +0 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +0 -5
- package/dist/constants.js.map +1 -1
- package/dist/formatters/adf-formatter.d.ts +5 -2
- package/dist/formatters/adf-formatter.d.ts.map +1 -1
- package/dist/formatters/adf-formatter.js +271 -52
- package/dist/formatters/adf-formatter.js.map +1 -1
- package/dist/formatters/adf-to-text.d.ts +9 -0
- package/dist/formatters/adf-to-text.d.ts.map +1 -0
- package/dist/formatters/adf-to-text.js +25 -0
- package/dist/formatters/adf-to-text.js.map +1 -0
- package/dist/formatters/index.d.ts +7 -1
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +7 -1
- package/dist/formatters/index.js.map +1 -1
- package/dist/formatters/markdown-to-adf.d.ts +6 -0
- package/dist/formatters/markdown-to-adf.d.ts.map +1 -0
- package/dist/formatters/markdown-to-adf.js +27 -0
- package/dist/formatters/markdown-to-adf.js.map +1 -0
- package/dist/handlers/bitbucket-handlers.d.ts +7 -0
- package/dist/handlers/bitbucket-handlers.d.ts.map +1 -1
- package/dist/handlers/bitbucket-handlers.js +20 -0
- package/dist/handlers/bitbucket-handlers.js.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/services/bitbucket.d.ts +5 -0
- package/dist/services/bitbucket.d.ts.map +1 -1
- package/dist/services/bitbucket.js +14 -0
- package/dist/services/bitbucket.js.map +1 -1
- package/dist/services/confluence.d.ts +52 -10
- package/dist/services/confluence.d.ts.map +1 -1
- package/dist/services/confluence.js +247 -312
- package/dist/services/confluence.js.map +1 -1
- package/dist/services/jira/base.d.ts +0 -1
- package/dist/services/jira/base.d.ts.map +1 -1
- package/dist/services/jira/base.js +2 -3
- package/dist/services/jira/base.js.map +1 -1
- package/dist/snapshot/decorators/bitbucket-snapshots.d.ts +4 -0
- package/dist/snapshot/decorators/bitbucket-snapshots.d.ts.map +1 -0
- package/dist/snapshot/decorators/bitbucket-snapshots.js +65 -0
- package/dist/snapshot/decorators/bitbucket-snapshots.js.map +1 -0
- package/dist/snapshot/decorators/confluence-snapshots.d.ts +4 -0
- package/dist/snapshot/decorators/confluence-snapshots.d.ts.map +1 -0
- package/dist/snapshot/decorators/confluence-snapshots.js +71 -0
- package/dist/snapshot/decorators/confluence-snapshots.js.map +1 -0
- package/dist/snapshot/decorators/jira-snapshots.d.ts +4 -0
- package/dist/snapshot/decorators/jira-snapshots.d.ts.map +1 -0
- package/dist/snapshot/decorators/jira-snapshots.js +171 -0
- package/dist/snapshot/decorators/jira-snapshots.js.map +1 -0
- package/dist/snapshot/index.d.ts +7 -0
- package/dist/snapshot/index.d.ts.map +1 -0
- package/dist/snapshot/index.js +7 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/snapshot/snapshot-decorator.d.ts +4 -0
- package/dist/snapshot/snapshot-decorator.d.ts.map +1 -0
- package/dist/snapshot/snapshot-decorator.js +74 -0
- package/dist/snapshot/snapshot-decorator.js.map +1 -0
- package/dist/snapshot/snapshot-manager.d.ts +9 -0
- package/dist/snapshot/snapshot-manager.d.ts.map +1 -0
- package/dist/snapshot/snapshot-manager.js +56 -0
- package/dist/snapshot/snapshot-manager.js.map +1 -0
- package/dist/snapshot/snapshot-types.d.ts +26 -0
- package/dist/snapshot/snapshot-types.d.ts.map +1 -0
- package/dist/snapshot/snapshot-types.js +2 -0
- package/dist/snapshot/snapshot-types.js.map +1 -0
- package/dist/tools/bitbucket-tools.d.ts.map +1 -1
- package/dist/tools/bitbucket-tools.js +30 -0
- package/dist/tools/bitbucket-tools.js.map +1 -1
- package/dist/tools/confluence-tools.js +12 -12
- package/dist/tools/confluence-tools.js.map +1 -1
- package/dist/types.d.ts +17 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
|
@@ -1,404 +1,339 @@
|
|
|
1
1
|
import { ConfluenceClient } from '../clients/confluence-client.js';
|
|
2
|
-
import {
|
|
2
|
+
import { markdownToADF } from '../formatters/index.js';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
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
|
+
*/
|
|
5
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';
|
|
6
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();
|
|
7
34
|
constructor() {
|
|
8
35
|
this.client = new ConfluenceClient();
|
|
9
36
|
}
|
|
37
|
+
// ---------- Internal helpers ----------
|
|
10
38
|
/**
|
|
11
39
|
* Get file buffer from either base64 content or file path
|
|
12
40
|
*/
|
|
13
41
|
async getFileBuffer(fileContent, filePath) {
|
|
14
42
|
if (filePath) {
|
|
15
|
-
// Read from file path
|
|
16
43
|
if (!fs.existsSync(filePath)) {
|
|
17
44
|
throw new Error(`File not found: ${filePath}`);
|
|
18
45
|
}
|
|
19
46
|
return fs.readFileSync(filePath);
|
|
20
47
|
}
|
|
21
48
|
else if (fileContent) {
|
|
22
|
-
// Decode from base64
|
|
23
49
|
return Buffer.from(fileContent, 'base64');
|
|
24
50
|
}
|
|
25
51
|
else {
|
|
26
52
|
throw new Error('Either fileContent (base64) or filePath must be provided');
|
|
27
53
|
}
|
|
28
54
|
}
|
|
29
|
-
/**
|
|
30
|
-
* Extract filename from file path
|
|
31
|
-
*/
|
|
32
55
|
getFilenameFromPath(filePath) {
|
|
33
56
|
return path.basename(filePath);
|
|
34
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 ----------
|
|
35
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.
|
|
36
123
|
const { query, limit = 25, startAt = 0, expand = ['body.storage', 'version', 'space'] } = options;
|
|
37
|
-
|
|
38
|
-
if (query) {
|
|
39
|
-
cql = `text ~ "${query}"`;
|
|
40
|
-
}
|
|
124
|
+
const cql = query ? `text ~ "${query}"` : '';
|
|
41
125
|
const params = {
|
|
42
126
|
cql,
|
|
43
127
|
limit,
|
|
44
128
|
start: startAt,
|
|
45
|
-
expand: expand.join(',')
|
|
129
|
+
expand: expand.join(','),
|
|
46
130
|
};
|
|
47
|
-
const response = await this.client.get(
|
|
48
|
-
return response.results.map((page) => ({
|
|
131
|
+
const response = await this.client.get(`${ConfluenceService.V1}/search`, params);
|
|
132
|
+
return (response.results ?? []).map((page) => ({
|
|
49
133
|
id: page.id,
|
|
50
134
|
title: page.title,
|
|
51
|
-
content: page.body?.storage?.value
|
|
52
|
-
spaceKey: page.space?.key
|
|
53
|
-
version: page.version?.number
|
|
54
|
-
created: page.created
|
|
55
|
-
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 ?? '',
|
|
56
140
|
}));
|
|
57
141
|
}
|
|
58
|
-
async getPage(pageId
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
const page = await this.client.get(`/wiki/rest/api/content/${pageId}`, params);
|
|
63
|
-
return {
|
|
64
|
-
id: page.id,
|
|
65
|
-
title: page.title,
|
|
66
|
-
content: page.body?.storage?.value || '',
|
|
67
|
-
spaceKey: page.space?.key || '',
|
|
68
|
-
version: page.version?.number || 1,
|
|
69
|
-
created: page.created || '',
|
|
70
|
-
updated: page.updated || ''
|
|
71
|
-
};
|
|
142
|
+
async getPage(pageId) {
|
|
143
|
+
const page = await this.client.get(`${ConfluenceService.V2}/pages/${pageId}?${ConfluenceService.BODY_FORMAT_QS}`);
|
|
144
|
+
return this.mapV2Page(page);
|
|
72
145
|
}
|
|
73
146
|
async createPage(spaceKey, title, content, parentId) {
|
|
147
|
+
const spaceId = await this.resolveSpaceId(spaceKey);
|
|
74
148
|
const data = {
|
|
75
|
-
|
|
149
|
+
spaceId,
|
|
150
|
+
status: 'current',
|
|
76
151
|
title,
|
|
77
|
-
|
|
78
|
-
body: {
|
|
79
|
-
storage: {
|
|
80
|
-
value: markdownToConfluenceStorage(content),
|
|
81
|
-
representation: 'storage'
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
if (parentId) {
|
|
86
|
-
data.ancestors = [{ id: parentId }];
|
|
87
|
-
}
|
|
88
|
-
const page = await this.client.post('/wiki/rest/api/content?expand=body.storage,version,space', data);
|
|
89
|
-
return {
|
|
90
|
-
id: page.id,
|
|
91
|
-
title: page.title,
|
|
92
|
-
content: page.body?.storage?.value || '',
|
|
93
|
-
spaceKey: page.space?.key || '',
|
|
94
|
-
version: page.version?.number || 1,
|
|
95
|
-
created: page.created || '',
|
|
96
|
-
updated: page.updated || ''
|
|
152
|
+
body: this.adfBodyEnvelope(content),
|
|
97
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);
|
|
98
158
|
}
|
|
99
159
|
async updatePage(pageId, title, content, version) {
|
|
100
160
|
const data = {
|
|
101
|
-
|
|
161
|
+
id: pageId,
|
|
162
|
+
status: 'current',
|
|
102
163
|
title,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
storage: {
|
|
106
|
-
value: markdownToConfluenceStorage(content),
|
|
107
|
-
representation: 'storage'
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
const page = await this.client.put(`/wiki/rest/api/content/${pageId}`, data);
|
|
112
|
-
return {
|
|
113
|
-
id: page.id,
|
|
114
|
-
title: page.title,
|
|
115
|
-
content: page.body?.storage?.value || '',
|
|
116
|
-
spaceKey: page.space?.key || '',
|
|
117
|
-
version: page.version?.number || 1,
|
|
118
|
-
created: page.created || '',
|
|
119
|
-
updated: page.updated || ''
|
|
164
|
+
body: this.adfBodyEnvelope(content),
|
|
165
|
+
version: { number: version + 1, message: '' },
|
|
120
166
|
};
|
|
167
|
+
const page = await this.client.put(`${ConfluenceService.V2}/pages/${pageId}?${ConfluenceService.BODY_FORMAT_QS}`, data);
|
|
168
|
+
return this.mapV2Page(page);
|
|
121
169
|
}
|
|
122
170
|
async deletePage(pageId) {
|
|
123
|
-
await this.client.delete(
|
|
171
|
+
await this.client.delete(`${ConfluenceService.V2}/pages/${pageId}`);
|
|
124
172
|
}
|
|
173
|
+
// ---------- Spaces ----------
|
|
125
174
|
async getSpaces(limit = 25) {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
expand: 'description.plain'
|
|
129
|
-
};
|
|
130
|
-
const response = await this.client.get('/wiki/rest/api/space', params);
|
|
131
|
-
return response.results;
|
|
175
|
+
const response = await this.client.get(`${ConfluenceService.V2}/spaces`, { limit });
|
|
176
|
+
return response?.results ?? [];
|
|
132
177
|
}
|
|
133
178
|
async getPagesBySpace(spaceKey, limit = 25) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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: '',
|
|
148
196
|
}));
|
|
149
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 ----------
|
|
150
239
|
async getAttachments(pageId) {
|
|
151
|
-
const response = await this.client.get(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
downloadUrl: att._links?.download || ''
|
|
162
|
-
})) || [];
|
|
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
|
+
}));
|
|
163
250
|
}
|
|
164
251
|
async addAttachment(pageId, filename, fileContent, filePath) {
|
|
252
|
+
// v2 has no upload endpoint — multipart upload stays on v1.
|
|
165
253
|
const FormData = (await import('form-data')).default;
|
|
166
254
|
const form = new FormData();
|
|
167
255
|
const buffer = await this.getFileBuffer(fileContent, filePath);
|
|
168
256
|
const finalFilename = filename || (filePath ? this.getFilenameFromPath(filePath) : 'attachment');
|
|
169
257
|
form.append('file', buffer, { filename: finalFilename });
|
|
170
|
-
const response = await this.client.postFormData(
|
|
258
|
+
const response = await this.client.postFormData(`${ConfluenceService.V1}/content/${pageId}/child/attachment`, form);
|
|
171
259
|
return response;
|
|
172
260
|
}
|
|
173
261
|
async deleteAttachment(attachmentId) {
|
|
174
|
-
await this.client.delete(
|
|
262
|
+
await this.client.delete(`${ConfluenceService.V2}/attachments/${attachmentId}`);
|
|
175
263
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
let updatedContent = currentPage.content;
|
|
194
|
-
const position = options.position || 'bottom';
|
|
195
|
-
if (position === 'top') {
|
|
196
|
-
updatedContent = imageMarkup + '\n' + updatedContent;
|
|
197
|
-
}
|
|
198
|
-
else if (position === 'after-heading' && options.headingText) {
|
|
199
|
-
// Find the heading and insert after it
|
|
200
|
-
const headingRegex = new RegExp(`(<h[1-6][^>]*>.*?${options.headingText}.*?</h[1-6]>)`, 'i');
|
|
201
|
-
const match = updatedContent.match(headingRegex);
|
|
202
|
-
if (match) {
|
|
203
|
-
updatedContent = updatedContent.replace(headingRegex, `$1\n${imageMarkup}`);
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
// Fallback to bottom if heading not found
|
|
207
|
-
updatedContent = updatedContent + '\n' + imageMarkup;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
// Default: bottom
|
|
212
|
-
updatedContent = updatedContent + '\n' + imageMarkup;
|
|
213
|
-
}
|
|
214
|
-
// Step 5: Update the page
|
|
215
|
-
const updatedPage = await this.updatePage(pageId, currentPage.title, updatedContent, currentPage.version);
|
|
216
|
-
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);
|
|
217
281
|
}
|
|
218
282
|
async createPageWithImages(spaceKey, title, content, parentId, images) {
|
|
219
|
-
// First create the page
|
|
220
|
-
const page = await this.createPage(spaceKey, title, content, parentId);
|
|
221
283
|
if (images && images.length > 0) {
|
|
222
|
-
|
|
223
|
-
const uploadedImages = [];
|
|
224
|
-
for (const image of images) {
|
|
225
|
-
try {
|
|
226
|
-
const resolvedFilename = image.filename || (image.filePath ? this.getFilenameFromPath(image.filePath) : 'image');
|
|
227
|
-
const attachment = await this.addAttachment(page.id, resolvedFilename, image.fileContent, image.filePath);
|
|
228
|
-
uploadedImages.push({
|
|
229
|
-
...image,
|
|
230
|
-
filename: resolvedFilename,
|
|
231
|
-
resolvedFilename,
|
|
232
|
-
attachmentId: attachment.results?.[0]?.id
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
console.warn(`Failed to upload image ${image.filename || image.filePath}:`, error);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
// If we have uploaded images, update the page content to include image references
|
|
240
|
-
if (uploadedImages.length > 0) {
|
|
241
|
-
let updatedContent = content;
|
|
242
|
-
const imagesWithoutPlaceholder = [];
|
|
243
|
-
for (const img of uploadedImages) {
|
|
244
|
-
const imageMarkup = this.buildImageMarkup(img);
|
|
245
|
-
const fname = img.resolvedFilename || img.filename || 'image';
|
|
246
|
-
// Check for placeholder {{IMAGE:filename}}
|
|
247
|
-
const placeholder = new RegExp(`\\{\\{IMAGE:${fname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\}\\}`, 'gi');
|
|
248
|
-
if (placeholder.test(updatedContent)) {
|
|
249
|
-
// Replace placeholder with image markup
|
|
250
|
-
updatedContent = updatedContent.replace(placeholder, imageMarkup);
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
// No placeholder found, add to list for appending at end
|
|
254
|
-
imagesWithoutPlaceholder.push(img);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// Append images without placeholders at the end
|
|
258
|
-
if (imagesWithoutPlaceholder.length > 0) {
|
|
259
|
-
const appendedMarkup = imagesWithoutPlaceholder
|
|
260
|
-
.map(img => this.buildImageMarkup(img))
|
|
261
|
-
.join('\n');
|
|
262
|
-
updatedContent += `\n\n${appendedMarkup}`;
|
|
263
|
-
}
|
|
264
|
-
// Update the page with the new content and return the updated version
|
|
265
|
-
const updatedPage = await this.updatePage(page.id, title, updatedContent, page.version);
|
|
266
|
-
return updatedPage;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return page;
|
|
270
|
-
}
|
|
271
|
-
buildImageMarkup(image) {
|
|
272
|
-
const filename = image.resolvedFilename || image.filename || 'image';
|
|
273
|
-
const alt = image.alt || filename;
|
|
274
|
-
const alignAttr = image.align ? ` ac:align="${image.align}" ac:layout="${image.align}"` : '';
|
|
275
|
-
const widthAttr = image.width ? ` ac:custom-width="true" ac:width="${image.width}"` : '';
|
|
276
|
-
let markup = `<ac:image${alignAttr}${widthAttr} ac:alt="${alt}"><ri:attachment ri:filename="${filename}" />`;
|
|
277
|
-
if (image.caption) {
|
|
278
|
-
markup += `<ac:caption><p>${image.caption}</p></ac:caption>`;
|
|
284
|
+
throw new Error(ConfluenceService.EMBED_NOT_SUPPORTED_MSG);
|
|
279
285
|
}
|
|
280
|
-
|
|
281
|
-
return markup;
|
|
282
|
-
}
|
|
283
|
-
// Page History Methods
|
|
284
|
-
async getPageHistory(pageId, limit = 25) {
|
|
285
|
-
const params = {
|
|
286
|
-
limit,
|
|
287
|
-
expand: 'content'
|
|
288
|
-
};
|
|
289
|
-
const response = await this.client.get(`/wiki/rest/api/content/${pageId}/version`, params);
|
|
290
|
-
return response.results?.map((version) => ({
|
|
291
|
-
number: version.number,
|
|
292
|
-
by: version.by?.displayName || 'Unknown',
|
|
293
|
-
byAccountId: version.by?.accountId,
|
|
294
|
-
when: version.when || '',
|
|
295
|
-
message: version.message || '',
|
|
296
|
-
minorEdit: version.minorEdit || false,
|
|
297
|
-
})) || [];
|
|
298
|
-
}
|
|
299
|
-
// Page Children Methods
|
|
300
|
-
async getPageChildren(pageId, limit = 25) {
|
|
301
|
-
const params = {
|
|
302
|
-
limit,
|
|
303
|
-
expand: 'version,space'
|
|
304
|
-
};
|
|
305
|
-
const response = await this.client.get(`/wiki/rest/api/content/${pageId}/child/page`, params);
|
|
306
|
-
return response.results?.map((page) => ({
|
|
307
|
-
id: page.id,
|
|
308
|
-
title: page.title,
|
|
309
|
-
content: '',
|
|
310
|
-
spaceKey: page.space?.key || '',
|
|
311
|
-
version: page.version?.number || 1,
|
|
312
|
-
created: page.history?.createdDate || '',
|
|
313
|
-
updated: page.version?.when || '',
|
|
314
|
-
})) || [];
|
|
315
|
-
}
|
|
316
|
-
// Label Methods
|
|
317
|
-
async getLabels(pageId) {
|
|
318
|
-
const response = await this.client.get(`/wiki/rest/api/content/${pageId}/label`);
|
|
319
|
-
return response.results?.map((label) => ({
|
|
320
|
-
prefix: label.prefix,
|
|
321
|
-
name: label.name,
|
|
322
|
-
id: label.id,
|
|
323
|
-
})) || [];
|
|
324
|
-
}
|
|
325
|
-
async addLabels(pageId, labels) {
|
|
326
|
-
const data = labels.map(name => ({ prefix: 'global', name }));
|
|
327
|
-
const response = await this.client.post(`/wiki/rest/api/content/${pageId}/label`, data);
|
|
328
|
-
return response.results?.map((label) => ({
|
|
329
|
-
prefix: label.prefix,
|
|
330
|
-
name: label.name,
|
|
331
|
-
id: label.id,
|
|
332
|
-
})) || [];
|
|
286
|
+
return this.createPage(spaceKey, title, content, parentId);
|
|
333
287
|
}
|
|
334
|
-
|
|
335
|
-
await this.client.delete(`/wiki/rest/api/content/${pageId}/label/${encodeURIComponent(label)}`);
|
|
336
|
-
}
|
|
337
|
-
// Comment Methods
|
|
288
|
+
// ---------- Comments (footer comments) ----------
|
|
338
289
|
async getComments(pageId) {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
id
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
})) || [];
|
|
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
|
+
}));
|
|
352
302
|
}
|
|
353
303
|
async addComment(pageId, body) {
|
|
354
304
|
const data = {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
id: pageId,
|
|
358
|
-
type: 'page'
|
|
359
|
-
},
|
|
360
|
-
body: {
|
|
361
|
-
storage: {
|
|
362
|
-
value: markdownToConfluenceStorage(body),
|
|
363
|
-
representation: 'storage'
|
|
364
|
-
}
|
|
365
|
-
}
|
|
305
|
+
pageId,
|
|
306
|
+
body: this.adfBodyEnvelope(body),
|
|
366
307
|
};
|
|
367
|
-
const comment = await this.client.post(
|
|
308
|
+
const comment = await this.client.post(`${ConfluenceService.V2}/footer-comments?${ConfluenceService.BODY_FORMAT_QS}`, data);
|
|
368
309
|
return {
|
|
369
|
-
id: comment
|
|
370
|
-
body: comment
|
|
371
|
-
author: comment
|
|
372
|
-
authorAccountId: comment
|
|
373
|
-
created: comment
|
|
374
|
-
updated: comment
|
|
375
|
-
version: comment
|
|
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,
|
|
376
317
|
};
|
|
377
318
|
}
|
|
378
319
|
async updateComment(commentId, body, version) {
|
|
379
320
|
const data = {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
body: {
|
|
383
|
-
storage: {
|
|
384
|
-
value: markdownToConfluenceStorage(body),
|
|
385
|
-
representation: 'storage'
|
|
386
|
-
}
|
|
387
|
-
}
|
|
321
|
+
version: { number: version + 1, message: '' },
|
|
322
|
+
body: this.adfBodyEnvelope(body),
|
|
388
323
|
};
|
|
389
|
-
const comment = await this.client.put(
|
|
324
|
+
const comment = await this.client.put(`${ConfluenceService.V2}/footer-comments/${commentId}?${ConfluenceService.BODY_FORMAT_QS}`, data);
|
|
390
325
|
return {
|
|
391
|
-
id: comment
|
|
392
|
-
body: comment
|
|
393
|
-
author: comment
|
|
394
|
-
authorAccountId: comment
|
|
395
|
-
created: comment
|
|
396
|
-
updated: comment
|
|
397
|
-
version: comment
|
|
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,
|
|
398
333
|
};
|
|
399
334
|
}
|
|
400
335
|
async deleteComment(commentId) {
|
|
401
|
-
await this.client.delete(
|
|
336
|
+
await this.client.delete(`${ConfluenceService.V2}/footer-comments/${commentId}`);
|
|
402
337
|
}
|
|
403
338
|
}
|
|
404
339
|
//# sourceMappingURL=confluence.js.map
|