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.
@@ -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(`https://${config.domain}/wiki${result._links.webui}`)}`);
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(`https://${config.domain}/wiki${result._links.webui}`)}`);
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(`https://${config.domain}/wiki${result._links.webui}`)}`);
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(`https://${config.domain}/wiki${result._links.webui}`)}`);
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(`https://${config.domain}/wiki${pageInfo.url}`)}`);
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(`https://${config.domain}/wiki${result.rootPage._links.webui}`)}`);
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: `https://${config.domain}/wiki/spaces/${page.space?.key}/pages/${page.id}`,
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 = `https://${config.domain}/wiki/spaces/${page.space?.key}/pages/${page.id}`;
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 = `https://${config.domain}/wiki/spaces/${node.space?.key}/pages/${node.id}`;
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,
@@ -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 = `https://${this.domain}${this.apiPath}`;
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 ? `https://${this.domain}/wiki${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 ? `https://${this.domain}/wiki${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}](https://${this.domain}/wiki/${spacePath})\n`;
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}](https://${this.domain}/wiki/spaces/${spaceKey}/pages/[PAGE_ID_HERE]) _(manual link correction required)_\n`;
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
- const normalized = pathOrUrl.startsWith('/') ? pathOrUrl : `/${pathOrUrl}`;
2018
- return `https://${this.domain}${normalized}`;
2024
+ return this.buildUrl(pathOrUrl);
2019
2025
  }
2020
2026
 
2021
2027
  parseNextStart(nextLink) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {