@de-otio/epimethian-mcp 5.5.0 → 5.6.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
@@ -35054,7 +35054,7 @@ async function getPage(pageId, includeBody) {
35054
35054
  async function _rawCreatePage(spaceId, title, body, parentId, clientLabel) {
35055
35055
  const cfg = await getConfig();
35056
35056
  const pageBody = stripAttributionFooter(toStorageFormat(body));
35057
- const epimethianTag = `Epimethian v${"5.5.0"}`;
35057
+ const epimethianTag = `Epimethian v${"5.6.0"}`;
35058
35058
  const versionMsg = cfg.attribution && clientLabel ? `Created by ${clientLabel} (via ${epimethianTag})` : `Created by ${epimethianTag}`;
35059
35059
  const payload = {
35060
35060
  title,
@@ -35079,7 +35079,7 @@ async function _rawCreatePage(spaceId, title, body, parentId, clientLabel) {
35079
35079
  async function _rawUpdatePage(pageId, opts) {
35080
35080
  const cfg = await getConfig();
35081
35081
  const newVersion = opts.version + 1;
35082
- const epimethianTag = `Epimethian v${"5.5.0"}`;
35082
+ const epimethianTag = `Epimethian v${"5.6.0"}`;
35083
35083
  const effectiveClient = cfg.attribution ? opts.clientLabel : void 0;
35084
35084
  let versionMessage;
35085
35085
  if (opts.versionMessage && effectiveClient)
@@ -46648,6 +46648,35 @@ var init_content_safety_guards = __esm({
46648
46648
  });
46649
46649
 
46650
46650
  // src/server/safe-write.ts
46651
+ function detectMixedInput(body) {
46652
+ let stripped = body.replace(
46653
+ /^(`{3,})[^\n]*\n[\s\S]*?^\1\s*$/gm,
46654
+ ""
46655
+ );
46656
+ stripped = stripped.replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "");
46657
+ stripped = stripped.replace(
46658
+ /<ac:plain-text-body>[\s\S]*?<\/ac:plain-text-body>/gi,
46659
+ ""
46660
+ );
46661
+ if (!/<ac:|<ri:|<time[\s/>]/i.test(stripped)) {
46662
+ return [];
46663
+ }
46664
+ const STRUCTURAL_MD = [
46665
+ { name: "ATX heading (## ...)", re: /^#{1,6}[ \t]+\S/m },
46666
+ { name: "fenced code block (```...)", re: /^```/m },
46667
+ { name: "GFM table separator (| --- |)", re: /^\|[\s\-:|]+\|\s*$/m },
46668
+ { name: "unordered list (- ... or * ...)", re: /^[-*][ \t]+\S/m },
46669
+ { name: "ordered list (1. ...)", re: /^\d+\.[ \t]+\S/m },
46670
+ {
46671
+ name: "GitHub alert (> [!NOTE])",
46672
+ re: /^>\s*\[!(INFO|NOTE|TIP|WARNING|CAUTION|IMPORTANT)\]/im
46673
+ },
46674
+ { name: "YAML frontmatter delimiter (---)", re: /^---\s*$/m }
46675
+ ];
46676
+ return STRUCTURAL_MD.filter(({ re }) => re.test(stripped)).map(
46677
+ ({ name }) => name
46678
+ );
46679
+ }
46651
46680
  function assertPostTransformBody(inputLen, outputBody) {
46652
46681
  if (outputBody.trim().length === 0) {
46653
46682
  throw new ConverterError(
@@ -46759,6 +46788,25 @@ async function safePrepareBody(input) {
46759
46788
  READ_ONLY_MARKDOWN_ROUND_TRIP
46760
46789
  );
46761
46790
  }
46791
+ const mixedSignals = detectMixedInput(body);
46792
+ if (mixedSignals.length > 0) {
46793
+ throw new ConverterError(
46794
+ `Body contains BOTH Confluence storage tags (<ac:.../>, <ri:.../>) AND markdown structural patterns: ${mixedSignals.join(", ")}.
46795
+
46796
+ The format detector classifies any body containing storage tags as storage format and skips markdown\u2192storage conversion. Submitting this would store the markdown verbatim and Confluence would render it as literal text.
46797
+
46798
+ Pick one path:
46799
+ \u2022 TOC macro from markdown \u2014 drop the inline <ac:structured-macro ac:name="toc"/> and add YAML frontmatter at the top of the body:
46800
+ ---
46801
+ toc:
46802
+ maxLevel: 3
46803
+ minLevel: 1
46804
+ ---
46805
+ \u2022 Other macros from markdown \u2014 use directive syntax (e.g. ":info[content]", ":mention[Name]{accountId=...}", ":date[2026-04-23]").
46806
+ \u2022 Pure storage format \u2014 convert the markdown structure (## headings, lists, tables, code fences) to <h1>/<h2>/<p>/<ul>/<table>/etc. before submitting.`,
46807
+ MIXED_INPUT_DETECTED
46808
+ );
46809
+ }
46762
46810
  const converterOptions = {
46763
46811
  allowRawHtml: allowRawHtml === true,
46764
46812
  ...confluenceBaseUrl ? { confluenceBaseUrl } : {}
@@ -46951,7 +46999,7 @@ async function safeSubmitPage(input) {
46951
46999
  throw err;
46952
47000
  }
46953
47001
  }
46954
- var DELETION_ACK_MISMATCH, POST_TRANSFORM_BODY_REJECTED, READ_ONLY_MARKDOWN_ROUND_TRIP, POST_TRANSFORM_MIN_INPUT_LEN, POST_TRANSFORM_MAX_REDUCTION_RATIO;
47002
+ var DELETION_ACK_MISMATCH, POST_TRANSFORM_BODY_REJECTED, READ_ONLY_MARKDOWN_ROUND_TRIP, MIXED_INPUT_DETECTED, POST_TRANSFORM_MIN_INPUT_LEN, POST_TRANSFORM_MAX_REDUCTION_RATIO;
46955
47003
  var init_safe_write = __esm({
46956
47004
  "src/server/safe-write.ts"() {
46957
47005
  "use strict";
@@ -46965,6 +47013,7 @@ var init_safe_write = __esm({
46965
47013
  DELETION_ACK_MISMATCH = "DELETION_ACK_MISMATCH";
46966
47014
  POST_TRANSFORM_BODY_REJECTED = "POST_TRANSFORM_BODY_REJECTED";
46967
47015
  READ_ONLY_MARKDOWN_ROUND_TRIP = "READ_ONLY_MARKDOWN_ROUND_TRIP";
47016
+ MIXED_INPUT_DETECTED = "MIXED_INPUT_DETECTED";
46968
47017
  POST_TRANSFORM_MIN_INPUT_LEN = 500;
46969
47018
  POST_TRANSFORM_MAX_REDUCTION_RATIO = 0.9;
46970
47019
  }
@@ -47944,7 +47993,7 @@ __export(upgrade_exports, {
47944
47993
  runUpgrade: () => runUpgrade
47945
47994
  });
47946
47995
  async function runUpgrade() {
47947
- const currentVersion = "5.5.0";
47996
+ const currentVersion = "5.6.0";
47948
47997
  console.log(`epimethian-mcp upgrade: current version v${currentVersion}`);
47949
47998
  let pending = await getPendingUpdate();
47950
47999
  if (!pending) {
@@ -58962,7 +59011,7 @@ function registerTools(server, config3) {
58962
59011
  {
58963
59012
  description: describeWithLock(
58964
59013
  withDestructiveWarning(
58965
- "Create a new page in Confluence. Accepts either Confluence storage format (XHTML) or GFM markdown \u2014 markdown is automatically converted to storage format before submission. Use allow_raw_html: true to permit raw HTML inside markdown (disabled by default for security). Use confluence_base_url to override the base URL used by the link rewriter (defaults to the configured Confluence URL)."
59014
+ "Create a new page in Confluence. Accepts either Confluence storage format (XHTML) or GFM markdown \u2014 markdown is automatically converted to storage format before submission. Do NOT mix the two: a body that contains both <ac:.../> storage tags AND markdown structural patterns (## headings, lists, fenced code blocks) is rejected with MIXED_INPUT_DETECTED. To inject a TOC macro from markdown, use YAML frontmatter at the top of the body: `---\\ntoc:\\n maxLevel: 3\\n minLevel: 1\\n---`. For other macros from markdown, use directive syntax: `:info[content]`, `:mention[Name]{accountId=...}`, `:date[2026-04-23]`. Use allow_raw_html: true to permit raw HTML inside markdown (disabled by default for security). Use confluence_base_url to override the base URL used by the link rewriter (defaults to the configured Confluence URL)."
58966
59015
  ),
58967
59016
  config3
58968
59017
  ),
@@ -58970,7 +59019,7 @@ function registerTools(server, config3) {
58970
59019
  title: external_exports.string().describe("Page title"),
58971
59020
  space_key: external_exports.string().describe("Confluence space key, e.g. 'DEV' or 'TEAM'"),
58972
59021
  body: external_exports.string().describe(
58973
- "Page content \u2014 GFM markdown or Confluence storage format (XHTML). Markdown is auto-detected and converted."
59022
+ "Page content \u2014 GFM markdown or Confluence storage format (XHTML). Markdown is auto-detected and converted. Do not mix the two: inlining <ac:.../> macros inside a markdown body is rejected. For a TOC use YAML frontmatter (toc: { maxLevel, minLevel }); for other macros use directive syntax (:info[...], :mention[...]{...})."
58974
59023
  ),
58975
59024
  parent_id: external_exports.string().optional().describe("Optional parent page ID"),
58976
59025
  allow_raw_html: external_exports.boolean().default(false).describe("Allow raw HTML passthrough inside markdown bodies (disabled by default; only enable for trusted content)."),
@@ -59099,7 +59148,7 @@ ${truncated}`);
59099
59148
  {
59100
59149
  description: describeWithLock(
59101
59150
  withDestructiveWarning(
59102
- "Update an existing Confluence page. Accepts GFM markdown or Confluence storage format \u2014 markdown is automatically converted via the token-aware write path, which preserves all existing macros and rich elements. You must provide the version number from your most recent get_page call. If the page was modified by someone else since then, this will return a conflict error \u2014 re-read the page and retry.\n\nFor narrow changes to a single section, prefer update_page_section \u2014 it leaves the rest of the page untouched and is safer for targeted edits.\n\nMarkdown update flags:\n- confirm_deletions: set to true to acknowledge removing preserved macros/elements (default false \u2014 any deletion errors until confirmed).\n- replace_body: set to true for a wholesale rewrite that skips preservation (default false).\n- confirm_shrinkage: set to true to acknowledge a >50% body size reduction (default false).\n- confirm_structure_loss: set to true to acknowledge a >50% heading count drop (default false).\n- allow_raw_html: allow raw HTML inside markdown bodies (default false).\n- confluence_base_url: override the URL used by the link rewriter.\n\nreplace_body skips all safety nets (token preservation, deletion confirmation). When delegating update_page to a subagent, ensure the agent includes the full existing body \u2014 replace_body replaces ALL content with only what you provide."
59151
+ "Update an existing Confluence page. Accepts GFM markdown or Confluence storage format \u2014 markdown is automatically converted via the token-aware write path, which preserves all existing macros and rich elements. Do NOT mix the two: a body that contains both <ac:.../> storage tags AND markdown structural patterns (## headings, lists, fenced code blocks) is rejected with MIXED_INPUT_DETECTED. To inject a TOC macro from markdown, use YAML frontmatter at the top of the body: `---\\ntoc:\\n maxLevel: 3\\n minLevel: 1\\n---`. For other macros from markdown, use directive syntax: `:info[content]`, `:mention[Name]{accountId=...}`, `:date[2026-04-23]`. You must provide the version number from your most recent get_page call. If the page was modified by someone else since then, this will return a conflict error \u2014 re-read the page and retry.\n\nFor narrow changes to a single section, prefer update_page_section \u2014 it leaves the rest of the page untouched and is safer for targeted edits.\n\nMarkdown update flags:\n- confirm_deletions: set to true to acknowledge removing preserved macros/elements (default false \u2014 any deletion errors until confirmed).\n- replace_body: set to true for a wholesale rewrite that skips preservation (default false).\n- confirm_shrinkage: set to true to acknowledge a >50% body size reduction (default false).\n- confirm_structure_loss: set to true to acknowledge a >50% heading count drop (default false).\n- allow_raw_html: allow raw HTML inside markdown bodies (default false).\n- confluence_base_url: override the URL used by the link rewriter.\n\nreplace_body skips all safety nets (token preservation, deletion confirmation). When delegating update_page to a subagent, ensure the agent includes the full existing body \u2014 replace_body replaces ALL content with only what you provide."
59103
59152
  ),
59104
59153
  config3
59105
59154
  ),
@@ -59107,7 +59156,7 @@ ${truncated}`);
59107
59156
  page_id: external_exports.string().describe("The Confluence page ID"),
59108
59157
  title: external_exports.string().describe("Page title (use the title from get_page if unchanged)"),
59109
59158
  version: external_exports.number().int().positive().describe("The page version number from your most recent get_page call"),
59110
- body: external_exports.string().optional().describe("New body content \u2014 GFM markdown or Confluence storage format (XHTML). Markdown is auto-detected and converted via the token-aware write path."),
59159
+ body: external_exports.string().optional().describe("New body content \u2014 GFM markdown or Confluence storage format (XHTML). Markdown is auto-detected and converted via the token-aware write path. Do not mix the two: inlining <ac:.../> macros inside a markdown body is rejected. For a TOC use YAML frontmatter (toc: { maxLevel, minLevel }); for other macros use directive syntax (:info[...], :mention[...]{...})."),
59111
59160
  version_message: external_exports.string().optional().describe("Optional version comment"),
59112
59161
  confirm_deletions: external_exports.boolean().default(false).describe("Set to true to acknowledge that your markdown removes preserved macros or rich elements. Required when any preserved element would be deleted."),
59113
59162
  replace_body: external_exports.boolean().default(false).describe("Set to true for a wholesale page rewrite that skips token preservation. All existing macros will be lost. Use only when intentionally replacing the full body."),
@@ -59207,7 +59256,7 @@ ${truncated}`);
59207
59256
  inputSchema: {
59208
59257
  page_id: external_exports.string().describe("The Confluence page ID"),
59209
59258
  section: external_exports.string().describe("Heading text identifying the section to replace (case-insensitive)"),
59210
- body: external_exports.string().describe("New content for this section \u2014 GFM markdown or Confluence storage format. Markdown is auto-detected and converted via the token-aware write path, which preserves existing macros and emoticons within the section. The heading itself is preserved; only content under it is replaced."),
59259
+ body: external_exports.string().describe("New content for this section \u2014 GFM markdown or Confluence storage format. Markdown is auto-detected and converted via the token-aware write path, which preserves existing macros and emoticons within the section. The heading itself is preserved; only content under it is replaced. Do not mix the two: inlining <ac:.../> macros inside a markdown body is rejected with MIXED_INPUT_DETECTED. For macros from markdown use directive syntax (:info[...], :mention[...]{...})."),
59211
59260
  version: external_exports.number().int().positive().describe("The page version number from your most recent get_page call"),
59212
59261
  version_message: external_exports.string().optional().describe("Optional version comment"),
59213
59262
  confirm_deletions: external_exports.boolean().default(false).describe("Set to true to acknowledge that your markdown removes preserved macros, emoticons, or rich elements from this section. Required when any preserved element would be deleted.")
@@ -60369,7 +60418,7 @@ ${titleFenced}${echo2}`
60369
60418
  inputSchema: {}
60370
60419
  },
60371
60420
  async () => {
60372
- let text2 = `epimethian-mcp v${"5.5.0"}`;
60421
+ let text2 = `epimethian-mcp v${"5.6.0"}`;
60373
60422
  try {
60374
60423
  const pending = await getPendingUpdate();
60375
60424
  if (pending) {
@@ -60400,7 +60449,7 @@ ${label} update available: v${pending.current} \u2192 v${pending.latest}. Run \`
60400
60449
  const pending = await getPendingUpdate();
60401
60450
  if (!pending) {
60402
60451
  return toolResult(
60403
- `epimethian-mcp v${"5.5.0"} is already up to date.`
60452
+ `epimethian-mcp v${"5.6.0"} is already up to date.`
60404
60453
  );
60405
60454
  }
60406
60455
  const output = await performUpgrade(pending.latest);
@@ -60422,7 +60471,7 @@ async function startRecoveryServer(profile) {
60422
60471
  const server = new McpServer(
60423
60472
  {
60424
60473
  name: `confluence-${profile}-setup-needed`,
60425
- version: "5.5.0"
60474
+ version: "5.6.0"
60426
60475
  },
60427
60476
  {
60428
60477
  instructions: `The Confluence profile "${profile}" referenced by CONFLUENCE_PROFILE has no keychain entry, so no Confluence tools are available. Call the setup_profile tool for instructions to create it.`
@@ -60470,21 +60519,21 @@ async function main() {
60470
60519
  const serverName = config3.profile ? `confluence-${config3.profile}` : "confluence";
60471
60520
  const server = new McpServer({
60472
60521
  name: serverName,
60473
- version: "5.5.0"
60522
+ version: "5.6.0"
60474
60523
  });
60475
60524
  registerTools(server, config3);
60476
60525
  const transport = new StdioServerTransport();
60477
60526
  await server.connect(transport);
60478
60527
  try {
60479
60528
  const pending = await getPendingUpdate();
60480
- if (pending && pending.current === "5.5.0") {
60529
+ if (pending && pending.current === "5.6.0") {
60481
60530
  console.error(
60482
60531
  `epimethian-mcp: update available: v${pending.current} \u2192 v${pending.latest} (${pending.type}). Run \`epimethian-mcp upgrade\` to install.`
60483
60532
  );
60484
60533
  }
60485
60534
  } catch {
60486
60535
  }
60487
- checkForUpdates("5.5.0").catch(() => {
60536
+ checkForUpdates("5.6.0").catch(() => {
60488
60537
  });
60489
60538
  }
60490
60539