@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.
- 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 +35 -4
- package/dist/formatters/adf-formatter.d.ts.map +1 -1
- package/dist/formatters/adf-formatter.js +691 -156
- 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/base-handler.d.ts +5 -0
- package/dist/handlers/base-handler.d.ts.map +1 -0
- package/dist/handlers/base-handler.js +7 -0
- package/dist/handlers/base-handler.js.map +1 -0
- package/dist/handlers/bitbucket-handlers.d.ts +9 -1
- package/dist/handlers/bitbucket-handlers.d.ts.map +1 -1
- package/dist/handlers/bitbucket-handlers.js +51 -191
- package/dist/handlers/bitbucket-handlers.js.map +1 -1
- package/dist/handlers/confluence-handlers.d.ts +2 -1
- package/dist/handlers/confluence-handlers.d.ts.map +1 -1
- package/dist/handlers/confluence-handlers.js +46 -243
- package/dist/handlers/confluence-handlers.js.map +1 -1
- package/dist/handlers/jira-handlers.d.ts +9 -1
- package/dist/handlers/jira-handlers.d.ts.map +1 -1
- package/dist/handlers/jira-handlers.js +144 -575
- package/dist/handlers/jira-handlers.js.map +1 -1
- package/dist/index.js +123 -194
- package/dist/index.js.map +1 -1
- package/dist/registry/tool-registry.d.ts +11 -0
- package/dist/registry/tool-registry.d.ts.map +1 -0
- package/dist/registry/tool-registry.js +24 -0
- package/dist/registry/tool-registry.js.map +1 -0
- package/dist/services/bitbucket.d.ts +7 -0
- package/dist/services/bitbucket.d.ts.map +1 -1
- package/dist/services/bitbucket.js +36 -74
- 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 -311
- package/dist/services/confluence.js.map +1 -1
- package/dist/services/jira/agile.d.ts +33 -0
- package/dist/services/jira/agile.d.ts.map +1 -0
- package/dist/services/jira/agile.js +118 -0
- package/dist/services/jira/agile.js.map +1 -0
- package/dist/services/jira/attachments.d.ts +7 -0
- package/dist/services/jira/attachments.d.ts.map +1 -0
- package/dist/services/jira/attachments.js +26 -0
- package/dist/services/jira/attachments.js.map +1 -0
- package/dist/services/jira/base.d.ts +8 -0
- package/dist/services/jira/base.d.ts.map +1 -0
- package/dist/services/jira/base.js +10 -0
- package/dist/services/jira/base.js.map +1 -0
- package/dist/services/jira/comments.d.ts +8 -0
- package/dist/services/jira/comments.d.ts.map +1 -0
- package/dist/services/jira/comments.js +33 -0
- package/dist/services/jira/comments.js.map +1 -0
- package/dist/services/jira/issues.d.ts +32 -0
- package/dist/services/jira/issues.d.ts.map +1 -0
- package/dist/services/jira/issues.js +177 -0
- package/dist/services/jira/issues.js.map +1 -0
- package/dist/services/jira/labels.d.ts +7 -0
- package/dist/services/jira/labels.d.ts.map +1 -0
- package/dist/services/jira/labels.js +21 -0
- package/dist/services/jira/labels.js.map +1 -0
- package/dist/services/jira/links.d.ts +9 -0
- package/dist/services/jira/links.d.ts.map +1 -0
- package/dist/services/jira/links.js +55 -0
- package/dist/services/jira/links.js.map +1 -0
- package/dist/services/jira/metadata.d.ts +8 -0
- package/dist/services/jira/metadata.d.ts.map +1 -0
- package/dist/services/jira/metadata.js +32 -0
- package/dist/services/jira/metadata.js.map +1 -0
- package/dist/services/jira/subtasks.d.ts +9 -0
- package/dist/services/jira/subtasks.d.ts.map +1 -0
- package/dist/services/jira/subtasks.js +32 -0
- package/dist/services/jira/subtasks.js.map +1 -0
- package/dist/services/jira/transitions.d.ts +16 -0
- package/dist/services/jira/transitions.d.ts.map +1 -0
- package/dist/services/jira/transitions.js +203 -0
- package/dist/services/jira/transitions.js.map +1 -0
- package/dist/services/jira/versions.d.ts +19 -0
- package/dist/services/jira/versions.d.ts.map +1 -0
- package/dist/services/jira/versions.js +62 -0
- package/dist/services/jira/versions.js.map +1 -0
- package/dist/services/jira/watchers.d.ts +7 -0
- package/dist/services/jira/watchers.d.ts.map +1 -0
- package/dist/services/jira/watchers.js +23 -0
- package/dist/services/jira/watchers.js.map +1 -0
- package/dist/services/jira/worklogs.d.ts +8 -0
- package/dist/services/jira/worklogs.d.ts.map +1 -0
- package/dist/services/jira/worklogs.js +37 -0
- package/dist/services/jira/worklogs.js.map +1 -0
- package/dist/services/jira.d.ts +58 -103
- package/dist/services/jira.d.ts.map +1 -1
- package/dist/services/jira.js +100 -1481
- package/dist/services/jira.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/tools/jira-tools.d.ts.map +1 -1
- package/dist/tools/jira-tools.js +35 -3
- package/dist/tools/jira-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,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
|
-
|
|
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(
|
|
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
|
|
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
|
|
58
|
-
const
|
|
59
|
-
|
|
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
|
-
|
|
149
|
+
spaceId,
|
|
150
|
+
status: 'current',
|
|
75
151
|
title,
|
|
76
|
-
|
|
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
|
-
|
|
161
|
+
id: pageId,
|
|
162
|
+
status: 'current',
|
|
101
163
|
title,
|
|
102
|
-
|
|
103
|
-
|
|
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(
|
|
171
|
+
await this.client.delete(`${ConfluenceService.V2}/pages/${pageId}`);
|
|
123
172
|
}
|
|
173
|
+
// ---------- Spaces ----------
|
|
124
174
|
async getSpaces(limit = 25) {
|
|
125
|
-
const
|
|
126
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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(
|
|
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(
|
|
262
|
+
await this.client.delete(`${ConfluenceService.V2}/attachments/${attachmentId}`);
|
|
174
263
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
id
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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(
|
|
308
|
+
const comment = await this.client.post(`${ConfluenceService.V2}/footer-comments?${ConfluenceService.BODY_FORMAT_QS}`, data);
|
|
367
309
|
return {
|
|
368
|
-
id: comment
|
|
369
|
-
body: comment
|
|
370
|
-
author: comment
|
|
371
|
-
authorAccountId: comment
|
|
372
|
-
created: comment
|
|
373
|
-
updated: comment
|
|
374
|
-
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,
|
|
375
317
|
};
|
|
376
318
|
}
|
|
377
319
|
async updateComment(commentId, body, version) {
|
|
378
320
|
const data = {
|
|
379
|
-
|
|
380
|
-
|
|
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(
|
|
324
|
+
const comment = await this.client.put(`${ConfluenceService.V2}/footer-comments/${commentId}?${ConfluenceService.BODY_FORMAT_QS}`, data);
|
|
389
325
|
return {
|
|
390
|
-
id: comment
|
|
391
|
-
body: comment
|
|
392
|
-
author: comment
|
|
393
|
-
authorAccountId: comment
|
|
394
|
-
created: comment
|
|
395
|
-
updated: comment
|
|
396
|
-
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,
|
|
397
333
|
};
|
|
398
334
|
}
|
|
399
335
|
async deleteComment(commentId) {
|
|
400
|
-
await this.client.delete(
|
|
336
|
+
await this.client.delete(`${ConfluenceService.V2}/footer-comments/${commentId}`);
|
|
401
337
|
}
|
|
402
338
|
}
|
|
403
339
|
//# sourceMappingURL=confluence.js.map
|