confluence-cli 1.23.0 → 1.24.0
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 +11 -0
- package/README.md +31 -0
- package/bin/confluence.js +15 -9
- package/lib/config.js +42 -1
- package/lib/confluence-client.js +13 -7
- package/package.json +1 -1
|
@@ -39,8 +39,19 @@ confluence init \
|
|
|
39
39
|
|
|
40
40
|
**Cloud vs Server/DC:**
|
|
41
41
|
- Atlassian Cloud (`*.atlassian.net`): use `--api-path "/wiki/rest/api"`, auth type `basic` with email + API token
|
|
42
|
+
- Atlassian Cloud (scoped token): use `--domain "api.atlassian.com"`, `--api-path "/ex/confluence/<your-cloud-id>/wiki/rest/api"`, auth type `basic` with email + scoped token. Get your Cloud ID from `https://<your-site>.atlassian.net/_edge/tenant_info`. Recommended for agents (least privilege).
|
|
42
43
|
- Self-hosted / Data Center: use `--api-path "/rest/api"`, auth type `bearer` with a personal access token (no email needed)
|
|
43
44
|
|
|
45
|
+
**Scoped API token for agents (recommended):**
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
export CONFLUENCE_DOMAIN="api.atlassian.com"
|
|
49
|
+
export CONFLUENCE_API_PATH="/ex/confluence/<your-cloud-id>/wiki/rest/api"
|
|
50
|
+
export CONFLUENCE_AUTH_TYPE="basic"
|
|
51
|
+
export CONFLUENCE_EMAIL="user@company.com"
|
|
52
|
+
export CONFLUENCE_API_TOKEN="your-scoped-token"
|
|
53
|
+
```
|
|
54
|
+
|
|
44
55
|
## Page ID Resolution
|
|
45
56
|
|
|
46
57
|
Most commands accept `<pageId>` — a numeric ID or any of the supported URL formats below.
|
package/README.md
CHANGED
|
@@ -104,6 +104,17 @@ confluence init \
|
|
|
104
104
|
--token "your-api-token"
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
**Scoped API token** (recommended for agents — least privilege):
|
|
108
|
+
```bash
|
|
109
|
+
# Replace <your-cloud-id> with your actual Cloud ID
|
|
110
|
+
confluence init \
|
|
111
|
+
--domain "api.atlassian.com" \
|
|
112
|
+
--api-path "/ex/confluence/<your-cloud-id>/wiki/rest/api" \
|
|
113
|
+
--auth-type "basic" \
|
|
114
|
+
--email "user@example.com" \
|
|
115
|
+
--token "your-scoped-token"
|
|
116
|
+
```
|
|
117
|
+
|
|
107
118
|
**Hybrid mode** (some fields provided, rest via prompts):
|
|
108
119
|
```bash
|
|
109
120
|
# Domain and token provided, will prompt for auth method and email
|
|
@@ -132,6 +143,15 @@ export CONFLUENCE_API_PATH="/wiki/rest/api" # Cloud default; use /rest/a
|
|
|
132
143
|
export CONFLUENCE_AUTH_TYPE="basic"
|
|
133
144
|
```
|
|
134
145
|
|
|
146
|
+
**Scoped API token** (recommended for agents):
|
|
147
|
+
```bash
|
|
148
|
+
export CONFLUENCE_DOMAIN="api.atlassian.com"
|
|
149
|
+
export CONFLUENCE_API_PATH="/ex/confluence/<your-cloud-id>/wiki/rest/api"
|
|
150
|
+
export CONFLUENCE_AUTH_TYPE="basic"
|
|
151
|
+
export CONFLUENCE_EMAIL="user@example.com"
|
|
152
|
+
export CONFLUENCE_API_TOKEN="your-scoped-token"
|
|
153
|
+
```
|
|
154
|
+
|
|
135
155
|
`CONFLUENCE_API_PATH` defaults to `/wiki/rest/api` for Atlassian Cloud domains and `/rest/api` otherwise. Override it when your site lives under a custom reverse proxy or on-premises path. `CONFLUENCE_AUTH_TYPE` defaults to `basic` when an email is present and falls back to `bearer` otherwise.
|
|
136
156
|
|
|
137
157
|
### Getting Your API Token
|
|
@@ -142,6 +162,17 @@ export CONFLUENCE_AUTH_TYPE="basic"
|
|
|
142
162
|
3. Give it a label (e.g., "confluence-cli")
|
|
143
163
|
4. Copy the generated token
|
|
144
164
|
|
|
165
|
+
**Atlassian Cloud — Scoped API Token** (recommended for agents and automation):
|
|
166
|
+
|
|
167
|
+
Scoped tokens restrict access to specific Atlassian products and permissions, following the principle of least privilege. They use a different API gateway (`api.atlassian.com`) instead of your site domain.
|
|
168
|
+
|
|
169
|
+
1. Create a scoped token in your [Atlassian Admin settings](https://admin.atlassian.com)
|
|
170
|
+
2. Find your Cloud ID by visiting `https://<your-site>.atlassian.net/_edge/tenant_info`
|
|
171
|
+
3. Configure with:
|
|
172
|
+
- **Domain:** `api.atlassian.com`
|
|
173
|
+
- **API path:** `/ex/confluence/<your-cloud-id>/wiki/rest/api`
|
|
174
|
+
- **Auth type:** `basic` (email + scoped token)
|
|
175
|
+
|
|
145
176
|
**On-premise / Data Center:** Use your Confluence username and password for basic authentication.
|
|
146
177
|
|
|
147
178
|
## Usage
|
package/bin/confluence.js
CHANGED
|
@@ -8,6 +8,11 @@ const { getConfig, initConfig } = require('../lib/config');
|
|
|
8
8
|
const Analytics = require('../lib/analytics');
|
|
9
9
|
const pkg = require('../package.json');
|
|
10
10
|
|
|
11
|
+
function buildPageUrl(config, path) {
|
|
12
|
+
const protocol = config.protocol || 'https';
|
|
13
|
+
return `${protocol}://${config.domain}${path}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
program
|
|
12
17
|
.name('confluence')
|
|
13
18
|
.description('CLI tool for Atlassian Confluence')
|
|
@@ -18,6 +23,7 @@ program
|
|
|
18
23
|
.command('init')
|
|
19
24
|
.description('Initialize Confluence CLI configuration')
|
|
20
25
|
.option('-d, --domain <domain>', 'Confluence domain')
|
|
26
|
+
.option('--protocol <protocol>', 'Protocol (http or https)')
|
|
21
27
|
.option('-p, --api-path <path>', 'REST API path')
|
|
22
28
|
.option('-a, --auth-type <type>', 'Authentication type (basic or bearer)')
|
|
23
29
|
.option('-e, --email <email>', 'Email or username for basic auth')
|
|
@@ -217,7 +223,7 @@ program
|
|
|
217
223
|
console.log(`Title: ${chalk.blue(result.title)}`);
|
|
218
224
|
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
219
225
|
console.log(`Space: ${chalk.blue(result.space.name)} (${result.space.key})`);
|
|
220
|
-
console.log(`URL: ${chalk.gray(
|
|
226
|
+
console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${result._links.webui}`)}`)}`);
|
|
221
227
|
|
|
222
228
|
analytics.track('create', true);
|
|
223
229
|
} catch (error) {
|
|
@@ -265,7 +271,7 @@ program
|
|
|
265
271
|
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
266
272
|
console.log(`Parent: ${chalk.blue(parentInfo.title)} (${parentId})`);
|
|
267
273
|
console.log(`Space: ${chalk.blue(result.space.name)} (${result.space.key})`);
|
|
268
|
-
console.log(`URL: ${chalk.gray(
|
|
274
|
+
console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${result._links.webui}`)}`)}`);
|
|
269
275
|
|
|
270
276
|
analytics.track('create_child', true);
|
|
271
277
|
} catch (error) {
|
|
@@ -312,7 +318,7 @@ program
|
|
|
312
318
|
console.log(`Title: ${chalk.blue(result.title)}`);
|
|
313
319
|
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
314
320
|
console.log(`Version: ${chalk.blue(result.version.number)}`);
|
|
315
|
-
console.log(`URL: ${chalk.gray(
|
|
321
|
+
console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${result._links.webui}`)}`)}`);
|
|
316
322
|
|
|
317
323
|
analytics.track('update', true);
|
|
318
324
|
} catch (error) {
|
|
@@ -339,7 +345,7 @@ program
|
|
|
339
345
|
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
340
346
|
console.log(`New Parent: ${chalk.blue(newParentId)}`);
|
|
341
347
|
console.log(`Version: ${chalk.blue(result.version.number)}`);
|
|
342
|
-
console.log(`URL: ${chalk.gray(
|
|
348
|
+
console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${result._links.webui}`)}`)}`);
|
|
343
349
|
|
|
344
350
|
analytics.track('move', true);
|
|
345
351
|
} catch (error) {
|
|
@@ -445,7 +451,7 @@ program
|
|
|
445
451
|
console.log(`Title: ${chalk.green(pageInfo.title)}`);
|
|
446
452
|
console.log(`ID: ${chalk.green(pageInfo.id)}`);
|
|
447
453
|
console.log(`Space: ${chalk.green(pageInfo.space.name)} (${pageInfo.space.key})`);
|
|
448
|
-
console.log(`URL: ${chalk.gray(
|
|
454
|
+
console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${pageInfo.url}`)}`)}`);
|
|
449
455
|
|
|
450
456
|
analytics.track('find', true);
|
|
451
457
|
} catch (error) {
|
|
@@ -1467,7 +1473,7 @@ program
|
|
|
1467
1473
|
console.log(` - ...and ${result.failures.length - 10} more`);
|
|
1468
1474
|
}
|
|
1469
1475
|
}
|
|
1470
|
-
console.log(`URL: ${chalk.gray(
|
|
1476
|
+
console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${result.rootPage._links.webui}`)}`)}`);
|
|
1471
1477
|
if (options.failOnError && result.failures?.length) {
|
|
1472
1478
|
analytics.track('copy_tree', false);
|
|
1473
1479
|
console.error(chalk.red('Completed with failures and --fail-on-error is set.'));
|
|
@@ -1529,7 +1535,7 @@ program
|
|
|
1529
1535
|
type: page.type,
|
|
1530
1536
|
status: page.status,
|
|
1531
1537
|
spaceKey: page.space?.key,
|
|
1532
|
-
url:
|
|
1538
|
+
url: `${buildPageUrl(config, `/wiki/spaces/${page.space?.key}/pages/${page.id}`)}`,
|
|
1533
1539
|
parentId: page.parentId || resolvedPageId
|
|
1534
1540
|
}))
|
|
1535
1541
|
};
|
|
@@ -1558,7 +1564,7 @@ program
|
|
|
1558
1564
|
}
|
|
1559
1565
|
|
|
1560
1566
|
if (options.showUrl) {
|
|
1561
|
-
const url =
|
|
1567
|
+
const url = `${buildPageUrl(config, `/wiki/spaces/${page.space?.key}/pages/${page.id}`)}`;
|
|
1562
1568
|
output += `\n ${chalk.gray(url)}`;
|
|
1563
1569
|
}
|
|
1564
1570
|
|
|
@@ -1623,7 +1629,7 @@ function printTree(nodes, config, options, depth = 1) {
|
|
|
1623
1629
|
}
|
|
1624
1630
|
|
|
1625
1631
|
if (options.showUrl) {
|
|
1626
|
-
const url =
|
|
1632
|
+
const url = `${buildPageUrl(config, `/wiki/spaces/${node.space?.key}/pages/${node.id}`)}`;
|
|
1627
1633
|
output += `\n${indent}${isLast ? ' ' : '│ '}${chalk.gray(url)}`;
|
|
1628
1634
|
}
|
|
1629
1635
|
|
package/lib/config.js
CHANGED
|
@@ -19,6 +19,19 @@ const requiredInput = (label) => (input) => {
|
|
|
19
19
|
return true;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
const PROTOCOL_CHOICES = [
|
|
23
|
+
{ name: 'HTTPS (recommended)', value: 'https' },
|
|
24
|
+
{ name: 'HTTP', value: 'http' }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const normalizeProtocol = (rawValue) => {
|
|
28
|
+
const normalized = (rawValue || '').trim().toLowerCase();
|
|
29
|
+
if (normalized === 'http' || normalized === 'https') {
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
return 'https';
|
|
33
|
+
};
|
|
34
|
+
|
|
22
35
|
const normalizeAuthType = (rawValue, hasEmail) => {
|
|
23
36
|
const normalized = (rawValue || '').trim().toLowerCase();
|
|
24
37
|
if (normalized === 'basic' || normalized === 'bearer') {
|
|
@@ -84,6 +97,10 @@ const validateCliOptions = (options) => {
|
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
99
|
|
|
100
|
+
if (options.protocol && !['http', 'https'].includes(options.protocol.toLowerCase())) {
|
|
101
|
+
errors.push('--protocol must be "http" or "https"');
|
|
102
|
+
}
|
|
103
|
+
|
|
87
104
|
if (options.authType && !['basic', 'bearer'].includes(options.authType.toLowerCase())) {
|
|
88
105
|
errors.push('--auth-type must be "basic" or "bearer"');
|
|
89
106
|
}
|
|
@@ -105,6 +122,7 @@ const saveConfig = (configData) => {
|
|
|
105
122
|
|
|
106
123
|
const config = {
|
|
107
124
|
domain: configData.domain.trim(),
|
|
125
|
+
protocol: normalizeProtocol(configData.protocol),
|
|
108
126
|
apiPath: normalizeApiPath(configData.apiPath, configData.domain),
|
|
109
127
|
token: configData.token.trim(),
|
|
110
128
|
authType: configData.authType,
|
|
@@ -122,6 +140,17 @@ const saveConfig = (configData) => {
|
|
|
122
140
|
const promptForMissingValues = async (providedValues) => {
|
|
123
141
|
const questions = [];
|
|
124
142
|
|
|
143
|
+
// Protocol question
|
|
144
|
+
if (!providedValues.protocol) {
|
|
145
|
+
questions.push({
|
|
146
|
+
type: 'list',
|
|
147
|
+
name: 'protocol',
|
|
148
|
+
message: 'Protocol:',
|
|
149
|
+
choices: PROTOCOL_CHOICES,
|
|
150
|
+
default: 'https'
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
125
154
|
// Domain question
|
|
126
155
|
if (!providedValues.domain) {
|
|
127
156
|
questions.push({
|
|
@@ -205,6 +234,7 @@ const promptForMissingValues = async (providedValues) => {
|
|
|
205
234
|
async function initConfig(cliOptions = {}) {
|
|
206
235
|
// Extract provided values from CLI options
|
|
207
236
|
const providedValues = {
|
|
237
|
+
protocol: cliOptions.protocol,
|
|
208
238
|
domain: cliOptions.domain,
|
|
209
239
|
apiPath: cliOptions.apiPath,
|
|
210
240
|
authType: cliOptions.authType,
|
|
@@ -221,6 +251,13 @@ async function initConfig(cliOptions = {}) {
|
|
|
221
251
|
console.log('Please provide your Confluence connection details:\n');
|
|
222
252
|
|
|
223
253
|
const answers = await inquirer.prompt([
|
|
254
|
+
{
|
|
255
|
+
type: 'list',
|
|
256
|
+
name: 'protocol',
|
|
257
|
+
message: 'Protocol:',
|
|
258
|
+
choices: PROTOCOL_CHOICES,
|
|
259
|
+
default: 'https'
|
|
260
|
+
},
|
|
224
261
|
{
|
|
225
262
|
type: 'input',
|
|
226
263
|
name: 'domain',
|
|
@@ -318,6 +355,7 @@ async function initConfig(cliOptions = {}) {
|
|
|
318
355
|
|
|
319
356
|
const configData = {
|
|
320
357
|
domain: normalizedDomain,
|
|
358
|
+
protocol: normalizeProtocol(providedValues.protocol),
|
|
321
359
|
apiPath: providedValues.apiPath || inferApiPath(normalizedDomain),
|
|
322
360
|
token: providedValues.token,
|
|
323
361
|
authType: normalizedAuthType,
|
|
@@ -355,6 +393,7 @@ function getConfig() {
|
|
|
355
393
|
const envEmail = process.env.CONFLUENCE_EMAIL || process.env.CONFLUENCE_USERNAME;
|
|
356
394
|
const envAuthType = process.env.CONFLUENCE_AUTH_TYPE;
|
|
357
395
|
const envApiPath = process.env.CONFLUENCE_API_PATH;
|
|
396
|
+
const envProtocol = process.env.CONFLUENCE_PROTOCOL;
|
|
358
397
|
|
|
359
398
|
if (envDomain && envToken) {
|
|
360
399
|
const authType = normalizeAuthType(envAuthType, Boolean(envEmail));
|
|
@@ -375,6 +414,7 @@ function getConfig() {
|
|
|
375
414
|
|
|
376
415
|
return {
|
|
377
416
|
domain: envDomain.trim(),
|
|
417
|
+
protocol: normalizeProtocol(envProtocol),
|
|
378
418
|
apiPath,
|
|
379
419
|
token: envToken.trim(),
|
|
380
420
|
email: envEmail ? envEmail.trim() : undefined,
|
|
@@ -385,7 +425,7 @@ function getConfig() {
|
|
|
385
425
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
386
426
|
console.error(chalk.red('❌ No configuration found!'));
|
|
387
427
|
console.log(chalk.yellow('Please run "confluence init" to set up your configuration.'));
|
|
388
|
-
console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN (or CONFLUENCE_PASSWORD), CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME), and optionally CONFLUENCE_API_PATH.'));
|
|
428
|
+
console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN (or CONFLUENCE_PASSWORD), CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME), and optionally CONFLUENCE_API_PATH, CONFLUENCE_PROTOCOL.'));
|
|
389
429
|
process.exit(1);
|
|
390
430
|
}
|
|
391
431
|
|
|
@@ -419,6 +459,7 @@ function getConfig() {
|
|
|
419
459
|
|
|
420
460
|
return {
|
|
421
461
|
domain: trimmedDomain,
|
|
462
|
+
protocol: normalizeProtocol(storedConfig.protocol),
|
|
422
463
|
apiPath,
|
|
423
464
|
token: trimmedToken,
|
|
424
465
|
email: trimmedEmail,
|
package/lib/confluence-client.js
CHANGED
|
@@ -29,11 +29,13 @@ const NAMED_ENTITIES = {
|
|
|
29
29
|
class ConfluenceClient {
|
|
30
30
|
constructor(config) {
|
|
31
31
|
this.domain = config.domain;
|
|
32
|
+
const rawProtocol = (config.protocol || 'https').trim().toLowerCase();
|
|
33
|
+
this.protocol = (rawProtocol === 'http' || rawProtocol === 'https') ? rawProtocol : 'https';
|
|
32
34
|
this.token = config.token;
|
|
33
35
|
this.email = config.email;
|
|
34
36
|
this.authType = (config.authType || (this.email ? 'basic' : 'bearer')).toLowerCase();
|
|
35
37
|
this.apiPath = this.sanitizeApiPath(config.apiPath);
|
|
36
|
-
this.baseURL =
|
|
38
|
+
this.baseURL = `${this.protocol}://${this.domain}${this.apiPath}`;
|
|
37
39
|
this.markdown = new MarkdownIt();
|
|
38
40
|
this.setupConfluenceMarkdownExtensions();
|
|
39
41
|
|
|
@@ -368,7 +370,7 @@ class ConfluenceClient {
|
|
|
368
370
|
const webui = page._links?.webui || '';
|
|
369
371
|
return {
|
|
370
372
|
title: page.title,
|
|
371
|
-
url: webui ?
|
|
373
|
+
url: webui ? this.buildUrl(`/wiki${webui}`) : ''
|
|
372
374
|
};
|
|
373
375
|
}
|
|
374
376
|
return null;
|
|
@@ -462,7 +464,7 @@ class ConfluenceClient {
|
|
|
462
464
|
// Format: - [Page Title](URL)
|
|
463
465
|
const childPagesList = childPages.map(page => {
|
|
464
466
|
const webui = page._links?.webui || '';
|
|
465
|
-
const url = webui ?
|
|
467
|
+
const url = webui ? this.buildUrl(`/wiki${webui}`) : '';
|
|
466
468
|
if (url) {
|
|
467
469
|
return `- [${page.title}](${url})`;
|
|
468
470
|
} else {
|
|
@@ -1294,11 +1296,11 @@ class ConfluenceClient {
|
|
|
1294
1296
|
// Try to build a proper URL - if spaceKey starts with ~, it's a user space
|
|
1295
1297
|
if (spaceKey.startsWith('~')) {
|
|
1296
1298
|
const spacePath = `display/${spaceKey}/${encodeURIComponent(title)}`;
|
|
1297
|
-
return `\n> 📄 **${labels.includePage}**: [${title}](
|
|
1299
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`/wiki/${spacePath}`)})\n`;
|
|
1298
1300
|
} else {
|
|
1299
1301
|
// For non-user spaces, we cannot construct a valid link without the page ID.
|
|
1300
1302
|
// Document that manual correction is required.
|
|
1301
|
-
return `\n> 📄 **${labels.includePage}**: [${title}](
|
|
1303
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`/wiki/spaces/${spaceKey}/pages/[PAGE_ID_HERE]`)}) _(manual link correction required)_\n`;
|
|
1302
1304
|
}
|
|
1303
1305
|
});
|
|
1304
1306
|
|
|
@@ -2005,6 +2007,11 @@ class ConfluenceClient {
|
|
|
2005
2007
|
};
|
|
2006
2008
|
}
|
|
2007
2009
|
|
|
2010
|
+
buildUrl(path) {
|
|
2011
|
+
const normalized = path && !path.startsWith('/') ? `/${path}` : (path || '');
|
|
2012
|
+
return `${this.protocol}://${this.domain}${normalized}`;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2008
2015
|
toAbsoluteUrl(pathOrUrl) {
|
|
2009
2016
|
if (!pathOrUrl) {
|
|
2010
2017
|
return null;
|
|
@@ -2014,8 +2021,7 @@ class ConfluenceClient {
|
|
|
2014
2021
|
return pathOrUrl;
|
|
2015
2022
|
}
|
|
2016
2023
|
|
|
2017
|
-
|
|
2018
|
-
return `https://${this.domain}${normalized}`;
|
|
2024
|
+
return this.buildUrl(pathOrUrl);
|
|
2019
2025
|
}
|
|
2020
2026
|
|
|
2021
2027
|
parseNextStart(nextLink) {
|