Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, line):\n return '----'\n\n # Regular paragraph - convert inline elements\n return self._convert_inline_elements(line)\n\n def _convert_heading(self, line: str) -> str:\n \"\"\"Convert Markdown heading to Wiki Markup.\"\"\"\n match = re.match(r'^(#{1,6})\\s+(.+)

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, line)\n if match:\n level = len(match.group(1))\n text = match.group(2).strip()\n\n # Remove trailing # if present\n text = re.sub(r'\\s*#+\\s*

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, '', text)\n\n # Remove {#anchor} syntax\n text = re.sub(r'\\s*\\{#[^}]+\\}\\s*

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, '', text)\n\n return f\"h{level}. {text}\"\n return line\n\n def _convert_unordered_list(self, line: str) -> str:\n \"\"\"Convert Markdown unordered list to Wiki Markup.\"\"\"\n match = re.match(r'^(\\s*)([-*+])\\s+(.+)

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, line)\n if match:\n indent = match.group(1)\n text = match.group(3)\n\n # Calculate nesting level (2 or 4 spaces per level)\n level = len(indent) // 2 if len(indent) % 2 == 0 else len(indent) // 4 + 1\n level = max(1, level)\n\n bullets = '*' * level\n converted_text = self._convert_inline_elements(text)\n return f\"{bullets} {converted_text}\"\n return line\n\n def _convert_ordered_list(self, line: str) -> str:\n \"\"\"Convert Markdown ordered list to Wiki Markup.\"\"\"\n match = re.match(r'^(\\s*)(\\d+\\.)\\s+(.+)

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, line)\n if match:\n indent = match.group(1)\n text = match.group(3)\n\n # Calculate nesting level\n level = len(indent) // 2 if len(indent) % 2 == 0 else len(indent) // 4 + 1\n level = max(1, level)\n\n numbers = '#' * level\n converted_text = self._convert_inline_elements(text)\n return f\"{numbers} {converted_text}\"\n return line\n\n def _convert_task_list(self, line: str) -> str:\n \"\"\"Convert GitHub-style task list to Wiki Markup.\"\"\"\n match = re.match(r'^(\\s*)[-*]\\s+\\[([ xX])\\]\\s+(.+)

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, line)\n if match:\n checked = match.group(2).lower() == 'x'\n text = match.group(3)\n converted_text = self._convert_inline_elements(text)\n\n if checked:\n return f\"[x] {converted_text}\"\n else:\n return f\"[] {converted_text}\"\n return line\n\n def _convert_table_row(self, line: str) -> str:\n \"\"\"Convert Markdown table row to Wiki Markup.\"\"\"\n # Skip separator rows\n if re.match(r'^\\s*\\|?\\s*:?-+:?\\s*(\\|\\s*:?-+:?\\s*)*\\|?\\s*

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, line):\n return ''\n\n # Split by pipes\n cells = [cell.strip() for cell in line.split('|')]\n\n # Remove empty first/last cells (from leading/trailing pipes)\n if cells and not cells[0]:\n cells = cells[1:]\n if cells and not cells[-1]:\n cells = cells[:-1]\n\n if not cells:\n return line\n\n # Convert inline elements in each cell\n converted_cells = [self._convert_inline_elements(cell) for cell in cells]\n\n # Detect if this is a header row (check if next line has separator)\n # For now, assume first row is header\n # In a full implementation, you'd need context from previous/next lines\n return '|' + '|'.join(converted_cells) + '|'\n\n def _convert_blockquote(self, line: str) -> str:\n \"\"\"Convert Markdown blockquote to Wiki Markup.\"\"\"\n # Remove leading >\n text = re.sub(r'^\\s*>\\s*', '', line)\n\n # Check for admonition style\n admonition_match = re.match(r'\\*\\*(\\w+):\\*\\*\\s*(.+)', text)\n if admonition_match:\n admonition_type = admonition_match.group(1).lower()\n content = admonition_match.group(2)\n\n if admonition_type in ['info', 'tip', 'note', 'warning']:\n return f\"{{{admonition_type}}}\\n{content}\\n{{{admonition_type}}}\"\n\n # Regular blockquote\n converted_text = self._convert_inline_elements(text)\n return f\"bq. {converted_text}\"\n\n def _convert_inline_elements(self, text: str) -> str:\n \"\"\"Convert inline Markdown elements to Wiki Markup.\"\"\"\n # Images: ![alt](url) -> !url|alt=alt!\n text = re.sub(\n r'!\\[([^\\]]*)\\]\\(([^)]+)\\)',\n lambda m: f\"!{m.group(2)}|alt={m.group(1)}!\" if m.group(1) else f\"!{m.group(2)}!\",\n text\n )\n\n # Links: [text](url) -> [text|url]\n text = re.sub(\n r'\\[([^\\]]+)\\]\\(([^)]+)\\)',\n r'[\\1|\\2]',\n text\n )\n\n # Bold: **text** or __text__ -> *text*\n text = re.sub(r'\\*\\*([^*]+)\\*\\*', r'*\\1*', text)\n text = re.sub(r'__([^_]+)__', r'*\\1*', text)\n\n # Italic: *text* or _text_ -> _text_\n # Be careful not to match already converted bold\n text = re.sub(r'(?\u003c!\\*)\\*(?!\\*)([^*]+)\\*(?!\\*)', r'_\\1_', text)\n text = re.sub(r'(?\u003c!_)_(?!_)([^_]+)_(?!_)', r'_\\1_', text)\n\n # Strikethrough: ~~text~~ -> -text-\n text = re.sub(r'~~([^~]+)~~', r'-\\1-', text)\n\n # Inline code: `text` -> {{text}}\n text = re.sub(r'`([^`]+)`', r'{{\\1}}', text)\n\n return text\n\n\ndef convert_file(input_path: Path, output_path: Path = None) -> str:\n \"\"\"\n Convert a Markdown file to Wiki Markup.\n\n Args:\n input_path: Path to input Markdown file\n output_path: Path to output Wiki file (optional)\n\n Returns:\n Converted Wiki Markup text\n \"\"\"\n # Read input\n with open(input_path, 'r', encoding='utf-8') as f:\n markdown_text = f.read()\n\n # Convert\n converter = MarkdownToWikiConverter()\n wiki_text = converter.convert(markdown_text)\n\n # Write output if path provided\n if output_path:\n with open(output_path, 'w', encoding='utf-8') as f:\n f.write(wiki_text)\n\n return wiki_text\n\n\ndef main():\n \"\"\"Main entry point for CLI usage.\"\"\"\n if len(sys.argv) \u003c 2:\n print(__doc__)\n sys.exit(1)\n\n input_file = Path(sys.argv[1])\n\n if not input_file.exists():\n print(f\"Error: File '{input_file}' not found\", file=sys.stderr)\n sys.exit(1)\n\n output_file = Path(sys.argv[2]) if len(sys.argv) > 2 else None\n\n try:\n wiki_text = convert_file(input_file, output_file)\n\n if not output_file:\n print(wiki_text)\n else:\n print(f\"Converted {input_file} -> {output_file}\")\n\n except Exception as e:\n print(f\"Error: {e}\", file=sys.stderr)\n sys.exit(1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10024,"content_sha256":"18dd891bd4b7784b4f5ec1332c85913d6fb1b62796497397e9f41387e0d7d161"},{"filename":"scripts/download_confluence.py","content":"#!/usr/bin/env python3\n\"\"\"\nConfluence Page Downloader - Download and convert Confluence pages to Markdown\n\nFeatures:\n- Downloads complete Confluence pages using REST API with pagination\n- Converts Confluence storage format (XHTML) to clean Markdown\n- Handles Confluence macros (code blocks with language, children lists, images)\n- Downloads all attachments and creates local links\n- Supports hierarchical child page downloads to subdirectories\n- Creates YAML frontmatter with complete page metadata\n- Retries with exponential backoff for failed downloads\n- HTML debugging mode for troubleshooting transformations\n\"\"\"\n\nimport os\nimport re\nimport sys\nimport time\nimport json\nimport logging\nimport argparse\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Tuple\nfrom datetime import datetime\nfrom urllib.parse import urljoin, urlparse, quote\n\nimport requests\nimport yaml\nfrom markdownify import markdownify as md\n\n# Import shared credential discovery\ntry:\n from confluence_auth import get_confluence_credentials\nexcept ImportError:\n print(\"ERROR: confluence_auth module not found. Ensure it's in the same directory.\", file=sys.stderr)\n sys.exit(1)\n\n# Configure logging\nlogging.basicConfig(\n level=logging.INFO,\n format='%(asctime)s - %(levelname)s - %(message)s'\n)\nlogger = logging.getLogger(__name__)\n\n\nclass ConfluenceValidator:\n \"\"\"Validates downloaded content against Confluence source.\"\"\"\n\n def __init__(self, confluence_url: str, username: str, api_token: str):\n self.confluence_url = confluence_url.rstrip('/')\n if self.confluence_url.endswith('/wiki'):\n self.confluence_url = self.confluence_url[:-5]\n\n self.api_base = f\"{self.confluence_url}/wiki/rest/api\"\n self.web_base = f\"{self.confluence_url}/wiki\" # Base URL for web resources (downloads, etc.)\n self.auth = (username, api_token)\n self.session = requests.Session()\n self.session.auth = self.auth\n\n def get_page_info(self, page_id: str) -> Dict:\n \"\"\"Get page metadata from Confluence.\"\"\"\n url = f\"{self.api_base}/content/{page_id}\"\n params = {\n 'expand': 'body.storage,version,space,ancestors,metadata.labels,history,children.page'\n }\n\n response = self.session.get(url, params=params)\n response.raise_for_status()\n return response.json()\n\n def get_children(self, page_id: str) -> List[Dict]:\n \"\"\"Get all child pages for a page.\"\"\"\n children = []\n start = 0\n limit = 50\n\n while True:\n url = f\"{self.api_base}/content/{page_id}/child/page\"\n params = {\n 'start': start,\n 'limit': limit\n }\n\n response = self.session.get(url, params=params)\n response.raise_for_status()\n data = response.json()\n\n children.extend(data.get('results', []))\n\n if len(data.get('results', [])) \u003c limit:\n break\n start += limit\n\n return children\n\n def get_attachments(self, page_id: str) -> List[Dict]:\n \"\"\"Get all attachments for a page.\"\"\"\n attachments = []\n start = 0\n limit = 50\n\n while True:\n url = f\"{self.api_base}/content/{page_id}/child/attachment\"\n params = {\n 'start': start,\n 'limit': limit,\n 'expand': 'version,metadata'\n }\n\n response = self.session.get(url, params=params)\n response.raise_for_status()\n data = response.json()\n\n attachments.extend(data.get('results', []))\n\n if len(data.get('results', [])) \u003c limit:\n break\n start += limit\n\n return attachments\n\n def download_attachment(self, attachment: Dict, output_dir: Path, page_title: str) -> Optional[Path]:\n \"\"\"Download an attachment to the page-specific attachments directory.\"\"\"\n try:\n download_url = attachment['_links'].get('download')\n if not download_url:\n logger.warning(f\"No download URL for attachment: {attachment.get('title')}\")\n return None\n\n # Construct full URL - use web_base which includes /wiki\n if download_url.startswith('/'):\n download_url = self.web_base + download_url\n\n # Create page-specific attachments directory\n safe_page_title = self._sanitize_filename(page_title)\n attachments_dir = output_dir / f'{safe_page_title}_attachments'\n attachments_dir.mkdir(exist_ok=True)\n\n # Sanitize filename\n filename = self._sanitize_filename(attachment['title'])\n filepath = attachments_dir / filename\n\n # Download file\n logger.info(f\"Downloading attachment: {filename}\")\n response = self.session.get(download_url, stream=True)\n response.raise_for_status()\n\n with open(filepath, 'wb') as f:\n for chunk in response.iter_content(chunk_size=8192):\n f.write(chunk)\n\n logger.info(f\"Downloaded: {filename} ({filepath.stat().st_size} bytes)\")\n return filepath\n\n except Exception as e:\n logger.error(f\"Error downloading attachment {attachment.get('title')}: {e}\")\n return None\n\n def _sanitize_filename(self, filename: str) -> str:\n \"\"\"Sanitize filename for filesystem.\"\"\"\n # Remove or replace invalid characters, replace spaces with underscores\n filename = re.sub(r'[\u003c>:\"/\\\\|?*]', '_', filename)\n filename = re.sub(r'\\s+', '_', filename)\n # Limit length\n if len(filename) > 200:\n name, ext = os.path.splitext(filename)\n filename = name[:200-len(ext)] + ext\n return filename\n\n\nclass ConfluenceDownloader:\n \"\"\"Downloads and converts Confluence pages to Markdown.\"\"\"\n\n def __init__(self, validator: ConfluenceValidator, output_dir: Path, save_html: bool = False, download_children: bool = False):\n self.validator = validator\n self.output_dir = output_dir\n self.output_dir.mkdir(exist_ok=True)\n self.save_html = save_html\n self.download_children = download_children\n\n # Create HTML debug directory if requested\n if self.save_html:\n self.html_dir = output_dir / '_html_debug'\n self.html_dir.mkdir(exist_ok=True)\n\n def download_page(self, page_id: str, max_retries: int = 3, target_dir: Optional[Path] = None, parent_title: Optional[str] = None) -> Tuple[bool, str]:\n \"\"\"\n Download a single page with validation.\n\n Args:\n page_id: Confluence page ID to download\n max_retries: Number of retry attempts for failed downloads\n target_dir: Optional target directory (defaults to self.output_dir)\n parent_title: Optional parent page title for frontmatter updates\n\n Returns:\n Tuple of (success: bool, message: str)\n \"\"\"\n if target_dir is None:\n target_dir = self.output_dir\n for attempt in range(max_retries):\n try:\n logger.info(f\"Downloading page {page_id} (attempt {attempt + 1}/{max_retries})\")\n\n # Get page info\n page_info = self.validator.get_page_info(page_id)\n\n # Extract metadata\n title = page_info['title']\n space_key = page_info['space']['key']\n version = page_info['version']['number']\n content_html = page_info['body']['storage']['value']\n\n logger.info(f\"Page: {title} (v{version})\")\n logger.info(f\"HTML content length: {len(content_html)} characters\")\n\n # STEP 1: Save original HTML for debugging if requested\n safe_title = self._sanitize_filename(title)\n if self.save_html:\n # Save original (raw from API)\n original_html_file = self.html_dir / f\"original_{safe_title}.html\"\n with open(original_html_file, 'w', encoding='utf-8') as f:\n f.write(content_html)\n\n # Save formatted (pretty-printed)\n try:\n from bs4 import BeautifulSoup\n soup = BeautifulSoup(content_html, 'html.parser')\n formatted_html = soup.prettify()\n formatted_html_file = self.html_dir / f\"formatted_{safe_title}.html\"\n with open(formatted_html_file, 'w', encoding='utf-8') as f:\n f.write(formatted_html)\n except ImportError:\n # If BeautifulSoup not available, just copy original\n formatted_html_file = self.html_dir / f\"formatted_{safe_title}.html\"\n with open(formatted_html_file, 'w', encoding='utf-8') as f:\n f.write(content_html)\n\n logger.info(f\"Saved original and formatted HTML debug files\")\n\n # Get attachments\n attachments = self.validator.get_attachments(page_id)\n logger.info(f\"Found {len(attachments)} attachments\")\n\n # Download attachments (pass sanitized page title for folder naming)\n safe_title = self._sanitize_filename(title)\n attachment_paths = {}\n for attachment in attachments:\n path = self.validator.download_attachment(attachment, target_dir, safe_title)\n if path:\n attachment_paths[attachment['title']] = path\n\n # Convert children macro to HTML list before other transformations\n content_html = self._convert_children_macro(content_html, page_id)\n\n # Update image links in HTML to point to local attachments\n # This also applies code block and image transformations\n content_html = self._localize_attachment_links(content_html, attachment_paths)\n\n # STEP 2: Save transformed HTML for debugging if requested\n if self.save_html:\n transformed_html_file = self.html_dir / f\"transformed_{safe_title}.html\"\n with open(transformed_html_file, 'w', encoding='utf-8') as f:\n f.write(content_html)\n logger.info(f\"Saved transformed HTML debug file\")\n\n # STEP 3: Convert HTML to Markdown using markdownify\n logger.info(\"Converting HTML to Markdown...\")\n markdown_content = md(\n content_html,\n heading_style=\"ATX\",\n bullets=\"-\",\n code_language=\"\",\n strip=['script', 'style']\n )\n\n # Clean up markdown\n markdown_content = self._clean_markdown(markdown_content)\n\n # Save original markdown (before post-processing) for debugging\n if self.save_html:\n original_md_file = self.html_dir / f\"original_{safe_title}.md\"\n with open(original_md_file, 'w', encoding='utf-8') as f:\n f.write(markdown_content)\n logger.info(f\"Saved original markdown debug file\")\n\n # STEP 4: Post-process markdown to add language tags to code fences\n markdown_content = self._postprocess_code_languages(markdown_content)\n\n logger.info(f\"Markdown content length: {len(markdown_content)} characters\")\n\n # Validate size (markdown should be roughly 50-150% of HTML size due to formatting)\n size_ratio = len(markdown_content) / len(content_html) if content_html else 0\n if size_ratio \u003c 0.3:\n logger.warning(f\"Markdown seems too small (ratio: {size_ratio:.2f})\")\n elif size_ratio > 2.0:\n logger.warning(f\"Markdown seems too large (ratio: {size_ratio:.2f})\")\n else:\n logger.info(f\"Size validation passed (ratio: {size_ratio:.2f})\")\n\n # Create frontmatter (pass parent_title if in subdirectory)\n frontmatter = self._create_frontmatter(page_info, attachments, parent_title)\n\n # Generate filename (without page ID)\n safe_title = self._sanitize_filename(title)\n filename = f\"{safe_title}.md\"\n filepath = target_dir / filename\n\n # Write file\n with open(filepath, 'w', encoding='utf-8') as f:\n f.write(\"---\\n\")\n f.write(yaml.dump(frontmatter, default_flow_style=False, allow_unicode=True))\n f.write(\"---\\n\\n\")\n f.write(f\"# {title}\\n\\n\")\n f.write(markdown_content)\n\n file_size = filepath.stat().st_size\n logger.info(f\"✅ Downloaded: {filename} ({file_size:,} bytes)\")\n\n # Download children if enabled\n if self.download_children:\n children_data = self.validator.get_children(page_id)\n if children_data:\n # Create subdirectory for children\n children_dir = target_dir / f\"{safe_title}_Children\"\n children_dir.mkdir(exist_ok=True)\n logger.info(f\"📁 Downloading {len(children_data)} children to {children_dir.relative_to(self.output_dir)}/\")\n\n for child in children_data:\n child_id = child['id']\n child_title = child['title']\n logger.info(f\" ↳ Child: {child_title} ({child_id})\")\n # Recursively download child page\n self.download_page(child_id, max_retries, children_dir, title)\n\n return True, f\"Success: {filename} ({file_size:,} bytes)\"\n\n except requests.exceptions.HTTPError as e:\n if e.response.status_code == 404:\n msg = f\"Page {page_id} not found (404)\"\n logger.error(msg)\n return False, msg\n elif e.response.status_code == 401:\n msg = f\"Authentication failed for page {page_id}\"\n logger.error(msg)\n return False, msg\n else:\n logger.error(f\"HTTP error on attempt {attempt + 1}: {e}\")\n if attempt \u003c max_retries - 1:\n time.sleep(2 ** attempt) # Exponential backoff\n except Exception as e:\n logger.error(f\"Error on attempt {attempt + 1}: {e}\")\n if attempt \u003c max_retries - 1:\n time.sleep(2 ** attempt)\n\n return False, f\"Failed after {max_retries} attempts\"\n\n def _convert_code_blocks(self, html: str) -> str:\n \"\"\"Convert Confluence code blocks to HTML with language markers for post-processing.\"\"\"\n # Pattern to match \u003cac:structured-macro ac:name=\"code\">...\u003c/ac:structured-macro>\n code_block_pattern = re.compile(\n r'\u003cac:structured-macro[^>]*ac:name=\"code\"[^>]*>(.*?)\u003c/ac:structured-macro>',\n re.DOTALL\n )\n\n def replace_code_block(match):\n code_macro = match.group(1)\n\n # Extract language parameter if present\n lang_match = re.search(r'\u003cac:parameter ac:name=\"language\">([^\u003c]+)\u003c/ac:parameter>', code_macro)\n language = lang_match.group(1) if lang_match else ''\n\n # Extract code content from CDATA section\n content_match = re.search(r'\u003c!\\[CDATA\\[(.*?)\\]\\]>', code_macro, re.DOTALL)\n if not content_match:\n # Try plain-text-body without CDATA\n content_match = re.search(r'\u003cac:plain-text-body>(.*?)\u003c/ac:plain-text-body>', code_macro, re.DOTALL)\n\n if not content_match:\n return match.group(0) # Keep original if we can't parse it\n\n code_content = content_match.group(1)\n\n # STEP 2: Create HTML with language marker as \u003cp> tag before code block\n # This marker will be converted to text by markdownify, then post-processed\n if language:\n return f'\u003cp>code-lang:{language}\u003c/p>\u003cpre>\u003ccode>{code_content}\u003c/code>\u003c/pre>'\n else:\n return f'\u003cpre>\u003ccode>{code_content}\u003c/code>\u003c/pre>'\n\n return code_block_pattern.sub(replace_code_block, html)\n\n def _convert_children_macro(self, html: str, page_id: str) -> str:\n \"\"\"Convert Confluence children macro to a markdown list placeholder.\"\"\"\n # Pattern to match \u003cac:structured-macro ac:name=\"children\">...\u003c/ac:structured-macro>\n children_macro_pattern = re.compile(\n r'\u003cac:structured-macro[^>]*ac:name=\"children\"[^>]*>.*?\u003c/ac:structured-macro>',\n re.DOTALL\n )\n\n def replace_children_macro(match):\n # Get actual children from API\n try:\n children_data = self.validator.get_children(page_id)\n if children_data:\n # Create HTML list that will be converted to markdown\n items = []\n for child in children_data:\n child_title = child['title']\n safe_child_title = self._sanitize_filename(child_title)\n\n # If download_children enabled, link to subdirectory\n if self.download_children:\n current_title = \"\" # Will be set when we have page_info\n # For now, just create a plain list - paths will be in frontmatter\n items.append(f\"\u003cli>{child_title}\u003c/li>\")\n else:\n items.append(f\"\u003cli>{child_title}\u003c/li>\")\n\n return \"\u003cul>\\n\" + \"\\n\".join(items) + \"\\n\u003c/ul>\"\n else:\n return \"\" # No children, remove macro\n except Exception as e:\n logger.warning(f\"Error converting children macro: {e}\")\n return \"\" # Remove macro on error\n\n return children_macro_pattern.sub(replace_children_macro, html)\n\n def _localize_attachment_links(self, html: str, attachment_paths: Dict[str, Path]) -> str:\n \"\"\"Replace Confluence attachment URLs with local file paths.\"\"\"\n # First, convert Confluence code blocks to standard HTML\n html = self._convert_code_blocks(html)\n\n # Then convert Confluence \u003cac:image> tags to standard \u003cimg> tags\n html = self._convert_confluence_images(html, attachment_paths)\n\n # Then replace any remaining attachment URLs\n for attachment_name, local_path in attachment_paths.items():\n # Get relative path from output_dir\n rel_path = local_path.relative_to(self.output_dir)\n\n # Replace various forms of attachment URLs\n patterns = [\n # Standard attachment URL\n re.compile(rf'/wiki/download/attachments/\\d+/{re.escape(quote(attachment_name))}', re.IGNORECASE),\n # Thumbnail URL\n re.compile(rf'/wiki/download/thumbnails/\\d+/{re.escape(quote(attachment_name))}', re.IGNORECASE),\n # Simple filename reference\n re.compile(rf'(?\u003c=src=\")[^\"]*{re.escape(attachment_name)}(?=\")', re.IGNORECASE),\n ]\n\n for pattern in patterns:\n html = pattern.sub(str(rel_path), html)\n\n return html\n\n def _convert_confluence_images(self, html: str, attachment_paths: Dict[str, Path]) -> str:\n \"\"\"Convert Confluence \u003cac:image> tags to standard HTML \u003cimg> tags with local paths.\"\"\"\n # Pattern to match \u003cac:image>...\u003c/ac:image> blocks\n ac_image_pattern = re.compile(\n r'\u003cac:image[^>]*>(.*?)\u003c/ac:image>',\n re.DOTALL\n )\n\n def replace_ac_image(match):\n ac_image_block = match.group(0)\n inner_content = match.group(1)\n\n # Extract filename from \u003cri:attachment ri:filename=\"...\">\n filename_match = re.search(r'\u003cri:attachment[^>]+ri:filename=\"([^\"]+)\"', inner_content)\n if not filename_match:\n return ac_image_block # Keep original if no filename found\n\n filename = filename_match.group(1)\n\n # Find the local path for this attachment\n local_path = attachment_paths.get(filename)\n if not local_path:\n # Try without URL parameters\n base_filename = filename.split('?')[0]\n local_path = attachment_paths.get(base_filename)\n\n if not local_path:\n return ac_image_block # Keep original if attachment not found\n\n # Get relative path\n rel_path = local_path.relative_to(self.output_dir)\n\n # Extract alt text from ac:alt attribute\n alt_match = re.search(r'ac:alt=\"([^\"]*)\"', ac_image_block)\n alt_text = alt_match.group(1) if alt_match else filename\n\n # Create standard HTML img tag\n return f'\u003cimg src=\"{rel_path}\" alt=\"{alt_text}\" />'\n\n return ac_image_pattern.sub(replace_ac_image, html)\n\n def _clean_markdown(self, markdown: str) -> str:\n \"\"\"Clean up markdown formatting.\"\"\"\n # Remove excessive blank lines\n markdown = re.sub(r'\\n{3,}', '\\n\\n', markdown)\n\n # Fix code blocks - ensure proper spacing\n markdown = re.sub(r'```(\\w*)\\n+', r'```\\1\\n', markdown)\n markdown = re.sub(r'\\n+```', r'\\n```', markdown)\n\n # Clean up table formatting\n markdown = re.sub(r'\\|\\s*\\n\\s*\\|', '|\\n|', markdown)\n\n # Remove trailing whitespace\n lines = [line.rstrip() for line in markdown.split('\\n')]\n markdown = '\\n'.join(lines)\n\n return markdown.strip() + '\\n'\n\n def _postprocess_code_languages(self, markdown: str) -> str:\n \"\"\"STEP 4: Post-process markdown to add language tags to code fences.\"\"\"\n # Pattern: code-lang:LANGUAGE followed by newline and code fence\n # Replace with just the code fence with language\n pattern = re.compile(\n r'code-lang:(\\w+)\\s*\\n\\s*```\\s*\\n',\n re.MULTILINE\n )\n\n def add_lang_to_fence(match):\n language = match.group(1)\n return f'```{language}\\n'\n\n markdown = pattern.sub(add_lang_to_fence, markdown)\n\n # Also handle case where there might be extra whitespace\n # code-lang:json\\n\\n```\n pattern2 = re.compile(\n r'code-lang:(\\w+)\\s*\\n+```',\n re.MULTILINE\n )\n markdown = pattern2.sub(lambda m: f'```{m.group(1)}\\n', markdown)\n\n return markdown\n\n def _create_frontmatter(self, page_info: Dict, attachments: List[Dict], parent_title: Optional[str] = None) -> Dict:\n \"\"\"Create YAML frontmatter from page info with hierarchy.\n\n Args:\n page_info: Page metadata from Confluence API\n attachments: List of attachment metadata\n parent_title: If in a subdirectory, the parent page title for path calculation\n \"\"\"\n # Construct full Confluence URL\n confluence_url = f\"{self.validator.web_base}{page_info['_links']['webui']}\"\n\n frontmatter = {\n 'title': page_info['title'],\n 'confluence_url': confluence_url,\n 'confluence': {\n 'id': page_info['id'],\n 'space': page_info['space']['key'],\n 'version': page_info['version']['number'],\n 'type': page_info['type']\n },\n 'exported_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),\n 'exported_by': 'confluence_downloader',\n 'validation': {\n 'html_content_length': len(page_info['body']['storage']['value']),\n 'status': 'validated'\n }\n }\n\n # Add full breadcrumb path from ancestors\n if page_info.get('ancestors'):\n breadcrumb = []\n for ancestor in page_info['ancestors']:\n breadcrumb.append({\n 'id': ancestor['id'],\n 'title': ancestor['title']\n })\n # Add current page to breadcrumb\n breadcrumb.append({\n 'id': page_info['id'],\n 'title': page_info['title']\n })\n frontmatter['breadcrumb'] = breadcrumb\n\n # Add direct parent info if available\n if page_info.get('ancestors'):\n parent = page_info['ancestors'][-1]\n safe_parent_title = self._sanitize_filename(parent['title'])\n # If we're in a subdirectory (parent_title provided), parent is up one level\n parent_path = f\"../{safe_parent_title}.md\" if parent_title else f\"{safe_parent_title}.md\"\n frontmatter['parent'] = {\n 'id': parent['id'],\n 'title': parent['title'],\n 'file': parent_path\n }\n\n # Add children if available\n children_data = self.validator.get_children(page_info['id'])\n if children_data:\n # If download_children is enabled, children will be in subdirectory\n current_title = self._sanitize_filename(page_info['title'])\n if self.download_children:\n # Children are in {Page_Title}_Children/ subdirectory\n frontmatter['children'] = [\n {\n 'id': child['id'],\n 'title': child['title'],\n 'file': f\"{current_title}_Children/{self._sanitize_filename(child['title'])}.md\"\n }\n for child in children_data\n ]\n else:\n # Children are in same directory\n frontmatter['children'] = [\n {\n 'id': child['id'],\n 'title': child['title'],\n 'file': f\"{self._sanitize_filename(child['title'])}.md\"\n }\n for child in children_data\n ]\n\n # Add labels if available\n if page_info.get('metadata', {}).get('labels', {}).get('results'):\n labels = [label['name'] for label in page_info['metadata']['labels']['results']]\n frontmatter['confluence']['labels'] = labels\n\n # Add attachment info\n if attachments:\n frontmatter['attachments'] = [\n {\n 'id': att['id'],\n 'title': att['title'],\n 'media_type': att['metadata'].get('mediaType', 'unknown'),\n 'file_size': att.get('extensions', {}).get('fileSize', 0)\n }\n for att in attachments\n ]\n\n return frontmatter\n\n def _sanitize_filename(self, title: str) -> str:\n \"\"\"Convert title to safe filename.\"\"\"\n # Replace spaces with underscores, remove special chars\n safe = re.sub(r'[^\\w\\s-]', '', title)\n safe = re.sub(r'[-\\s]+', '_', safe)\n # Limit length\n if len(safe) > 100:\n safe = safe[:100]\n return safe.strip('_')\n\n\ndef load_configuration(env_file: Optional[str] = None, output_override: Optional[str] = None) -> Dict:\n \"\"\"\n Load configuration using shared credential discovery.\n\n Args:\n env_file: Optional path to specific .env file\n output_override: Optional output directory override\n\n Returns:\n Dict with confluence_url, username, api_token, output_dir\n \"\"\"\n try:\n creds = get_confluence_credentials(env_file=env_file)\n except ValueError as e:\n logger.error(f\"Credential discovery failed: {e}\")\n logger.info(\"\\nCreate one of these files with credentials:\")\n logger.info(\" .env, .env.confluence, .env.jira, .env.atlassian\")\n logger.info(\"\\nRequired variables:\")\n logger.info(\" CONFLUENCE_URL=https://yourcompany.atlassian.net\")\n logger.info(\" [email protected]\")\n logger.info(\" CONFLUENCE_API_TOKEN=your_api_token\")\n logger.info(\"\\nGet API Token: https://id.atlassian.com/manage-profile/security/api-tokens\")\n sys.exit(1)\n\n return {\n 'confluence_url': creds['url'],\n 'username': creds['username'],\n 'api_token': creds['token'],\n 'output_dir': output_override or os.getenv('CONFLUENCE_OUTPUT_DIR', 'confluence_docs')\n }\n\n\ndef main():\n \"\"\"Main execution function.\"\"\"\n parser = argparse.ArgumentParser(\n description='Download Confluence pages to Markdown',\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog='''\nExamples:\n # Single page\n %(prog)s 123456789\n\n # Multiple pages\n %(prog)s 123456 456789 789012\n\n # From file\n %(prog)s --page-ids-file page_ids.txt\n\n # With child pages in subdirectories\n %(prog)s --download-children 123456789\n\n # With HTML debugging\n %(prog)s --save-html 123456789\n\n # Custom output directory\n %(prog)s --output-dir ./docs 123456789\n\n # Custom .env file\n %(prog)s --env-file /path/to/.env 123456789\n '''\n )\n\n parser.add_argument('page_ids', nargs='*', help='Page IDs to download')\n parser.add_argument('--env-file', default='.env', help='Path to .env file (default: .env)')\n parser.add_argument('--output-dir', help='Output directory (overrides .env CONFLUENCE_OUTPUT_DIR)')\n parser.add_argument('--download-children', action='store_true', help='Download child pages to subdirectories')\n parser.add_argument('--save-html', action='store_true', help='Save intermediate HTML files for debugging')\n parser.add_argument('--page-ids-file', help='File containing page IDs (one per line)')\n\n args = parser.parse_args()\n\n # Load configuration\n config = load_configuration(args.env_file, args.output_dir)\n\n # Setup output directory\n output_dir = Path(config['output_dir'])\n output_dir.mkdir(exist_ok=True)\n\n # Initialize validator and downloader\n validator = ConfluenceValidator(config['confluence_url'], config['username'], config['api_token'])\n downloader = ConfluenceDownloader(validator, output_dir, save_html=args.save_html, download_children=args.download_children)\n\n if args.save_html:\n logger.info(\"HTML debug mode enabled - saving original XHTML to _html_debug/\")\n if args.download_children:\n logger.info(\"Child page download enabled - children will be downloaded to {Parent_Name}_Children/ subdirectories\")\n\n # Get page IDs\n if args.page_ids_file:\n page_ids_file = Path(args.page_ids_file)\n if not page_ids_file.exists():\n logger.error(f\"Page IDs file not found: {page_ids_file}\")\n sys.exit(1)\n\n with open(page_ids_file) as f:\n page_ids = [\n line.strip()\n for line in f\n if line.strip() and not line.strip().startswith('#')\n ]\n elif args.page_ids:\n page_ids = args.page_ids\n else:\n logger.error(\"No page IDs specified. Use page_ids arguments or --page-ids-file\")\n parser.print_help()\n sys.exit(1)\n\n # Download pages\n logger.info(f\"Downloading {len(page_ids)} pages...\")\n results = []\n\n for i, page_id in enumerate(page_ids, 1):\n logger.info(f\"\\n{'='*60}\")\n logger.info(f\"Processing page {i}/{len(page_ids)}: {page_id}\")\n logger.info(f\"{'='*60}\")\n\n success, message = downloader.download_page(page_id)\n results.append({\n 'page_id': page_id,\n 'success': success,\n 'message': message\n })\n\n # Rate limiting\n if i \u003c len(page_ids):\n time.sleep(1)\n\n # Print summary\n logger.info(f\"\\n{'='*60}\")\n logger.info(\"DOWNLOAD SUMMARY\")\n logger.info(f\"{'='*60}\")\n\n successful = sum(1 for r in results if r['success'])\n failed = len(results) - successful\n\n logger.info(f\"✅ Successful: {successful}/{len(results)}\")\n logger.info(f\"❌ Failed: {failed}/{len(results)}\")\n\n if failed > 0:\n logger.info(\"\\nFailed pages:\")\n for result in results:\n if not result['success']:\n logger.info(f\" - {result['page_id']}: {result['message']}\")\n\n # Write results to JSON\n results_file = output_dir / 'download_results.json'\n with open(results_file, 'w') as f:\n json.dump({\n 'timestamp': datetime.now().isoformat(),\n 'total': len(results),\n 'successful': successful,\n 'failed': failed,\n 'results': results\n }, f, indent=2)\n\n logger.info(f\"\\nResults saved to: {results_file}\")\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":32992,"content_sha256":"89e3416e997abdb7524b633ada2170b99436d72a8056b889063937b5f58fd1b4"},{"filename":"scripts/generate_mark_metadata.py","content":"#!/usr/bin/env python3\n\"\"\"\nGenerate mark-compatible metadata headers for Markdown files.\n\nThis script adds or updates metadata headers in Markdown files for use\nwith the mark CLI tool (https://github.com/kovetskiy/mark).\n\nUsage:\n python generate_mark_metadata.py file.md --space DEV --title \"Page Title\"\n python generate_mark_metadata.py file.md --space DEV --parent \"Parent Page\" --labels api,docs\n\"\"\"\n\nimport argparse\nimport re\nimport sys\nfrom pathlib import Path\nfrom typing import List, Optional\n\n\nclass MarkMetadata:\n \"\"\"Represents mark metadata for a Confluence page.\"\"\"\n\n def __init__(\n self,\n space: str,\n title: Optional[str] = None,\n parent: Optional[str] = None,\n parents: Optional[List[str]] = None,\n labels: Optional[List[str]] = None,\n attachments: Optional[List[str]] = None\n ):\n self.space = space\n self.title = title\n self.parent = parent\n self.parents = parents or []\n self.labels = labels or []\n self.attachments = attachments or []\n\n def to_header(self) -> str:\n \"\"\"\n Generate mark metadata header.\n\n Returns:\n Formatted metadata header as string\n \"\"\"\n lines = []\n\n # Space (required)\n lines.append(f\"\u003c!-- Space: {self.space} -->\")\n\n # Title (optional, can be inferred from first heading)\n if self.title:\n lines.append(f\"\u003c!-- Title: {self.title} -->\")\n\n # Parent page(s)\n if self.parent:\n lines.append(f\"\u003c!-- Parent: {self.parent} -->\")\n\n for parent in self.parents:\n lines.append(f\"\u003c!-- Parent: {parent} -->\")\n\n # Labels\n for label in self.labels:\n lines.append(f\"\u003c!-- Label: {label} -->\")\n\n # Attachments\n for attachment in self.attachments:\n lines.append(f\"\u003c!-- Attachment: {attachment} -->\")\n\n return '\\n'.join(lines)\n\n\ndef extract_title_from_markdown(content: str) -> Optional[str]:\n \"\"\"\n Extract title from first H1 heading in Markdown.\n\n Args:\n content: Markdown content\n\n Returns:\n Title if found, None otherwise\n \"\"\"\n lines = content.split('\\n')\n\n for line in lines:\n # ATX-style heading\n match = re.match(r'^#\\s+(.+)

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, line)\n if match:\n title = match.group(1).strip()\n # Remove trailing #\n title = re.sub(r'\\s*#+\\s*

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, '', title)\n # Remove anchor\n title = re.sub(r'\\s*\\{#[^}]+\\}\\s*

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, '', title)\n return title\n\n # Setext-style heading\n if line.strip() and not line.startswith('#'):\n next_idx = lines.index(line) + 1\n if next_idx \u003c len(lines):\n next_line = lines[next_idx]\n if re.match(r'^=+\\s*

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, next_line):\n return line.strip()\n\n return None\n\n\ndef extract_existing_metadata(content: str) -> dict:\n \"\"\"\n Extract existing mark metadata from Markdown content.\n\n Args:\n content: Markdown content\n\n Returns:\n Dictionary of existing metadata\n \"\"\"\n metadata = {\n 'space': None,\n 'title': None,\n 'parents': [],\n 'labels': [],\n 'attachments': []\n }\n\n # Match metadata comments\n space_match = re.search(r'\u003c!-- Space:\\s*(.+?)\\s*-->', content)\n if space_match:\n metadata['space'] = space_match.group(1)\n\n title_match = re.search(r'\u003c!-- Title:\\s*(.+?)\\s*-->', content)\n if title_match:\n metadata['title'] = title_match.group(1)\n\n # Find all parents\n for match in re.finditer(r'\u003c!-- Parent:\\s*(.+?)\\s*-->', content):\n metadata['parents'].append(match.group(1))\n\n # Find all labels\n for match in re.finditer(r'\u003c!-- Label:\\s*(.+?)\\s*-->', content):\n metadata['labels'].append(match.group(1))\n\n # Find all attachments\n for match in re.finditer(r'\u003c!-- Attachment:\\s*(.+?)\\s*-->', content):\n metadata['attachments'].append(match.group(1))\n\n return metadata\n\n\ndef remove_existing_metadata(content: str) -> str:\n \"\"\"\n Remove existing mark metadata from content.\n\n Args:\n content: Markdown content with metadata\n\n Returns:\n Content without metadata\n \"\"\"\n # Remove all mark metadata comments\n content = re.sub(r'\u003c!-- Space:.*?-->\\n?', '', content)\n content = re.sub(r'\u003c!-- Title:.*?-->\\n?', '', content)\n content = re.sub(r'\u003c!-- Parent:.*?-->\\n?', '', content)\n content = re.sub(r'\u003c!-- Label:.*?-->\\n?', '', content)\n content = re.sub(r'\u003c!-- Attachment:.*?-->\\n?', '', content)\n\n # Remove leading blank lines\n content = re.sub(r'^\\n+', '', content)\n\n return content\n\n\ndef add_metadata_to_file(\n file_path: Path,\n metadata: MarkMetadata,\n infer_title: bool = True,\n preserve_existing: bool = False\n) -> None:\n \"\"\"\n Add or update mark metadata in a Markdown file.\n\n Args:\n file_path: Path to Markdown file\n metadata: Metadata to add\n infer_title: Infer title from first H1 heading if not provided\n preserve_existing: Preserve existing metadata values\n \"\"\"\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n\n # Extract existing metadata if preserving\n if preserve_existing:\n existing = extract_existing_metadata(content)\n\n # Merge with new metadata (new values take precedence)\n if not metadata.space and existing['space']:\n metadata.space = existing['space']\n if not metadata.title and existing['title']:\n metadata.title = existing['title']\n if not metadata.parents and existing['parents']:\n metadata.parents = existing['parents']\n if not metadata.labels and existing['labels']:\n metadata.labels = existing['labels']\n if not metadata.attachments and existing['attachments']:\n metadata.attachments = existing['attachments']\n\n # Infer title if requested and not provided\n if infer_title and not metadata.title:\n inferred_title = extract_title_from_markdown(content)\n if inferred_title:\n metadata.title = inferred_title\n\n # Remove any existing metadata\n clean_content = remove_existing_metadata(content)\n\n # Generate new metadata header\n header = metadata.to_header()\n\n # Combine header and content\n new_content = f\"{header}\\n\\n{clean_content}\"\n\n # Write back to file\n with open(file_path, 'w', encoding='utf-8') as f:\n f.write(new_content)\n\n\ndef main():\n \"\"\"Main entry point for CLI usage.\"\"\"\n parser = argparse.ArgumentParser(\n description='Generate mark-compatible metadata for Markdown files'\n )\n\n parser.add_argument(\n 'file',\n type=Path,\n help='Markdown file to add metadata to'\n )\n parser.add_argument(\n '--space', '-s',\n required=True,\n help='Confluence space key (e.g., DEV, PROJ)'\n )\n parser.add_argument(\n '--title', '-t',\n help='Page title (inferred from H1 if not provided)'\n )\n parser.add_argument(\n '--parent', '-p',\n help='Parent page title'\n )\n parser.add_argument(\n '--parents',\n help='Multiple parent pages (comma-separated, innermost first)'\n )\n parser.add_argument(\n '--labels', '-l',\n help='Page labels (comma-separated)'\n )\n parser.add_argument(\n '--attachments', '-a',\n help='Attachment file paths (comma-separated)'\n )\n parser.add_argument(\n '--no-infer-title',\n action='store_true',\n help='Do not infer title from first H1 heading'\n )\n parser.add_argument(\n '--preserve-existing',\n action='store_true',\n help='Preserve existing metadata values'\n )\n\n args = parser.parse_args()\n\n if not args.file.exists():\n print(f\"Error: File '{args.file}' not found\", file=sys.stderr)\n sys.exit(1)\n\n # Parse comma-separated lists\n parents = args.parents.split(',') if args.parents else []\n labels = args.labels.split(',') if args.labels else []\n attachments = args.attachments.split(',') if args.attachments else []\n\n # Clean up whitespace\n parents = [p.strip() for p in parents]\n labels = [l.strip() for l in labels]\n attachments = [a.strip() for a in attachments]\n\n # Create metadata\n metadata = MarkMetadata(\n space=args.space,\n title=args.title,\n parent=args.parent,\n parents=parents,\n labels=labels,\n attachments=attachments\n )\n\n try:\n add_metadata_to_file(\n args.file,\n metadata,\n infer_title=not args.no_infer_title,\n preserve_existing=args.preserve_existing\n )\n print(f\"Added metadata to {args.file}\")\n\n except Exception as e:\n print(f\"Error: {e}\", file=sys.stderr)\n sys.exit(1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8903,"content_sha256":"a1d75d511ddd40191f7776377ffd04111e352ba0e421b72e96e440d29a11a0b8"},{"filename":"scripts/mermaid_renderer.py","content":"\"\"\"\nMermaid Diagram Support for Confluence\n\nExtends md2cf's ConfluenceRenderer to detect Mermaid code blocks,\nrender them to SVG images, and convert to Confluence image macros.\n\nRequirements:\n - mermaid-cli (mmdc): npm install -g @mermaid-js/mermaid-cli\n - md2cf: pip install md2cf\n\nUsage:\n from mermaid_renderer import MermaidConfluenceRenderer\n import mistune\n\n renderer = MermaidConfluenceRenderer()\n markdown = \"# Title\\n\\n```mermaid\\ngraph TD\\n A-->B\\n```\"\n markdown_parser = mistune.Markdown(renderer=renderer)\n html = markdown_parser(markdown)\n\n # Access generated attachments\n for attachment in renderer.attachments:\n print(f\"Upload: {attachment}\")\n\"\"\"\n\nimport os\nimport sys\nimport subprocess\nimport tempfile\nimport hashlib\nfrom pathlib import Path\nfrom typing import List, Dict, Optional\n\ntry:\n from md2cf.confluence_renderer import ConfluenceRenderer\nexcept ImportError:\n print(\"ERROR: md2cf not installed. Install with: pip install md2cf\", file=sys.stderr)\n sys.exit(1)\n\n\nclass MermaidConfluenceRenderer(ConfluenceRenderer):\n \"\"\"\n Confluence renderer with Mermaid diagram support.\n\n Extends md2cf's ConfluenceRenderer to:\n - Detect ```mermaid code blocks\n - Render to SVG using mermaid-cli (mmdc)\n - Track attachments for upload\n - Convert to Confluence image macros\n \"\"\"\n\n def __init__(self, output_dir: Optional[str] = None):\n \"\"\"\n Initialize renderer.\n\n Args:\n output_dir: Directory for generated diagram files (default: temp dir)\n \"\"\"\n super().__init__()\n self.attachments: List[Dict[str, str]] = []\n self.output_dir = Path(output_dir) if output_dir else Path(tempfile.mkdtemp(prefix='mermaid_'))\n self.output_dir.mkdir(parents=True, exist_ok=True)\n self._mermaid_count = 0\n\n def block_code(self, code: str, lang: Optional[str] = None) -> str:\n \"\"\"\n Override block_code to handle Mermaid diagrams.\n\n Args:\n code: Code block content\n lang: Language identifier (e.g., 'python', 'mermaid')\n\n Returns:\n Confluence storage format HTML\n \"\"\"\n if lang and lang.lower() == 'mermaid':\n return self._render_mermaid(code)\n else:\n # Use parent renderer for regular code blocks\n return super().block_code(code, lang)\n\n def _render_mermaid(self, mermaid_code: str) -> str:\n \"\"\"\n Render Mermaid diagram to SVG and return image macro.\n\n Args:\n mermaid_code: Mermaid diagram syntax\n\n Returns:\n Confluence image macro HTML\n \"\"\"\n self._mermaid_count += 1\n\n # Generate unique filename\n code_hash = hashlib.md5(mermaid_code.encode()).hexdigest()[:8]\n filename = f\"diagram-{self._mermaid_count:03d}-{code_hash}.svg\"\n svg_path = self.output_dir / filename\n\n try:\n # Render Mermaid to SVG\n self._run_mmdc(mermaid_code, svg_path)\n\n # Track for attachment upload\n self.attachments.append({\n 'path': str(svg_path),\n 'filename': filename,\n 'type': 'image/svg+xml'\n })\n\n # Return Confluence image macro\n return f'\u003cp>\u003cac:image>\u003cri:attachment ri:filename=\"{filename}\"/>\u003c/ac:image>\u003c/p>'\n\n except Exception as e:\n # Fallback: render as regular code block with error comment\n print(f\"WARNING: Failed to render Mermaid diagram: {e}\", file=sys.stderr)\n fallback = super().block_code(mermaid_code, 'text')\n error_msg = f'\u003cp>\u003cac:structured-macro ac:name=\"warning\">\u003cac:rich-text-body>\u003cp>Mermaid diagram rendering failed: {e}\u003c/p>\u003c/ac:rich-text-body>\u003c/ac:structured-macro>\u003c/p>'\n return error_msg + fallback\n\n def _run_mmdc(self, mermaid_code: str, output_path: Path) -> None:\n \"\"\"\n Run mermaid-cli (mmdc) to render diagram.\n\n Args:\n mermaid_code: Mermaid syntax\n output_path: Path to output SVG file\n\n Raises:\n RuntimeError: If mmdc is not installed or rendering fails\n subprocess.CalledProcessError: If mmdc returns non-zero exit code\n \"\"\"\n # Check if mmdc is available\n if not self._check_mmdc_installed():\n raise RuntimeError(\n \"mermaid-cli (mmdc) not found. Install with: npm install -g @mermaid-js/mermaid-cli\"\n )\n\n # Write Mermaid code to temp file\n with tempfile.NamedTemporaryFile(mode='w', suffix='.mmd', delete=False) as f:\n f.write(mermaid_code)\n input_path = f.name\n\n try:\n # Run mmdc with explicit background and format\n result = subprocess.run(\n [\n 'mmdc',\n '-i', input_path,\n '-o', str(output_path),\n '-b', 'transparent', # Transparent background\n '-t', 'default' # Default theme\n ],\n capture_output=True,\n text=True,\n timeout=30,\n check=True\n )\n\n # Verify output file was created\n if not output_path.exists():\n raise RuntimeError(f\"mmdc did not create output file: {output_path}\")\n\n # Verify output file is not empty\n if output_path.stat().st_size == 0:\n raise RuntimeError(f\"mmdc created empty file: {output_path}\")\n\n except subprocess.TimeoutExpired:\n raise RuntimeError(\"mmdc timed out after 30 seconds\")\n\n except subprocess.CalledProcessError as e:\n error_msg = f\"mmdc failed with exit code {e.returncode}\"\n if e.stderr:\n error_msg += f\": {e.stderr}\"\n raise RuntimeError(error_msg)\n\n finally:\n # Clean up temp input file\n try:\n os.unlink(input_path)\n except OSError:\n pass\n\n @staticmethod\n def _check_mmdc_installed() -> bool:\n \"\"\"Check if mermaid-cli (mmdc) is installed and available\"\"\"\n try:\n result = subprocess.run(\n ['mmdc', '--version'],\n capture_output=True,\n timeout=5\n )\n return result.returncode == 0\n except (FileNotFoundError, subprocess.TimeoutExpired):\n return False\n\n def get_attachments(self) -> List[Dict[str, str]]:\n \"\"\"\n Get list of attachments generated during rendering.\n\n Returns:\n List of dicts with 'path', 'filename', 'type' keys\n \"\"\"\n return self.attachments\n\n def clear_attachments(self) -> None:\n \"\"\"Clear attachment list and reset counter\"\"\"\n self.attachments = []\n self._mermaid_count = 0\n\n\ndef test_mermaid_rendering():\n \"\"\"Test Mermaid rendering functionality\"\"\"\n import mistune\n\n # Sample Mermaid diagram\n markdown = \"\"\"\n# Test Document\n\nRegular paragraph.\n\n## Architecture Diagram\n\n```mermaid\ngraph TD\n A[Client] --> B[Load Balancer]\n B --> C[Web Server 1]\n B --> D[Web Server 2]\n C --> E[Database]\n D --> E\n```\n\n## Another Section\n\nMore content here.\n\n```python\ndef hello():\n print(\"Hello, Confluence!\")\n```\n\"\"\"\n\n renderer = MermaidConfluenceRenderer()\n\n try:\n # Use mistune 0.8.x API\n markdown_parser = mistune.Markdown(renderer=renderer)\n html = markdown_parser(markdown)\n\n print(\"✅ Rendering successful\")\n print(f\"\\nGenerated {len(renderer.attachments)} attachment(s):\")\n for att in renderer.attachments:\n print(f\" - {att['filename']} ({att['type']}) at {att['path']}\")\n\n print(f\"\\nHTML output preview (first 500 chars):\")\n print(html[:500] + \"...\")\n\n # Verify SVG files were created\n for att in renderer.attachments:\n path = Path(att['path'])\n if not path.exists():\n print(f\"❌ ERROR: Attachment file not found: {path}\")\n elif path.stat().st_size == 0:\n print(f\"❌ ERROR: Attachment file is empty: {path}\")\n else:\n print(f\"✅ Attachment file OK: {path} ({path.stat().st_size} bytes)\")\n\n except Exception as e:\n print(f\"❌ ERROR: {e}\", file=sys.stderr)\n import traceback\n traceback.print_exc()\n sys.exit(1)\n\n\nif __name__ == '__main__':\n test_mermaid_rendering()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8500,"content_sha256":"3db480bdc37cdbb0c8fa6e324b985fd4ea49329ff76fe071eb39dc772e8dea9c"},{"filename":"scripts/render_mermaid.py","content":"#!/usr/bin/env python3\n\"\"\"\nRender Mermaid diagrams to PNG or SVG images.\n\nThis script uses the Mermaid CLI (mmdc) to render Mermaid diagram code\nto image files. Requires @mermaid-js/mermaid-cli to be installed.\n\nInstallation:\n npm install -g @mermaid-js/mermaid-cli\n\nUsage:\n python render_mermaid.py input.mmd output.png\n python render_mermaid.py input.mmd output.svg\n python render_mermaid.py -c \"graph TD; A-->B\" output.png\n\"\"\"\n\nimport argparse\nimport subprocess\nimport sys\nimport tempfile\nfrom pathlib import Path\nfrom typing import Optional\n\n\ndef check_mermaid_cli() -> bool:\n \"\"\"\n Check if mermaid-cli (mmdc) is installed.\n\n Returns:\n True if mmdc is available, False otherwise\n \"\"\"\n try:\n result = subprocess.run(\n ['mmdc', '--version'],\n capture_output=True,\n text=True,\n check=False\n )\n return result.returncode == 0\n except FileNotFoundError:\n return False\n\n\ndef render_mermaid(\n input_path: Optional[Path] = None,\n output_path: Path = None,\n mermaid_code: Optional[str] = None,\n format: str = 'png',\n theme: str = 'default',\n background_color: str = 'white',\n width: Optional[int] = None,\n height: Optional[int] = None\n) -> bool:\n \"\"\"\n Render Mermaid diagram to image file.\n\n Args:\n input_path: Path to .mmd file (optional if mermaid_code provided)\n output_path: Path to output image file\n mermaid_code: Mermaid diagram code (optional if input_path provided)\n format: Output format ('png', 'svg', or 'pdf')\n theme: Mermaid theme ('default', 'forest', 'dark', 'neutral')\n background_color: Background color (e.g., 'white', 'transparent', '#f0f0f0')\n width: Output width in pixels (optional)\n height: Output height in pixels (optional)\n\n Returns:\n True if rendering succeeded, False otherwise\n \"\"\"\n if not check_mermaid_cli():\n print(\"Error: mermaid-cli (mmdc) is not installed\", file=sys.stderr)\n print(\"Install with: npm install -g @mermaid-js/mermaid-cli\", file=sys.stderr)\n return False\n\n # Create temporary file if code provided instead of file\n temp_file = None\n if mermaid_code:\n temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.mmd', delete=False)\n temp_file.write(mermaid_code)\n temp_file.close()\n input_path = Path(temp_file.name)\n\n if not input_path or not input_path.exists():\n print(f\"Error: Input file '{input_path}' not found\", file=sys.stderr)\n return False\n\n # Build mmdc command\n cmd = [\n 'mmdc',\n '-i', str(input_path),\n '-o', str(output_path),\n '-t', theme,\n '-b', background_color\n ]\n\n # Add width and height if specified\n if width:\n cmd.extend(['-w', str(width)])\n if height:\n cmd.extend(['-H', str(height)])\n\n try:\n result = subprocess.run(\n cmd,\n capture_output=True,\n text=True,\n check=False\n )\n\n if result.returncode != 0:\n print(f\"Error rendering Mermaid diagram:\", file=sys.stderr)\n print(result.stderr, file=sys.stderr)\n return False\n\n print(f\"Successfully rendered: {output_path}\")\n return True\n\n except Exception as e:\n print(f\"Error: {e}\", file=sys.stderr)\n return False\n\n finally:\n # Clean up temporary file\n if temp_file:\n Path(temp_file.name).unlink(missing_ok=True)\n\n\ndef extract_mermaid_from_markdown(markdown_text: str) -> list:\n \"\"\"\n Extract Mermaid diagram code blocks from Markdown text.\n\n Args:\n markdown_text: Markdown content\n\n Returns:\n List of Mermaid diagram code strings\n \"\"\"\n import re\n\n pattern = r'```mermaid\\s*\\n(.*?)\\n```'\n matches = re.findall(pattern, markdown_text, re.DOTALL)\n return matches\n\n\ndef render_markdown_diagrams(\n markdown_file: Path,\n output_dir: Path,\n format: str = 'png',\n prefix: str = 'diagram'\n) -> list:\n \"\"\"\n Extract and render all Mermaid diagrams from a Markdown file.\n\n Args:\n markdown_file: Path to Markdown file\n output_dir: Directory to save rendered diagrams\n format: Output format ('png', 'svg', or 'pdf')\n prefix: Prefix for output filenames\n\n Returns:\n List of tuples (diagram_index, output_path)\n \"\"\"\n with open(markdown_file, 'r', encoding='utf-8') as f:\n markdown_text = f.read()\n\n diagrams = extract_mermaid_from_markdown(markdown_text)\n output_dir.mkdir(parents=True, exist_ok=True)\n\n rendered_files = []\n\n for i, diagram_code in enumerate(diagrams):\n output_filename = f\"{prefix}-{i+1}.{format}\"\n output_path = output_dir / output_filename\n\n success = render_mermaid(\n mermaid_code=diagram_code,\n output_path=output_path,\n format=format\n )\n\n if success:\n rendered_files.append((i, output_path))\n\n return rendered_files\n\n\ndef main():\n \"\"\"Main entry point for CLI usage.\"\"\"\n parser = argparse.ArgumentParser(\n description='Render Mermaid diagrams to image files'\n )\n\n parser.add_argument(\n 'input',\n nargs='?',\n help='Input .mmd file or Markdown file'\n )\n parser.add_argument(\n 'output',\n nargs='?',\n help='Output image file path'\n )\n parser.add_argument(\n '-c', '--code',\n help='Mermaid diagram code (alternative to input file)'\n )\n parser.add_argument(\n '-f', '--format',\n choices=['png', 'svg', 'pdf'],\n default='png',\n help='Output format (default: png)'\n )\n parser.add_argument(\n '-t', '--theme',\n choices=['default', 'forest', 'dark', 'neutral'],\n default='default',\n help='Mermaid theme (default: default)'\n )\n parser.add_argument(\n '-b', '--background',\n default='white',\n help='Background color (default: white)'\n )\n parser.add_argument(\n '-w', '--width',\n type=int,\n help='Output width in pixels'\n )\n parser.add_argument(\n '-H', '--height',\n type=int,\n help='Output height in pixels'\n )\n parser.add_argument(\n '--extract-from-markdown',\n action='store_true',\n help='Extract and render all diagrams from Markdown file'\n )\n parser.add_argument(\n '--output-dir',\n type=Path,\n help='Output directory (for --extract-from-markdown)'\n )\n\n args = parser.parse_args()\n\n # Check if mmdc is installed\n if not check_mermaid_cli():\n sys.exit(1)\n\n # Extract from Markdown mode\n if args.extract_from_markdown:\n if not args.input:\n print(\"Error: Input Markdown file required\", file=sys.stderr)\n sys.exit(1)\n\n input_path = Path(args.input)\n output_dir = args.output_dir or Path('diagrams')\n\n rendered = render_markdown_diagrams(\n input_path,\n output_dir,\n format=args.format\n )\n\n print(f\"\\nRendered {len(rendered)} diagrams to {output_dir}/\")\n for idx, path in rendered:\n print(f\" [{idx+1}] {path.name}\")\n\n sys.exit(0)\n\n # Regular rendering mode\n if not args.code and not args.input:\n parser.print_help()\n sys.exit(1)\n\n if not args.output:\n print(\"Error: Output file path required\", file=sys.stderr)\n sys.exit(1)\n\n input_path = Path(args.input) if args.input else None\n output_path = Path(args.output)\n\n success = render_mermaid(\n input_path=input_path,\n output_path=output_path,\n mermaid_code=args.code,\n format=args.format,\n theme=args.theme,\n background_color=args.background,\n width=args.width,\n height=args.height\n )\n\n sys.exit(0 if success else 1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7965,"content_sha256":"9ce42610951c15c20a84e6eb9f44bd2e8baa60fdb7f46acd7f473480360b2886"},{"filename":"scripts/requirements.txt","content":"# Confluence Upload/Download Dependencies\n\n# Core dependencies for both upload and download\nrequests>=2.31.0\npython-dotenv>=1.0.0\nPyYAML>=6.0.1\n\n# Upload dependencies\natlassian-python-api>=3.41.0 # Confluence REST API client\nmd2cf>=1.0.0 # Markdown to Confluence storage format\nmistune==0.8.4 # Markdown parser (md2cf 1.0.x requires 0.8.x)\n\n# Download dependencies\nmarkdownify>=0.11.6 # HTML to Markdown conversion\nbeautifulsoup4>=4.12.0 # HTML parsing for macro transformation\n\n# Optional external tool (not Python):\n# - mermaid-cli (mmdc): npm install -g @mermaid-js/mermaid-cli\n# Required for Mermaid diagram rendering in uploads\n","content_type":"text/plain; charset=utf-8","language":null,"size":689,"content_sha256":"fa55f5ffd139ea3709329fc5135ddc8d90a4f55add86fc05ec6a9739b3332a3e"},{"filename":"scripts/upload_confluence_v2.py","content":"#!/usr/bin/env python3\n\"\"\"\nUpload Markdown to Confluence (v2 - Improved)\n\nCRITICAL: This version fixes major issues from v1:\n- ✅ Uses base ConfluenceRenderer (doesn't break regular images)\n- ✅ Handles all image types correctly (Mermaid, PlantUML, regular markdown)\n- ✅ Skips re-uploading existing attachments\n- ✅ Uses REST API directly (no MCP - avoids size limits)\n- ✅ Proper error handling for API responses\n\nIMPORTANT: DO NOT USE MCP FOR CONFLUENCE PAGE UPLOADS - size limits apply!\n\nUsage:\n # Update existing page with images\n python3 upload_confluence_v2.py document.md --id 780369923\n\n # Create new page\n python3 upload_confluence_v2.py document.md --space ARCP --parent-id 123456\n\n # Dry-run (preview without uploading)\n python3 upload_confluence_v2.py document.md --id 780369923 --dry-run\n\nRequirements:\n pip install atlassian-python-api md2cf python-dotenv PyYAML mistune\n\"\"\"\n\nimport sys\nimport argparse\nimport re\nimport os\nfrom pathlib import Path\nfrom typing import Dict, Optional, List, Tuple\nimport yaml\n\n# Check dependencies\ntry:\n import mistune\n from md2cf.confluence_renderer import ConfluenceRenderer\n from confluence_auth import get_confluence_client\nexcept ImportError as e:\n print(f\"ERROR: Missing dependency: {e}\", file=sys.stderr)\n print(\"Install with: pip install atlassian-python-api md2cf python-dotenv PyYAML mistune\", file=sys.stderr)\n sys.exit(1)\n\n\ndef parse_markdown_file(file_path: Path) -> Tuple[Dict, str, Optional[str]]:\n \"\"\"\n Parse markdown file and extract frontmatter, content, and title.\n\n Args:\n file_path: Path to markdown file\n\n Returns:\n Tuple of (frontmatter_dict, markdown_content, extracted_title)\n \"\"\"\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n\n # Check for YAML frontmatter\n frontmatter = {}\n markdown_content = content\n title = None\n\n # Parse frontmatter (between --- markers)\n if content.startswith('---\\n'):\n parts = content.split('---\\n', 2)\n if len(parts) >= 3:\n try:\n frontmatter = yaml.safe_load(parts[1]) or {}\n markdown_content = parts[2].strip()\n except yaml.YAMLError as e:\n print(f\"WARNING: Failed to parse YAML frontmatter: {e}\", file=sys.stderr)\n\n # Extract title from frontmatter\n if 'title' in frontmatter:\n title = frontmatter['title']\n\n # Fallback: extract title from first H1 heading\n if not title:\n match = re.search(r'^#\\s+(.+)

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, markdown_content, re.MULTILINE)\n if match:\n title = match.group(1).strip()\n\n # Last fallback: use filename\n if not title:\n title = file_path.stem.replace('_', ' ')\n\n return frontmatter, markdown_content, title\n\n\ndef convert_markdown_to_storage(markdown_content: str) -> Tuple[str, List[str]]:\n \"\"\"\n Convert markdown to Confluence storage format using md2cf.\n\n IMPORTANT: Uses base ConfluenceRenderer (NOT MermaidConfluenceRenderer)\n to avoid breaking regular markdown image handling.\n\n For Mermaid/PlantUML diagrams: Convert them to PNG/SVG files BEFORE calling\n this function, then use markdown image syntax: ![alt](path/to/image.png)\n\n Args:\n markdown_content: Markdown text with image paths\n\n Returns:\n Tuple of (storage_html, attachments_list)\n\n Example:\n markdown = '''\n # Document Title\n\n ![Architecture Diagram](./diagrams/architecture.png)\n\n Regular content here.\n '''\n\n storage_html, attachments = convert_markdown_to_storage(markdown)\n # attachments = ['./diagrams/architecture.png']\n \"\"\"\n # Use base ConfluenceRenderer (NOT MermaidConfluenceRenderer - it's broken)\n renderer = ConfluenceRenderer()\n\n # Parse markdown\n parser = mistune.Markdown(renderer=renderer)\n storage_html = parser(markdown_content)\n\n # Get attachments (image paths found in markdown)\n # Note: ConfluenceRenderer stores paths as strings in attachments list\n attachments = getattr(renderer, 'attachments', [])\n\n return storage_html, attachments\n\n\ndef upload_to_confluence(\n confluence,\n page_id: str,\n title: str,\n storage_html: str,\n attachments: List[str],\n space_key: Optional[str] = None,\n parent_id: Optional[str] = None,\n skip_existing_attachments: bool = True\n) -> Dict:\n \"\"\"\n Upload page content and attachments to Confluence via REST API.\n\n Args:\n confluence: Confluence client instance\n page_id: Page ID (for updates) or None (for creates)\n title: Page title\n storage_html: Content in Confluence storage format\n attachments: List of file paths to upload as attachments\n space_key: Space key (required for creates)\n parent_id: Optional parent page ID\n skip_existing_attachments: If True, skip uploading attachments that already exist\n\n Returns:\n Dict with 'id', 'title', 'version', 'url' keys\n \"\"\"\n if page_id:\n # UPDATE MODE\n # Get current page version\n try:\n page_info = confluence.get_page_by_id(page_id, expand='version')\n current_version = page_info['version']['number']\n except Exception as e:\n raise ValueError(f\"Failed to fetch current version for page {page_id}: {e}\")\n\n new_version = current_version + 1\n\n print(f\"📄 Updating page {page_id}\")\n print(f\" Current version: {current_version}\")\n print(f\" New version: {new_version}\")\n print(f\" Storage content length: {len(storage_html)} characters\")\n print(f\" Attachments to upload: {len(attachments)}\")\n\n # Update page content\n try:\n result = confluence.update_page(\n page_id=page_id,\n title=title,\n body=storage_html,\n parent_id=parent_id,\n type='page',\n representation='storage', # CRITICAL: Must be 'storage' format\n minor_edit=False,\n version_comment=f\"Updated with images (v{current_version} → v{new_version})\"\n )\n\n print(f\"✅ Page updated successfully\")\n print(f\" Version: {result.get('version', {}).get('number', 'unknown')}\")\n\n except Exception as e:\n print(f\"❌ ERROR updating page: {e}\")\n raise\n\n # Upload attachments\n if attachments:\n print(f\"\\n📎 Uploading {len(attachments)} attachments...\")\n _upload_attachments(confluence, page_id, attachments, skip_existing_attachments)\n\n return {\n 'id': result['id'],\n 'title': result['title'],\n 'version': result.get('version', {}).get('number', 'unknown'),\n 'url': confluence.url + result['_links']['webui']\n }\n\n else:\n # CREATE MODE\n if not space_key:\n raise ValueError(\"space_key is required to create new page\")\n\n print(f\"📄 Creating new page in space {space_key}\")\n print(f\" Storage content length: {len(storage_html)} characters\")\n print(f\" Attachments to upload: {len(attachments)}\")\n\n try:\n result = confluence.create_page(\n space=space_key,\n title=title,\n body=storage_html,\n parent_id=parent_id,\n type='page',\n representation='storage'\n )\n\n new_page_id = result['id']\n print(f\"✅ Page created successfully\")\n print(f\" Page ID: {new_page_id}\")\n print(f\" Version: {result.get('version', {}).get('number', 'unknown')}\")\n\n except Exception as e:\n print(f\"❌ ERROR creating page: {e}\")\n raise\n\n # Upload attachments\n if attachments:\n print(f\"\\n📎 Uploading {len(attachments)} attachments...\")\n _upload_attachments(confluence, new_page_id, attachments, skip_existing_attachments)\n\n return {\n 'id': result['id'],\n 'title': result['title'],\n 'version': result.get('version', {}).get('number', 'unknown'),\n 'url': confluence.url + result['_links']['webui']\n }\n\n\ndef _upload_attachments(\n confluence,\n page_id: str,\n attachments: List[str],\n skip_existing: bool = True\n) -> None:\n \"\"\"\n Upload attachment files to a Confluence page.\n\n Args:\n confluence: Confluence client instance\n page_id: Page ID to attach files to\n attachments: List of file paths to upload\n skip_existing: If True, skip files that already exist as attachments\n \"\"\"\n for i, attachment_path in enumerate(attachments, 1):\n filename = os.path.basename(attachment_path)\n print(f\" {i}. {filename}...\", end=' ')\n\n # Verify file exists\n if not os.path.exists(attachment_path):\n print(f\"❌ File not found: {attachment_path}\")\n continue\n\n try:\n # Check if attachment already exists\n if skip_existing:\n existing_attachments = confluence.get_attachments_from_content(page_id)\n already_exists = any(\n att['title'] == filename\n for att in existing_attachments.get('results', [])\n )\n\n if already_exists:\n print(\"(already exists, skipping)\")\n continue\n\n # Determine content type from file extension\n ext = os.path.splitext(filename)[1].lower()\n content_type_map = {\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.pdf': 'application/pdf'\n }\n content_type = content_type_map.get(ext, 'application/octet-stream')\n\n # Upload attachment\n confluence.attach_file(\n filename=attachment_path,\n name=filename,\n content_type=content_type,\n page_id=page_id,\n comment=f\"Uploaded via upload_confluence_v2.py\"\n )\n print(\"✅\")\n\n except Exception as e:\n print(f\"❌ Error: {e}\")\n\n\ndef dry_run_preview(\n title: str,\n content: str,\n space_key: Optional[str],\n page_id: Optional[str],\n parent_id: Optional[str],\n attachments: List[str]\n) -> None:\n \"\"\"Print preview of what would be uploaded\"\"\"\n print(\"=\" * 70)\n print(\"DRY-RUN MODE - No changes will be made\")\n print(\"=\" * 70)\n\n mode = \"UPDATE\" if page_id else \"CREATE\"\n print(f\"\\nMode: {mode}\")\n print(f\"Title: {title}\")\n\n if page_id:\n print(f\"Page ID: {page_id}\")\n else:\n print(f\"Space: {space_key}\")\n\n if parent_id:\n print(f\"Parent ID: {parent_id}\")\n\n if attachments:\n print(f\"\\nAttachments ({len(attachments)}):\")\n for att in attachments:\n exists = \"✅\" if os.path.exists(att) else \"❌ NOT FOUND\"\n print(f\" - {att} {exists}\")\n\n print(f\"\\nContent preview (first 500 chars):\")\n print(\"-\" * 70)\n print(content[:500])\n if len(content) > 500:\n print(\"...\")\n print(\"-\" * 70)\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description='Upload Markdown to Confluence (v2 - Improved)',\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nExamples:\n # Update existing page with images\n %(prog)s document.md --id 780369923\n\n # Create new page\n %(prog)s document.md --space ARCP --parent-id 123456\n\n # Dry-run preview\n %(prog)s document.md --id 780369923 --dry-run\n\n # Use custom credentials file\n %(prog)s document.md --id 780369923 --env-file /path/to/.env.jira\n\nIMPORTANT:\n - For Mermaid/PlantUML diagrams: Convert to PNG/SVG FIRST, then reference\n in markdown using: ![alt](path/to/diagram.png)\n - DO NOT use MCP for page uploads - use this script (no size limits)\n - Images are automatically detected from markdown image syntax\n \"\"\"\n )\n\n parser.add_argument('file', type=str, help='Markdown file to upload')\n parser.add_argument('--id', type=str, help='Page ID (for updates)')\n parser.add_argument('--space', type=str, help='Space key (required for new pages)')\n parser.add_argument('--title', type=str, help='Page title (overrides frontmatter/H1)')\n parser.add_argument('--parent-id', type=str, help='Parent page ID')\n parser.add_argument('--dry-run', action='store_true', help='Preview without uploading')\n parser.add_argument('--env-file', type=str, help='Path to .env file with credentials')\n parser.add_argument('--force-reupload', action='store_true',\n help='Re-upload all attachments even if they already exist')\n\n args = parser.parse_args()\n\n # Validate file\n file_path = Path(args.file)\n if not file_path.exists():\n print(f\"ERROR: File not found: {args.file}\", file=sys.stderr)\n sys.exit(1)\n\n # Parse markdown file\n try:\n frontmatter, markdown_content, extracted_title = parse_markdown_file(file_path)\n except Exception as e:\n print(f\"ERROR: Failed to parse markdown file: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # Determine parameters (CLI flags override frontmatter)\n title = args.title or extracted_title\n page_id = args.id or frontmatter.get('confluence', {}).get('id')\n space_key = args.space or frontmatter.get('confluence', {}).get('space')\n parent_id = args.parent_id or frontmatter.get('parent', {}).get('id')\n\n # Validate required parameters\n if not page_id and not space_key:\n print(\"ERROR: Either --id (for update) or --space (for create) must be provided\", file=sys.stderr)\n print(\" or ensure frontmatter contains 'confluence.id' or 'confluence.space'\", file=sys.stderr)\n sys.exit(1)\n\n # Convert markdown to storage format\n try:\n print(f\"\\n📖 Reading markdown file: {args.file}\")\n print(f\" Length: {len(markdown_content)} characters\")\n\n print(f\"\\n🔄 Converting markdown to Confluence storage format...\")\n storage_content, attachments = convert_markdown_to_storage(markdown_content)\n\n print(f\" Storage HTML length: {len(storage_content)} characters\")\n print(f\" Found {len(attachments)} images:\")\n for att in attachments:\n print(f\" - {att}\")\n\n except Exception as e:\n print(f\"ERROR: Conversion failed: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # Dry-run mode\n if args.dry_run:\n dry_run_preview(title, storage_content, space_key, page_id, parent_id, attachments)\n return\n\n # Get Confluence client\n try:\n confluence = get_confluence_client(env_file=args.env_file)\n except Exception as e:\n print(f\"ERROR: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # Upload to Confluence\n print(f\"\\n📤 Uploading to Confluence...\")\n print(\"=\" * 70)\n\n try:\n result = upload_to_confluence(\n confluence=confluence,\n page_id=page_id,\n title=title,\n storage_html=storage_content,\n attachments=attachments,\n space_key=space_key,\n parent_id=parent_id,\n skip_existing_attachments=not args.force_reupload\n )\n\n print(\"=\" * 70)\n print(\"✅ UPLOAD COMPLETE!\")\n print(f\" Title: {result['title']}\")\n print(f\" ID: {result['id']}\")\n print(f\" Version: {result['version']}\")\n print(f\" URL: {result['url']}\")\n if attachments:\n print(f\" Attachments: {len(attachments)}\")\n print(\"=\" * 70)\n\n except Exception as e:\n print(\"=\" * 70)\n print(f\"❌ ERROR: {e}\", file=sys.stderr)\n print(\"=\" * 70)\n sys.exit(1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15908,"content_sha256":"782656d977e98911f898e1220f65cc12948ee1282cc646b6a03078ef892466cc"},{"filename":"scripts/upload_confluence.py","content":"#!/usr/bin/env python3\n\"\"\"\nUpload Markdown to Confluence\n\nConverts Markdown files to Confluence storage format and uploads via REST API.\nSupports frontmatter-based smart uploads, version management, and Mermaid diagrams.\n\nUsage:\n # Smart upload (reads from frontmatter)\n python3 upload_confluence.py page.md\n\n # Update specific page by ID\n python3 upload_confluence.py page.md --id 450855912\n\n # Create new page\n python3 upload_confluence.py page.md --space ARCP --parent-id 123456\n\n # Dry-run (preview without uploading)\n python3 upload_confluence.py page.md --dry-run\n\nRequirements:\n pip install atlassian-python-api md2cf python-dotenv PyYAML mistune\n npm install -g @mermaid-js/mermaid-cli # Optional, for Mermaid diagrams\n\"\"\"\n\nimport sys\nimport argparse\nimport re\nfrom pathlib import Path\nfrom typing import Dict, Optional, List, Tuple\nimport yaml\n\n# Check dependencies\ntry:\n import mistune\n from confluence_auth import get_confluence_client\n from mermaid_renderer import MermaidConfluenceRenderer\nexcept ImportError as e:\n print(f\"ERROR: Missing dependency: {e}\", file=sys.stderr)\n print(\"Install with: pip install atlassian-python-api md2cf python-dotenv PyYAML mistune\", file=sys.stderr)\n sys.exit(1)\n\n\ndef parse_markdown_file(file_path: Path) -> Tuple[Dict, str, Optional[str]]:\n \"\"\"\n Parse markdown file and extract frontmatter, content, and title.\n\n Args:\n file_path: Path to markdown file\n\n Returns:\n Tuple of (frontmatter_dict, markdown_content, extracted_title)\n \"\"\"\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n\n # Check for YAML frontmatter\n frontmatter = {}\n markdown_content = content\n title = None\n\n # Parse frontmatter (between --- markers)\n if content.startswith('---\\n'):\n parts = content.split('---\\n', 2)\n if len(parts) >= 3:\n try:\n frontmatter = yaml.safe_load(parts[1]) or {}\n markdown_content = parts[2].strip()\n except yaml.YAMLError as e:\n print(f\"WARNING: Failed to parse YAML frontmatter: {e}\", file=sys.stderr)\n\n # Extract title from frontmatter\n if 'title' in frontmatter:\n title = frontmatter['title']\n\n # Fallback: extract title from first H1 heading\n if not title:\n match = re.search(r'^#\\s+(.+)

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…

, markdown_content, re.MULTILINE)\n if match:\n title = match.group(1).strip()\n\n # Last fallback: use filename\n if not title:\n title = file_path.stem.replace('_', ' ')\n\n return frontmatter, markdown_content, title\n\n\ndef convert_to_storage_format(markdown_content: str, output_dir: Optional[Path] = None) -> Tuple[str, List[Dict]]:\n \"\"\"\n Convert Markdown to Confluence storage format.\n\n Args:\n markdown_content: Markdown text\n output_dir: Directory for Mermaid diagrams (optional)\n\n Returns:\n Tuple of (storage_format_html, attachments_list)\n \"\"\"\n renderer = MermaidConfluenceRenderer(output_dir=str(output_dir) if output_dir else None)\n\n try:\n # Use mistune 0.8.x API\n markdown_parser = mistune.Markdown(renderer=renderer)\n html = markdown_parser(markdown_content)\n attachments = renderer.get_attachments()\n return html, attachments\n except Exception as e:\n print(f\"ERROR: Markdown conversion failed: {e}\", file=sys.stderr)\n raise\n\n\ndef upload_to_confluence(\n confluence,\n title: str,\n content: str,\n space_key: Optional[str] = None,\n page_id: Optional[str] = None,\n parent_id: Optional[str] = None,\n version: Optional[int] = None,\n attachments: Optional[List[Dict]] = None\n) -> Dict:\n \"\"\"\n Upload or update page in Confluence.\n\n Args:\n confluence: Confluence client instance\n title: Page title\n content: Page content (storage format)\n space_key: Space key (required for create)\n page_id: Page ID (for updates)\n parent_id: Parent page ID (optional)\n version: Current version number (required for updates)\n attachments: List of attachment dicts with 'path', 'filename', 'type'\n\n Returns:\n Page metadata dict with 'id', 'title', 'url', 'version'\n \"\"\"\n # Determine mode: create or update\n if page_id:\n # UPDATE mode\n if not version:\n # Fetch current version\n try:\n page_info = confluence.get_page_by_id(page_id, expand='version')\n version = page_info['version']['number']\n except Exception as e:\n raise ValueError(f\"Failed to fetch current version for page {page_id}: {e}\")\n\n new_version = version + 1\n\n try:\n result = confluence.update_page(\n page_id=page_id,\n title=title,\n body=content,\n parent_id=parent_id,\n type='page',\n representation='storage',\n minor_edit=False,\n version_comment=f\"Updated via upload_confluence.py (v{version} → v{new_version})\"\n )\n\n # Upload attachments\n if attachments:\n for att in attachments:\n confluence.attach_file(\n filename=att['path'],\n name=att['filename'],\n content_type=att['type'],\n page_id=page_id,\n comment=f\"Uploaded via upload_confluence.py\"\n )\n\n return {\n 'id': result['id'],\n 'title': result['title'],\n 'version': result['version']['number'],\n 'url': confluence.url + result['_links']['webui']\n }\n\n except Exception as e:\n raise RuntimeError(f\"Failed to update page {page_id}: {e}\")\n\n else:\n # CREATE mode\n if not space_key:\n raise ValueError(\"space_key is required to create new page\")\n\n try:\n result = confluence.create_page(\n space=space_key,\n title=title,\n body=content,\n parent_id=parent_id,\n type='page',\n representation='storage'\n )\n\n new_page_id = result['id']\n\n # Upload attachments\n if attachments:\n for att in attachments:\n confluence.attach_file(\n filename=att['path'],\n name=att['filename'],\n content_type=att['type'],\n page_id=new_page_id,\n comment=f\"Uploaded via upload_confluence.py\"\n )\n\n return {\n 'id': result['id'],\n 'title': result['title'],\n 'version': result['version']['number'],\n 'url': confluence.url + result['_links']['webui']\n }\n\n except Exception as e:\n raise RuntimeError(f\"Failed to create page '{title}' in space {space_key}: {e}\")\n\n\ndef dry_run_preview(\n title: str,\n content: str,\n space_key: Optional[str],\n page_id: Optional[str],\n parent_id: Optional[str],\n version: Optional[int],\n attachments: Optional[List[Dict]]\n) -> None:\n \"\"\"Print preview of what would be uploaded\"\"\"\n print(\"=\" * 70)\n print(\"DRY-RUN MODE - No changes will be made\")\n print(\"=\" * 70)\n\n mode = \"UPDATE\" if page_id else \"CREATE\"\n print(f\"\\nMode: {mode}\")\n print(f\"Title: {title}\")\n\n if page_id:\n print(f\"Page ID: {page_id}\")\n if version:\n print(f\"Version: {version} → {version + 1}\")\n else:\n print(f\"Space: {space_key}\")\n\n if parent_id:\n print(f\"Parent ID: {parent_id}\")\n\n if attachments:\n print(f\"\\nAttachments ({len(attachments)}):\")\n for att in attachments:\n print(f\" - {att['filename']} ({att['type']})\")\n\n print(f\"\\nContent preview (first 500 chars):\")\n print(\"-\" * 70)\n print(content[:500])\n if len(content) > 500:\n print(\"...\")\n print(\"-\" * 70)\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description='Upload Markdown to Confluence',\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nExamples:\n # Smart upload from frontmatter\n %(prog)s page.md\n\n # Update specific page\n %(prog)s page.md --id 450855912\n\n # Create new page\n %(prog)s page.md --space ARCP --parent-id 123456\n\n # Dry-run preview\n %(prog)s page.md --dry-run\n\n # Use custom credentials file\n %(prog)s page.md --env-file /path/to/.env\n \"\"\"\n )\n\n parser.add_argument('file', type=str, help='Markdown file to upload')\n parser.add_argument('--id', type=str, help='Page ID (for updates)')\n parser.add_argument('--space', type=str, help='Space key (required for new pages)')\n parser.add_argument('--title', type=str, help='Page title (overrides frontmatter/H1)')\n parser.add_argument('--parent-id', type=str, help='Parent page ID (specify parent to move page)')\n parser.add_argument('--ignore-frontmatter', action='store_true',\n help='Ignore parent_id in frontmatter (update page in place without moving)')\n parser.add_argument('--dry-run', action='store_true', help='Preview without uploading')\n parser.add_argument('--env-file', type=str, help='Path to .env file with credentials')\n parser.add_argument('--update-frontmatter', action='store_true',\n help='Update markdown file frontmatter after upload')\n parser.add_argument('--output-dir', type=str, help='Directory for generated diagrams')\n\n args = parser.parse_args()\n\n # Validate file\n file_path = Path(args.file)\n if not file_path.exists():\n print(f\"ERROR: File not found: {args.file}\", file=sys.stderr)\n sys.exit(1)\n\n # Parse markdown file\n try:\n frontmatter, markdown_content, extracted_title = parse_markdown_file(file_path)\n except Exception as e:\n print(f\"ERROR: Failed to parse markdown file: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # Determine parameters (CLI flags override frontmatter)\n title = args.title or extracted_title\n page_id = args.id or frontmatter.get('confluence', {}).get('id')\n space_key = args.space or frontmatter.get('confluence', {}).get('space')\n\n # Handle parent_id with --ignore-frontmatter option\n if args.ignore_frontmatter:\n # Only use --parent-id if explicitly provided, don't read from frontmatter\n parent_id = args.parent_id\n else:\n # Default behavior: CLI --parent-id overrides frontmatter\n parent_id = args.parent_id or frontmatter.get('parent', {}).get('id')\n\n version = frontmatter.get('confluence', {}).get('version')\n\n # Validate required parameters\n if not page_id and not space_key:\n print(\"ERROR: Either --id (for update) or --space (for create) must be provided\", file=sys.stderr)\n print(\" or ensure frontmatter contains 'confluence.id' or 'confluence.space'\", file=sys.stderr)\n sys.exit(1)\n\n # Convert markdown to storage format\n try:\n output_dir = Path(args.output_dir) if args.output_dir else None\n storage_content, attachments = convert_to_storage_format(markdown_content, output_dir)\n except Exception as e:\n print(f\"ERROR: Conversion failed: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # Dry-run mode\n if args.dry_run:\n dry_run_preview(title, storage_content, space_key, page_id, parent_id, version, attachments)\n return\n\n # Get Confluence client\n try:\n confluence = get_confluence_client(env_file=args.env_file)\n except Exception as e:\n print(f\"ERROR: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # Upload to Confluence\n try:\n result = upload_to_confluence(\n confluence=confluence,\n title=title,\n content=storage_content,\n space_key=space_key,\n page_id=page_id,\n parent_id=parent_id,\n version=version,\n attachments=attachments\n )\n\n print(\"✅ Upload successful!\")\n print(f\" Title: {result['title']}\")\n print(f\" ID: {result['id']}\")\n print(f\" Version: {result['version']}\")\n print(f\" URL: {result['url']}\")\n\n if attachments:\n print(f\" Attachments uploaded: {len(attachments)}\")\n\n # Update frontmatter if requested\n if args.update_frontmatter:\n # TODO: Implement frontmatter update\n print(\"\\nNote: --update-frontmatter not yet implemented\")\n\n except Exception as e:\n print(f\"❌ ERROR: {e}\", file=sys.stderr)\n sys.exit(1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12789,"content_sha256":"a17b1eeccdd9edc26af74fd52d40beab128926a32323e94bbfb1887068fea5e8"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Confluence Management Skill","type":"text"}]},{"type":"paragraph","content":[{"text":"Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Table of Contents","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Quick Decision Matrix","type":"text","marks":[{"type":"link","attrs":{"href":"#quick-decision-matrix","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"MCP Size Limits","type":"text","marks":[{"type":"link","attrs":{"href":"#mcp-size-limits","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prerequisites","type":"text","marks":[{"type":"link","attrs":{"href":"#prerequisites","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Core Workflows","type":"text","marks":[{"type":"link","attrs":{"href":"#core-workflows","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reference Documentation","type":"text","marks":[{"type":"link","attrs":{"href":"#reference-documentation","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Decision Matrix","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Task","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tool","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Notes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Read pages","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MCP tools","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_get_page","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"confluence_search","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Small text-only uploads (\u003c10KB)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MCP tools","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_create_page","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"confluence_update_page","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Large documents (>10KB)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"upload_confluence_v2.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"REST API, no size limits","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Documents with images","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"upload_confluence_v2.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Handles attachments automatically","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Git-to-Confluence sync","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mark CLI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Best for CI/CD workflows","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download pages to Markdown","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"download_confluence.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Converts macros, downloads attachments","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"MCP Size Limits","type":"text"}]},{"type":"paragraph","content":[{"text":"MCP tools have size limits (10-20KB) for uploads. For large documents or pages with images, use the REST API via ","type":"text"},{"text":"upload_confluence_v2.py","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Upload large document\npython3 ~/.claude/skills/confluence/scripts/upload_confluence_v2.py \\\n document.md --id 780369923\n\n# Dry-run preview\npython3 ~/.claude/skills/confluence/scripts/upload_confluence_v2.py \\\n document.md --id 780369923 --dry-run","type":"text"}]},{"type":"paragraph","content":[{"text":"MCP works for reading pages but not for uploading large content.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Required","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Atlassian MCP Server","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"mcp__atlassian-evinova","type":"text","marks":[{"type":"code_inline"}]},{"text":") with Confluence credentials","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Optional","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"mark CLI","type":"text","marks":[{"type":"strong"}]},{"text":": Git-to-Confluence sync (","type":"text"},{"text":"brew install kovetskiy/mark/mark","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mermaid CLI","type":"text","marks":[{"type":"strong"}]},{"text":": Diagram rendering (","type":"text"},{"text":"npm install -g @mermaid-js/mermaid-cli","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Core Workflows","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Download Pages to Markdown","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Single page\npython3 ~/.claude/skills/confluence/scripts/download_confluence.py 123456789\n\n# With child pages\npython3 ~/.claude/skills/confluence/scripts/download_confluence.py --download-children 123456789\n\n# Custom output directory\npython3 ~/.claude/skills/confluence/scripts/download_confluence.py --output-dir ./docs 123456789","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"Downloading Guide","type":"text","marks":[{"type":"link","attrs":{"href":"references/conversion_guide.md","title":null}}]},{"text":" for details.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Upload Pages with Images","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Convert diagrams to images first using ","type":"text"},{"text":"design-doc-mermaid","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"plantuml","type":"text","marks":[{"type":"code_inline"}]},{"text":" skills","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reference images with standard markdown: ","type":"text"},{"text":"![Description](./images/diagram.png)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Upload via REST API:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 ~/.claude/skills/confluence/scripts/upload_confluence_v2.py \\\n document.md --id PAGE_ID","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"Image Handling Best Practices","type":"text","marks":[{"type":"link","attrs":{"href":"references/image_handling_best_practices.md","title":null}}]},{"text":" for details.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Search Confluence","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"javascript"},"content":[{"text":"mcp__atlassian-evinova__confluence_search({\n query: 'space = \"DEV\" AND text ~ \"API\"',\n limit: 10\n})","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Create/Update Pages (Small Documents)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"javascript"},"content":[{"text":"// Create page\nmcp__atlassian-evinova__confluence_create_page({\n space_key: \"DEV\",\n title: \"API Documentation\",\n content: \"h1. Overview\\n\\nContent here...\",\n content_format: \"wiki\"\n})\n\n// Update page\nmcp__atlassian-evinova__confluence_update_page({\n page_id: \"123456789\",\n title: \"Updated Title\",\n content: \"h1. New Content\",\n version_comment: \"Updated via Claude Code\"\n})","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Sync from Git (mark CLI)","type":"text"}]},{"type":"paragraph","content":[{"text":"Add metadata to Markdown files:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"markdown"},"content":[{"text":"\u003c!-- Space: DEV -->\n\u003c!-- Parent: Documentation -->\n\u003c!-- Title: API Guide -->\n\n# API Guide\nContent...","type":"text"}]},{"type":"paragraph","content":[{"text":"Sync to Confluence:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mark -f documentation.md\nmark --dry-run -f documentation.md # Preview first","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"mark Tool Guide","type":"text","marks":[{"type":"link","attrs":{"href":"references/mark_tool_guide.md","title":null}}]},{"text":" for details.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Convert Between Formats","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"Conversion Guide","type":"text","marks":[{"type":"link","attrs":{"href":"references/conversion_guide.md","title":null}}]},{"text":" for the complete conversion matrix.","type":"text"}]},{"type":"paragraph","content":[{"text":"Quick reference:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Markdown","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wiki Markup","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"# Heading","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"h1. Heading","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"**bold**","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"*bold*","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"*italic*","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"_italic_","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"`code`","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{{code}}","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"[text](url)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"[text|url]","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reference Documentation","type":"text"}]},{"type":"paragraph","content":[{"text":"Detailed guides in the ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Guide","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wiki Markup Reference","type":"text","marks":[{"type":"link","attrs":{"href":"references/wiki_markup_guide.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Complete syntax for Confluence Wiki Markup","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Conversion Guide","type":"text","marks":[{"type":"link","attrs":{"href":"references/conversion_guide.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Markdown to Wiki Markup conversion rules","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Storage Format","type":"text","marks":[{"type":"link","attrs":{"href":"references/confluence_storage_format.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Confluence XML storage format details","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Image Handling","type":"text","marks":[{"type":"link","attrs":{"href":"references/image_handling_best_practices.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Workflows for images, Mermaid, PlantUML","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mark Tool Guide","type":"text","marks":[{"type":"link","attrs":{"href":"references/mark_tool_guide.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Git-to-Confluence sync with mark CLI","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Troubleshooting","type":"text","marks":[{"type":"link","attrs":{"href":"references/troubleshooting_guide.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Common errors and solutions","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Available MCP Tools","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tool","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_search","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Search using CQL or text","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_get_page","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Retrieve page by ID or title","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_create_page","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create new page","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_update_page","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Update existing page","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_delete_page","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Delete page","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_get_page_children","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Get child pages","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_add_label","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add label to page","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_get_labels","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Get page labels","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_add_comment","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add comment to page","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"confluence_get_comments","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Get page comments","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Utility Scripts","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/upload_confluence_v2.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Upload large documents with images","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/download_confluence.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Download pages to Markdown","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/convert_markdown_to_wiki.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Convert Markdown to Wiki Markup","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/convert_wiki_to_markdown.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Convert Wiki Markup to Markdown","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/render_mermaid.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Render Mermaid diagrams","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Version","type":"text","marks":[{"type":"strong"}]},{"text":": 2.1.0 | ","type":"text"},{"text":"Last Updated","type":"text","marks":[{"type":"strong"}]},{"text":": 2025-01-21","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"confluence","author":"@skillopedia","source":{"stars":46,"repo_name":"confluence-skill","origin_url":"https://github.com/spillwavesolutions/confluence-skill/blob/HEAD/SKILL.md","repo_owner":"spillwavesolutions","body_sha256":"822f60a1180644d88dec08c64a88f379e1616daf11a220379116c60ebb29e534","cluster_key":"8103fea6721089ccbbea6c61081fd9236c6fb879e068ed1c8175f28e7088b595","clean_bundle":{"format":"clean-skill-bundle-v1","source":"spillwavesolutions/confluence-skill/SKILL.md","attachments":[{"id":"a02ea090-597d-52ef-921f-13811d95f077","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a02ea090-597d-52ef-921f-13811d95f077/attachment.md","path":"INSTALLATION.md","size":6233,"sha256":"e5b1f5e2cb4981e4d9d06da5c80f52888cdd7c10a09e0ef97c38272d222395ac","contentType":"text/markdown; charset=utf-8"},{"id":"211fef2a-b513-511c-bd4b-5e7b2c25527b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/211fef2a-b513-511c-bd4b-5e7b2c25527b/attachment.md","path":"PARENT_RELATIONSHIP_GUIDE.md","size":17194,"sha256":"c22887280e54eb2bac11ad82bec1871cf01c3e41b266bcdc4c2f7ed515eac0d0","contentType":"text/markdown; charset=utf-8"},{"id":"9f7000c7-4a0c-5209-9f5a-a513f519b028","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9f7000c7-4a0c-5209-9f5a-a513f519b028/attachment.md","path":"QUICK_REFERENCE.md","size":3810,"sha256":"3c166a5e93600bed02bb396bcc8ca3bbc04c95e852764fa4b0233540366b4bf0","contentType":"text/markdown; charset=utf-8"},{"id":"1bcdaad0-01be-5c1d-b32d-f21bd397ed3a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1bcdaad0-01be-5c1d-b32d-f21bd397ed3a/attachment.md","path":"README.md","size":32380,"sha256":"ed057e034a4b0e4d72971234f2a56e41858fe6ae279b531f43d8d9b1c37aa9e6","contentType":"text/markdown; charset=utf-8"},{"id":"dec4d370-41d0-58d9-9492-8c7285ed648c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dec4d370-41d0-58d9-9492-8c7285ed648c/attachment.example","path":"examples/.env.confluence.example","size":734,"sha256":"b3d8e25a05edcb7159f1ffe6a22cb79d8657373aad9222e5e784da10c9dec0ca","contentType":"text/plain; charset=utf-8"},{"id":"7b02baa4-dc67-533c-b709-6b66b1d82561","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7b02baa4-dc67-533c-b709-6b66b1d82561/attachment.example","path":"examples/.env.example","size":585,"sha256":"c8f031866561c38a02943736ce5d01c2873d7ecbea649e2223b660b8368c5551","contentType":"text/plain; charset=utf-8"},{"id":"cc1f21ec-6934-53e8-a690-4687881c472c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cc1f21ec-6934-53e8-a690-4687881c472c/attachment.txt","path":"examples/page_ids.example.txt","size":415,"sha256":"19895f6b1804387b147550ae7bb2b224bfaeb7750690afbf27a75a1fc4c6cd97","contentType":"text/plain; charset=utf-8"},{"id":"4234a7e0-57d7-5c6a-9f97-62f890ace676","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4234a7e0-57d7-5c6a-9f97-62f890ace676/attachment.md","path":"examples/sample-confluence-page.md","size":3641,"sha256":"699aa1348597e370b93c314cbcf3c6172406569b67fe096426ede0ea65cdd174","contentType":"text/markdown; charset=utf-8"},{"id":"0c58d30d-0010-5689-a98f-e43ab9ef0455","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0c58d30d-0010-5689-a98f-e43ab9ef0455/attachment.md","path":"examples/upload_example.md","size":3481,"sha256":"de6a8f2bb5040f7f1149991908044d0c48a45e92b51d118e350c98ed580d2e90","contentType":"text/markdown; charset=utf-8"},{"id":"40dc8633-5ab8-5f02-a759-8c79ada3deed","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/40dc8633-5ab8-5f02-a759-8c79ada3deed/attachment.md","path":"references/confluence_storage_format.md","size":7327,"sha256":"c9e86c870257a9048a3b8cacb600ef7d2dad0aeb8c6b05526609658b11a13243","contentType":"text/markdown; charset=utf-8"},{"id":"40c0d8ca-d09c-5754-bada-41c82a7f26e2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/40c0d8ca-d09c-5754-bada-41c82a7f26e2/attachment.md","path":"references/conversion_guide.md","size":13847,"sha256":"a4e00043e6980ac0ee6edfa07b6add5ae0afef163b258b8a5b6872e9bbc90eb2","contentType":"text/markdown; charset=utf-8"},{"id":"ea305e6c-c7ad-53eb-9464-d8afaf872961","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ea305e6c-c7ad-53eb-9464-d8afaf872961/attachment.md","path":"references/image_handling_best_practices.md","size":11860,"sha256":"8ce228869ee5d24202a06702dd137d12b54eea448348d1b96cf8b5f1ea86af6b","contentType":"text/markdown; charset=utf-8"},{"id":"718cb20b-14d6-54ec-8047-b4a25215ae63","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/718cb20b-14d6-54ec-8047-b4a25215ae63/attachment.md","path":"references/mark_tool_guide.md","size":18517,"sha256":"787c4c37f7d65778148ad998c5508bc208ea221241a4b2ab9fb88b6dfb1e6d6f","contentType":"text/markdown; charset=utf-8"},{"id":"cd454cdf-711d-5053-a2ff-4ba6c5ac6581","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cd454cdf-711d-5053-a2ff-4ba6c5ac6581/attachment.md","path":"references/troubleshooting_guide.md","size":13305,"sha256":"e66bdf12f0db82fdaaef69ad45e36bae270b6fee96a4410e17f2c542ce8d947d","contentType":"text/markdown; charset=utf-8"},{"id":"812cd3c7-78ba-567d-b04f-9eac7d4588b4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/812cd3c7-78ba-567d-b04f-9eac7d4588b4/attachment.md","path":"references/wiki_markup_guide.md","size":9546,"sha256":"a25051e69c222894a4a20c4742734b70384ef3d0c26d60a87c703e5d9097876a","contentType":"text/markdown; charset=utf-8"},{"id":"62af6190-e429-5027-bdf2-ab158ede6189","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/62af6190-e429-5027-bdf2-ab158ede6189/attachment.pyc","path":"scripts/__pycache__/confluence_auth.cpython-311.pyc","size":9900,"sha256":"5929cff1bc014496564aba07529c9d4bc50d2b62b86bc3a45c4c08aae4be0e3d","contentType":"application/x-python-code"},{"id":"70da570b-de49-5128-8c84-d8dd3a8286fc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/70da570b-de49-5128-8c84-d8dd3a8286fc/attachment.pyc","path":"scripts/__pycache__/mermaid_renderer.cpython-311.pyc","size":12140,"sha256":"2212713c69bed34641c35150cbf9d600253c0d4cba391034c2d268c006f28682","contentType":"application/x-python-code"},{"id":"0dacf381-63a0-512b-9721-bb3052c98324","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0dacf381-63a0-512b-9721-bb3052c98324/attachment.py","path":"scripts/confluence_auth.py","size":7967,"sha256":"e66ba033ce3c2a21c31f4fe318d5f04c0dbc290943204591f27bbda544e9c3a8","contentType":"text/x-python; charset=utf-8"},{"id":"9161d76c-eaaf-5b6a-b0d7-dbf52dec4afb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9161d76c-eaaf-5b6a-b0d7-dbf52dec4afb/attachment.py","path":"scripts/convert_markdown_to_wiki.py","size":10024,"sha256":"18dd891bd4b7784b4f5ec1332c85913d6fb1b62796497397e9f41387e0d7d161","contentType":"text/x-python; charset=utf-8"},{"id":"d72cebec-0c55-5af9-823f-20da2af78612","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d72cebec-0c55-5af9-823f-20da2af78612/attachment.py","path":"scripts/download_confluence.py","size":32992,"sha256":"89e3416e997abdb7524b633ada2170b99436d72a8056b889063937b5f58fd1b4","contentType":"text/x-python; charset=utf-8"},{"id":"23ef5988-7f3f-504b-86d9-83bae793ddb6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/23ef5988-7f3f-504b-86d9-83bae793ddb6/attachment.py","path":"scripts/generate_mark_metadata.py","size":8903,"sha256":"a1d75d511ddd40191f7776377ffd04111e352ba0e421b72e96e440d29a11a0b8","contentType":"text/x-python; charset=utf-8"},{"id":"b303f87c-866d-51d4-a47b-cfdc0a5bd957","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b303f87c-866d-51d4-a47b-cfdc0a5bd957/attachment.py","path":"scripts/mermaid_renderer.py","size":8500,"sha256":"3db480bdc37cdbb0c8fa6e324b985fd4ea49329ff76fe071eb39dc772e8dea9c","contentType":"text/x-python; charset=utf-8"},{"id":"82bdba50-61b1-581c-a5ad-bbadfead1d33","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/82bdba50-61b1-581c-a5ad-bbadfead1d33/attachment.py","path":"scripts/render_mermaid.py","size":7965,"sha256":"9ce42610951c15c20a84e6eb9f44bd2e8baa60fdb7f46acd7f473480360b2886","contentType":"text/x-python; charset=utf-8"},{"id":"f893d5b0-da1c-53ae-8aa3-0a24e5b8ff87","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f893d5b0-da1c-53ae-8aa3-0a24e5b8ff87/attachment.txt","path":"scripts/requirements.txt","size":689,"sha256":"fa55f5ffd139ea3709329fc5135ddc8d90a4f55add86fc05ec6a9739b3332a3e","contentType":"text/plain; charset=utf-8"},{"id":"a1ae544c-ca34-5ce2-ae2b-ed2e217c204a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a1ae544c-ca34-5ce2-ae2b-ed2e217c204a/attachment.py","path":"scripts/upload_confluence.py","size":12789,"sha256":"a17b1eeccdd9edc26af74fd52d40beab128926a32323e94bbfb1887068fea5e8","contentType":"text/x-python; charset=utf-8"},{"id":"f654488d-bda9-5620-90ef-5f9bdde352a9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f654488d-bda9-5620-90ef-5f9bdde352a9/attachment.py","path":"scripts/upload_confluence_v2.py","size":15908,"sha256":"782656d977e98911f898e1220f65cc12948ee1282cc646b6a03078ef892466cc","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"a3baeaaffc212f96ef2616fb9c33fe88e25f255cd213ff7daf7e9e9b10b2a2fb","attachment_count":26,"text_attachments":22,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":4,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"documents-office","category_label":"Documents"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"documents-office","import_tag":"clean-skills-v1","description":"Manage Confluence documentation with downloads, uploads, conversions, and diagrams. Use when asked to \"download Confluence pages\", \"upload to Confluence\", \"convert Wiki Markup\", \"sync markdown to Confluence\", \"create Confluence page\", or \"handle Confluence images\"."}},"renderedAt":1782981035474}

Confluence Management Skill Manage Confluence documentation through Claude Code: download pages to Markdown, upload large documents with images, convert between formats, and integrate Mermaid/PlantUML diagrams. Table of Contents - Quick Decision Matrix - MCP Size Limits - Prerequisites - Core Workflows - Reference Documentation Quick Decision Matrix | Task | Tool | Notes | |------|------|-------| | Read pages | MCP tools | , | | Small text-only uploads (<10KB) | MCP tools | , | | Large documents ( 10KB) | | REST API, no size limits | | Documents with images | | Handles attachments automatical…