Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions .claude/skills/confluence/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ confluence --version # verify install
| `CONFLUENCE_EMAIL` | Email address (basic auth only) | `user@company.com` |
| `CONFLUENCE_API_TOKEN` | API token or personal access token | `ATATT3x...` |
| `CONFLUENCE_PROFILE` | Named profile to use (optional) | `staging` |
| `CONFLUENCE_READ_ONLY` | Block all write operations when `true` | `true` |

**Global `--profile` flag (use a named profile for any command):**

Expand Down Expand Up @@ -63,6 +64,27 @@ export CONFLUENCE_EMAIL="user@company.com"
export CONFLUENCE_API_TOKEN="your-scoped-token"
```

**Read-only mode (recommended for AI agents):**

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.

```sh
# Via profile flag
confluence profile add agent --domain "company.atlassian.net" --token "xxx" --read-only

# Via environment variable (overrides config file)
export CONFLUENCE_READ_ONLY=true
```

When read-only mode is active, any write command exits with an error:
```
Error: This profile is in read-only mode. Write operations are not allowed.
```

`profile list` shows read-only profiles with a `[read-only]` badge.

---

## Page ID Resolution

Most commands accept `<pageId>` — a numeric ID or any of the supported URL formats below.
Expand Down Expand Up @@ -102,7 +124,7 @@ confluence read "https://company.atlassian.net/wiki/spaces/MYSPACE/pages/1234567
Initialize configuration. Saves credentials to `~/.confluence-cli/config.json`.

```sh
confluence init [--domain <domain>] [--api-path <path>] [--auth-type basic|bearer] [--email <email>] [--token <token>]
confluence init [--domain <domain>] [--api-path <path>] [--auth-type basic|bearer] [--email <email>] [--token <token>] [--read-only]
```

All flags are optional; omitting any flag triggers an interactive prompt for that field. Provide all flags to run fully non-interactive. Use the global `--profile` flag to save to a named profile:
Expand Down Expand Up @@ -564,7 +586,7 @@ confluence profile use staging
Add a new configuration profile. Supports the same options as `init` (interactive, non-interactive, or hybrid).

```sh
confluence profile add <name> [--domain <domain>] [--api-path <path>] [--auth-type basic|bearer] [--email <email>] [--token <token>] [--protocol http|https]
confluence profile add <name> [--domain <domain>] [--api-path <path>] [--auth-type basic|bearer] [--email <email>] [--token <token>] [--protocol http|https] [--read-only]
```

Profile names may contain letters, numbers, hyphens, and underscores only.
Expand Down Expand Up @@ -684,6 +706,7 @@ confluence search "release notes" --limit 20
- **Page ID vs URL**: when you have a Confluence URL, extract `?pageId=<number>` and pass the number. Do not pass pretty/display URLs — they are not supported.
- **Cross-space moves**: `confluence move` only works within the same space. Moving across spaces is not supported.
- **Multiple instances**: Use `--profile <name>` or `CONFLUENCE_PROFILE` env var to target different Confluence instances without reconfiguring.
- **Read-only mode**: Set `CONFLUENCE_READ_ONLY=true` or use `--read-only` when creating profiles to prevent accidental writes. This is enforced at the CLI level — all write commands will be blocked.

## Error Patterns

Expand All @@ -696,3 +719,4 @@ confluence search "release notes" --limit 20
| `At least one of --title, --file, or --content must be provided` | `update` called with no content options | Provide at least one of the required options |
| `Profile "<name>" not found!` | Specified profile doesn't exist | Run `confluence profile list` to see available profiles |
| `Cannot delete the only remaining profile.` | Tried to remove the last profile | Add another profile before removing |
| `This profile is in read-only mode` | Write command used with a read-only profile | Use a writable profile or remove `readOnly` from config |
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ A powerful command-line interface for Atlassian Confluence that allows you to re
- 📦 **Export** - Save a page and its attachments to a local folder
- 🛠️ **Edit workflow** - Export page content for editing and re-import
- 🔀 **Profiles** - Manage multiple Confluence instances with named configuration profiles
- 🔒 **Read-only mode** - Profile-level write protection for safe AI agent usage
- 🔧 **Easy setup** - Simple configuration with environment variables or interactive setup

## Installation
Expand Down Expand Up @@ -140,6 +141,7 @@ confluence init --email "user@example.com" --token "your-api-token"
- `-a, --auth-type <type>` - Authentication type: `basic` or `bearer`
- `-e, --email <email>` - Email or username for basic authentication
- `-t, --token <token>` - API token or password
- `--read-only` - Enable read-only mode (blocks all write operations)

⚠️ **Security note:** While flags work, storing tokens in shell history is risky. Prefer environment variables (Option 3) for production environments.

Expand All @@ -166,6 +168,12 @@ export CONFLUENCE_API_TOKEN="your-scoped-token"

`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.

**Read-only mode** (recommended for AI agents):
```bash
export CONFLUENCE_READ_ONLY=true
```
When set, all write operations (`create`, `update`, `delete`, etc.) are blocked at the CLI level. The environment variable overrides the profile's `readOnly` setting.

### Getting Your API Token

**Atlassian Cloud:**
Expand Down Expand Up @@ -460,13 +468,38 @@ confluence profile add staging
# Add a new profile non-interactively
confluence profile add staging --domain "staging.example.com" --auth-type bearer --token "xyz"

# Add a read-only profile (blocks all write operations)
confluence profile add agent --domain "company.atlassian.net" --auth-type basic --email "bot@example.com" --token "xyz" --read-only

# Remove a profile
confluence profile remove staging

# Use a specific profile for a single command
confluence --profile staging spaces
```

### Read-Only Mode

Read-only mode blocks all write operations at the CLI level, making it safe to hand the tool to AI agents (Claude Code, Copilot, etc.) without risking accidental edits.

**Enable via profile:**
```bash
# During init
confluence init --read-only

# When adding a profile
confluence profile add agent --domain "company.atlassian.net" --token "xyz" --read-only
```

**Enable via environment variable:**
```bash
export CONFLUENCE_READ_ONLY=true # overrides profile setting
```

When read-only mode is active, any write command (`create`, `create-child`, `update`, `delete`, `move`, `edit`, `comment`, `attachment-upload`, `attachment-delete`, `property-set`, `property-delete`, `comment-delete`, `copy-tree`) exits with code 1 and prints an error message.

Comment thread
pchuri marked this conversation as resolved.
`confluence profile list` shows a `[read-only]` badge next to protected profiles.

### View Usage Statistics
```bash
confluence stats
Expand All @@ -476,7 +509,7 @@ confluence stats

| Command | Description | Options |
|---|---|---|
| `init` | Initialize CLI configuration | |
| `init` | Initialize CLI configuration | `--read-only` |
| `read <pageId_or_url>` | Read page content | `--format <html\|text\|markdown>` |
| `info <pageId_or_url>` | Get page information | |
| `search <query>` | Search for pages | `--limit <number>` |
Expand All @@ -503,7 +536,7 @@ confluence stats
| `export <pageId_or_url>` | Export a page to a directory with its attachments | `--format <html\|text\|markdown>`, `--dest <directory>`, `--file <filename>`, `--attachments-dir <name>`, `--pattern <glob>`, `--referenced-only`, `--skip-attachments` |
| `profile list` | List all configuration profiles | |
| `profile use <name>` | Set the active configuration profile | |
| `profile add <name>` | Add a new configuration profile | `-d, --domain`, `-p, --api-path`, `-a, --auth-type`, `-e, --email`, `-t, --token`, `--protocol` |
| `profile add <name>` | Add a new configuration profile | `-d, --domain`, `-p, --api-path`, `-a, --auth-type`, `-e, --email`, `-t, --token`, `--protocol`, `--read-only` |
| `profile remove <name>` | Remove a configuration profile | |
| `stats` | View your usage statistics | |

Expand Down
35 changes: 30 additions & 5 deletions bin/confluence.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ function buildPageUrl(config, path) {
return `${protocol}://${config.domain}${path}`;
}

function assertWritable(config) {
if (config.readOnly) {
console.error(chalk.red('Error: This profile is in read-only mode. Write operations are not allowed.'));
console.error(chalk.yellow('Tip: Use "confluence profile add <name>" without --read-only, or set readOnly to false in config.'));
process.exit(1);
}
}

program
.name('confluence')
.description('CLI tool for Atlassian Confluence')
Expand All @@ -34,6 +42,7 @@ program
.option('-a, --auth-type <type>', 'Authentication type (basic or bearer)')
.option('-e, --email <email>', 'Email or username for basic auth')
.option('-t, --token <token>', 'API token')
.option('--read-only', 'Set profile to read-only mode (blocks write operations)')
.action(async (options) => {
const profile = getProfileName();
await initConfig({ ...options, profile });
Expand Down Expand Up @@ -208,8 +217,9 @@ program
const analytics = new Analytics();
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);

let content = '';

if (options.file) {
Expand Down Expand Up @@ -251,8 +261,9 @@ program
const analytics = new Analytics();
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);

// Get parent page info to get space key
const parentInfo = await client.getPageInfo(parentId);
const spaceKey = parentInfo.space.key;
Expand Down Expand Up @@ -305,8 +316,9 @@ program
}

const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);

let content = null; // Use null to indicate no content change

if (options.file) {
Expand Down Expand Up @@ -344,6 +356,7 @@ program
const analytics = new Analytics();
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);
const result = await client.movePage(pageId, newParentId, options.title);

Expand Down Expand Up @@ -371,6 +384,7 @@ program
const analytics = new Analytics();
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);
const pageInfo = await client.getPageInfo(pageIdOrUrl);

Expand Down Expand Up @@ -414,6 +428,7 @@ program
const analytics = new Analytics();
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);
const pageData = await client.getPageForEdit(pageId);

Expand Down Expand Up @@ -613,6 +628,7 @@ program
const fs = require('fs');
const path = require('path');
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);

const resolvedFiles = files.map((filePath) => ({
Expand Down Expand Up @@ -660,6 +676,7 @@ program
const analytics = new Analytics();
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);

if (!options.yes) {
Expand Down Expand Up @@ -810,6 +827,7 @@ program
const analytics = new Analytics();
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);

if (!options.value && !options.file) {
Expand Down Expand Up @@ -866,6 +884,7 @@ program
const analytics = new Analytics();
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);

if (!options.yes) {
Expand Down Expand Up @@ -1067,6 +1086,7 @@ program
let location = null;
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);

let content = '';
Expand Down Expand Up @@ -1181,6 +1201,7 @@ program
const analytics = new Analytics();
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);

if (!options.yes) {
Expand Down Expand Up @@ -1599,8 +1620,9 @@ program
const analytics = new Analytics();
try {
const config = getConfig(getProfileName());
assertWritable(config);
const client = new ConfluenceClient(config);

// Parse numeric flags with safe fallbacks
const parsedDepth = parseInt(options.maxDepth, 10);
const maxDepth = Number.isNaN(parsedDepth) ? 10 : parsedDepth;
Expand Down Expand Up @@ -1876,7 +1898,8 @@ profileCmd
console.log(chalk.blue('Configuration profiles:\n'));
profiles.forEach(p => {
const marker = p.active ? chalk.green(' (active)') : '';
console.log(` ${p.active ? chalk.green('*') : ' '} ${chalk.cyan(p.name)}${marker} - ${chalk.gray(p.domain)}`);
const readOnlyBadge = p.readOnly ? chalk.red(' [read-only]') : '';
console.log(` ${p.active ? chalk.green('*') : ' '} ${chalk.cyan(p.name)}${marker}${readOnlyBadge} - ${chalk.gray(p.domain)}`);
});
});

Expand All @@ -1902,6 +1925,7 @@ profileCmd
.option('-a, --auth-type <type>', 'Authentication type (basic or bearer)')
.option('-e, --email <email>', 'Email or username for basic auth')
.option('-t, --token <token>', 'API token')
.option('--read-only', 'Set profile to read-only mode (blocks write operations)')
.action(async (name, options) => {
if (!isValidProfileName(name)) {
console.error(chalk.red('Invalid profile name. Use only letters, numbers, hyphens, and underscores.'));
Expand Down Expand Up @@ -1943,6 +1967,7 @@ module.exports = {
uniquePathFor,
exportRecursive,
sanitizeTitle,
assertWritable,
},
};

Expand Down
Loading
Loading