mrvn-cli 0.5.11 → 0.5.13

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.
@@ -15676,9 +15676,14 @@ function collectSprintSummaryData(store, sprintId) {
15676
15676
  };
15677
15677
  });
15678
15678
  const sprintTag = `sprint:${fm.id}`;
15679
- const workItemDocs = allDocs.filter(
15679
+ const sprintTaggedDocs = allDocs.filter(
15680
15680
  (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "meeting" && d.frontmatter.type !== "decision" && d.frontmatter.type !== "question" && d.frontmatter.tags?.includes(sprintTag)
15681
15681
  );
15682
+ const sprintTaggedIds = new Set(sprintTaggedDocs.map((d) => d.frontmatter.id));
15683
+ const orphanContributions = allDocs.filter(
15684
+ (d) => d.frontmatter.type === "contribution" && !sprintTaggedIds.has(d.frontmatter.id) && d.frontmatter.aboutArtifact && sprintTaggedIds.has(d.frontmatter.aboutArtifact)
15685
+ );
15686
+ const workItemDocs = [...sprintTaggedDocs, ...orphanContributions];
15682
15687
  const primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
15683
15688
  const byStatus = {};
15684
15689
  const byType = {};
@@ -15697,7 +15702,7 @@ function collectSprintSummaryData(store, sprintId) {
15697
15702
  }
15698
15703
  const allItemsById = /* @__PURE__ */ new Map();
15699
15704
  const childrenByParent = /* @__PURE__ */ new Map();
15700
- const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
15705
+ const workItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
15701
15706
  for (const doc of workItemDocs) {
15702
15707
  const about = doc.frontmatter.aboutArtifact;
15703
15708
  const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
@@ -15711,10 +15716,12 @@ function collectSprintSummaryData(store, sprintId) {
15711
15716
  workFocus: focusTag ? focusTag.slice(6) : void 0,
15712
15717
  aboutArtifact: about,
15713
15718
  jiraKey: doc.frontmatter.jiraKey,
15714
- jiraUrl: doc.frontmatter.jiraUrl
15719
+ jiraUrl: doc.frontmatter.jiraUrl,
15720
+ confluenceUrl: doc.frontmatter.confluenceUrl,
15721
+ confluenceTitle: doc.frontmatter.confluenceTitle
15715
15722
  };
15716
15723
  allItemsById.set(item.id, item);
15717
- if (about && sprintItemIds.has(about)) {
15724
+ if (about && workItemIds.has(about)) {
15718
15725
  if (!childrenByParent.has(about)) childrenByParent.set(about, []);
15719
15726
  childrenByParent.get(about).push(item);
15720
15727
  }
@@ -19026,11 +19033,14 @@ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
19026
19033
  var JiraClient = class {
19027
19034
  baseUrl;
19028
19035
  baseUrlV3;
19036
+ confluenceBaseUrl;
19029
19037
  authHeader;
19038
+ host;
19030
19039
  constructor(config2) {
19031
- const host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
19032
- this.baseUrl = `https://${host}/rest/api/2`;
19033
- this.baseUrlV3 = `https://${host}/rest/api/3`;
19040
+ this.host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
19041
+ this.baseUrl = `https://${this.host}/rest/api/2`;
19042
+ this.baseUrlV3 = `https://${this.host}/rest/api/3`;
19043
+ this.confluenceBaseUrl = `https://${this.host}/wiki/api/v2`;
19034
19044
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
19035
19045
  }
19036
19046
  async request(path11, method = "GET", body) {
@@ -19118,6 +19128,30 @@ var JiraClient = class {
19118
19128
  { body }
19119
19129
  );
19120
19130
  }
19131
+ // --- Confluence methods ---
19132
+ async getConfluencePage(pageId) {
19133
+ return this.doRequest(
19134
+ `${this.confluenceBaseUrl}/pages/${encodeURIComponent(pageId)}?body-format=atlas_doc_format`,
19135
+ "GET"
19136
+ );
19137
+ }
19138
+ /**
19139
+ * Extract a Confluence page ID from various URL formats.
19140
+ * Returns null if the URL doesn't match any known pattern.
19141
+ */
19142
+ static extractPageId(url2) {
19143
+ const pagesMatch = url2.match(/\/pages\/(\d+)/);
19144
+ if (pagesMatch) return pagesMatch[1];
19145
+ const paramMatch = url2.match(/[?&]pageId=(\d+)/);
19146
+ if (paramMatch) return paramMatch[1];
19147
+ return null;
19148
+ }
19149
+ /**
19150
+ * Build a web URL for a Confluence page.
19151
+ */
19152
+ getConfluencePageUrl(pageId) {
19153
+ return `https://${this.host}/wiki/pages/viewpage.action?pageId=${pageId}`;
19154
+ }
19121
19155
  };
19122
19156
  function createJiraClient(jiraUserConfig) {
19123
19157
  const host = jiraUserConfig?.host ?? process.env.JIRA_HOST;
@@ -20178,6 +20212,111 @@ function createJiraTools(store, projectConfig) {
20178
20212
  };
20179
20213
  }
20180
20214
  ),
20215
+ // --- Confluence tools ---
20216
+ tool20(
20217
+ "link_to_confluence",
20218
+ "Link a Confluence page to any Marvin artifact. Validates the page exists and fetches its title.",
20219
+ {
20220
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'A-003', 'T-002')"),
20221
+ confluenceUrl: external_exports.string().describe("Confluence page URL")
20222
+ },
20223
+ async (args) => {
20224
+ const jira = createJiraClient(jiraUserConfig);
20225
+ if (!jira) return jiraNotConfiguredError();
20226
+ const artifact = store.get(args.artifactId);
20227
+ if (!artifact) {
20228
+ return {
20229
+ content: [
20230
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
20231
+ ],
20232
+ isError: true
20233
+ };
20234
+ }
20235
+ const pageId = JiraClient.extractPageId(args.confluenceUrl);
20236
+ if (!pageId) {
20237
+ return {
20238
+ content: [
20239
+ { type: "text", text: `Could not extract page ID from URL: ${args.confluenceUrl}` }
20240
+ ],
20241
+ isError: true
20242
+ };
20243
+ }
20244
+ const page = await jira.client.getConfluencePage(pageId);
20245
+ const existingTags = artifact.frontmatter.tags ?? [];
20246
+ store.update(args.artifactId, {
20247
+ confluenceUrl: args.confluenceUrl,
20248
+ confluencePageId: pageId,
20249
+ confluenceTitle: page.title,
20250
+ tags: [...existingTags.filter((t) => !t.startsWith("confluence:")), `confluence:${page.title}`]
20251
+ });
20252
+ return {
20253
+ content: [
20254
+ {
20255
+ type: "text",
20256
+ text: `Linked ${args.artifactId} to Confluence page "${page.title}" (ID: ${pageId}).`
20257
+ }
20258
+ ]
20259
+ };
20260
+ }
20261
+ ),
20262
+ tool20(
20263
+ "read_confluence_page",
20264
+ "Read the content of a Confluence page by URL or page ID. Returns the page title, metadata, and body as plain text.",
20265
+ {
20266
+ pageUrl: external_exports.string().optional().describe("Confluence page URL"),
20267
+ pageId: external_exports.string().optional().describe("Confluence page ID (alternative to URL)")
20268
+ },
20269
+ async (args) => {
20270
+ const jira = createJiraClient(jiraUserConfig);
20271
+ if (!jira) return jiraNotConfiguredError();
20272
+ const resolvedId = args.pageId ?? (args.pageUrl ? JiraClient.extractPageId(args.pageUrl) : null);
20273
+ if (!resolvedId) {
20274
+ return {
20275
+ content: [
20276
+ {
20277
+ type: "text",
20278
+ text: "Provide either pageUrl or pageId. Could not extract page ID from the given URL."
20279
+ }
20280
+ ],
20281
+ isError: true
20282
+ };
20283
+ }
20284
+ const page = await jira.client.getConfluencePage(resolvedId);
20285
+ let bodyText = "";
20286
+ if (page.body?.atlas_doc_format?.value) {
20287
+ try {
20288
+ const adf = JSON.parse(page.body.atlas_doc_format.value);
20289
+ bodyText = extractCommentText(adf);
20290
+ } catch {
20291
+ bodyText = page.body.atlas_doc_format.value;
20292
+ }
20293
+ }
20294
+ const parts = [
20295
+ `# ${page.title}`,
20296
+ "",
20297
+ `Page ID: ${page.id}`,
20298
+ `Status: ${page.status}`,
20299
+ `Version: ${page.version.number} (${page.version.createdAt.slice(0, 10)})`,
20300
+ "",
20301
+ "---",
20302
+ "",
20303
+ bodyText || "(empty page)"
20304
+ ];
20305
+ const allDocs = store.registeredTypes.flatMap((t) => store.list({ type: t }));
20306
+ const linkedArtifacts = allDocs.filter(
20307
+ (d) => d.frontmatter.confluencePageId === resolvedId || d.frontmatter.confluenceUrl === args.pageUrl
20308
+ );
20309
+ if (linkedArtifacts.length > 0) {
20310
+ parts.push("");
20311
+ parts.push("---");
20312
+ parts.push(`Linked Marvin artifacts: ${linkedArtifacts.map((d) => d.frontmatter.id).join(", ")}`);
20313
+ }
20314
+ return {
20315
+ content: [{ type: "text", text: parts.join("\n") }]
20316
+ };
20317
+ },
20318
+ { annotations: { readOnlyHint: true } }
20319
+ ),
20181
20320
  // --- Jira status fetch (read-only) ---
20182
20321
  tool20(
20183
20322
  "fetch_jira_status",
@@ -20488,6 +20627,8 @@ function formatIssueEntry(issue2) {
20488
20627
  var COMMON_TOOLS = `**Available tools:**
20489
20628
  - \`push_artifact_to_jira\` \u2014 create a Jira issue from any Marvin artifact and link it directly via \`jiraKey\` on the artifact.
20490
20629
  - \`link_to_jira\` \u2014 link an existing Jira issue to any Marvin artifact (sets \`jiraKey\` directly on the artifact).
20630
+ - \`link_to_confluence\` \u2014 link a Confluence page to any Marvin artifact. Validates the page exists and fetches its title.
20631
+ - \`read_confluence_page\` \u2014 **read-only**: fetch and return the content of a Confluence page by URL or page ID. Use this to review Confluence content for updating tasks, generating contributions, or answering questions.
20491
20632
  - \`fetch_jira_status\` \u2014 **read-only**: fetch current Jira status, subtask progress, and linked issues for Jira-linked actions/tasks. Returns proposed changes without applying them.
20492
20633
  - \`fetch_jira_daily\` \u2014 **read-only**: fetch a daily/range summary of all Jira changes \u2014 status transitions, comments, linked Confluence pages, and cross-references with Marvin artifacts. Returns proposed actions (status updates, unlinked issues, question candidates, Confluence pages to review).
20493
20634
  - \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
@@ -21188,11 +21329,30 @@ function formatDate(iso) {
21188
21329
  function typeLabel(type) {
21189
21330
  return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
21190
21331
  }
21332
+ var JIRA_SVG = `<svg class="integration-icon" viewBox="0 0 256 256" width="14" height="14" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="jg1" x1="98.03%" y1="0.16%" x2="58.89%" y2="40.87%"><stop offset="18%" stop-color="#0052CC"/><stop offset="100%" stop-color="#2684FF"/></linearGradient><linearGradient id="jg2" x1="100.17%" y1="0.05%" x2="55.76%" y2="45.19%"><stop offset="18%" stop-color="#0052CC"/><stop offset="100%" stop-color="#2684FF"/></linearGradient></defs><path d="M244.658 0H121.707a55.502 55.502 0 0 0 55.502 55.502h22.649V77.37c.02 30.625 24.841 55.447 55.466 55.502V10.666C255.324 4.777 250.55 0 244.658 0z" fill="#2684FF"/><path d="M183.822 61.262H60.872c.019 30.625 24.84 55.447 55.466 55.502h22.649v21.868c.02 30.597 24.798 55.408 55.395 55.502V71.928c0-5.891-4.776-10.666-10.56-10.666z" fill="url(#jg1)"/><path d="M122.951 122.489H0c0 30.653 24.85 55.502 55.502 55.502h22.72v21.868c.02 30.597 24.798 55.408 55.396 55.502V133.155c0-5.891-4.776-10.666-10.667-10.666z" fill="url(#jg2)"/></svg>`;
21333
+ var CONFLUENCE_SVG = `<svg class="integration-icon" viewBox="0 0 256 246" width="14" height="14" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cg1" x1="99.14%" y1="113.9%" x2="33.86%" y2="37.96%"><stop offset="18%" stop-color="#0052CC"/><stop offset="100%" stop-color="#2684FF"/></linearGradient><linearGradient id="cg2" x1="0.86%" y1="-13.9%" x2="66.14%" y2="62.04%"><stop offset="18%" stop-color="#0052CC"/><stop offset="100%" stop-color="#2684FF"/></linearGradient></defs><path d="M9.26 187.28c-3.14 5.06-6.71 10.98-9.26 15.53a7.84 7.84 0 0 0 2.83 10.72l67.58 40.48a7.85 7.85 0 0 0 10.72-2.63c2.14-3.54 5.01-8.25 8.15-13.41 22.18-36.47 44.67-32.02 85.83-13.41l68.59 31.05a7.85 7.85 0 0 0 10.42-3.94l29.24-66.24a7.85 7.85 0 0 0-3.84-10.32c-20.53-9.27-61.49-27.75-87.33-39.45-72.2-32.73-133.87-30.05-182.93 51.62z" fill="url(#cg1)"/><path d="M246.11 58.24c3.14-5.06 6.71-10.98 9.26-15.53a7.84 7.84 0 0 0-2.83-10.72L184.96 0a7.85 7.85 0 0 0-10.72 2.63c-2.14 3.54-5.01 8.25-8.15 13.41-22.18 36.47-44.67 32.02-85.83 13.41L12.37 -1.6a7.85 7.85 0 0 0-10.42 3.94L-27.29 68.58a7.85 7.85 0 0 0 3.84 10.32c20.53 9.27 61.49 27.75 87.33 39.45 72.2 32.73 133.87 30.05 182.23-60.11z" fill="url(#cg2)"/></svg>`;
21191
21334
  function jiraIcon(jiraKey, jiraUrl) {
21192
21335
  if (!jiraKey) return "";
21193
21336
  const href = jiraUrl ?? "#";
21194
21337
  const title = escapeHtml(jiraKey);
21195
- return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener" title="Jira: ${title}" class="jira-link"><svg class="jira-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.005 2L6.5 7.505l5.505 5.505L17.51 7.505 12.005 2z" fill="#2684FF"/><path d="M6.5 7.505L1 13.01l5.505 5.505L12.01 13.01 6.5 7.505z" fill="url(#jira-g1)"/><path d="M17.51 7.505L12.005 13.01l5.505 5.505L23.015 13.01 17.51 7.505z" fill="url(#jira-g2)"/><path d="M12.005 13.01L6.5 18.515l5.505 5.505 5.505-5.505-5.505-5.505z" fill="#2684FF"/><defs><linearGradient id="jira-g1" x1="9.25" y1="7.51" x2="3.85" y2="12.91" gradientUnits="userSpaceOnUse"><stop stop-color="#0052CC"/><stop offset="1" stop-color="#2684FF"/></linearGradient><linearGradient id="jira-g2" x1="14.76" y1="7.51" x2="20.16" y2="12.91" gradientUnits="userSpaceOnUse"><stop stop-color="#0052CC"/><stop offset="1" stop-color="#2684FF"/></linearGradient></defs></svg></a>`;
21338
+ return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener" title="Jira: ${title}" class="integration-link jira-link">${JIRA_SVG}</a>`;
21339
+ }
21340
+ function confluenceIcon(confluenceUrl, confluenceTitle) {
21341
+ if (!confluenceUrl) return "";
21342
+ const title = confluenceTitle ? escapeHtml(confluenceTitle) : "Confluence";
21343
+ return `<a href="${escapeHtml(confluenceUrl)}" target="_blank" rel="noopener" title="${title}" class="integration-link confluence-link">${CONFLUENCE_SVG}</a>`;
21344
+ }
21345
+ function integrationIcons(frontmatter) {
21346
+ const jira = jiraIcon(
21347
+ frontmatter.jiraKey,
21348
+ frontmatter.jiraUrl
21349
+ );
21350
+ const confluence = confluenceIcon(
21351
+ frontmatter.confluenceUrl,
21352
+ frontmatter.confluenceTitle
21353
+ );
21354
+ if (!jira && !confluence) return "";
21355
+ return `<span class="integration-icons">${jira}${confluence}</span>`;
21196
21356
  }
21197
21357
  function renderMarkdown(md) {
21198
21358
  const lines = md.split("\n");
@@ -23059,17 +23219,22 @@ tr:hover td {
23059
23219
  .owner-badge-dm { background: rgba(52, 211, 153, 0.18); color: #34d399; }
23060
23220
  .owner-badge-other { background: rgba(139, 143, 164, 0.12); color: var(--text-dim); }
23061
23221
 
23062
- /* Jira link icon */
23063
- .jira-link {
23222
+ /* Integration icons (Jira, Confluence) */
23223
+ .integration-icons {
23064
23224
  display: inline-flex;
23065
23225
  align-items: center;
23226
+ gap: 0.25rem;
23066
23227
  vertical-align: middle;
23067
- margin-left: 0.35rem;
23228
+ margin-left: 0.5rem;
23229
+ }
23230
+ .integration-link {
23231
+ display: inline-flex;
23232
+ align-items: center;
23068
23233
  opacity: 0.7;
23069
23234
  transition: opacity 0.15s;
23070
23235
  }
23071
- .jira-link:hover { opacity: 1; }
23072
- .jira-icon { vertical-align: middle; }
23236
+ .integration-link:hover { opacity: 1; }
23237
+ .integration-icon { vertical-align: middle; }
23073
23238
 
23074
23239
  /* Group header rows (PO dashboard decisions/deps) */
23075
23240
  .group-header-row td {
@@ -23095,7 +23260,7 @@ function documentsPage(data) {
23095
23260
  (doc) => `
23096
23261
  <tr>
23097
23262
  <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
23098
- <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a>${jiraIcon(doc.frontmatter.jiraKey, doc.frontmatter.jiraUrl)}</td>
23263
+ <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a>${integrationIcons(doc.frontmatter)}</td>
23099
23264
  <td>${statusBadge(doc.frontmatter.status)}</td>
23100
23265
  <td>${escapeHtml(doc.frontmatter.owner ?? "\u2014")}</td>
23101
23266
  <td>${doc.frontmatter.priority ? `<span class="priority-${doc.frontmatter.priority.toLowerCase()}">${escapeHtml(doc.frontmatter.priority)}</span>` : "\u2014"}</td>
@@ -23184,7 +23349,7 @@ function documentDetailPage(doc) {
23184
23349
  </div>
23185
23350
 
23186
23351
  <div class="page-header">
23187
- <h2>${escapeHtml(fm.title)}${jiraIcon(fm.jiraKey, fm.jiraUrl)}</h2>
23352
+ <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
23188
23353
  <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
23189
23354
  </div>
23190
23355
 
@@ -24615,7 +24780,7 @@ function renderItemRows(items, borderColor, showOwner, depth = 0) {
24615
24780
  const row = `
24616
24781
  <tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
24617
24782
  <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
24618
- <td>${escapeHtml(w.title)}${jiraIcon(w.jiraKey, w.jiraUrl)}</td>
24783
+ <td>${escapeHtml(w.title)}${jiraIcon(w.jiraKey, w.jiraUrl)}${confluenceIcon(w.confluenceUrl, w.confluenceTitle)}</td>
24619
24784
  ${ownerCell}
24620
24785
  <td>${statusBadge(w.status)}</td>
24621
24786
  <td>${progressCell}</td>
@@ -26240,7 +26405,7 @@ function boardPage(data, basePath = "/board") {
26240
26405
  <div class="board-card">
26241
26406
  <a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">
26242
26407
  <div class="bc-id">${escapeHtml(doc.frontmatter.id)}</div>
26243
- <div class="bc-title">${escapeHtml(doc.frontmatter.title)}${jiraIcon(doc.frontmatter.jiraKey, doc.frontmatter.jiraUrl)}</div>
26408
+ <div class="bc-title">${escapeHtml(doc.frontmatter.title)}${integrationIcons(doc.frontmatter)}</div>
26244
26409
  ${doc.frontmatter.owner ? `<div class="bc-owner">${escapeHtml(doc.frontmatter.owner)}</div>` : ""}
26245
26410
  </a>
26246
26411
  </div>`