confluence-cli 1.27.1 → 1.27.2
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/.claude/skills/confluence/SKILL.md +5 -0
- package/README.md +16 -0
- package/lib/confluence-client.js +62 -7
- package/package.json +1 -1
|
@@ -64,6 +64,11 @@ export CONFLUENCE_EMAIL="user@company.com"
|
|
|
64
64
|
export CONFLUENCE_API_TOKEN="your-scoped-token"
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Required classic scopes for scoped tokens:
|
|
68
|
+
- Read-only: `read:confluence-content.all`, `read:confluence-space.summary`, `search:confluence`
|
|
69
|
+
- Write: add `write:confluence-content`, `write:confluence-file`, `write:confluence-space`
|
|
70
|
+
- Attachments: `readonly:content.attachment:confluence` (download), `write:confluence-file` (upload)
|
|
71
|
+
|
|
67
72
|
**Read-only mode (recommended for AI agents):**
|
|
68
73
|
|
|
69
74
|
Prevents all write operations (create, update, delete, move, etc.) at the profile level. Useful when giving an AI agent access to Confluence for reading only.
|
package/README.md
CHANGED
|
@@ -193,6 +193,22 @@ Scoped tokens restrict access to specific Atlassian products and permissions, fo
|
|
|
193
193
|
- **API path:** `/ex/confluence/<your-cloud-id>/wiki/rest/api`
|
|
194
194
|
- **Auth type:** `basic` (email + scoped token)
|
|
195
195
|
|
|
196
|
+
**Required scopes for scoped API tokens:**
|
|
197
|
+
|
|
198
|
+
When creating a scoped token, select the following [classic scopes](https://developer.atlassian.com/cloud/confluence/scopes-for-oauth-2-3LO-and-forge-apps/) based on your needs:
|
|
199
|
+
|
|
200
|
+
| Scope | Required for |
|
|
201
|
+
|-------|-------------|
|
|
202
|
+
| `read:confluence-content.all` | Reading pages and blog posts (`read`, `info`) |
|
|
203
|
+
| `read:confluence-space.summary` | Listing spaces (`spaces`) |
|
|
204
|
+
| `search:confluence` | Searching content (`search`) |
|
|
205
|
+
| `readonly:content.attachment:confluence` | Downloading attachments (`attachments --download`) |
|
|
206
|
+
| `write:confluence-content` | Creating and updating pages (`create`, `update`) |
|
|
207
|
+
| `write:confluence-file` | Uploading attachments (`attachments --upload`) |
|
|
208
|
+
| `write:confluence-space` | Managing spaces |
|
|
209
|
+
|
|
210
|
+
For **read-only** usage, select at minimum: `read:confluence-content.all`, `read:confluence-space.summary`, and `search:confluence`.
|
|
211
|
+
|
|
196
212
|
**On-premise / Data Center:** Use your Confluence username and password for basic authentication.
|
|
197
213
|
|
|
198
214
|
## Usage
|
package/lib/confluence-client.js
CHANGED
|
@@ -48,6 +48,48 @@ class ConfluenceClient {
|
|
|
48
48
|
baseURL: this.baseURL,
|
|
49
49
|
headers
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
this.client.interceptors.response.use(
|
|
53
|
+
response => response,
|
|
54
|
+
error => {
|
|
55
|
+
if (error.response?.status === 401) {
|
|
56
|
+
const hints = ['Authentication failed (401 Unauthorized).'];
|
|
57
|
+
if (this.isScopedToken()) {
|
|
58
|
+
hints.push(
|
|
59
|
+
'You are using a scoped API token (api.atlassian.com). Please verify:',
|
|
60
|
+
' - Your token has the required scopes (e.g., read:confluence-content.all, read:confluence-space.summary)',
|
|
61
|
+
' - Your Cloud ID in the API path is correct',
|
|
62
|
+
' - Your email matches the account that created the token',
|
|
63
|
+
'See: https://developer.atlassian.com/cloud/confluence/scopes-for-oauth-2-3LO-and-forge-apps/'
|
|
64
|
+
);
|
|
65
|
+
} else if (this.authType === 'basic' && this.isCloud()) {
|
|
66
|
+
hints.push(
|
|
67
|
+
'Please verify your email and API token are correct.',
|
|
68
|
+
'Generate a token at: https://id.atlassian.com/manage-profile/security/api-tokens'
|
|
69
|
+
);
|
|
70
|
+
} else if (this.authType === 'basic') {
|
|
71
|
+
hints.push(
|
|
72
|
+
'Please verify your username and password are correct.'
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
hints.push(
|
|
76
|
+
'Please verify your personal access token is valid and not expired.'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
error.message = hints.join('\n');
|
|
80
|
+
}
|
|
81
|
+
return Promise.reject(error);
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
isCloud() {
|
|
87
|
+
return this.isScopedToken() || (this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
isScopedToken() {
|
|
91
|
+
const d = (this.domain || '').trim().toLowerCase();
|
|
92
|
+
return d === 'api.atlassian.com' || this.apiPath?.includes('/ex/confluence/');
|
|
51
93
|
}
|
|
52
94
|
|
|
53
95
|
sanitizeApiPath(rawPath) {
|
|
@@ -1026,10 +1068,15 @@ class ConfluenceClient {
|
|
|
1026
1068
|
// Convert code blocks to Confluence code macro
|
|
1027
1069
|
storage = storage.replace(/<pre><code(?:\s+class="language-(\w+)")?>(.*?)<\/code><\/pre>/gs, (_, lang, code) => {
|
|
1028
1070
|
const language = lang || 'text';
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1071
|
+
// Trim trailing newline added by markdown-it during HTML rendering,
|
|
1072
|
+
// and decode HTML entities that markdown-it encodes inside <code> blocks
|
|
1073
|
+
// so they appear as literal characters in the CDATA output
|
|
1074
|
+
const decodedCode = code.replace(/\n$/, '')
|
|
1075
|
+
.replace(/"/g, '"')
|
|
1076
|
+
.replace(/&/g, '&')
|
|
1077
|
+
.replace(/</g, '<')
|
|
1078
|
+
.replace(/>/g, '>');
|
|
1079
|
+
return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${language}</ac:parameter><ac:plain-text-body><![CDATA[${decodedCode}]]></ac:plain-text-body></ac:structured-macro>`;
|
|
1033
1080
|
});
|
|
1034
1081
|
|
|
1035
1082
|
// Convert inline code
|
|
@@ -1070,13 +1117,21 @@ class ConfluenceClient {
|
|
|
1070
1117
|
storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
|
|
1071
1118
|
|
|
1072
1119
|
// Convert links
|
|
1073
|
-
|
|
1120
|
+
// Confluence Cloud does not render ac:link + ri:url; use smart links instead.
|
|
1121
|
+
// Server/Data Center instances continue to use the ac:link storage format.
|
|
1122
|
+
if (this.isCloud()) {
|
|
1123
|
+
storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<a href="$1" data-card-appearance="inline">$2</a>');
|
|
1124
|
+
} else {
|
|
1125
|
+
storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<ac:link><ri:url ri:value="$1" /><ac:plain-text-link-body><![CDATA[$2]]></ac:plain-text-link-body></ac:link>');
|
|
1126
|
+
}
|
|
1074
1127
|
|
|
1075
1128
|
// Convert horizontal rules
|
|
1076
1129
|
storage = storage.replace(/<hr\s*\/?>/g, '<hr />');
|
|
1077
1130
|
|
|
1078
|
-
//
|
|
1079
|
-
|
|
1131
|
+
// Note: Do NOT globally decode < > & here. These represent literal
|
|
1132
|
+
// characters in user content (e.g. <placeholder> in inline text) and
|
|
1133
|
+
// Confluence storage format renders them correctly as-is. Code block
|
|
1134
|
+
// entities are decoded separately above before CDATA insertion.
|
|
1080
1135
|
|
|
1081
1136
|
return storage;
|
|
1082
1137
|
}
|