@hanhnd/agent-kit 1.0.42 → 1.0.44

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/server.js CHANGED
@@ -36189,9 +36189,6 @@ import * as path6 from "path";
36189
36189
  // src/core/security/index.ts
36190
36190
  import { execFileSync } from "child_process";
36191
36191
  import * as path5 from "path";
36192
- function sanitize(input) {
36193
- return String(input).replace(/[;&|`$<>\\]/g, "").trim().slice(0, 500);
36194
- }
36195
36192
  function validatePath(filePath, baseDir = process.cwd()) {
36196
36193
  const resolved = path5.resolve(baseDir, filePath);
36197
36194
  const root = path5.resolve(baseDir);
@@ -36479,19 +36476,19 @@ function adfToMarkdown(adf) {
36479
36476
  if (!adf || typeof adf !== "object") return "No description";
36480
36477
  const content = adf.content;
36481
36478
  if (!content || !Array.isArray(content)) return "No description";
36482
- function renderNodes(nodes, listDepth = 0, parentType) {
36479
+ function renderNodes2(nodes, listDepth = 0, parentType) {
36483
36480
  let result = "";
36484
36481
  if (!nodes || !Array.isArray(nodes)) return result;
36485
36482
  for (const node of nodes) {
36486
36483
  switch (node.type) {
36487
36484
  case "paragraph": {
36488
- const text = renderNodes(node.content, listDepth, node.type);
36485
+ const text = renderNodes2(node.content, listDepth, node.type);
36489
36486
  result += parentType === "listItem" ? text : text + "\n\n";
36490
36487
  break;
36491
36488
  }
36492
36489
  case "heading": {
36493
36490
  const level = node.attrs?.level || 1;
36494
- result += "#".repeat(level) + " " + renderNodes(node.content, listDepth, node.type) + "\n\n";
36491
+ result += "#".repeat(level) + " " + renderNodes2(node.content, listDepth, node.type) + "\n\n";
36495
36492
  break;
36496
36493
  }
36497
36494
  case "text": {
@@ -36522,23 +36519,23 @@ function adfToMarkdown(adf) {
36522
36519
  }
36523
36520
  case "bulletList":
36524
36521
  case "orderedList":
36525
- result += renderNodes(node.content, listDepth + 1, node.type);
36522
+ result += renderNodes2(node.content, listDepth + 1, node.type);
36526
36523
  if (listDepth === 0) result += "\n";
36527
36524
  break;
36528
36525
  case "listItem": {
36529
36526
  const indent = " ".repeat(Math.max(0, listDepth - 1));
36530
36527
  const bullet = parentType === "orderedList" ? "1." : "-";
36531
- const text = renderNodes(node.content, listDepth, node.type).trim();
36528
+ const text = renderNodes2(node.content, listDepth, node.type).trim();
36532
36529
  result += `${indent}${bullet} ${text}
36533
36530
  `;
36534
36531
  break;
36535
36532
  }
36536
36533
  case "blockquote":
36537
- result += "> " + renderNodes(node.content, listDepth, node.type).trim().replace(/\n/g, "\n> ") + "\n\n";
36534
+ result += "> " + renderNodes2(node.content, listDepth, node.type).trim().replace(/\n/g, "\n> ") + "\n\n";
36538
36535
  break;
36539
36536
  case "codeBlock":
36540
36537
  result += `\`\`\`${node.attrs?.language || ""}
36541
- ${renderNodes(node.content, listDepth, node.type).trim()}
36538
+ ${renderNodes2(node.content, listDepth, node.type).trim()}
36542
36539
  \`\`\`
36543
36540
 
36544
36541
  `;
@@ -36550,7 +36547,7 @@ ${renderNodes(node.content, listDepth, node.type).trim()}
36550
36547
  result += "\n";
36551
36548
  break;
36552
36549
  case "table":
36553
- result += renderTable(node.content);
36550
+ result += renderTable2(node.content);
36554
36551
  break;
36555
36552
  case "mention":
36556
36553
  result += `@${node.attrs?.text || node.attrs?.displayName || "user"}`;
@@ -36570,21 +36567,21 @@ ${renderNodes(node.content, listDepth, node.type).trim()}
36570
36567
  break;
36571
36568
  default:
36572
36569
  if (node.content) {
36573
- result += renderNodes(node.content, listDepth, node.type);
36570
+ result += renderNodes2(node.content, listDepth, node.type);
36574
36571
  }
36575
36572
  break;
36576
36573
  }
36577
36574
  }
36578
36575
  return result;
36579
36576
  }
36580
- function renderTable(rows) {
36577
+ function renderTable2(rows) {
36581
36578
  if (!rows || !Array.isArray(rows)) return "";
36582
36579
  let result = "\n";
36583
36580
  let isFirstRow = true;
36584
36581
  for (const row of rows) {
36585
36582
  if (row.type !== "tableRow") continue;
36586
36583
  const cells = row.content || [];
36587
- const rendered = cells.map((c) => renderNodes(c.content).trim().replace(/\n+/g, " ").replace(/\|/g, "\\|"));
36584
+ const rendered = cells.map((c) => renderNodes2(c.content).trim().replace(/\n+/g, " ").replace(/\|/g, "\\|"));
36588
36585
  result += "| " + rendered.join(" | ") + " |\n";
36589
36586
  if (isFirstRow) {
36590
36587
  result += "| " + cells.map(() => "---").join(" | ") + " |\n";
@@ -36593,10 +36590,474 @@ ${renderNodes(node.content, listDepth, node.type).trim()}
36593
36590
  }
36594
36591
  return result + "\n";
36595
36592
  }
36596
- return renderNodes(content).trim() || "No description";
36593
+ return renderNodes2(content).trim() || "No description";
36594
+ }
36595
+
36596
+ // src/services/integration/parser/storage.ts
36597
+ var MAX_STORAGE_DEPTH = 100;
36598
+ var DEPTH_TRUNCATION_MARKER = "[content nested too deeply - truncated]";
36599
+ var VOID_TAGS = /* @__PURE__ */ new Set([
36600
+ "br",
36601
+ "hr",
36602
+ "img",
36603
+ "col",
36604
+ "input",
36605
+ "meta",
36606
+ "link",
36607
+ "ri:page",
36608
+ "ri:attachment",
36609
+ "ri:url",
36610
+ "ri:user",
36611
+ "ri:space",
36612
+ "ri:blog-post",
36613
+ "ac:emoticon"
36614
+ ]);
36615
+ var NAMED_ENTITIES = {
36616
+ // XML predefined
36617
+ nbsp: "\xA0",
36618
+ amp: "&",
36619
+ lt: "<",
36620
+ gt: ">",
36621
+ quot: '"',
36622
+ apos: "'",
36623
+ // dashes & punctuation
36624
+ ndash: "\u2013",
36625
+ mdash: "\u2014",
36626
+ hellip: "\u2026",
36627
+ middot: "\xB7",
36628
+ bull: "\u2022",
36629
+ dagger: "\u2020",
36630
+ Dagger: "\u2021",
36631
+ sect: "\xA7",
36632
+ para: "\xB6",
36633
+ // smart quotes
36634
+ lsquo: "\u2018",
36635
+ rsquo: "\u2019",
36636
+ sbquo: "\u201A",
36637
+ ldquo: "\u201C",
36638
+ rdquo: "\u201D",
36639
+ bdquo: "\u201E",
36640
+ laquo: "\xAB",
36641
+ raquo: "\xBB",
36642
+ prime: "\u2032",
36643
+ Prime: "\u2033",
36644
+ // arrows
36645
+ larr: "\u2190",
36646
+ rarr: "\u2192",
36647
+ uarr: "\u2191",
36648
+ darr: "\u2193",
36649
+ harr: "\u2194",
36650
+ // symbols & math
36651
+ copy: "\xA9",
36652
+ reg: "\xAE",
36653
+ trade: "\u2122",
36654
+ deg: "\xB0",
36655
+ plusmn: "\xB1",
36656
+ times: "\xD7",
36657
+ divide: "\xF7",
36658
+ micro: "\xB5",
36659
+ frac12: "\xBD",
36660
+ frac14: "\xBC",
36661
+ frac34: "\xBE",
36662
+ ne: "\u2260",
36663
+ le: "\u2264",
36664
+ ge: "\u2265",
36665
+ infin: "\u221E",
36666
+ // currency
36667
+ euro: "\u20AC",
36668
+ pound: "\xA3",
36669
+ yen: "\xA5",
36670
+ cent: "\xA2"
36671
+ };
36672
+ var PANEL_MACROS = /* @__PURE__ */ new Set(["info", "note", "warning", "tip", "panel"]);
36673
+ var PLACEHOLDER_MACROS = /* @__PURE__ */ new Set(["toc", "children", "excerpt-include"]);
36674
+ var ATTRIBUTE_PATTERN = /([A-Za-z_:][-A-Za-z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
36675
+ function codePointOrLiteral(code, literal2) {
36676
+ if (!Number.isInteger(code) || code < 1 || code > 1114111) return literal2;
36677
+ try {
36678
+ return String.fromCodePoint(code);
36679
+ } catch {
36680
+ return literal2;
36681
+ }
36682
+ }
36683
+ function decodeEntities(text) {
36684
+ if (!text.includes("&")) return text;
36685
+ return text.replace(/&(#[Xx][0-9A-Fa-f]+|#\d+|[A-Za-z][A-Za-z0-9]*);/g, (literal2, body) => {
36686
+ if (body[0] === "#") {
36687
+ const isHex = body[1] === "x" || body[1] === "X";
36688
+ const code = Number.parseInt(isHex ? body.slice(2) : body.slice(1), isHex ? 16 : 10);
36689
+ return codePointOrLiteral(code, literal2);
36690
+ }
36691
+ return NAMED_ENTITIES[body] ?? literal2;
36692
+ });
36693
+ }
36694
+ function parseAttributes(source) {
36695
+ const attrs = {};
36696
+ ATTRIBUTE_PATTERN.lastIndex = 0;
36697
+ let match;
36698
+ while ((match = ATTRIBUTE_PATTERN.exec(source)) !== null) {
36699
+ const raw = match[2] ?? match[3] ?? match[4] ?? "";
36700
+ attrs[match[1].toLowerCase()] = decodeEntities(raw);
36701
+ }
36702
+ return attrs;
36703
+ }
36704
+ function tokenize(input) {
36705
+ const tokens = [];
36706
+ let cursor = 0;
36707
+ let textStart = 0;
36708
+ const flushText = (end) => {
36709
+ if (end > textStart) tokens.push({ kind: "text", value: input.slice(textStart, end) });
36710
+ };
36711
+ while (cursor < input.length) {
36712
+ const lt = input.indexOf("<", cursor);
36713
+ if (lt === -1) break;
36714
+ if (input.startsWith("<![CDATA[", lt)) {
36715
+ const end = input.indexOf("]]>", lt + 9);
36716
+ if (end === -1) break;
36717
+ flushText(lt);
36718
+ tokens.push({ kind: "cdata", value: input.slice(lt + 9, end) });
36719
+ cursor = end + 3;
36720
+ textStart = cursor;
36721
+ continue;
36722
+ }
36723
+ if (input.startsWith("<!--", lt)) {
36724
+ const end = input.indexOf("-->", lt + 4);
36725
+ if (end === -1) break;
36726
+ flushText(lt);
36727
+ tokens.push({ kind: "comment" });
36728
+ cursor = end + 3;
36729
+ textStart = cursor;
36730
+ continue;
36731
+ }
36732
+ const gt = input.indexOf(">", lt + 1);
36733
+ if (gt === -1) break;
36734
+ const raw = input.slice(lt + 1, gt);
36735
+ if (!/^\/?[A-Za-z]/.test(raw)) {
36736
+ cursor = lt + 1;
36737
+ continue;
36738
+ }
36739
+ flushText(lt);
36740
+ if (raw[0] === "/") {
36741
+ tokens.push({ kind: "close", name: raw.slice(1).trim().toLowerCase() });
36742
+ } else {
36743
+ const selfClosing = raw.endsWith("/");
36744
+ const inner = selfClosing ? raw.slice(0, -1) : raw;
36745
+ const nameEnd = inner.search(/[\s/]/);
36746
+ const name = (nameEnd === -1 ? inner : inner.slice(0, nameEnd)).toLowerCase();
36747
+ const attrs = parseAttributes(nameEnd === -1 ? "" : inner.slice(nameEnd));
36748
+ tokens.push({ kind: selfClosing ? "selfclose" : "open", name, attrs });
36749
+ }
36750
+ cursor = gt + 1;
36751
+ textStart = cursor;
36752
+ }
36753
+ flushText(input.length);
36754
+ return tokens;
36755
+ }
36756
+ function buildTree(tokens) {
36757
+ const root = [];
36758
+ const stack = [];
36759
+ let suppressDepth = 0;
36760
+ const sink = () => stack.length > 0 ? stack[stack.length - 1].children : root;
36761
+ for (const token of tokens) {
36762
+ switch (token.kind) {
36763
+ case "comment":
36764
+ break;
36765
+ case "text":
36766
+ case "cdata":
36767
+ if (suppressDepth === 0) sink().push(token);
36768
+ break;
36769
+ case "open":
36770
+ case "selfclose": {
36771
+ const overDepth = stack.length >= MAX_STORAGE_DEPTH;
36772
+ if (suppressDepth === 0 && overDepth) {
36773
+ sink().push({ kind: "text", value: DEPTH_TRUNCATION_MARKER });
36774
+ }
36775
+ const suppressing = suppressDepth > 0 || overDepth;
36776
+ const isLeaf = token.kind === "selfclose" || VOID_TAGS.has(token.name);
36777
+ if (suppressing) {
36778
+ if (!isLeaf) {
36779
+ stack.push({ name: token.name, children: [], suppressing: true });
36780
+ suppressDepth++;
36781
+ }
36782
+ break;
36783
+ }
36784
+ const element = { kind: "element", name: token.name, attrs: token.attrs, children: [] };
36785
+ sink().push(element);
36786
+ if (!isLeaf) stack.push({ name: token.name, children: element.children, suppressing: false });
36787
+ break;
36788
+ }
36789
+ case "close": {
36790
+ let target = -1;
36791
+ for (let i = stack.length - 1; i >= 0; i--) {
36792
+ if (stack[i].name === token.name) {
36793
+ target = i;
36794
+ break;
36795
+ }
36796
+ }
36797
+ if (target === -1) break;
36798
+ while (stack.length > target) {
36799
+ const frame = stack.pop();
36800
+ if (frame?.suppressing) suppressDepth--;
36801
+ }
36802
+ break;
36803
+ }
36804
+ }
36805
+ }
36806
+ return root;
36807
+ }
36808
+ function isElement(node, name) {
36809
+ return node.kind === "element" && (name === void 0 || node.name === name);
36810
+ }
36811
+ function findChild(element, name) {
36812
+ return element.children.find((child) => isElement(child, name));
36813
+ }
36814
+ function rawText(nodes) {
36815
+ let result = "";
36816
+ for (const node of nodes) {
36817
+ if (node.kind === "element") result += rawText(node.children);
36818
+ else result += node.value;
36819
+ }
36820
+ return result;
36821
+ }
36822
+ function macroParameters(element) {
36823
+ const params = {};
36824
+ for (const child of element.children) {
36825
+ if (!isElement(child, "ac:parameter")) continue;
36826
+ const key = child.attrs["ac:name"];
36827
+ if (key) params[key] = rawText(child.children).trim();
36828
+ }
36829
+ return params;
36830
+ }
36831
+ function collectRows(nodes) {
36832
+ const rows = [];
36833
+ for (const node of nodes) {
36834
+ if (!isElement(node)) continue;
36835
+ if (node.name === "tr") rows.push(node);
36836
+ else if (node.name === "thead" || node.name === "tbody" || node.name === "tfoot") {
36837
+ rows.push(...collectRows(node.children));
36838
+ }
36839
+ }
36840
+ return rows;
36841
+ }
36842
+ function renderTable(nodes) {
36843
+ const rows = collectRows(nodes);
36844
+ if (rows.length === 0) return "";
36845
+ let result = "\n";
36846
+ let isFirstRow = true;
36847
+ for (const row of rows) {
36848
+ const cells = row.children.filter(
36849
+ (child) => isElement(child, "th") || isElement(child, "td")
36850
+ );
36851
+ const rendered = cells.map((cell) => renderNodes(cell.children).trim().replace(/\n+/g, " ").replace(/\|/g, "\\|"));
36852
+ result += "| " + rendered.join(" | ") + " |\n";
36853
+ if (isFirstRow) {
36854
+ result += "| " + cells.map(() => "---").join(" | ") + " |\n";
36855
+ isFirstRow = false;
36856
+ }
36857
+ }
36858
+ return result + "\n";
36859
+ }
36860
+ function renderMacro(element, listDepth) {
36861
+ const name = element.attrs["ac:name"] || "";
36862
+ const params = macroParameters(element);
36863
+ const richBody = findChild(element, "ac:rich-text-body");
36864
+ if (name === "code") {
36865
+ const plainBody = findChild(element, "ac:plain-text-body");
36866
+ const code = plainBody ? rawText(plainBody.children) : rawText(element.children);
36867
+ return `\`\`\`${params.language || ""}
36868
+ ${code.replace(/^\n+|\n+$/g, "")}
36869
+ \`\`\`
36870
+
36871
+ `;
36872
+ }
36873
+ if (PANEL_MACROS.has(name)) {
36874
+ const body = richBody ? renderNodes(richBody.children, listDepth, name).trim() : "";
36875
+ const block = `**${name.toUpperCase()}**${body ? `
36876
+ ${body}` : ""}`;
36877
+ return "> " + block.replace(/\n/g, "\n> ") + "\n\n";
36878
+ }
36879
+ if (name === "expand") {
36880
+ const body = richBody ? renderNodes(richBody.children, listDepth, name).trim() : "";
36881
+ return `**${params.title || "Details"}**
36882
+
36883
+ ${body ? `${body}
36884
+
36885
+ ` : ""}`;
36886
+ }
36887
+ if (name === "jira") {
36888
+ return params.key ? `\`${params.key}\`` : "[macro: jira]";
36889
+ }
36890
+ if (PLACEHOLDER_MACROS.has(name)) return `[macro: ${name}]`;
36891
+ return `[macro: ${name || "unknown"}]`;
36892
+ }
36893
+ function renderLink(element, listDepth) {
36894
+ const bodyElement = findChild(element, "ac:plain-text-link-body") ?? findChild(element, "ac:link-body");
36895
+ const bodyText = bodyElement ? renderNodes(bodyElement.children, listDepth, element.name).trim() : "";
36896
+ const page = findChild(element, "ri:page");
36897
+ if (page) return `[[${bodyText || page.attrs["ri:content-title"] || "page"}]]`;
36898
+ const attachment = findChild(element, "ri:attachment");
36899
+ if (attachment) return `[attachment: ${attachment.attrs["ri:filename"] || "unknown"}]`;
36900
+ const externalUrl = findChild(element, "ri:url");
36901
+ if (externalUrl) {
36902
+ const href = externalUrl.attrs["ri:value"] || "";
36903
+ return `[${bodyText || href}](${href})`;
36904
+ }
36905
+ if (findChild(element, "ri:user")) return "@user";
36906
+ return bodyText;
36907
+ }
36908
+ function renderTaskList(element, listDepth) {
36909
+ let result = "";
36910
+ for (const task of element.children) {
36911
+ if (!isElement(task, "ac:task")) continue;
36912
+ const status = findChild(task, "ac:task-status");
36913
+ const body = findChild(task, "ac:task-body");
36914
+ const marker = rawText(status?.children ?? []).trim() === "complete" ? "[x]" : "[ ]";
36915
+ const text = body ? renderNodes(body.children, listDepth, "ac:task").trim() : "";
36916
+ result += `- ${marker} ${text}
36917
+ `;
36918
+ }
36919
+ return result ? result + "\n" : "";
36920
+ }
36921
+ function renderNodes(nodes, listDepth = 0, parentType) {
36922
+ let result = "";
36923
+ for (const node of nodes) {
36924
+ if (node.kind === "cdata") {
36925
+ result += node.value;
36926
+ continue;
36927
+ }
36928
+ if (node.kind === "text") {
36929
+ result += decodeEntities(node.value);
36930
+ continue;
36931
+ }
36932
+ const children = node.children;
36933
+ switch (node.name) {
36934
+ case "h1":
36935
+ case "h2":
36936
+ case "h3":
36937
+ case "h4":
36938
+ case "h5":
36939
+ case "h6":
36940
+ result += "#".repeat(Number(node.name[1])) + " " + renderNodes(children, listDepth, node.name).trim() + "\n\n";
36941
+ break;
36942
+ case "p": {
36943
+ const text = renderNodes(children, listDepth, node.name);
36944
+ result += parentType === "li" ? text + "\n" : text + "\n\n";
36945
+ break;
36946
+ }
36947
+ case "strong":
36948
+ case "b":
36949
+ result += `**${renderNodes(children, listDepth, node.name)}**`;
36950
+ break;
36951
+ case "em":
36952
+ case "i":
36953
+ result += `*${renderNodes(children, listDepth, node.name)}*`;
36954
+ break;
36955
+ case "code":
36956
+ result += `\`${renderNodes(children, listDepth, node.name)}\``;
36957
+ break;
36958
+ case "s":
36959
+ case "strike":
36960
+ case "del":
36961
+ result += `~~${renderNodes(children, listDepth, node.name)}~~`;
36962
+ break;
36963
+ case "sup":
36964
+ result += `^${renderNodes(children, listDepth, node.name)}^`;
36965
+ break;
36966
+ case "sub":
36967
+ result += `~${renderNodes(children, listDepth, node.name)}~`;
36968
+ break;
36969
+ case "pre":
36970
+ result += `\`\`\`
36971
+ ${renderNodes(children, listDepth, node.name).trim()}
36972
+ \`\`\`
36973
+
36974
+ `;
36975
+ break;
36976
+ case "blockquote":
36977
+ result += "> " + renderNodes(children, listDepth, node.name).trim().replace(/\n/g, "\n> ") + "\n\n";
36978
+ break;
36979
+ case "hr":
36980
+ result += "---\n\n";
36981
+ break;
36982
+ case "br":
36983
+ result += "\n";
36984
+ break;
36985
+ case "a": {
36986
+ const text = renderNodes(children, listDepth, node.name);
36987
+ const href = node.attrs.href ?? "";
36988
+ result += href ? `[${text}](${href})` : text;
36989
+ break;
36990
+ }
36991
+ case "ul":
36992
+ case "ol": {
36993
+ const inner = renderNodes(children, listDepth + 1, node.name);
36994
+ if (parentType === "li") {
36995
+ if (result.length > 0 && !result.endsWith("\n")) result += "\n";
36996
+ result += inner;
36997
+ } else {
36998
+ result += inner;
36999
+ if (listDepth === 0) result += "\n";
37000
+ }
37001
+ break;
37002
+ }
37003
+ case "li": {
37004
+ const indent = " ".repeat(Math.max(0, listDepth - 1));
37005
+ const bullet = parentType === "ol" ? "1." : "-";
37006
+ result += `${indent}${bullet} ${renderNodes(children, listDepth, "li").trim()}
37007
+ `;
37008
+ break;
37009
+ }
37010
+ case "table":
37011
+ result += renderTable(children);
37012
+ break;
37013
+ case "ac:structured-macro":
37014
+ result += renderMacro(node, listDepth);
37015
+ break;
37016
+ case "ac:link":
37017
+ result += renderLink(node, listDepth);
37018
+ break;
37019
+ case "ac:image": {
37020
+ const attachment = findChild(node, "ri:attachment");
37021
+ const externalUrl = findChild(node, "ri:url");
37022
+ const label = attachment?.attrs["ri:filename"] ?? externalUrl?.attrs["ri:value"] ?? "unknown";
37023
+ const marker = `[image: ${label}]`;
37024
+ if (parentType === "p") {
37025
+ result += marker;
37026
+ } else {
37027
+ if (result.length > 0 && !result.endsWith("\n")) result += "\n";
37028
+ result += `${marker}
37029
+
37030
+ `;
37031
+ }
37032
+ break;
37033
+ }
37034
+ case "ac:task-list":
37035
+ result += renderTaskList(node, listDepth);
37036
+ break;
37037
+ case "ac:emoticon":
37038
+ result += `:${node.attrs["ac:name"] || "emoticon"}:`;
37039
+ break;
37040
+ default:
37041
+ result += renderNodes(children, listDepth, node.name);
37042
+ break;
37043
+ }
37044
+ }
37045
+ return result;
37046
+ }
37047
+ function extractPlainText(xhtml) {
37048
+ return decodeEntities(xhtml.replace(/<[^>]*>/g, " ")).replace(/\s+/g, " ").trim();
37049
+ }
37050
+ function storageToMarkdown(xhtml) {
37051
+ if (typeof xhtml !== "string" || xhtml.length === 0) return "";
37052
+ try {
37053
+ return renderNodes(buildTree(tokenize(xhtml))).trim();
37054
+ } catch {
37055
+ return extractPlainText(xhtml);
37056
+ }
36597
37057
  }
36598
37058
 
36599
37059
  // src/mcp/integration.ts
37060
+ var LARGE_PAYLOAD_THRESHOLD = 5e4;
36600
37061
  var BitbucketPrSchema = external_exports.object({
36601
37062
  id: external_exports.number(),
36602
37063
  title: external_exports.string(),
@@ -36606,6 +37067,42 @@ var BitbucketPrSchema = external_exports.object({
36606
37067
  source: external_exports.object({ branch: external_exports.object({ name: external_exports.string() }) }),
36607
37068
  destination: external_exports.object({ branch: external_exports.object({ name: external_exports.string() }) })
36608
37069
  });
37070
+ var BitbucketCreatedPrSchema = external_exports.object({
37071
+ id: external_exports.number(),
37072
+ title: external_exports.string(),
37073
+ state: external_exports.string().optional(),
37074
+ draft: external_exports.boolean().optional(),
37075
+ source: external_exports.object({ branch: external_exports.object({ name: external_exports.string() }) }).optional(),
37076
+ destination: external_exports.object({ branch: external_exports.object({ name: external_exports.string() }) }).optional(),
37077
+ reviewers: external_exports.array(external_exports.object({ display_name: external_exports.string().optional() }).passthrough()).optional(),
37078
+ links: external_exports.object({ html: external_exports.object({ href: external_exports.string() }).optional() }).passthrough().optional()
37079
+ });
37080
+ var BitbucketCommentSchema = external_exports.object({
37081
+ id: external_exports.number(),
37082
+ created_on: external_exports.string().optional(),
37083
+ updated_on: external_exports.string().optional(),
37084
+ content: external_exports.object({ raw: external_exports.string().nullable().optional() }).passthrough().optional(),
37085
+ user: external_exports.object({ display_name: external_exports.string().optional() }).passthrough().nullable().optional(),
37086
+ deleted: external_exports.boolean().optional(),
37087
+ pending: external_exports.boolean().optional(),
37088
+ parent: external_exports.object({ id: external_exports.number() }).passthrough().nullable().optional(),
37089
+ inline: external_exports.object({
37090
+ path: external_exports.string(),
37091
+ from: external_exports.number().nullable().optional(),
37092
+ to: external_exports.number().nullable().optional(),
37093
+ start_from: external_exports.number().nullable().optional(),
37094
+ start_to: external_exports.number().nullable().optional()
37095
+ }).passthrough().nullable().optional(),
37096
+ resolution: external_exports.object({
37097
+ type: external_exports.string().optional(),
37098
+ user: external_exports.object({ display_name: external_exports.string().optional() }).passthrough().nullable().optional(),
37099
+ created_on: external_exports.string().optional()
37100
+ }).passthrough().nullable().optional()
37101
+ }).passthrough();
37102
+ var BitbucketCommentPageSchema = external_exports.object({
37103
+ values: external_exports.array(BitbucketCommentSchema),
37104
+ next: external_exports.string().optional()
37105
+ });
36609
37106
  var AdfContentSchema = external_exports.object({
36610
37107
  type: external_exports.string().optional(),
36611
37108
  content: external_exports.array(external_exports.unknown()).optional(),
@@ -36634,19 +37131,540 @@ var JiraTicketSchema = external_exports.object({
36634
37131
  errorMessages: external_exports.array(external_exports.string()).optional(),
36635
37132
  fields: JiraFieldsSchema
36636
37133
  });
37134
+ var ConfluencePageSchema = external_exports.object({
37135
+ id: external_exports.string(),
37136
+ title: external_exports.string(),
37137
+ status: external_exports.string().optional(),
37138
+ spaceId: external_exports.string().optional(),
37139
+ version: external_exports.object({
37140
+ number: external_exports.number().optional(),
37141
+ createdAt: external_exports.string().optional(),
37142
+ message: external_exports.string().optional(),
37143
+ minorEdit: external_exports.boolean().optional(),
37144
+ authorId: external_exports.string().optional()
37145
+ }).optional(),
37146
+ body: external_exports.object({
37147
+ storage: external_exports.object({ value: external_exports.string().optional() }).passthrough().optional()
37148
+ }).passthrough().optional(),
37149
+ labels: external_exports.object({ results: external_exports.array(external_exports.object({ name: external_exports.string() }).passthrough()).optional() }).passthrough().optional(),
37150
+ _links: external_exports.object({ base: external_exports.string().optional(), webui: external_exports.string().optional() }).passthrough().optional()
37151
+ });
37152
+ var REQUIRED_SCOPES = {
37153
+ // The PR diff endpoint redirects to the repository diff, which needs read:repository.
37154
+ kit_get_bitbucket_pr: ["read:pullrequest:bitbucket", "read:repository:bitbucket"],
37155
+ kit_get_bitbucket_pr_comments: ["read:pullrequest:bitbucket"],
37156
+ // read:workspace resolves reviewer names; read:user skips the author (optional — degrades, never fails).
37157
+ kit_create_bitbucket_pr: [
37158
+ "read:pullrequest:bitbucket",
37159
+ "write:pullrequest:bitbucket",
37160
+ "read:workspace:bitbucket",
37161
+ "read:user:bitbucket"
37162
+ ],
37163
+ kit_find_bitbucket_reviewers: ["read:workspace:bitbucket", "read:user:bitbucket"],
37164
+ kit_jira_get_ticket: ["read:jira-work"],
37165
+ kit_confluence_get_page: ["read:page:confluence"]
37166
+ };
37167
+ function errorText(error2, tool) {
37168
+ const message = error2 instanceof Error ? error2.message : String(error2);
37169
+ if (!/\((401|403)\)/.test(message) || message.includes(":bitbucket") || message.includes("scope")) return message;
37170
+ return `${message}
37171
+
37172
+ Check the API token has these scopes: ${REQUIRED_SCOPES[tool].join(", ")}.`;
37173
+ }
36637
37174
  function buildBasicAuth(emailVar, tokenVar) {
36638
37175
  const email2 = getCredential(emailVar);
36639
37176
  const token = getCredential(tokenVar);
36640
37177
  if (!email2 || !token) throw new Error(`Missing ${emailVar} or ${tokenVar}`);
36641
37178
  return "Basic " + Buffer.from(`${email2}:${token}`).toString("base64");
36642
37179
  }
36643
- async function callRestApi(url, auth, accept = "application/json") {
36644
- const resp = await fetch(url, { headers: { Authorization: auth, Accept: accept } });
37180
+ var PRODUCT_TOKEN_KEY = {
37181
+ jira: "JIRA_API_TOKEN",
37182
+ confluence: "CONFLUENCE_API_TOKEN"
37183
+ };
37184
+ function buildAtlassianContext(product) {
37185
+ const cloudId = getCredential("ATLASSIAN_CLOUD_ID");
37186
+ if (!cloudId) throw new Error("Missing ATLASSIAN_CLOUD_ID");
37187
+ return { auth: buildBasicAuth("ATLASSIAN_USER_EMAIL", PRODUCT_TOKEN_KEY[product]), cloudId };
37188
+ }
37189
+ async function callRestApi(url, auth, accept = "application/json", body) {
37190
+ const headers = { Authorization: auth, Accept: accept };
37191
+ const init = { headers };
37192
+ if (body !== void 0) {
37193
+ init.method = "POST";
37194
+ headers["Content-Type"] = "application/json";
37195
+ init.body = JSON.stringify(body);
37196
+ }
37197
+ const resp = await fetch(url, init);
36645
37198
  if (resp.status === 401) throw new Error(`\u274C Auth failed (401): ${url}`);
37199
+ if (resp.status === 403)
37200
+ throw new Error(`\u274C Access denied (403): ${url}
37201
+
37202
+ You lack permission for this resource, or it is restricted.`);
36646
37203
  if (resp.status === 404) throw new Error(`\u274C Not found: ${url}`);
36647
- if (!resp.ok) throw new Error(`\u274C API error ${resp.status}: ${await resp.text()}`);
37204
+ if (!resp.ok) throw new Error(`\u274C API error ${resp.status}: ${extractApiError(await resp.text())}`);
36648
37205
  return accept === "text/plain" ? resp.text() : resp.json();
36649
37206
  }
37207
+ function extractApiError(text) {
37208
+ try {
37209
+ const err = JSON.parse(text).error;
37210
+ if (err && typeof err.message === "string") {
37211
+ const parts = [err.message];
37212
+ if (typeof err.detail === "string" && err.detail) parts.push(err.detail);
37213
+ if (err.fields && typeof err.fields === "object") parts.push(JSON.stringify(err.fields));
37214
+ return parts.join("\n");
37215
+ }
37216
+ } catch {
37217
+ }
37218
+ return text;
37219
+ }
37220
+ var CONFLUENCE_INPUT_FORMS = [
37221
+ "Accepted input forms:",
37222
+ " \u2022 https://<site>.atlassian.net/wiki/spaces/<SPACEKEY>/pages/<pageId>/<Title>",
37223
+ " \u2022 https://<site>.atlassian.net/pages/viewpage.action?pageId=<pageId>",
37224
+ " \u2022 a bare numeric page ID (e.g. 123456789)"
37225
+ ].join("\n");
37226
+ function resolveConfluencePageId(input) {
37227
+ const trimmed = input.trim();
37228
+ if (!trimmed) return { kind: "unknown" };
37229
+ if (/\/blog(?:posts?)?\//.test(trimmed)) return { kind: "blog" };
37230
+ if (/\/wiki\/x\/[A-Za-z0-9]+/.test(trimmed)) return { kind: "tiny" };
37231
+ const spacesPageMatch = trimmed.match(/\/wiki\/spaces\/[^/]+\/pages\/(\d+)(?:[/?#]|$)/);
37232
+ if (spacesPageMatch) return { kind: "id", pageId: spacesPageMatch[1] };
37233
+ const queryMatch = trimmed.match(/[?&]pageId=(\d+)(?:[&#]|$)/);
37234
+ if (queryMatch) return { kind: "id", pageId: queryMatch[1] };
37235
+ if (/^\d+$/.test(trimmed)) return { kind: "id", pageId: trimmed };
37236
+ return { kind: "unknown" };
37237
+ }
37238
+ function buildConfluenceHeader(page, absoluteUrl) {
37239
+ const webui = page._links?.webui ?? "";
37240
+ const spaceKey = webui.match(/\/spaces\/([^/]+)\//)?.[1];
37241
+ const lines = [`## \u{1F4C4} ${page.title}`, "", `**Status:** ${page.status || "Unknown"}`];
37242
+ if (spaceKey) lines.push(`**Space:** ${spaceKey}`);
37243
+ lines.push(`**Version:** ${page.version?.number ?? "Unknown"}`);
37244
+ lines.push(`**Last updated:** ${page.version?.createdAt || "Unknown"}`);
37245
+ lines.push(`**URL:** ${absoluteUrl || "Unknown"}`);
37246
+ return lines.join("\n");
37247
+ }
37248
+ async function handleConfluenceGetPage(args) {
37249
+ try {
37250
+ const resolution = resolveConfluencePageId(args.input);
37251
+ if (resolution.kind === "tiny") {
37252
+ return mcpText(
37253
+ `\u274C Confluence tiny links cannot be resolved without following a redirect.
37254
+
37255
+ Open the link in a browser and pass the full page URL instead.
37256
+
37257
+ ${CONFLUENCE_INPUT_FORMS}`
37258
+ );
37259
+ }
37260
+ if (resolution.kind === "blog") {
37261
+ return mcpText(
37262
+ `\u274C Confluence blog posts are not supported \u2014 they live on a different endpoint than pages.
37263
+
37264
+ ${CONFLUENCE_INPUT_FORMS}`
37265
+ );
37266
+ }
37267
+ if (resolution.kind === "unknown") {
37268
+ return mcpText(`\u274C Could not extract a Confluence page ID from: ${args.input}
37269
+
37270
+ ${CONFLUENCE_INPUT_FORMS}`);
37271
+ }
37272
+ const { auth, cloudId } = buildAtlassianContext("confluence");
37273
+ const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${resolution.pageId}?body-format=storage&include-labels=true&include-version=true`;
37274
+ const jsonData = await callRestApi(url, auth);
37275
+ const parseResult = ConfluencePageSchema.safeParse(jsonData);
37276
+ if (!parseResult.success) {
37277
+ return mcpText(`\u274C Invalid Confluence response format: ${parseResult.error.message}`);
37278
+ }
37279
+ const page = parseResult.data;
37280
+ const base = page._links?.base ?? "";
37281
+ const webui = page._links?.webui ?? "";
37282
+ const absoluteUrl = base && webui ? `${base}${webui}` : base || webui;
37283
+ const markdown = storageToMarkdown(page.body?.storage?.value);
37284
+ let contentSection;
37285
+ if (!markdown.trim()) {
37286
+ contentSection = `\u26A0\uFE0F Confluence returned no readable body for this page (status: ${page.status || "Unknown"}). Open ${absoluteUrl || "the page in Confluence"} to view it directly.`;
37287
+ } else if (markdown.length >= LARGE_PAYLOAD_THRESHOLD) {
37288
+ const filePath = `/tmp/kit-confluence-${resolution.pageId}-${Date.now()}.md`;
37289
+ try {
37290
+ writeFileSync4(filePath, sanitizeOutput(markdown), "utf8");
37291
+ contentSection = `Content is large (${markdown.length} chars). Full markdown written to: \`${filePath}\`. Read this file before proceeding.`;
37292
+ } catch {
37293
+ contentSection = `\u26A0\uFE0F Could not write page content to a temp file. Showing inline (may be very large).
37294
+
37295
+ ${markdown}`;
37296
+ }
37297
+ } else {
37298
+ contentSection = markdown;
37299
+ }
37300
+ const labels = page.labels?.results?.map((label) => label.name).filter((name) => name.length > 0) ?? [];
37301
+ const output = `${buildConfluenceHeader(page, absoluteUrl)}
37302
+
37303
+ ### Content
37304
+ ${contentSection}
37305
+
37306
+ ### Labels
37307
+ ${labels.length > 0 ? labels.join(", ") : "None"}`;
37308
+ return mcpText(sanitizeOutput(output));
37309
+ } catch (error2) {
37310
+ return mcpText(`Error: ${errorText(error2, "kit_confluence_get_page")}`);
37311
+ }
37312
+ }
37313
+ var BITBUCKET_API = "https://api.bitbucket.org/2.0";
37314
+ var BITBUCKET_SLUG = /^(?!\.+$)[A-Za-z0-9._-]+$/;
37315
+ var MAX_PAGES = 20;
37316
+ var MISSING_WORKSPACE = `\u274C workspace is required. Pass it as a parameter or set BITBUCKET_DEFAULT_WORKSPACE in your MCP env config.`;
37317
+ function checkSlugs(ws, repo) {
37318
+ if (!BITBUCKET_SLUG.test(ws)) return `\u274C Invalid workspace slug: ${ws}`;
37319
+ if (!BITBUCKET_SLUG.test(repo)) return `\u274C Invalid repo slug: ${repo}`;
37320
+ return void 0;
37321
+ }
37322
+ function resolveBitbucketPr(input, workspace, repoSlug) {
37323
+ const trimmed = input.trim();
37324
+ let ws;
37325
+ let repo;
37326
+ let prId;
37327
+ const urlMatch = trimmed.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)/);
37328
+ if (urlMatch) {
37329
+ ws = urlMatch[1];
37330
+ repo = urlMatch[2];
37331
+ prId = parseInt(urlMatch[3], 10);
37332
+ } else if (/^\d+$/.test(trimmed)) {
37333
+ prId = parseInt(trimmed, 10);
37334
+ ws = workspace || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
37335
+ repo = repoSlug;
37336
+ }
37337
+ if (!ws) return { ok: false, error: MISSING_WORKSPACE };
37338
+ if (!repo || !prId) {
37339
+ return { ok: false, error: `\u274C Could not parse PR URL. Expected: bitbucket.org/{ws}/{repo}/pull-requests/{id}` };
37340
+ }
37341
+ const slugError = checkSlugs(ws, repo);
37342
+ if (slugError) return { ok: false, error: slugError };
37343
+ return { ok: true, ws, repo, prId };
37344
+ }
37345
+ function resolveBitbucketRepo(repository, workspace) {
37346
+ const trimmed = repository.trim().replace(/\.git$/, "");
37347
+ let ws;
37348
+ let repo;
37349
+ const urlMatch = trimmed.match(/bitbucket\.org[/:]([^/]+)\/([^/?#]+)/);
37350
+ const pairMatch = trimmed.match(/^([^/]+)\/([^/]+)$/);
37351
+ if (urlMatch) {
37352
+ ws = urlMatch[1];
37353
+ repo = urlMatch[2];
37354
+ } else if (pairMatch) {
37355
+ ws = pairMatch[1];
37356
+ repo = pairMatch[2];
37357
+ } else if (trimmed) {
37358
+ ws = workspace || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
37359
+ repo = trimmed;
37360
+ }
37361
+ if (!repo) {
37362
+ return { ok: false, error: `\u274C repository is required: a Bitbucket repo URL, "workspace/repo", or a repo slug.` };
37363
+ }
37364
+ if (!ws) return { ok: false, error: MISSING_WORKSPACE };
37365
+ const slugError = checkSlugs(ws, repo);
37366
+ if (slugError) return { ok: false, error: slugError };
37367
+ return { ok: true, ws, repo };
37368
+ }
37369
+ function normalizeReviewerUuid(raw) {
37370
+ const bare = raw.trim().replace(/^\{|\}$/g, "");
37371
+ return /^[0-9a-fA-F-]{36}$/.test(bare) ? `{${bare}}` : void 0;
37372
+ }
37373
+ var BitbucketMembersPageSchema = external_exports.object({
37374
+ values: external_exports.array(
37375
+ external_exports.object({
37376
+ user: external_exports.object({ uuid: external_exports.string(), display_name: external_exports.string().optional(), nickname: external_exports.string().optional() }).passthrough()
37377
+ }).passthrough()
37378
+ ),
37379
+ next: external_exports.string().optional()
37380
+ });
37381
+ function normalizeName(value) {
37382
+ return value.normalize("NFD").replace(/\p{M}/gu, "").replace(/đ/gi, "d").toLowerCase().replace(/\s+/g, " ").trim();
37383
+ }
37384
+ function nameTokens(value) {
37385
+ return normalizeName(value).split(/[^\p{L}\p{N}]+/u).filter(Boolean);
37386
+ }
37387
+ function matchReviewers(queries, members) {
37388
+ const result = { resolved: [], ambiguous: [], unmatched: [] };
37389
+ for (const query of queries) {
37390
+ const q = normalizeName(query);
37391
+ const qTokens = nameTokens(query);
37392
+ if (qTokens.length === 0) continue;
37393
+ let hits = members.filter(
37394
+ (m) => normalizeName(m.displayName) === q || m.nickname !== void 0 && normalizeName(m.nickname) === q
37395
+ );
37396
+ if (hits.length === 0) {
37397
+ hits = members.filter((m) => {
37398
+ const words = /* @__PURE__ */ new Set([...nameTokens(m.displayName), ...nameTokens(m.nickname ?? "")]);
37399
+ return qTokens.every((t) => words.has(t));
37400
+ });
37401
+ }
37402
+ if (hits.length === 1) result.resolved.push({ query, member: hits[0] });
37403
+ else if (hits.length > 1) result.ambiguous.push({ query, candidates: hits });
37404
+ else result.unmatched.push(query);
37405
+ }
37406
+ return result;
37407
+ }
37408
+ async function fetchWorkspaceMembers(ws, auth) {
37409
+ let url = `${BITBUCKET_API}/workspaces/${ws}/members?pagelen=100`;
37410
+ const members = [];
37411
+ let pages = 0;
37412
+ while (url && pages < MAX_PAGES) {
37413
+ let data;
37414
+ try {
37415
+ data = await callRestApi(url, auth);
37416
+ } catch (error2) {
37417
+ const message = error2 instanceof Error ? error2.message : String(error2);
37418
+ throw new Error(
37419
+ `${message}
37420
+
37421
+ Looking up reviewers by name lists workspace members \u2014 the API token needs the read:workspace:bitbucket scope.`
37422
+ );
37423
+ }
37424
+ const parseResult = BitbucketMembersPageSchema.safeParse(data);
37425
+ if (!parseResult.success) throw new Error(`\u274C Invalid workspace members response: ${parseResult.error.message}`);
37426
+ for (const { user } of parseResult.data.values) {
37427
+ members.push({ uuid: user.uuid, displayName: user.display_name ?? "", nickname: user.nickname });
37428
+ }
37429
+ pages++;
37430
+ url = parseResult.data.next?.startsWith(`${BITBUCKET_API}/`) ? parseResult.data.next : void 0;
37431
+ }
37432
+ return members;
37433
+ }
37434
+ async function fetchCurrentUserUuid(auth) {
37435
+ try {
37436
+ const data = await callRestApi(`${BITBUCKET_API}/user`, auth);
37437
+ return typeof data.uuid === "string" ? data.uuid : void 0;
37438
+ } catch {
37439
+ return void 0;
37440
+ }
37441
+ }
37442
+ async function lookupReviewers(ws, auth, queries) {
37443
+ const byUuid = [];
37444
+ const names = [];
37445
+ for (const query of queries) {
37446
+ const uuid2 = normalizeReviewerUuid(query);
37447
+ if (uuid2) byUuid.push({ uuid: uuid2, displayName: uuid2 });
37448
+ else if (query.trim()) names.push(query);
37449
+ }
37450
+ const [members, selfUuid] = await Promise.all([
37451
+ names.length > 0 ? fetchWorkspaceMembers(ws, auth) : Promise.resolve([]),
37452
+ fetchCurrentUserUuid(auth)
37453
+ ]);
37454
+ const match = matchReviewers(names, members);
37455
+ const seen = /* @__PURE__ */ new Set();
37456
+ const reviewers = [];
37457
+ let skippedSelf = false;
37458
+ for (const member of [...byUuid, ...match.resolved.map((r) => r.member)]) {
37459
+ const key = member.uuid.toLowerCase();
37460
+ if (seen.has(key)) continue;
37461
+ seen.add(key);
37462
+ if (selfUuid && key === selfUuid.toLowerCase()) {
37463
+ skippedSelf = true;
37464
+ continue;
37465
+ }
37466
+ reviewers.push(member);
37467
+ }
37468
+ return { reviewers, skippedSelf, ambiguous: match.ambiguous, unmatched: match.unmatched };
37469
+ }
37470
+ function describeMember(member) {
37471
+ return `${member.displayName}${member.nickname ? ` (@${member.nickname})` : ""} ${member.uuid}`;
37472
+ }
37473
+ function describeReviewerProblems(lookup) {
37474
+ const lines = [];
37475
+ for (const { query, candidates } of lookup.ambiguous) {
37476
+ lines.push(`- \u2753 "${query}" matches ${candidates.length} members \u2014 use a fuller name or a UUID:`);
37477
+ for (const candidate of candidates.slice(0, 10)) lines.push(` - ${describeMember(candidate)}`);
37478
+ if (candidates.length > 10) lines.push(` - \u2026and ${candidates.length - 10} more`);
37479
+ }
37480
+ for (const query of lookup.unmatched) lines.push(`- \u274C "${query}" matches no workspace member`);
37481
+ return lines;
37482
+ }
37483
+ async function handleFindBitbucketReviewers(args) {
37484
+ try {
37485
+ const ws = args.workspace?.trim() || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
37486
+ if (!ws) return mcpText(MISSING_WORKSPACE);
37487
+ if (!BITBUCKET_SLUG.test(ws)) return mcpText(`\u274C Invalid workspace slug: ${ws}`);
37488
+ if (args.names.every((name) => !name.trim())) return mcpText("\u274C names is required.");
37489
+ const auth = buildBasicAuth("BITBUCKET_USER_EMAIL", "BITBUCKET_API_TOKEN");
37490
+ const lookup = await lookupReviewers(ws, auth, args.names);
37491
+ const lines = [`## Reviewer lookup (${ws})`];
37492
+ for (const member of lookup.reviewers) lines.push(`- \u2705 ${describeMember(member)}`);
37493
+ if (lookup.skippedSelf) lines.push("- \u23ED\uFE0F You (the PR author) were named \u2014 skipped, Bitbucket rejects self-review");
37494
+ lines.push(...describeReviewerProblems(lookup));
37495
+ return mcpText(lines.join("\n"));
37496
+ } catch (error2) {
37497
+ return mcpText(errorText(error2, "kit_find_bitbucket_reviewers"));
37498
+ }
37499
+ }
37500
+ async function handleCreateBitbucketPr(args) {
37501
+ try {
37502
+ const ref = resolveBitbucketRepo(args.repository, args.workspace);
37503
+ if (!ref.ok) return mcpText(ref.error);
37504
+ const title = args.title.trim();
37505
+ const sourceBranch = args.sourceBranch.trim();
37506
+ if (!title) return mcpText("\u274C title is required.");
37507
+ if (!sourceBranch) return mcpText("\u274C sourceBranch is required.");
37508
+ const auth = buildBasicAuth("BITBUCKET_USER_EMAIL", "BITBUCKET_API_TOKEN");
37509
+ let reviewerUuids = [];
37510
+ let skippedSelf = false;
37511
+ if (args.reviewers && args.reviewers.some((r) => r.trim())) {
37512
+ const lookup = await lookupReviewers(ref.ws, auth, args.reviewers);
37513
+ const problems = describeReviewerProblems(lookup);
37514
+ if (problems.length > 0) {
37515
+ return mcpText(
37516
+ `\u274C PR not created \u2014 some reviewers did not resolve to exactly one member:
37517
+ ${problems.join("\n")}`
37518
+ );
37519
+ }
37520
+ reviewerUuids = lookup.reviewers.map((member) => member.uuid);
37521
+ skippedSelf = lookup.skippedSelf;
37522
+ }
37523
+ const body = { title, source: { branch: { name: sourceBranch } } };
37524
+ const destinationBranch = args.destinationBranch?.trim();
37525
+ if (destinationBranch) body.destination = { branch: { name: destinationBranch } };
37526
+ if (args.description) body.description = args.description;
37527
+ if (args.closeSourceBranch !== void 0) body.close_source_branch = args.closeSourceBranch;
37528
+ if (args.draft !== void 0) body.draft = args.draft;
37529
+ if (reviewerUuids.length > 0) body.reviewers = reviewerUuids.map((uuid2) => ({ uuid: uuid2 }));
37530
+ const url = `${BITBUCKET_API}/repositories/${ref.ws}/${ref.repo}/pullrequests`;
37531
+ const jsonData = await callRestApi(url, auth, "application/json", body);
37532
+ const parseResult = BitbucketCreatedPrSchema.safeParse(jsonData);
37533
+ if (!parseResult.success) {
37534
+ return mcpText(`\u26A0\uFE0F PR was likely created, but the response could not be parsed: ${parseResult.error.message}`);
37535
+ }
37536
+ const pr = parseResult.data;
37537
+ const reviewers = pr.reviewers?.map((r) => r.display_name).filter(Boolean) ?? [];
37538
+ const lines = [
37539
+ `\u2705 Created PR #${pr.id}: ${pr.title}`,
37540
+ `**URL:** ${pr.links?.html?.href ?? `https://bitbucket.org/${ref.ws}/${ref.repo}/pull-requests/${pr.id}`}`,
37541
+ `**Branch:** ${pr.source?.branch.name ?? sourceBranch} \u2192 ${pr.destination?.branch.name ?? destinationBranch ?? "(main branch)"}`,
37542
+ `**State:** ${pr.state ?? "OPEN"}${pr.draft ? " (draft)" : ""}`
37543
+ ];
37544
+ if (reviewers.length > 0) lines.push(`**Reviewers:** ${reviewers.join(", ")}`);
37545
+ if (skippedSelf)
37546
+ lines.push("\u23ED\uFE0F You (the PR author) were named as a reviewer \u2014 skipped, Bitbucket rejects self-review.");
37547
+ return mcpText(lines.join("\n"));
37548
+ } catch (error2) {
37549
+ return mcpText(errorText(error2, "kit_create_bitbucket_pr"));
37550
+ }
37551
+ }
37552
+ function describeInlineAnchor(inline) {
37553
+ if (inline.to) {
37554
+ return inline.start_to && inline.start_to !== inline.to ? `lines ${inline.start_to}\u2013${inline.to}` : `line ${inline.to}`;
37555
+ }
37556
+ if (inline.from) {
37557
+ return inline.start_from && inline.start_from !== inline.from ? `old lines ${inline.start_from}\u2013${inline.from}` : `old line ${inline.from}`;
37558
+ }
37559
+ return "file";
37560
+ }
37561
+ function renderComment(comment, depth, isRoot) {
37562
+ const indent = " ".repeat(depth);
37563
+ const meta = [`#${comment.id}`, comment.user?.display_name || "Unknown", comment.created_on?.slice(0, 10) ?? ""];
37564
+ if (isRoot && comment.inline) meta.push(describeInlineAnchor(comment.inline));
37565
+ if (comment.pending) meta.push("pending (unpublished)");
37566
+ if (isRoot) {
37567
+ meta.push(
37568
+ comment.resolution ? `\u2705 resolved${comment.resolution.user?.display_name ? ` by ${comment.resolution.user.display_name}` : ""}` : "\u{1F7E1} open"
37569
+ );
37570
+ }
37571
+ const header = `${indent}${depth > 0 ? "\u21B3 " : "- "}**${meta.filter(Boolean).join(" \xB7 ")}**`;
37572
+ const raw = comment.deleted ? "_(deleted)_" : comment.content?.raw?.trim() || "_(empty)_";
37573
+ const body = raw.split("\n").map((line) => `${indent} > ${line}`).join("\n");
37574
+ return `${header}
37575
+ ${body}`;
37576
+ }
37577
+ async function handleGetBitbucketPrComments(args) {
37578
+ try {
37579
+ const ref = resolveBitbucketPr(args.input, args.workspace, args.repoSlug);
37580
+ if (!ref.ok) return mcpText(ref.error);
37581
+ const includeResolved = args.includeResolved ?? true;
37582
+ const includeDeleted = args.includeDeleted ?? false;
37583
+ const auth = buildBasicAuth("BITBUCKET_USER_EMAIL", "BITBUCKET_API_TOKEN");
37584
+ let url = `${BITBUCKET_API}/repositories/${ref.ws}/${ref.repo}/pullrequests/${ref.prId}/comments?pagelen=100`;
37585
+ const comments = [];
37586
+ let pages = 0;
37587
+ while (url && pages < MAX_PAGES) {
37588
+ const parseResult = BitbucketCommentPageSchema.safeParse(await callRestApi(url, auth));
37589
+ if (!parseResult.success) {
37590
+ return mcpText(`\u274C Invalid Bitbucket comments response: ${parseResult.error.message}`);
37591
+ }
37592
+ comments.push(...parseResult.data.values);
37593
+ pages++;
37594
+ url = parseResult.data.next?.startsWith(`${BITBUCKET_API}/`) ? parseResult.data.next : void 0;
37595
+ }
37596
+ const truncated = Boolean(url);
37597
+ const byId = new Map(comments.map((c) => [c.id, c]));
37598
+ const children = /* @__PURE__ */ new Map();
37599
+ const roots = [];
37600
+ for (const comment of comments) {
37601
+ const parentId = comment.parent?.id;
37602
+ if (parentId !== void 0 && byId.has(parentId)) {
37603
+ children.set(parentId, [...children.get(parentId) ?? [], comment]);
37604
+ } else {
37605
+ roots.push(comment);
37606
+ }
37607
+ }
37608
+ const renderThread = (comment, depth) => {
37609
+ const replies = (children.get(comment.id) ?? []).flatMap((child) => renderThread(child, depth + 1));
37610
+ if (comment.deleted && !includeDeleted && replies.length === 0) return [];
37611
+ return [renderComment(comment, depth, depth === 0), ...replies];
37612
+ };
37613
+ const visibleRoots = roots.filter((root) => includeResolved || !root.resolution);
37614
+ const general = [];
37615
+ const inlineByFile = /* @__PURE__ */ new Map();
37616
+ for (const root of visibleRoots) {
37617
+ const rendered = renderThread(root, 0);
37618
+ if (rendered.length === 0) continue;
37619
+ if (root.inline) {
37620
+ inlineByFile.set(root.inline.path, [...inlineByFile.get(root.inline.path) ?? [], ...rendered]);
37621
+ } else {
37622
+ general.push(...rendered);
37623
+ }
37624
+ }
37625
+ const liveRoots = roots.filter((root) => !root.deleted);
37626
+ const unresolved = liveRoots.filter((root) => !root.resolution).length;
37627
+ const summary = `${comments.length} comments in ${liveRoots.length} threads \xB7 ${unresolved} open \xB7 ${liveRoots.length - unresolved} resolved`;
37628
+ let output = `## PR #${ref.prId} comments
37629
+ ${summary}${includeResolved ? "" : " (resolved threads hidden)"}`;
37630
+ if (truncated) {
37631
+ output += `
37632
+
37633
+ \u26A0\uFE0F Stopped after ${MAX_PAGES} pages (${comments.length} comments); later comments are not shown.`;
37634
+ }
37635
+ if (general.length === 0 && inlineByFile.size === 0) {
37636
+ output += "\n\nNo comments to show.";
37637
+ } else {
37638
+ if (general.length > 0) output += `
37639
+
37640
+ ### General
37641
+ ${general.join("\n")}`;
37642
+ if (inlineByFile.size > 0) {
37643
+ output += "\n\n### Inline";
37644
+ for (const [path16, rendered] of inlineByFile) output += `
37645
+
37646
+ #### \`${path16}\`
37647
+ ${rendered.join("\n")}`;
37648
+ }
37649
+ }
37650
+ if (output.length >= LARGE_PAYLOAD_THRESHOLD) {
37651
+ const filePath = `/tmp/kit-pr-${ref.prId}-comments-${Date.now()}.md`;
37652
+ try {
37653
+ writeFileSync4(filePath, sanitizeOutput(output), "utf8");
37654
+ return mcpText(
37655
+ `## PR #${ref.prId} comments
37656
+ ${summary}
37657
+
37658
+ Comments are large (${output.length} chars). Full markdown written to: \`${filePath}\`. Read this file before proceeding.`
37659
+ );
37660
+ } catch {
37661
+ }
37662
+ }
37663
+ return mcpText(sanitizeOutput(output));
37664
+ } catch (error2) {
37665
+ return mcpText(errorText(error2, "kit_get_bitbucket_pr_comments"));
37666
+ }
37667
+ }
36650
37668
  function registerIntegrationTools(server2) {
36651
37669
  server2.registerTool(
36652
37670
  "kit_get_bitbucket_pr",
@@ -36663,29 +37681,9 @@ function registerIntegrationTools(server2) {
36663
37681
  },
36664
37682
  async ({ input, workspace, repoSlug, includeDiff }) => {
36665
37683
  try {
36666
- let ws;
36667
- let repo;
36668
- let prId;
36669
- const urlMatch = input.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)/);
36670
- if (urlMatch) {
36671
- ws = urlMatch[1];
36672
- repo = urlMatch[2];
36673
- prId = parseInt(urlMatch[3], 10);
36674
- } else if (input.match(/^\d+$/)) {
36675
- prId = parseInt(input, 10);
36676
- ws = workspace || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
36677
- repo = repoSlug;
36678
- }
36679
- if (!ws) {
36680
- return mcpText(
36681
- `\u274C workspace is required. Pass it as a parameter or set BITBUCKET_DEFAULT_WORKSPACE in your MCP env config.`
36682
- );
36683
- }
36684
- if (!repo || !prId) {
36685
- return mcpText(`\u274C Could not parse PR URL. Expected: bitbucket.org/{ws}/{repo}/pull-requests/{id}`);
36686
- }
36687
- const safeWs = sanitize(ws);
36688
- const safeRepo = sanitize(repo);
37684
+ const ref = resolveBitbucketPr(input, workspace, repoSlug);
37685
+ if (!ref.ok) return mcpText(ref.error);
37686
+ const { ws: safeWs, repo: safeRepo, prId } = ref;
36689
37687
  const auth = buildBasicAuth("BITBUCKET_USER_EMAIL", "BITBUCKET_API_TOKEN");
36690
37688
  const prUrl = `https://api.bitbucket.org/2.0/repositories/${safeWs}/${safeRepo}/pullrequests/${prId}`;
36691
37689
  const jsonData = await callRestApi(prUrl, auth);
@@ -36703,8 +37701,7 @@ ${pr.description || "No description"}`;
36703
37701
  if (includeDiff) {
36704
37702
  const diffUrl = `https://api.bitbucket.org/2.0/repositories/${safeWs}/${safeRepo}/pullrequests/${prId}/diff`;
36705
37703
  const diff = await callRestApi(diffUrl, auth, "text/plain");
36706
- const DIFF_FILE_THRESHOLD = 5e4;
36707
- if (diff.length < DIFF_FILE_THRESHOLD) {
37704
+ if (diff.length < LARGE_PAYLOAD_THRESHOLD) {
36708
37705
  output += `
36709
37706
 
36710
37707
  ### Diff
@@ -36732,10 +37729,63 @@ ${diff}
36732
37729
  }
36733
37730
  return mcpText(output);
36734
37731
  } catch (error2) {
36735
- return mcpText(error2 instanceof Error ? error2.message : String(error2));
37732
+ return mcpText(errorText(error2, "kit_get_bitbucket_pr"));
36736
37733
  }
36737
37734
  }
36738
37735
  );
37736
+ server2.registerTool(
37737
+ "kit_create_bitbucket_pr",
37738
+ {
37739
+ title: "Create Bitbucket PR",
37740
+ description: "Create a Bitbucket Cloud pull request from an already-pushed source branch. If destinationBranch is omitted, Bitbucket targets the repository main branch. Returns the new PR id and URL.",
37741
+ inputSchema: {
37742
+ repository: external_exports.string().describe(
37743
+ 'Bitbucket repo URL, "workspace/repo", or a repo slug (uses workspace / BITBUCKET_DEFAULT_WORKSPACE)'
37744
+ ),
37745
+ workspace: external_exports.string().optional().describe("Bitbucket workspace slug, used when repository is a bare slug"),
37746
+ title: external_exports.string().describe("PR title"),
37747
+ sourceBranch: external_exports.string().describe("Source branch name (must already exist on the remote)"),
37748
+ destinationBranch: external_exports.string().optional().describe("Destination branch name; defaults to the repo main branch"),
37749
+ description: external_exports.string().optional().describe("PR description (markdown)"),
37750
+ closeSourceBranch: external_exports.boolean().optional().describe("Close the source branch when the PR is merged"),
37751
+ draft: external_exports.boolean().optional().describe("Create the PR as a draft"),
37752
+ reviewers: external_exports.array(external_exports.string()).optional().describe(
37753
+ 'Reviewers as names, nicknames or account UUIDs, e.g. ["Dien", "Nguyen Tien Dung"]. Matching is case- and accent-insensitive; a name that matches no member or several aborts with no PR created. The PR author is skipped.'
37754
+ )
37755
+ },
37756
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
37757
+ },
37758
+ async (args) => handleCreateBitbucketPr(args)
37759
+ );
37760
+ server2.registerTool(
37761
+ "kit_find_bitbucket_reviewers",
37762
+ {
37763
+ title: "Find Bitbucket Reviewers",
37764
+ description: "Resolve reviewer names, nicknames or UUIDs to Bitbucket workspace members \u2014 the same matching kit_create_bitbucket_pr uses. Reports matched, ambiguous (with candidates) and unknown names, so reviewers can be verified before pushing or creating a PR.",
37765
+ inputSchema: {
37766
+ names: external_exports.array(external_exports.string()).describe('Names, nicknames or account UUIDs, e.g. ["Dien", "Dung"]'),
37767
+ workspace: external_exports.string().optional().describe("Bitbucket workspace slug (defaults to BITBUCKET_DEFAULT_WORKSPACE)")
37768
+ },
37769
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
37770
+ },
37771
+ async (args) => handleFindBitbucketReviewers(args)
37772
+ );
37773
+ server2.registerTool(
37774
+ "kit_get_bitbucket_pr_comments",
37775
+ {
37776
+ title: "Get Bitbucket PR Comments",
37777
+ description: "Get all comments on a Bitbucket PR as threads: general comments, then inline comments grouped by file with line anchors and resolved/open status. Accepts a full PR URL or a numeric PR ID with workspace + repoSlug.",
37778
+ inputSchema: {
37779
+ input: external_exports.string().describe("Bitbucket PR URL or numeric PR ID"),
37780
+ workspace: external_exports.string().optional().describe("Bitbucket workspace slug (required for numeric ID if BITBUCKET_DEFAULT_WORKSPACE not set)"),
37781
+ repoSlug: external_exports.string().optional().describe("Bitbucket repo slug (required for numeric ID)"),
37782
+ includeResolved: external_exports.boolean().optional().default(true).describe("Include resolved threads"),
37783
+ includeDeleted: external_exports.boolean().optional().default(false).describe("Include deleted comments")
37784
+ },
37785
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
37786
+ },
37787
+ async (args) => handleGetBitbucketPrComments(args)
37788
+ );
36739
37789
  server2.registerTool(
36740
37790
  "kit_jira_get_ticket",
36741
37791
  {
@@ -36754,9 +37804,7 @@ ${diff}
36754
37804
 
36755
37805
  Expected format: PROJ-123`);
36756
37806
  }
36757
- const cloudId = getCredential("ATLASSIAN_CLOUD_ID");
36758
- if (!cloudId) throw new Error("Missing ATLASSIAN_CLOUD_ID");
36759
- const auth = buildBasicAuth("ATLASSIAN_USER_EMAIL", "ATLASSIAN_API_TOKEN");
37807
+ const { auth, cloudId } = buildAtlassianContext("jira");
36760
37808
  const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${safeTicketId}`;
36761
37809
  const jsonData = await callRestApi(url, auth);
36762
37810
  const parseResult = JiraTicketSchema.safeParse(jsonData);
@@ -36784,10 +37832,22 @@ ${typeof ticket.fields.description === "string" ? ticket.fields.description : ad
36784
37832
  ${ticket.fields.labels?.join(", ") || "None"}`;
36785
37833
  return mcpText(output);
36786
37834
  } catch (error2) {
36787
- return mcpText(`Error: ${error2 instanceof Error ? error2.message : String(error2)}`);
37835
+ return mcpText(`Error: ${errorText(error2, "kit_jira_get_ticket")}`);
36788
37836
  }
36789
37837
  }
36790
37838
  );
37839
+ server2.registerTool(
37840
+ "kit_confluence_get_page",
37841
+ {
37842
+ title: "Get Confluence Page",
37843
+ description: "Get a Confluence page as markdown using the Atlassian REST API. Accepts a full Confluence page URL or a numeric page ID.",
37844
+ inputSchema: {
37845
+ input: external_exports.string().describe("Confluence page URL or numeric page ID")
37846
+ },
37847
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
37848
+ },
37849
+ async ({ input }) => handleConfluenceGetPage({ input })
37850
+ );
36791
37851
  }
36792
37852
 
36793
37853
  // src/mcp/memory.ts