@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/README.md +2 -2
- package/dist/cli/index.js +371 -110
- package/dist/cli/index.js.map +4 -4
- package/package.json +1 -1
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
|
|
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 =
|
|
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
|
|
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
|
|
48844
|
-
var
|
|
48845
|
-
var
|
|
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 += "&";
|
|
48861
|
+
break;
|
|
48862
|
+
case 60:
|
|
48863
|
+
out += "<";
|
|
48864
|
+
break;
|
|
48865
|
+
case 62:
|
|
48866
|
+
out += ">";
|
|
48867
|
+
break;
|
|
48868
|
+
case 34:
|
|
48869
|
+
out += """;
|
|
48870
|
+
break;
|
|
48871
|
+
case 39:
|
|
48872
|
+
out += "'";
|
|
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 += "&";
|
|
48891
|
+
break;
|
|
48892
|
+
case 60:
|
|
48893
|
+
out += "<";
|
|
48894
|
+
break;
|
|
48895
|
+
case 62:
|
|
48896
|
+
out += ">";
|
|
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
|
|
49268
|
-
const
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
49499
|
-
var
|
|
49500
|
-
|
|
49501
|
-
|
|
49502
|
-
|
|
49503
|
-
|
|
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
|
|
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
|
|
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 += "&";
|
|
55748
|
-
break;
|
|
55749
|
-
case 60:
|
|
55750
|
-
out += "<";
|
|
55751
|
-
break;
|
|
55752
|
-
case 62:
|
|
55753
|
-
out += ">";
|
|
55754
|
-
break;
|
|
55755
|
-
case 34:
|
|
55756
|
-
out += """;
|
|
55757
|
-
break;
|
|
55758
|
-
case 39:
|
|
55759
|
-
out += "'";
|
|
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 += "&";
|
|
55778
|
-
break;
|
|
55779
|
-
case 60:
|
|
55780
|
-
out += "<";
|
|
55781
|
-
break;
|
|
55782
|
-
case 62:
|
|
55783
|
-
out += ">";
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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 =
|
|
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 &&
|
|
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},
|
|
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,
|
|
57636
|
-
const cwd = await (0,
|
|
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,
|
|
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,
|
|
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,
|
|
57687
|
-
await (0,
|
|
57688
|
-
const fileData = await (0,
|
|
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,
|
|
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 () =>
|
|
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,
|
|
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.
|
|
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
|