@de-otio/epimethian-mcp 5.1.1 → 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/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());
@@ -48850,6 +48850,61 @@ init_keychain();
48850
48850
  init_profiles();
48851
48851
  init_test_connection();
48852
48852
 
48853
+ // src/server/converter/escape.ts
48854
+ function escapeXmlAttr(s) {
48855
+ let out = "";
48856
+ for (let i = 0; i < s.length; i++) {
48857
+ const ch = s.charCodeAt(i);
48858
+ switch (ch) {
48859
+ case 38:
48860
+ out += "&amp;";
48861
+ break;
48862
+ case 60:
48863
+ out += "&lt;";
48864
+ break;
48865
+ case 62:
48866
+ out += "&gt;";
48867
+ break;
48868
+ case 34:
48869
+ out += "&quot;";
48870
+ break;
48871
+ case 39:
48872
+ out += "&#39;";
48873
+ break;
48874
+ default:
48875
+ if (ch >= 0 && ch <= 31 || ch >= 127 && ch <= 159) {
48876
+ out += `&#x${ch.toString(16).toUpperCase()};`;
48877
+ } else {
48878
+ out += s[i];
48879
+ }
48880
+ }
48881
+ }
48882
+ return out;
48883
+ }
48884
+ function escapeXmlText(s) {
48885
+ let out = "";
48886
+ for (let i = 0; i < s.length; i++) {
48887
+ const ch = s.charCodeAt(i);
48888
+ switch (ch) {
48889
+ case 38:
48890
+ out += "&amp;";
48891
+ break;
48892
+ case 60:
48893
+ out += "&lt;";
48894
+ break;
48895
+ case 62:
48896
+ out += "&gt;";
48897
+ break;
48898
+ default:
48899
+ out += s[i];
48900
+ }
48901
+ }
48902
+ return out;
48903
+ }
48904
+ function escapeCdata(s) {
48905
+ return s.replace(/\]\]>/g, "]]]]><![CDATA[>");
48906
+ }
48907
+
48853
48908
  // src/server/page-cache.ts
48854
48909
  var PageCache = class {
48855
48910
  cache = /* @__PURE__ */ new Map();
@@ -48959,6 +49014,10 @@ var PageCache = class {
48959
49014
  var pageCache = new PageCache();
48960
49015
 
48961
49016
  // src/server/confluence-client.ts
49017
+ var _clientLabel;
49018
+ function setClientLabel(label) {
49019
+ _clientLabel = label ? label.slice(0, 80) : void 0;
49020
+ }
48962
49021
  var _config = null;
48963
49022
  async function resolveCredentials() {
48964
49023
  const profileEnv = process.env.CONFLUENCE_PROFILE || "";
@@ -49262,10 +49321,11 @@ async function getPage(pageId, includeBody) {
49262
49321
  }
49263
49322
  return page;
49264
49323
  }
49265
- async function createPage(spaceId, title, body, parentId) {
49324
+ async function createPage(spaceId, title, body, parentId, clientLabel) {
49266
49325
  const cfg = await getConfig();
49267
- const cleanBody = stripAttributionFooter(toStorageFormat(body));
49268
- const pageBody = cfg.attribution ? cleanBody + "\n" + buildAttributionFooter("created") : cleanBody;
49326
+ const pageBody = stripAttributionFooter(toStorageFormat(body));
49327
+ const epimethianTag = `Epimethian v${"5.3.0"}`;
49328
+ const versionMsg = cfg.attribution && clientLabel ? `Created by ${clientLabel} (via ${epimethianTag})` : `Created by ${epimethianTag}`;
49269
49329
  const payload = {
49270
49330
  title,
49271
49331
  spaceId,
@@ -49274,14 +49334,14 @@ async function createPage(spaceId, title, body, parentId) {
49274
49334
  representation: "storage",
49275
49335
  value: pageBody
49276
49336
  },
49277
- version: { message: `Created by Epimethian v${"5.1.1"}` }
49337
+ version: { message: versionMsg }
49278
49338
  };
49279
49339
  if (parentId) payload.parentId = parentId;
49280
49340
  const raw = await v2Post("/pages", payload);
49281
49341
  const page = PageSchema.parse(raw);
49282
49342
  pageCache.set(page.id, page.version?.number ?? 1, pageBody);
49283
49343
  try {
49284
- await addLabels(page.id, [ATTRIBUTION_LABEL]);
49344
+ await ensureAttributionLabel(page.id);
49285
49345
  } catch {
49286
49346
  }
49287
49347
  return page;
@@ -49289,7 +49349,17 @@ async function createPage(spaceId, title, body, parentId) {
49289
49349
  async function updatePage(pageId, opts) {
49290
49350
  const cfg = await getConfig();
49291
49351
  const newVersion = opts.version + 1;
49292
- const versionMessage = opts.versionMessage ? `${opts.versionMessage} (via Epimethian v${"5.1.1"})` : `Updated by Epimethian v${"5.1.1"}`;
49352
+ const epimethianTag = `Epimethian v${"5.3.0"}`;
49353
+ const effectiveClient = cfg.attribution ? opts.clientLabel : void 0;
49354
+ let versionMessage;
49355
+ if (opts.versionMessage && effectiveClient)
49356
+ versionMessage = `${opts.versionMessage} (${effectiveClient} via ${epimethianTag})`;
49357
+ else if (opts.versionMessage)
49358
+ versionMessage = `${opts.versionMessage} (via ${epimethianTag})`;
49359
+ else if (effectiveClient)
49360
+ versionMessage = `Updated by ${effectiveClient} (via ${epimethianTag})`;
49361
+ else
49362
+ versionMessage = `Updated by ${epimethianTag}`;
49293
49363
  const payload = {
49294
49364
  id: pageId,
49295
49365
  status: "current",
@@ -49297,8 +49367,7 @@ async function updatePage(pageId, opts) {
49297
49367
  version: { number: newVersion, message: versionMessage }
49298
49368
  };
49299
49369
  if (opts.body) {
49300
- const cleanBody = stripAttributionFooter(toStorageFormat(opts.body));
49301
- const pageBody = cfg.attribution ? cleanBody + "\n" + buildAttributionFooter("updated") : cleanBody;
49370
+ const pageBody = stripAttributionFooter(toStorageFormat(opts.body));
49302
49371
  payload.body = {
49303
49372
  representation: "storage",
49304
49373
  value: pageBody
@@ -49318,12 +49387,11 @@ async function updatePage(pageId, opts) {
49318
49387
  }
49319
49388
  const page = PageSchema.parse(raw);
49320
49389
  if (opts.body) {
49321
- const cleanBody = stripAttributionFooter(toStorageFormat(opts.body));
49322
- const pageBody = cfg.attribution ? cleanBody + "\n" + buildAttributionFooter("updated") : cleanBody;
49390
+ const pageBody = stripAttributionFooter(toStorageFormat(opts.body));
49323
49391
  pageCache.set(pageId, newVersion, pageBody);
49324
49392
  }
49325
49393
  try {
49326
- await addLabels(page.id, [ATTRIBUTION_LABEL]);
49394
+ await ensureAttributionLabel(page.id);
49327
49395
  } catch {
49328
49396
  }
49329
49397
  return { page, newVersion };
@@ -49495,12 +49563,14 @@ async function uploadAttachment(pageId, fileData, filename, comment2) {
49495
49563
  if (!att) throw new Error("Attachment uploaded but no details returned.");
49496
49564
  return { title: att.title, id: att.id, fileSize: att.extensions?.fileSize };
49497
49565
  }
49498
- var GITHUB_URL = "https://github.com/de-otio/epimethian-mcp";
49499
- var ATTRIBUTION_LABEL = "epimethian-managed";
49500
- var ATTRIBUTION_START = "<!-- epimethian-attribution-start -->";
49501
- var ATTRIBUTION_END = "<!-- epimethian-attribution-end -->";
49502
- function buildAttributionFooter(action) {
49503
- return ATTRIBUTION_START + `<p style="font-size:9px;color:#999;margin-top:2em;"><em>This page was ${action} with <a href="${GITHUB_URL}">Epimethian</a> v${"5.1.1"}.</em></p>` + ATTRIBUTION_END;
49566
+ var ATTRIBUTION_LABEL = "epimethian-edited";
49567
+ var LEGACY_ATTRIBUTION_LABEL = "epimethian-managed";
49568
+ async function ensureAttributionLabel(pageId) {
49569
+ await addLabels(pageId, [ATTRIBUTION_LABEL]);
49570
+ const labels = await getLabels(pageId);
49571
+ if (labels.some((l) => l.name === LEGACY_ATTRIBUTION_LABEL)) {
49572
+ await removeLabel(pageId, LEGACY_ATTRIBUTION_LABEL);
49573
+ }
49504
49574
  }
49505
49575
  function stripAttributionFooter(body) {
49506
49576
  return body.replace(
@@ -49593,8 +49663,10 @@ async function getCommentReplies(commentId, type) {
49593
49663
  return CommentsResultSchema.parse(raw).results;
49594
49664
  }
49595
49665
  async function createFooterComment(pageId, body, parentCommentId) {
49666
+ const cfg = await getConfig();
49596
49667
  const sanitized = sanitizeCommentBody(toStorageFormat(body));
49597
- const attributed = `<p><em>[AI-generated via Epimethian]</em></p>${sanitized}`;
49668
+ const label = cfg.attribution ? _clientLabel : void 0;
49669
+ const attributed = label ? `<p><em>[AI-generated by ${escapeXmlText(label)} via Epimethian]</em></p>${sanitized}` : `<p><em>[AI-generated via Epimethian]</em></p>${sanitized}`;
49598
49670
  const payload = parentCommentId ? {
49599
49671
  parentCommentId,
49600
49672
  body: { representation: "storage", value: attributed }
@@ -49606,8 +49678,10 @@ async function createFooterComment(pageId, body, parentCommentId) {
49606
49678
  return CommentSchema.parse(raw);
49607
49679
  }
49608
49680
  async function createInlineComment(pageId, body, textSelection, textSelectionMatchIndex = 0, parentCommentId) {
49681
+ const cfg = await getConfig();
49609
49682
  const sanitized = sanitizeCommentBody(toStorageFormat(body));
49610
- const attributed = `<p><em>[AI-generated via Epimethian]</em></p>${sanitized}`;
49683
+ const label = cfg.attribution ? _clientLabel : void 0;
49684
+ const attributed = label ? `<p><em>[AI-generated by ${escapeXmlText(label)} via Epimethian]</em></p>${sanitized}` : `<p><em>[AI-generated via Epimethian]</em></p>${sanitized}`;
49611
49685
  if (parentCommentId) {
49612
49686
  const raw2 = await v2Post("/inline-comments", {
49613
49687
  parentCommentId,
@@ -55737,61 +55811,6 @@ function container_plugin(md, name, options2) {
55737
55811
  var import_gray_matter = __toESM(require_gray_matter());
55738
55812
  var import_crypto = require("crypto");
55739
55813
 
55740
- // src/server/converter/escape.ts
55741
- function escapeXmlAttr(s) {
55742
- let out = "";
55743
- for (let i = 0; i < s.length; i++) {
55744
- const ch = s.charCodeAt(i);
55745
- switch (ch) {
55746
- case 38:
55747
- out += "&amp;";
55748
- break;
55749
- case 60:
55750
- out += "&lt;";
55751
- break;
55752
- case 62:
55753
- out += "&gt;";
55754
- break;
55755
- case 34:
55756
- out += "&quot;";
55757
- break;
55758
- case 39:
55759
- out += "&#39;";
55760
- break;
55761
- default:
55762
- if (ch >= 0 && ch <= 31 || ch >= 127 && ch <= 159) {
55763
- out += `&#x${ch.toString(16).toUpperCase()};`;
55764
- } else {
55765
- out += s[i];
55766
- }
55767
- }
55768
- }
55769
- return out;
55770
- }
55771
- function escapeXmlText(s) {
55772
- let out = "";
55773
- for (let i = 0; i < s.length; i++) {
55774
- const ch = s.charCodeAt(i);
55775
- switch (ch) {
55776
- case 38:
55777
- out += "&amp;";
55778
- break;
55779
- case 60:
55780
- out += "&lt;";
55781
- break;
55782
- case 62:
55783
- out += "&gt;";
55784
- break;
55785
- default:
55786
- out += s[i];
55787
- }
55788
- }
55789
- return out;
55790
- }
55791
- function escapeCdata(s) {
55792
- return s.replace(/\]\]>/g, "]]]]><![CDATA[>");
55793
- }
55794
-
55795
55814
  // src/server/converter/url-parser.ts
55796
55815
  var DEFAULT_PORTS = {
55797
55816
  "http:": "80",
@@ -56864,10 +56883,162 @@ function errorRecord(operation, pageId, err, extra) {
56864
56883
  };
56865
56884
  }
56866
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
+
56867
57032
  // src/server/index.ts
57033
+ function getClientLabel(server) {
57034
+ const client = server.server.getClientVersion();
57035
+ const raw = client?.title || client?.name || void 0;
57036
+ return raw ? raw.slice(0, 80) : void 0;
57037
+ }
56868
57038
  function escapeXml(s) {
56869
57039
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
56870
57040
  }
57041
+ var READ_ONLY_MARKDOWN_MARKER = "<!-- epimethian:read-only-markdown \u2014 do not pass this content to update_page -->";
56871
57042
  function formatMarkdownWithTokens(markdown, sidecar, header) {
56872
57043
  const tokenCount = Object.keys(sidecar).length;
56873
57044
  let body = markdown;
@@ -56880,13 +57051,19 @@ function formatMarkdownWithTokens(markdown, sidecar, header) {
56880
57051
  const name = m && m[2] ? ` ac:name="${m[2]}"` : "";
56881
57052
  return `- [[epi:${id}]]: <${tag}${name}>`;
56882
57053
  }).join("\n");
56883
- 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 -->
56884
57057
 
56885
57058
  ${markdown}
56886
57059
 
56887
57060
  ---
56888
57061
  Tokens:
56889
57062
  ${table2}`;
57063
+ } else {
57064
+ body = `${READ_ONLY_MARKDOWN_MARKER}
57065
+
57066
+ ${markdown}`;
56890
57067
  }
56891
57068
  return `${header}
56892
57069
 
@@ -56921,6 +57098,7 @@ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
56921
57098
  "get_page_version",
56922
57099
  "diff_page_versions",
56923
57100
  "get_version",
57101
+ "upgrade",
56924
57102
  "lookup_user",
56925
57103
  "resolve_page_link"
56926
57104
  ]);
@@ -56997,6 +57175,12 @@ function registerTools(server, config3) {
56997
57175
  );
56998
57176
  const pageIdSchema = external_exports.string().regex(/^\d+$/, "Page ID must be numeric");
56999
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
+ }
57000
57184
  const currentPage = await getPage(page_id, true);
57001
57185
  const currentStorage = currentPage.body?.storage?.value ?? currentPage.body?.value ?? "";
57002
57186
  const isMarkdown = looksLikeMarkdown(newContent);
@@ -57024,7 +57208,8 @@ function registerTools(server, config3) {
57024
57208
  title: currentPage.title,
57025
57209
  body: newBody,
57026
57210
  version: version2,
57027
- versionMessage: opts.versionMessage
57211
+ versionMessage: opts.versionMessage,
57212
+ clientLabel: getClientLabel(server)
57028
57213
  });
57029
57214
  return { page, newVersion, oldLen: currentStorage.length, newLen: newBody.length };
57030
57215
  }
@@ -57051,6 +57236,12 @@ function registerTools(server, config3) {
57051
57236
  const blocked = writeGuard("create_page", config3);
57052
57237
  if (blocked) return blocked;
57053
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
+ }
57054
57245
  let finalBody = body;
57055
57246
  if (looksLikeMarkdown(body)) {
57056
57247
  const cfg = await getConfig();
@@ -57060,7 +57251,7 @@ function registerTools(server, config3) {
57060
57251
  });
57061
57252
  }
57062
57253
  const spaceId = await resolveSpaceId(space_key);
57063
- const page = await createPage(spaceId, title, finalBody, parent_id);
57254
+ const page = await createPage(spaceId, title, finalBody, parent_id, getClientLabel(server));
57064
57255
  logMutation({
57065
57256
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
57066
57257
  operation: "create_page",
@@ -57200,7 +57391,15 @@ ${truncated}`);
57200
57391
  const currentStorage = currentPage.body?.storage?.value ?? currentPage.body?.value ?? "";
57201
57392
  let finalStorage;
57202
57393
  let effectiveVersionMessage = version_message;
57203
- 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)) {
57204
57403
  const plan = planUpdate({
57205
57404
  currentStorage,
57206
57405
  callerMarkdown: body,
@@ -57214,20 +57413,23 @@ ${truncated}`);
57214
57413
  finalStorage = plan.newStorage;
57215
57414
  effectiveVersionMessage = plan.versionMessage && version_message ? `${version_message}; ${plan.versionMessage}` : plan.versionMessage ?? version_message;
57216
57415
  } else {
57217
- 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
+ });
57218
57425
  }
57219
- enforceContentSafetyGuards({
57220
- oldStorage: currentStorage,
57221
- newStorage: finalStorage,
57222
- confirmShrinkage: confirm_shrinkage,
57223
- confirmStructureLoss: confirm_structure_loss
57224
- });
57225
57426
  const { page, newVersion } = await updatePage(page_id, {
57226
57427
  title,
57227
57428
  body: finalStorage,
57228
57429
  version: version2,
57229
57430
  versionMessage: effectiveVersionMessage,
57230
- previousBody: currentStorage
57431
+ previousBody: currentStorage,
57432
+ clientLabel: getClientLabel(server)
57231
57433
  });
57232
57434
  logMutation({
57233
57435
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -57236,11 +57438,12 @@ ${truncated}`);
57236
57438
  oldVersion: version2,
57237
57439
  newVersion,
57238
57440
  oldBodyLen: currentStorage.length,
57239
- newBodyLen: finalStorage.length,
57441
+ newBodyLen: finalStorage?.length ?? currentStorage.length,
57240
57442
  replaceBody: replace_body || void 0
57241
57443
  });
57444
+ const bodyReport = finalStorage !== void 0 ? `body: ${currentStorage.length}\u2192${finalStorage.length} chars` : `title only, body unchanged`;
57242
57445
  return toolResult(
57243
- `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
57244
57447
  );
57245
57448
  } catch (err) {
57246
57449
  logMutation(errorRecord("update_page", page_id, err, {
@@ -57297,6 +57500,12 @@ ${truncated}`);
57297
57500
  const blocked = writeGuard("update_page_section", config3);
57298
57501
  if (blocked) return blocked;
57299
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
+ }
57300
57509
  const page = await getPage(page_id, true);
57301
57510
  const fullBody = page.body?.storage?.value ?? page.body?.value ?? "";
57302
57511
  const newFullBody = replaceSection(fullBody, section, body);
@@ -57310,7 +57519,8 @@ ${truncated}`);
57310
57519
  body: newFullBody,
57311
57520
  version: version2,
57312
57521
  versionMessage: version_message,
57313
- previousBody: fullBody
57522
+ previousBody: fullBody,
57523
+ clientLabel: getClientLabel(server)
57314
57524
  });
57315
57525
  logMutation({
57316
57526
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -57632,8 +57842,8 @@ ${truncated}`);
57632
57842
  const blocked = writeGuard("add_attachment", config3);
57633
57843
  if (blocked) return blocked;
57634
57844
  try {
57635
- const resolved = await (0, import_promises2.realpath)((0, import_node_path3.resolve)(file_path));
57636
- 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());
57637
57847
  if (!resolved.startsWith(cwd + "/") && resolved !== cwd) {
57638
57848
  return toolError(
57639
57849
  new Error(
@@ -57641,7 +57851,7 @@ ${truncated}`);
57641
57851
  )
57642
57852
  );
57643
57853
  }
57644
- const fileData = await (0, import_promises2.readFile)(resolved);
57854
+ const fileData = await (0, import_promises3.readFile)(resolved);
57645
57855
  const name = filename ?? resolved.split("/").pop() ?? "attachment";
57646
57856
  const att = await uploadAttachment(page_id, fileData, name, comment2);
57647
57857
  return toolResult(
@@ -57681,14 +57891,14 @@ ${truncated}`);
57681
57891
  if (blocked) return blocked;
57682
57892
  try {
57683
57893
  const filename = diagram_name.endsWith(".drawio") ? diagram_name : `${diagram_name}.drawio`;
57684
- 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-"));
57685
57895
  try {
57686
- const tmpPath = (0, import_node_path3.join)(tmpDir, filename);
57687
- await (0, import_promises2.writeFile)(tmpPath, diagram_xml, "utf-8");
57688
- 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);
57689
57899
  await uploadAttachment(page_id, fileData, filename);
57690
57900
  } finally {
57691
- await (0, import_promises2.rm)(tmpDir, { recursive: true, force: true });
57901
+ await (0, import_promises3.rm)(tmpDir, { recursive: true, force: true });
57692
57902
  }
57693
57903
  const macroId = crypto.randomUUID();
57694
57904
  const localId = crypto.randomUUID();
@@ -57714,7 +57924,8 @@ ${macro}` : macro;
57714
57924
  title: current.title,
57715
57925
  body: newBody,
57716
57926
  version: current.version?.number ?? 0,
57717
- versionMessage: `Added diagram: ${filename}`
57927
+ versionMessage: `Added diagram: ${filename}`,
57928
+ clientLabel: getClientLabel(server)
57718
57929
  });
57719
57930
  return toolResult(
57720
57931
  `Diagram "${filename}" added to page ${page.title} (ID: ${page.id}, version: ${newVersion})` + echo
@@ -57954,6 +58165,7 @@ ${lines}`);
57954
58165
  async ({ page_id, body, type, parent_comment_id, text_selection, text_selection_match_index }) => {
57955
58166
  const blocked = writeGuard("create_comment", config3);
57956
58167
  if (blocked) return blocked;
58168
+ setClientLabel(getClientLabel(server));
57957
58169
  try {
57958
58170
  let comment2;
57959
58171
  if (type === "inline") {
@@ -58232,7 +58444,8 @@ ${result.diff}${truncNote}` + echo
58232
58444
  title: currentPage.title,
58233
58445
  body: historical.rawBody,
58234
58446
  version: current_version,
58235
- versionMessage: version_message ?? `Revert to version ${target_version}`
58447
+ versionMessage: version_message ?? `Revert to version ${target_version}`,
58448
+ clientLabel: getClientLabel(server)
58236
58449
  });
58237
58450
  logMutation({
58238
58451
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -58320,27 +58533,75 @@ ${lines.join("\n")}${echo2}`
58320
58533
  server.registerTool(
58321
58534
  "get_version",
58322
58535
  {
58323
- description: "Return the epimethian-mcp server version.",
58536
+ description: "Return the epimethian-mcp server version. Also reports available updates, if any.",
58324
58537
  inputSchema: {}
58325
58538
  },
58326
- async () => toolResult(`epimethian-mcp v${"5.1.1"}`)
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
+ }
58327
58586
  );
58328
58587
  }
58329
58588
  async function main() {
58330
58589
  const config3 = await getConfig();
58331
58590
  await validateStartup(config3);
58332
58591
  if (process.env.EPIMETHIAN_MUTATION_LOG === "true") {
58333
- 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");
58334
58593
  initMutationLog(logDir);
58335
58594
  }
58336
58595
  const serverName = config3.profile ? `confluence-${config3.profile}` : "confluence";
58337
58596
  const server = new McpServer({
58338
58597
  name: serverName,
58339
- version: "5.1.1"
58598
+ version: "5.3.0"
58340
58599
  });
58341
58600
  registerTools(server, config3);
58342
58601
  const transport = new StdioServerTransport();
58343
58602
  await server.connect(transport);
58603
+ checkForUpdates("5.3.0").catch(() => {
58604
+ });
58344
58605
  }
58345
58606
 
58346
58607
  // src/cli/index.ts