@de-otio/epimethian-mcp 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ The official [Atlassian MCP server](https://github.com/atlassian/atlassian-mcp-s
14
14
  - **Multi-tenant profile isolation** — Each Atlassian tenant gets its own named profile with fully separate credentials and keychain entries. No risk of cross-tenant writes when switching between clients.
15
15
  - **Tenant-aware write safety** — Write operations echo the target tenant so the AI agent (and you) always see where changes are going before they land.
16
16
  - **draw.io diagram support** — Create and embed draw.io diagrams directly in Confluence pages, something the official server doesn't expose.
17
- - **Attribution tracking** — Managed pages carry metadata so you can trace which AI-assisted edits touched which content.
17
+ - **Attribution tracking** — Edited pages are labelled `epimethian-edited` for easy discovery. Confluence version messages include the MCP client name (e.g. "Updated by Claude Code (via Epimethian v5.2.0)") so you can trace which AI-assisted edits touched which content.
18
18
 
19
19
  If you don't need any of the above, the official Atlassian server is a fine choice.
20
20
 
@@ -113,7 +113,7 @@ Confluence pages are verbose — storage format HTML with macro markup can easil
113
113
  | `create_page` | Create a new page |
114
114
  | `get_page` | Read a page by ID (`headings_only`, `section`, `max_length`, `format`) |
115
115
  | `get_page_by_title` | Look up a page by title (same options as `get_page`) |
116
- | `update_page` | Update an existing page (rejects markdown input) |
116
+ | `update_page` | Update an existing page |
117
117
  | `update_page_section`| Update a single section by heading name |
118
118
  | `delete_page` | Delete a page |
119
119
  | `list_pages` | List pages in a space |
package/dist/cli/index.js CHANGED
@@ -24073,14 +24073,14 @@ var require_turndown_cjs = __commonJS({
24073
24073
  } else if (node.nodeType === 1) {
24074
24074
  replacement = replacementForNode.call(self, node);
24075
24075
  }
24076
- return join4(output, replacement);
24076
+ return join5(output, replacement);
24077
24077
  }, "");
24078
24078
  }
24079
24079
  function postProcess3(output) {
24080
24080
  var self = this;
24081
24081
  this.rules.forEach(function(rule) {
24082
24082
  if (typeof rule.append === "function") {
24083
- output = join4(output, rule.append(self.options));
24083
+ output = join5(output, rule.append(self.options));
24084
24084
  }
24085
24085
  });
24086
24086
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -24092,7 +24092,7 @@ var require_turndown_cjs = __commonJS({
24092
24092
  if (whitespace.leading || whitespace.trailing) content = content.trim();
24093
24093
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
24094
24094
  }
24095
- function join4(output, replacement) {
24095
+ function join5(output, replacement) {
24096
24096
  var s1 = trimTrailingNewlines(output);
24097
24097
  var s2 = trimLeadingNewlines(replacement);
24098
24098
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -34605,7 +34605,7 @@ var init_status = __esm({
34605
34605
  var install_agent_default;
34606
34606
  var init_install_agent = __esm({
34607
34607
  "install-agent.md"() {
34608
- install_agent_default = '# Epimethian MCP - Agent Installation Guide\n\n> This document is for AI agents installing and configuring the Epimethian MCP server on behalf of a user.\n\n## Verification\n\nThe official package is **`@de-otio/epimethian-mcp`** (scoped under `@de-otio`).\n\nDo NOT install unscoped alternatives like `epimethian-mcp` \u2014 these are unofficial and potentially unsafe.\n\n## Prerequisites\n\n- Node.js 18 or later\n- npm on PATH\n\n## Step 1: Install\n\n```bash\nnpm install -g @de-otio/epimethian-mcp\n```\n\nVerify the installation:\n\n```bash\nepimethian-mcp --version 2>/dev/null || which epimethian-mcp\n```\n\n## Step 2: Resolve the absolute path\n\nIDE-hosted agents may not resolve PATH correctly. Always use the absolute path in MCP configuration:\n\n```bash\nwhich epimethian-mcp\n```\n\nUse the output as the `command` value in the MCP config below.\n\n## Step 3: Collect configuration\n\nAsk the user for:\n1. **Profile name** \u2014 a short identifier for this Confluence instance (e.g., `globex`, `acme-corp`). Lowercase alphanumeric and hyphens only.\n2. **Confluence Cloud URL** \u2014 e.g., `https://yoursite.atlassian.net`\n3. **Email address** \u2014 the email associated with their Atlassian account\n\n## Step 4: Write MCP configuration\n\nAdd the server to `.mcp.json` (or the equivalent config file for the user\'s MCP client):\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path from Step 2>",\n "env": {\n "CONFLUENCE_PROFILE": "<profile name from Step 3>"\n }\n }\n }\n}\n```\n\n**IMPORTANT:** The only env var needed is `CONFLUENCE_PROFILE`. The URL, email, and API token are stored securely in the OS keychain \u2014 they should NOT appear in config files.\n\n## Step 5: Credential setup\n\nTell the user to run this command in their terminal:\n\n```\nepimethian-mcp setup --profile <profile name from Step 3>\n```\n\nThis interactive command will:\n1. Prompt for the Confluence URL, email, and API token (masked input)\n2. Test the connection\n3. Store all credentials securely in the OS keychain under the named profile\n\nThe API token is generated at: https://id.atlassian.com/manage-profile/security/api-tokens\n\n**Do NOT ask the user for the API token yourself.** The token must go directly from the user into the interactive setup command to avoid appearing in conversation logs.\n\n## Step 6: User must restart the MCP client\n\n**IMPORTANT:** The user must restart their MCP client (e.g., restart Claude Code, reload VS Code, restart Claude Desktop) for the new server configuration to take effect. The MCP client reads `.mcp.json` at startup and does not detect changes while running.\n\nTell the user:\n> Please restart your MCP client now to activate the Confluence tools.\n\n## Step 7: Validation\n\nAfter the user restarts, verify the server is working by listing available Confluence tools or running a simple operation like listing spaces.\n\n## Adding Additional Tenants\n\nTo add a second Confluence instance (e.g., for a different customer):\n\n1. Run `epimethian-mcp setup --profile <new-profile-name>` with the new credentials\n2. In the project that uses the new tenant, update `.mcp.json` to set `CONFLUENCE_PROFILE` to the new profile name\n3. Restart the MCP client\n\nEach VS Code window / Claude Code session uses the profile specified in its `.mcp.json`. Profiles are fully isolated \u2014 different OS keychain entries, different Confluence instances.\n\n## Managing Profiles\n\n- List all profiles: `epimethian-mcp profiles`\n- Show details: `epimethian-mcp profiles --verbose`\n- Check connection: `CONFLUENCE_PROFILE=<name> epimethian-mcp status`\n- Set read-only: `epimethian-mcp profiles --set-read-only <name>`\n- Set read-write: `epimethian-mcp profiles --set-read-write <name>`\n\n### Read-Only Mode\n\nNew profiles default to **read-only**. When read-only, all write tools are blocked and return an error. To enable writes for a profile:\n\n```bash\nepimethian-mcp profiles --set-read-write <name>\n```\n\nOr during setup: `epimethian-mcp setup --profile <name> --read-write`\n\n**Important:** Restart any running MCP servers after changing the read-only flag.\n\n### Removing a Profile\n\nTo delete a profile and its credentials, run:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\n**Agents must pass `--force`** because the command normally prompts for interactive confirmation (`Remove profile "<name>" and delete its credentials? [y/N]`), which will fail in non-TTY environments like agent shell sessions. The `--force` flag skips the confirmation prompt when stdin is not a TTY.\n\nThis command:\n1. Deletes the credential entry (URL, email, API token) from the OS keychain\n2. Removes the profile from the registry at `~/.config/epimethian-mcp/profiles.json`\n3. Writes an entry to the audit log at `~/.config/epimethian-mcp/audit.log`\n\nAfter removing a profile, also remove or update any `.mcp.json` files that reference it \u2014 otherwise the MCP server will fail to start with a missing-profile error.\n\n**Errors:**\n- If the profile name is invalid (not matching lowercase alphanumeric/hyphens, 1\u201363 chars), the command exits with code 1\n- If the profile does not exist in the keychain, the keychain deletion is silently skipped \u2014 the registry entry is still removed\n\n## Accessing This Guide Post-Install\n\nOnce installed, this guide is available locally via:\n\n```bash\nepimethian-mcp agent-guide\n```\n\nThis prints the full agent guide to stdout \u2014 no web fetch required.\n\n## Uninstallation\n\nWhen a user asks to uninstall Epimethian MCP, follow these steps:\n\n### Step 1: Check for existing profiles\n\n```bash\nepimethian-mcp profiles\n```\n\n### Step 2: Ask the user about credential cleanup\n\nIf profiles exist, ask the user:\n\n> You have Epimethian profiles configured: [list the profile names]. Would you like to delete all stored credentials before uninstalling? (This removes API tokens from your OS keychain.)\n\n### Step 3: Delete credentials (if the user agrees)\n\nFor each profile the user wants removed:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\nOr to remove all profiles:\n\n```bash\nfor name in $(epimethian-mcp profiles | grep \'^ \'); do epimethian-mcp profiles --remove "$name" --force; done\n```\n\n### Step 4: Remove MCP configuration\n\nDelete the `confluence` entry (or the tenant-specific entry like `confluence-globex`) from the project\'s `.mcp.json`.\n\n### Step 5: Uninstall the package\n\n```bash\nnpm uninstall -g @de-otio/epimethian-mcp\n```\n\n### Step 6: Restart the MCP client\n\nTell the user to restart their MCP client so it stops trying to launch the removed server.\n\n## CI/CD (No Keychain)\n\nFor environments where the OS keychain is unavailable (Docker, CI), set all three env vars directly:\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path>",\n "env": {\n "CONFLUENCE_URL": "<url>",\n "CONFLUENCE_EMAIL": "<email>",\n "CONFLUENCE_API_TOKEN": "<token>"\n }\n }\n }\n}\n```\n\n**Warning:** This exposes the API token in the process environment. Use profile-based auth whenever possible.\n\n## Troubleshooting\n\nIf **npm install fails**:\n- Verify Node.js 18+ is installed: `node --version`\n- Verify npm is on PATH: `npm --version`\n- If permission errors occur, the user may need to fix their npm prefix or use a Node version manager (nvm, fnm)\n\nIf **`epimethian-mcp setup` fails**:\n- "Connection failed": Verify the Confluence URL is correct and accessible\n- "Token is invalid or expired": The user needs to generate a new API token at https://id.atlassian.com/manage-profile/security/api-tokens\n- Keychain errors on Linux: The user may need to install `libsecret` / `gnome-keyring` (`apt install libsecret-tools` or equivalent)\n\nIf **the server doesn\'t appear after restart**:\n- Verify the `.mcp.json` path is correct for the user\'s MCP client\n- Verify the `command` value is an absolute path (run `which epimethian-mcp` to confirm)\n- Check that `.mcp.json` contains valid JSON (no trailing commas, correct quoting)\n\n## Available Tools (32)\n\n| Tool | Description |\n|------|-------------|\n| `create_page` | Create a new Confluence page |\n| `get_page` | Read a page by ID (use `headings_only` to preview structure first) |\n| `get_page_by_title` | Look up a page by title (use `headings_only` to preview structure first) |\n| `update_page` | Update an existing page |\n| `update_page_section` | Update a single section by heading name |\n| `prepend_to_page` | Insert content at the beginning of an existing page (additive, safe) |\n| `append_to_page` | Insert content at the end of an existing page (additive, safe) |\n| `delete_page` | Delete a page |\n| `revert_page` | Revert a page to a previous version |\n| `list_pages` | List pages in a space |\n| `get_page_children` | Get child pages of a page |\n| `search_pages` | Search pages using CQL (Confluence Query Language) |\n| `get_spaces` | List available Confluence spaces |\n| `add_attachment` | Upload a file attachment to a page |\n| `get_attachments` | List attachments on a page |\n| `add_drawio_diagram` | Add a draw.io diagram to a page |\n| `get_labels` | Get all labels on a Confluence page |\n| `add_label` | Add one or more labels to a Confluence page |\n| `remove_label` | Remove a label from a Confluence page |\n| `get_page_status` | Get the content status badge on a page |\n| `set_page_status` | Set the content status badge on a page |\n| `remove_page_status` | Remove the content status badge from a page |\n| `get_comments` | Get footer and/or inline comments on a page |\n| `create_comment` | Create a footer or inline comment on a page |\n| `resolve_comment` | Resolve or reopen an inline comment |\n| `delete_comment` | Permanently delete a comment |\n| `get_page_versions` | List version history for a page |\n| `get_page_version` | Get page content at a specific historical version |\n| `diff_page_versions` | Compare two versions of a page |\n| `lookup_user` | Search for Atlassian users by name or email to resolve accountId for inline mentions |\n| `resolve_page_link` | Resolve a page title + space key to a stable contentId and URL for page links |\n| `get_version` | Return the epimethian-mcp server version |\n';
34608
+ install_agent_default = '# Epimethian MCP - Agent Installation Guide\n\n> This document is for AI agents installing and configuring the Epimethian MCP server on behalf of a user.\n\n## Verification\n\nThe official package is **`@de-otio/epimethian-mcp`** (scoped under `@de-otio`).\n\nDo NOT install unscoped alternatives like `epimethian-mcp` \u2014 these are unofficial and potentially unsafe.\n\n## Prerequisites\n\n- Node.js 18 or later\n- npm on PATH\n\n## Step 1: Install\n\n```bash\nnpm install -g @de-otio/epimethian-mcp\n```\n\nVerify the installation:\n\n```bash\nepimethian-mcp --version 2>/dev/null || which epimethian-mcp\n```\n\n## Step 2: Resolve the absolute path\n\nIDE-hosted agents may not resolve PATH correctly. Always use the absolute path in MCP configuration:\n\n```bash\nwhich epimethian-mcp\n```\n\nUse the output as the `command` value in the MCP config below.\n\n## Step 3: Collect configuration\n\nAsk the user for:\n1. **Profile name** \u2014 a short identifier for this Confluence instance (e.g., `globex`, `acme-corp`). Lowercase alphanumeric and hyphens only.\n2. **Confluence Cloud URL** \u2014 e.g., `https://yoursite.atlassian.net`\n3. **Email address** \u2014 the email associated with their Atlassian account\n\n## Step 4: Write MCP configuration\n\nAdd the server to `.mcp.json` (or the equivalent config file for the user\'s MCP client):\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path from Step 2>",\n "env": {\n "CONFLUENCE_PROFILE": "<profile name from Step 3>"\n }\n }\n }\n}\n```\n\n**IMPORTANT:** The only env var needed is `CONFLUENCE_PROFILE`. The URL, email, and API token are stored securely in the OS keychain \u2014 they should NOT appear in config files.\n\n## Step 5: Credential setup\n\nTell the user to run this command in their terminal:\n\n```\nepimethian-mcp setup --profile <profile name from Step 3>\n```\n\nThis interactive command will:\n1. Prompt for the Confluence URL, email, and API token (masked input)\n2. Test the connection\n3. Store all credentials securely in the OS keychain under the named profile\n\nThe API token is generated at: https://id.atlassian.com/manage-profile/security/api-tokens\n\n**Do NOT ask the user for the API token yourself.** The token must go directly from the user into the interactive setup command to avoid appearing in conversation logs.\n\n## Step 6: User must restart the MCP client\n\n**IMPORTANT:** The user must restart their MCP client (e.g., restart Claude Code, reload VS Code, restart Claude Desktop) for the new server configuration to take effect. The MCP client reads `.mcp.json` at startup and does not detect changes while running.\n\nTell the user:\n> Please restart your MCP client now to activate the Confluence tools.\n\n## Step 7: Validation\n\nAfter the user restarts, verify the server is working by listing available Confluence tools or running a simple operation like listing spaces.\n\n## Adding Additional Tenants\n\nTo add a second Confluence instance (e.g., for a different customer):\n\n1. Run `epimethian-mcp setup --profile <new-profile-name>` with the new credentials\n2. In the project that uses the new tenant, update `.mcp.json` to set `CONFLUENCE_PROFILE` to the new profile name\n3. Restart the MCP client\n\nEach VS Code window / Claude Code session uses the profile specified in its `.mcp.json`. Profiles are fully isolated \u2014 different OS keychain entries, different Confluence instances.\n\n## Managing Profiles\n\n- List all profiles: `epimethian-mcp profiles`\n- Show details: `epimethian-mcp profiles --verbose`\n- Check connection: `CONFLUENCE_PROFILE=<name> epimethian-mcp status`\n- Set read-only: `epimethian-mcp profiles --set-read-only <name>`\n- Set read-write: `epimethian-mcp profiles --set-read-write <name>`\n\n### Read-Only Mode\n\nNew profiles default to **read-only**. When read-only, all write tools are blocked and return an error. To enable writes for a profile:\n\n```bash\nepimethian-mcp profiles --set-read-write <name>\n```\n\nOr during setup: `epimethian-mcp setup --profile <name> --read-write`\n\n**Important:** Restart any running MCP servers after changing the read-only flag.\n\n### Removing a Profile\n\nTo delete a profile and its credentials, run:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\n**Agents must pass `--force`** because the command normally prompts for interactive confirmation (`Remove profile "<name>" and delete its credentials? [y/N]`), which will fail in non-TTY environments like agent shell sessions. The `--force` flag skips the confirmation prompt when stdin is not a TTY.\n\nThis command:\n1. Deletes the credential entry (URL, email, API token) from the OS keychain\n2. Removes the profile from the registry at `~/.config/epimethian-mcp/profiles.json`\n3. Writes an entry to the audit log at `~/.config/epimethian-mcp/audit.log`\n\nAfter removing a profile, also remove or update any `.mcp.json` files that reference it \u2014 otherwise the MCP server will fail to start with a missing-profile error.\n\n**Errors:**\n- If the profile name is invalid (not matching lowercase alphanumeric/hyphens, 1\u201363 chars), the command exits with code 1\n- If the profile does not exist in the keychain, the keychain deletion is silently skipped \u2014 the registry entry is still removed\n\n## Accessing This Guide Post-Install\n\nOnce installed, this guide is available locally via:\n\n```bash\nepimethian-mcp agent-guide\n```\n\nThis prints the full agent guide to stdout \u2014 no web fetch required.\n\n## Uninstallation\n\nWhen a user asks to uninstall Epimethian MCP, follow these steps:\n\n### Step 1: Check for existing profiles\n\n```bash\nepimethian-mcp profiles\n```\n\n### Step 2: Ask the user about credential cleanup\n\nIf profiles exist, ask the user:\n\n> You have Epimethian profiles configured: [list the profile names]. Would you like to delete all stored credentials before uninstalling? (This removes API tokens from your OS keychain.)\n\n### Step 3: Delete credentials (if the user agrees)\n\nFor each profile the user wants removed:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\nOr to remove all profiles:\n\n```bash\nfor name in $(epimethian-mcp profiles | grep \'^ \'); do epimethian-mcp profiles --remove "$name" --force; done\n```\n\n### Step 4: Remove MCP configuration\n\nDelete the `confluence` entry (or the tenant-specific entry like `confluence-globex`) from the project\'s `.mcp.json`.\n\n### Step 5: Uninstall the package\n\n```bash\nnpm uninstall -g @de-otio/epimethian-mcp\n```\n\n### Step 6: Restart the MCP client\n\nTell the user to restart their MCP client so it stops trying to launch the removed server.\n\n## CI/CD (No Keychain)\n\nFor environments where the OS keychain is unavailable (Docker, CI), set all three env vars directly:\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path>",\n "env": {\n "CONFLUENCE_URL": "<url>",\n "CONFLUENCE_EMAIL": "<email>",\n "CONFLUENCE_API_TOKEN": "<token>"\n }\n }\n }\n}\n```\n\n**Warning:** This exposes the API token in the process environment. Use profile-based auth whenever possible.\n\n## Troubleshooting\n\nIf **npm install fails**:\n- Verify Node.js 18+ is installed: `node --version`\n- Verify npm is on PATH: `npm --version`\n- If permission errors occur, the user may need to fix their npm prefix or use a Node version manager (nvm, fnm)\n\nIf **`epimethian-mcp setup` fails**:\n- "Connection failed": Verify the Confluence URL is correct and accessible\n- "Token is invalid or expired": The user needs to generate a new API token at https://id.atlassian.com/manage-profile/security/api-tokens\n- Keychain errors on Linux: The user may need to install `libsecret` / `gnome-keyring` (`apt install libsecret-tools` or equivalent)\n\nIf **the server doesn\'t appear after restart**:\n- Verify the `.mcp.json` path is correct for the user\'s MCP client\n- Verify the `command` value is an absolute path (run `which epimethian-mcp` to confirm)\n- Check that `.mcp.json` contains valid JSON (no trailing commas, correct quoting)\n\n## Available Tools (33)\n\n| Tool | Description |\n|------|-------------|\n| `create_page` | Create a new Confluence page |\n| `get_page` | Read a page by ID (use `headings_only` to preview structure first) |\n| `get_page_by_title` | Look up a page by title (use `headings_only` to preview structure first) |\n| `update_page` | Update an existing page |\n| `update_page_section` | Update a single section by heading name |\n| `prepend_to_page` | Insert content at the beginning of an existing page (additive, safe) |\n| `append_to_page` | Insert content at the end of an existing page (additive, safe) |\n| `delete_page` | Delete a page |\n| `revert_page` | Revert a page to a previous version |\n| `list_pages` | List pages in a space |\n| `get_page_children` | Get child pages of a page |\n| `search_pages` | Search pages using CQL (Confluence Query Language) |\n| `get_spaces` | List available Confluence spaces |\n| `add_attachment` | Upload a file attachment to a page |\n| `get_attachments` | List attachments on a page |\n| `add_drawio_diagram` | Add a draw.io diagram to a page |\n| `get_labels` | Get all labels on a Confluence page |\n| `add_label` | Add one or more labels to a Confluence page |\n| `remove_label` | Remove a label from a Confluence page |\n| `get_page_status` | Get the content status badge on a page |\n| `set_page_status` | Set the content status badge on a page |\n| `remove_page_status` | Remove the content status badge from a page |\n| `get_comments` | Get footer and/or inline comments on a page |\n| `create_comment` | Create a footer or inline comment on a page |\n| `resolve_comment` | Resolve or reopen an inline comment |\n| `delete_comment` | Permanently delete a comment |\n| `get_page_versions` | List version history for a page |\n| `get_page_version` | Get page content at a specific historical version |\n| `diff_page_versions` | Compare two versions of a page |\n| `lookup_user` | Search for Atlassian users by name or email to resolve accountId for inline mentions |\n| `resolve_page_link` | Resolve a page title + space key to a stable contentId and URL for page links |\n| `get_version` | Return the epimethian-mcp server version and report available updates |\n| `upgrade` | Upgrade epimethian-mcp to the latest available version (restart required after) |\n';
34609
34609
  }
34610
34610
  });
34611
34611
 
@@ -48840,9 +48840,9 @@ var StdioServerTransport = class {
48840
48840
  };
48841
48841
 
48842
48842
  // src/server/index.ts
48843
- var import_promises2 = require("node:fs/promises");
48844
- var import_node_os2 = require("node:os");
48845
- var import_node_path3 = require("node:path");
48843
+ var import_promises3 = require("node:fs/promises");
48844
+ var import_node_os3 = require("node:os");
48845
+ var import_node_path4 = require("node:path");
48846
48846
 
48847
48847
  // src/server/confluence-client.ts
48848
48848
  var import_turndown = __toESM(require_turndown_cjs());
@@ -49324,7 +49324,7 @@ async function getPage(pageId, includeBody) {
49324
49324
  async function createPage(spaceId, title, body, parentId, clientLabel) {
49325
49325
  const cfg = await getConfig();
49326
49326
  const pageBody = stripAttributionFooter(toStorageFormat(body));
49327
- const epimethianTag = `Epimethian v${"5.2.0"}`;
49327
+ const epimethianTag = `Epimethian v${"5.3.0"}`;
49328
49328
  const versionMsg = cfg.attribution && clientLabel ? `Created by ${clientLabel} (via ${epimethianTag})` : `Created by ${epimethianTag}`;
49329
49329
  const payload = {
49330
49330
  title,
@@ -49349,7 +49349,7 @@ async function createPage(spaceId, title, body, parentId, clientLabel) {
49349
49349
  async function updatePage(pageId, opts) {
49350
49350
  const cfg = await getConfig();
49351
49351
  const newVersion = opts.version + 1;
49352
- const epimethianTag = `Epimethian v${"5.2.0"}`;
49352
+ const epimethianTag = `Epimethian v${"5.3.0"}`;
49353
49353
  const effectiveClient = cfg.attribution ? opts.clientLabel : void 0;
49354
49354
  let versionMessage;
49355
49355
  if (opts.versionMessage && effectiveClient)
@@ -56883,6 +56883,152 @@ function errorRecord(operation, pageId, err, extra) {
56883
56883
  };
56884
56884
  }
56885
56885
 
56886
+ // src/shared/update-check.ts
56887
+ var import_promises2 = require("node:fs/promises");
56888
+ var import_node_path3 = require("node:path");
56889
+ var import_node_os2 = require("node:os");
56890
+ var import_node_crypto2 = require("node:crypto");
56891
+ var import_node_child_process2 = require("node:child_process");
56892
+ var import_node_util = require("node:util");
56893
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
56894
+ var CONFIG_DIR2 = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".config", "epimethian-mcp");
56895
+ var UPDATE_CHECK_FILE = (0, import_node_path3.join)(CONFIG_DIR2, "update-check.json");
56896
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
56897
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@de-otio/epimethian-mcp/latest";
56898
+ var PACKAGE_NAME = "@de-otio/epimethian-mcp";
56899
+ function parseSemVer(version2) {
56900
+ const match2 = version2.match(/^(\d+)\.(\d+)\.(\d+)$/);
56901
+ if (!match2) return null;
56902
+ return {
56903
+ major: parseInt(match2[1], 10),
56904
+ minor: parseInt(match2[2], 10),
56905
+ patch: parseInt(match2[3], 10)
56906
+ };
56907
+ }
56908
+ function classifyUpdate(current, latest) {
56909
+ if (latest.major > current.major) return "major";
56910
+ if (latest.major === current.major && latest.minor > current.minor)
56911
+ return "minor";
56912
+ if (latest.major === current.major && latest.minor === current.minor && latest.patch > current.patch)
56913
+ return "patch";
56914
+ return null;
56915
+ }
56916
+ async function readCheckState() {
56917
+ try {
56918
+ const raw = await (0, import_promises2.readFile)(UPDATE_CHECK_FILE, "utf-8");
56919
+ const parsed = JSON.parse(raw);
56920
+ if (parsed && typeof parsed === "object" && typeof parsed.lastCheck === "string") {
56921
+ return parsed;
56922
+ }
56923
+ return null;
56924
+ } catch {
56925
+ return null;
56926
+ }
56927
+ }
56928
+ async function writeCheckState(state) {
56929
+ await (0, import_promises2.mkdir)(CONFIG_DIR2, { recursive: true, mode: 448 });
56930
+ const data = JSON.stringify(state, null, 2) + "\n";
56931
+ const tmpFile = (0, import_node_path3.join)(
56932
+ CONFIG_DIR2,
56933
+ `.update-check.${(0, import_node_crypto2.randomBytes)(4).toString("hex")}.tmp`
56934
+ );
56935
+ await (0, import_promises2.writeFile)(tmpFile, data, { mode: 384 });
56936
+ await (0, import_promises2.rename)(tmpFile, UPDATE_CHECK_FILE);
56937
+ }
56938
+ async function fetchLatestVersion() {
56939
+ try {
56940
+ const response = await fetch(NPM_REGISTRY_URL, {
56941
+ headers: { Accept: "application/json" },
56942
+ signal: AbortSignal.timeout(1e4)
56943
+ });
56944
+ if (!response.ok) return null;
56945
+ const data = await response.json();
56946
+ return typeof data.version === "string" ? data.version : null;
56947
+ } catch {
56948
+ return null;
56949
+ }
56950
+ }
56951
+ async function getPendingUpdate() {
56952
+ const state = await readCheckState();
56953
+ return state?.pendingUpdate ?? null;
56954
+ }
56955
+ async function clearPendingUpdate() {
56956
+ const state = await readCheckState();
56957
+ if (state) {
56958
+ delete state.pendingUpdate;
56959
+ await writeCheckState(state);
56960
+ }
56961
+ }
56962
+ async function performUpgrade(version2) {
56963
+ const { stdout: stdout3, stderr } = await execFileAsync(
56964
+ "npm",
56965
+ ["install", "-g", `${PACKAGE_NAME}@${version2}`],
56966
+ { timeout: 12e4 }
56967
+ );
56968
+ return (stdout3 + stderr).trim();
56969
+ }
56970
+ async function checkForUpdates(currentVersion) {
56971
+ if (process.env.EPIMETHIAN_NO_UPDATE_CHECK === "true") return null;
56972
+ try {
56973
+ const state = await readCheckState();
56974
+ if (state?.lastCheck) {
56975
+ const elapsed = Date.now() - new Date(state.lastCheck).getTime();
56976
+ if (elapsed < ONE_DAY_MS) {
56977
+ return state.pendingUpdate ?? null;
56978
+ }
56979
+ }
56980
+ const latestStr = await fetchLatestVersion();
56981
+ if (!latestStr) {
56982
+ return state?.pendingUpdate ?? null;
56983
+ }
56984
+ const current = parseSemVer(currentVersion);
56985
+ const latest = parseSemVer(latestStr);
56986
+ if (!current || !latest) return null;
56987
+ const type = classifyUpdate(current, latest);
56988
+ const newState = {
56989
+ lastCheck: (/* @__PURE__ */ new Date()).toISOString()
56990
+ };
56991
+ if (!type) {
56992
+ await writeCheckState(newState);
56993
+ return null;
56994
+ }
56995
+ const info = {
56996
+ current: currentVersion,
56997
+ latest: latestStr,
56998
+ type
56999
+ };
57000
+ if (type === "patch") {
57001
+ try {
57002
+ await performUpgrade(latestStr);
57003
+ info.autoInstalled = true;
57004
+ newState.pendingUpdate = info;
57005
+ await writeCheckState(newState);
57006
+ console.error(
57007
+ `[epimethian-mcp] Bugfix v${latestStr} installed automatically. Restart the MCP server to apply.`
57008
+ );
57009
+ } catch (err) {
57010
+ newState.pendingUpdate = info;
57011
+ await writeCheckState(newState);
57012
+ console.error(
57013
+ `[epimethian-mcp] Auto-update to v${latestStr} failed: ${err instanceof Error ? err.message : err}`
57014
+ );
57015
+ }
57016
+ return info;
57017
+ }
57018
+ newState.pendingUpdate = info;
57019
+ await writeCheckState(newState);
57020
+ console.error(
57021
+ `[epimethian-mcp] ${type === "major" ? "Major" : "Minor"} update available: v${currentVersion} \u2192 v${latestStr}. Use the upgrade tool to install.`
57022
+ );
57023
+ return info;
57024
+ } catch (err) {
57025
+ console.error(
57026
+ `[epimethian-mcp] Update check failed: ${err instanceof Error ? err.message : err}`
57027
+ );
57028
+ return null;
57029
+ }
57030
+ }
57031
+
56886
57032
  // src/server/index.ts
56887
57033
  function getClientLabel(server) {
56888
57034
  const client = server.server.getClientVersion();
@@ -56892,6 +57038,7 @@ function getClientLabel(server) {
56892
57038
  function escapeXml(s) {
56893
57039
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
56894
57040
  }
57041
+ var READ_ONLY_MARKDOWN_MARKER = "<!-- epimethian:read-only-markdown \u2014 do not pass this content to update_page -->";
56895
57042
  function formatMarkdownWithTokens(markdown, sidecar, header) {
56896
57043
  const tokenCount = Object.keys(sidecar).length;
56897
57044
  let body = markdown;
@@ -56904,13 +57051,19 @@ function formatMarkdownWithTokens(markdown, sidecar, header) {
56904
57051
  const name = m && m[2] ? ` ac:name="${m[2]}"` : "";
56905
57052
  return `- [[epi:${id}]]: <${tag}${name}>`;
56906
57053
  }).join("\n");
56907
- body = `<!-- ${tokenCount} Confluence macro${tokenCount === 1 ? "" : "s"} preserved as tokens; remove a token to delete that macro on the next update_page -->
57054
+ body = `${READ_ONLY_MARKDOWN_MARKER}
57055
+
57056
+ <!-- ${tokenCount} Confluence macro${tokenCount === 1 ? "" : "s"} preserved as tokens; remove a token to delete that macro on the next update_page -->
56908
57057
 
56909
57058
  ${markdown}
56910
57059
 
56911
57060
  ---
56912
57061
  Tokens:
56913
57062
  ${table2}`;
57063
+ } else {
57064
+ body = `${READ_ONLY_MARKDOWN_MARKER}
57065
+
57066
+ ${markdown}`;
56914
57067
  }
56915
57068
  return `${header}
56916
57069
 
@@ -56945,6 +57098,7 @@ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
56945
57098
  "get_page_version",
56946
57099
  "diff_page_versions",
56947
57100
  "get_version",
57101
+ "upgrade",
56948
57102
  "lookup_user",
56949
57103
  "resolve_page_link"
56950
57104
  ]);
@@ -57021,6 +57175,12 @@ function registerTools(server, config3) {
57021
57175
  );
57022
57176
  const pageIdSchema = external_exports.string().regex(/^\d+$/, "Page ID must be numeric");
57023
57177
  async function concatPageContent(page_id, version2, newContent, position, opts = {}) {
57178
+ if (newContent.includes("epimethian:read-only-markdown")) {
57179
+ throw new ConverterError(
57180
+ "The content contains output produced by get_page with format: 'markdown', which is a read-only rendering not suitable for prepend/append operations. Compose new content from scratch instead.",
57181
+ "READ_ONLY_MARKDOWN_ROUND_TRIP"
57182
+ );
57183
+ }
57024
57184
  const currentPage = await getPage(page_id, true);
57025
57185
  const currentStorage = currentPage.body?.storage?.value ?? currentPage.body?.value ?? "";
57026
57186
  const isMarkdown = looksLikeMarkdown(newContent);
@@ -57076,6 +57236,12 @@ function registerTools(server, config3) {
57076
57236
  const blocked = writeGuard("create_page", config3);
57077
57237
  if (blocked) return blocked;
57078
57238
  try {
57239
+ if (body.includes("epimethian:read-only-markdown")) {
57240
+ throw new ConverterError(
57241
+ "The body contains content produced by get_page with format: 'markdown', which is a read-only rendering not suitable for creating pages (tables, macros, and rich elements may be lost). Compose new markdown from scratch instead.",
57242
+ "READ_ONLY_MARKDOWN_ROUND_TRIP"
57243
+ );
57244
+ }
57079
57245
  let finalBody = body;
57080
57246
  if (looksLikeMarkdown(body)) {
57081
57247
  const cfg = await getConfig();
@@ -57225,7 +57391,15 @@ ${truncated}`);
57225
57391
  const currentStorage = currentPage.body?.storage?.value ?? currentPage.body?.value ?? "";
57226
57392
  let finalStorage;
57227
57393
  let effectiveVersionMessage = version_message;
57228
- if (body && looksLikeMarkdown(body)) {
57394
+ if (body && body.includes("epimethian:read-only-markdown")) {
57395
+ throw new ConverterError(
57396
+ "The body contains content produced by get_page with format: 'markdown', which is a read-only rendering not suitable for round-trip updates (tables, macros, and rich elements may be lost). To update this page, either: (1) read with format: 'storage' and edit the storage XML, (2) use update_page_section for targeted edits, or (3) compose new markdown from scratch (do not copy from format: 'markdown' output).",
57397
+ "READ_ONLY_MARKDOWN_ROUND_TRIP"
57398
+ );
57399
+ }
57400
+ if (body === void 0 || body === null) {
57401
+ finalStorage = void 0;
57402
+ } else if (looksLikeMarkdown(body)) {
57229
57403
  const plan = planUpdate({
57230
57404
  currentStorage,
57231
57405
  callerMarkdown: body,
@@ -57239,14 +57413,16 @@ ${truncated}`);
57239
57413
  finalStorage = plan.newStorage;
57240
57414
  effectiveVersionMessage = plan.versionMessage && version_message ? `${version_message}; ${plan.versionMessage}` : plan.versionMessage ?? version_message;
57241
57415
  } else {
57242
- finalStorage = body ?? "";
57416
+ finalStorage = body;
57417
+ }
57418
+ if (finalStorage !== void 0) {
57419
+ enforceContentSafetyGuards({
57420
+ oldStorage: currentStorage,
57421
+ newStorage: finalStorage,
57422
+ confirmShrinkage: confirm_shrinkage,
57423
+ confirmStructureLoss: confirm_structure_loss
57424
+ });
57243
57425
  }
57244
- enforceContentSafetyGuards({
57245
- oldStorage: currentStorage,
57246
- newStorage: finalStorage,
57247
- confirmShrinkage: confirm_shrinkage,
57248
- confirmStructureLoss: confirm_structure_loss
57249
- });
57250
57426
  const { page, newVersion } = await updatePage(page_id, {
57251
57427
  title,
57252
57428
  body: finalStorage,
@@ -57262,11 +57438,12 @@ ${truncated}`);
57262
57438
  oldVersion: version2,
57263
57439
  newVersion,
57264
57440
  oldBodyLen: currentStorage.length,
57265
- newBodyLen: finalStorage.length,
57441
+ newBodyLen: finalStorage?.length ?? currentStorage.length,
57266
57442
  replaceBody: replace_body || void 0
57267
57443
  });
57444
+ const bodyReport = finalStorage !== void 0 ? `body: ${currentStorage.length}\u2192${finalStorage.length} chars` : `title only, body unchanged`;
57268
57445
  return toolResult(
57269
- `Updated: ${page.title} (ID: ${page.id}, version: ${newVersion}, body: ${currentStorage.length}\u2192${finalStorage.length} chars)` + echo
57446
+ `Updated: ${page.title} (ID: ${page.id}, version: ${newVersion}, ${bodyReport})` + echo
57270
57447
  );
57271
57448
  } catch (err) {
57272
57449
  logMutation(errorRecord("update_page", page_id, err, {
@@ -57323,6 +57500,12 @@ ${truncated}`);
57323
57500
  const blocked = writeGuard("update_page_section", config3);
57324
57501
  if (blocked) return blocked;
57325
57502
  try {
57503
+ if (body.includes("epimethian:read-only-markdown")) {
57504
+ throw new ConverterError(
57505
+ "The body contains content produced by get_page with format: 'markdown', which is a read-only rendering not suitable for section updates. Use format: 'storage' to read the section, then edit the storage XML.",
57506
+ "READ_ONLY_MARKDOWN_ROUND_TRIP"
57507
+ );
57508
+ }
57326
57509
  const page = await getPage(page_id, true);
57327
57510
  const fullBody = page.body?.storage?.value ?? page.body?.value ?? "";
57328
57511
  const newFullBody = replaceSection(fullBody, section, body);
@@ -57659,8 +57842,8 @@ ${truncated}`);
57659
57842
  const blocked = writeGuard("add_attachment", config3);
57660
57843
  if (blocked) return blocked;
57661
57844
  try {
57662
- const resolved = await (0, import_promises2.realpath)((0, import_node_path3.resolve)(file_path));
57663
- const cwd = await (0, import_promises2.realpath)(process.cwd());
57845
+ const resolved = await (0, import_promises3.realpath)((0, import_node_path4.resolve)(file_path));
57846
+ const cwd = await (0, import_promises3.realpath)(process.cwd());
57664
57847
  if (!resolved.startsWith(cwd + "/") && resolved !== cwd) {
57665
57848
  return toolError(
57666
57849
  new Error(
@@ -57668,7 +57851,7 @@ ${truncated}`);
57668
57851
  )
57669
57852
  );
57670
57853
  }
57671
- const fileData = await (0, import_promises2.readFile)(resolved);
57854
+ const fileData = await (0, import_promises3.readFile)(resolved);
57672
57855
  const name = filename ?? resolved.split("/").pop() ?? "attachment";
57673
57856
  const att = await uploadAttachment(page_id, fileData, name, comment2);
57674
57857
  return toolResult(
@@ -57708,14 +57891,14 @@ ${truncated}`);
57708
57891
  if (blocked) return blocked;
57709
57892
  try {
57710
57893
  const filename = diagram_name.endsWith(".drawio") ? diagram_name : `${diagram_name}.drawio`;
57711
- const tmpDir = await (0, import_promises2.mkdtemp)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "drawio-"));
57894
+ const tmpDir = await (0, import_promises3.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "drawio-"));
57712
57895
  try {
57713
- const tmpPath = (0, import_node_path3.join)(tmpDir, filename);
57714
- await (0, import_promises2.writeFile)(tmpPath, diagram_xml, "utf-8");
57715
- const fileData = await (0, import_promises2.readFile)(tmpPath);
57896
+ const tmpPath = (0, import_node_path4.join)(tmpDir, filename);
57897
+ await (0, import_promises3.writeFile)(tmpPath, diagram_xml, "utf-8");
57898
+ const fileData = await (0, import_promises3.readFile)(tmpPath);
57716
57899
  await uploadAttachment(page_id, fileData, filename);
57717
57900
  } finally {
57718
- await (0, import_promises2.rm)(tmpDir, { recursive: true, force: true });
57901
+ await (0, import_promises3.rm)(tmpDir, { recursive: true, force: true });
57719
57902
  }
57720
57903
  const macroId = crypto.randomUUID();
57721
57904
  const localId = crypto.randomUUID();
@@ -58350,27 +58533,75 @@ ${lines.join("\n")}${echo2}`
58350
58533
  server.registerTool(
58351
58534
  "get_version",
58352
58535
  {
58353
- description: "Return the epimethian-mcp server version.",
58536
+ description: "Return the epimethian-mcp server version. Also reports available updates, if any.",
58354
58537
  inputSchema: {}
58355
58538
  },
58356
- async () => toolResult(`epimethian-mcp v${"5.2.0"}`)
58539
+ async () => {
58540
+ let text2 = `epimethian-mcp v${"5.3.0"}`;
58541
+ try {
58542
+ const pending = await getPendingUpdate();
58543
+ if (pending) {
58544
+ if (pending.autoInstalled) {
58545
+ text2 += `
58546
+
58547
+ Bugfix v${pending.latest} has been installed automatically. Restart the MCP server to apply.`;
58548
+ } else {
58549
+ text2 += `
58550
+
58551
+ ${pending.type === "major" ? "Major" : "Minor"} update available: v${pending.current} \u2192 v${pending.latest}. Call the upgrade tool to install.`;
58552
+ }
58553
+ }
58554
+ } catch {
58555
+ }
58556
+ return toolResult(text2);
58557
+ }
58558
+ );
58559
+ server.registerTool(
58560
+ "upgrade",
58561
+ {
58562
+ description: "Upgrade epimethian-mcp to the latest available version. After a successful upgrade the user must restart the MCP server (reload the VS Code window or restart Claude).",
58563
+ inputSchema: {}
58564
+ },
58565
+ async () => {
58566
+ try {
58567
+ const pending = await getPendingUpdate();
58568
+ if (!pending) {
58569
+ return toolResult(
58570
+ `epimethian-mcp v${"5.3.0"} is already up to date.`
58571
+ );
58572
+ }
58573
+ const output = await performUpgrade(pending.latest);
58574
+ await clearPendingUpdate();
58575
+ return toolResult(
58576
+ `Upgraded epimethian-mcp from v${pending.current} to v${pending.latest}.
58577
+
58578
+ \u26A0 Restart required: reload the VS Code window (or restart Claude) so the new version takes effect.
58579
+
58580
+ ` + output
58581
+ );
58582
+ } catch (err) {
58583
+ return toolError(err);
58584
+ }
58585
+ }
58357
58586
  );
58358
58587
  }
58359
58588
  async function main() {
58360
58589
  const config3 = await getConfig();
58361
58590
  await validateStartup(config3);
58362
58591
  if (process.env.EPIMETHIAN_MUTATION_LOG === "true") {
58363
- const logDir = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".epimethian", "logs");
58592
+ const logDir = (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".epimethian", "logs");
58364
58593
  initMutationLog(logDir);
58365
58594
  }
58366
58595
  const serverName = config3.profile ? `confluence-${config3.profile}` : "confluence";
58367
58596
  const server = new McpServer({
58368
58597
  name: serverName,
58369
- version: "5.2.0"
58598
+ version: "5.3.0"
58370
58599
  });
58371
58600
  registerTools(server, config3);
58372
58601
  const transport = new StdioServerTransport();
58373
58602
  await server.connect(transport);
58603
+ checkForUpdates("5.3.0").catch(() => {
58604
+ });
58374
58605
  }
58375
58606
 
58376
58607
  // src/cli/index.ts