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.
package/dist/index.js CHANGED
@@ -15740,9 +15740,14 @@ function collectSprintSummaryData(store, sprintId) {
15740
15740
  };
15741
15741
  });
15742
15742
  const sprintTag = `sprint:${fm.id}`;
15743
- const workItemDocs = allDocs.filter(
15743
+ const sprintTaggedDocs = allDocs.filter(
15744
15744
  (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)
15745
15745
  );
15746
+ const sprintTaggedIds = new Set(sprintTaggedDocs.map((d) => d.frontmatter.id));
15747
+ const orphanContributions = allDocs.filter(
15748
+ (d) => d.frontmatter.type === "contribution" && !sprintTaggedIds.has(d.frontmatter.id) && d.frontmatter.aboutArtifact && sprintTaggedIds.has(d.frontmatter.aboutArtifact)
15749
+ );
15750
+ const workItemDocs = [...sprintTaggedDocs, ...orphanContributions];
15746
15751
  const primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
15747
15752
  const byStatus = {};
15748
15753
  const byType = {};
@@ -15761,7 +15766,7 @@ function collectSprintSummaryData(store, sprintId) {
15761
15766
  }
15762
15767
  const allItemsById = /* @__PURE__ */ new Map();
15763
15768
  const childrenByParent = /* @__PURE__ */ new Map();
15764
- const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
15769
+ const workItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
15765
15770
  for (const doc of workItemDocs) {
15766
15771
  const about = doc.frontmatter.aboutArtifact;
15767
15772
  const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
@@ -15775,10 +15780,12 @@ function collectSprintSummaryData(store, sprintId) {
15775
15780
  workFocus: focusTag ? focusTag.slice(6) : void 0,
15776
15781
  aboutArtifact: about,
15777
15782
  jiraKey: doc.frontmatter.jiraKey,
15778
- jiraUrl: doc.frontmatter.jiraUrl
15783
+ jiraUrl: doc.frontmatter.jiraUrl,
15784
+ confluenceUrl: doc.frontmatter.confluenceUrl,
15785
+ confluenceTitle: doc.frontmatter.confluenceTitle
15779
15786
  };
15780
15787
  allItemsById.set(item.id, item);
15781
- if (about && sprintItemIds.has(about)) {
15788
+ if (about && workItemIds.has(about)) {
15782
15789
  if (!childrenByParent.has(about)) childrenByParent.set(about, []);
15783
15790
  childrenByParent.get(about).push(item);
15784
15791
  }
@@ -16286,11 +16293,30 @@ function formatDate(iso) {
16286
16293
  function typeLabel(type) {
16287
16294
  return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
16288
16295
  }
16296
+ 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>`;
16297
+ 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>`;
16289
16298
  function jiraIcon(jiraKey, jiraUrl) {
16290
16299
  if (!jiraKey) return "";
16291
16300
  const href = jiraUrl ?? "#";
16292
16301
  const title = escapeHtml(jiraKey);
16293
- 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>`;
16302
+ return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener" title="Jira: ${title}" class="integration-link jira-link">${JIRA_SVG}</a>`;
16303
+ }
16304
+ function confluenceIcon(confluenceUrl, confluenceTitle) {
16305
+ if (!confluenceUrl) return "";
16306
+ const title = confluenceTitle ? escapeHtml(confluenceTitle) : "Confluence";
16307
+ return `<a href="${escapeHtml(confluenceUrl)}" target="_blank" rel="noopener" title="${title}" class="integration-link confluence-link">${CONFLUENCE_SVG}</a>`;
16308
+ }
16309
+ function integrationIcons(frontmatter) {
16310
+ const jira = jiraIcon(
16311
+ frontmatter.jiraKey,
16312
+ frontmatter.jiraUrl
16313
+ );
16314
+ const confluence = confluenceIcon(
16315
+ frontmatter.confluenceUrl,
16316
+ frontmatter.confluenceTitle
16317
+ );
16318
+ if (!jira && !confluence) return "";
16319
+ return `<span class="integration-icons">${jira}${confluence}</span>`;
16294
16320
  }
16295
16321
  function renderMarkdown(md) {
16296
16322
  const lines = md.split("\n");
@@ -18157,17 +18183,22 @@ tr:hover td {
18157
18183
  .owner-badge-dm { background: rgba(52, 211, 153, 0.18); color: #34d399; }
18158
18184
  .owner-badge-other { background: rgba(139, 143, 164, 0.12); color: var(--text-dim); }
18159
18185
 
18160
- /* Jira link icon */
18161
- .jira-link {
18186
+ /* Integration icons (Jira, Confluence) */
18187
+ .integration-icons {
18162
18188
  display: inline-flex;
18163
18189
  align-items: center;
18190
+ gap: 0.25rem;
18164
18191
  vertical-align: middle;
18165
- margin-left: 0.35rem;
18192
+ margin-left: 0.5rem;
18193
+ }
18194
+ .integration-link {
18195
+ display: inline-flex;
18196
+ align-items: center;
18166
18197
  opacity: 0.7;
18167
18198
  transition: opacity 0.15s;
18168
18199
  }
18169
- .jira-link:hover { opacity: 1; }
18170
- .jira-icon { vertical-align: middle; }
18200
+ .integration-link:hover { opacity: 1; }
18201
+ .integration-icon { vertical-align: middle; }
18171
18202
 
18172
18203
  /* Group header rows (PO dashboard decisions/deps) */
18173
18204
  .group-header-row td {
@@ -18193,7 +18224,7 @@ function documentsPage(data) {
18193
18224
  (doc) => `
18194
18225
  <tr>
18195
18226
  <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
18196
- <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a>${jiraIcon(doc.frontmatter.jiraKey, doc.frontmatter.jiraUrl)}</td>
18227
+ <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a>${integrationIcons(doc.frontmatter)}</td>
18197
18228
  <td>${statusBadge(doc.frontmatter.status)}</td>
18198
18229
  <td>${escapeHtml(doc.frontmatter.owner ?? "\u2014")}</td>
18199
18230
  <td>${doc.frontmatter.priority ? `<span class="priority-${doc.frontmatter.priority.toLowerCase()}">${escapeHtml(doc.frontmatter.priority)}</span>` : "\u2014"}</td>
@@ -18282,7 +18313,7 @@ function documentDetailPage(doc) {
18282
18313
  </div>
18283
18314
 
18284
18315
  <div class="page-header">
18285
- <h2>${escapeHtml(fm.title)}${jiraIcon(fm.jiraKey, fm.jiraUrl)}</h2>
18316
+ <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
18286
18317
  <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
18287
18318
  </div>
18288
18319
 
@@ -19704,7 +19735,7 @@ function renderItemRows(items, borderColor, showOwner, depth = 0) {
19704
19735
  const row = `
19705
19736
  <tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
19706
19737
  <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
19707
- <td>${escapeHtml(w.title)}${jiraIcon(w.jiraKey, w.jiraUrl)}</td>
19738
+ <td>${escapeHtml(w.title)}${jiraIcon(w.jiraKey, w.jiraUrl)}${confluenceIcon(w.confluenceUrl, w.confluenceTitle)}</td>
19708
19739
  ${ownerCell}
19709
19740
  <td>${statusBadge(w.status)}</td>
19710
19741
  <td>${progressCell}</td>
@@ -21329,7 +21360,7 @@ function boardPage(data, basePath = "/board") {
21329
21360
  <div class="board-card">
21330
21361
  <a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">
21331
21362
  <div class="bc-id">${escapeHtml(doc.frontmatter.id)}</div>
21332
- <div class="bc-title">${escapeHtml(doc.frontmatter.title)}${jiraIcon(doc.frontmatter.jiraKey, doc.frontmatter.jiraUrl)}</div>
21363
+ <div class="bc-title">${escapeHtml(doc.frontmatter.title)}${integrationIcons(doc.frontmatter)}</div>
21333
21364
  ${doc.frontmatter.owner ? `<div class="bc-owner">${escapeHtml(doc.frontmatter.owner)}</div>` : ""}
21334
21365
  </a>
21335
21366
  </div>`
@@ -24916,11 +24947,14 @@ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
24916
24947
  var JiraClient = class {
24917
24948
  baseUrl;
24918
24949
  baseUrlV3;
24950
+ confluenceBaseUrl;
24919
24951
  authHeader;
24952
+ host;
24920
24953
  constructor(config2) {
24921
- const host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
24922
- this.baseUrl = `https://${host}/rest/api/2`;
24923
- this.baseUrlV3 = `https://${host}/rest/api/3`;
24954
+ this.host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
24955
+ this.baseUrl = `https://${this.host}/rest/api/2`;
24956
+ this.baseUrlV3 = `https://${this.host}/rest/api/3`;
24957
+ this.confluenceBaseUrl = `https://${this.host}/wiki/api/v2`;
24924
24958
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
24925
24959
  }
24926
24960
  async request(path21, method = "GET", body) {
@@ -25008,6 +25042,30 @@ var JiraClient = class {
25008
25042
  { body }
25009
25043
  );
25010
25044
  }
25045
+ // --- Confluence methods ---
25046
+ async getConfluencePage(pageId) {
25047
+ return this.doRequest(
25048
+ `${this.confluenceBaseUrl}/pages/${encodeURIComponent(pageId)}?body-format=atlas_doc_format`,
25049
+ "GET"
25050
+ );
25051
+ }
25052
+ /**
25053
+ * Extract a Confluence page ID from various URL formats.
25054
+ * Returns null if the URL doesn't match any known pattern.
25055
+ */
25056
+ static extractPageId(url2) {
25057
+ const pagesMatch = url2.match(/\/pages\/(\d+)/);
25058
+ if (pagesMatch) return pagesMatch[1];
25059
+ const paramMatch = url2.match(/[?&]pageId=(\d+)/);
25060
+ if (paramMatch) return paramMatch[1];
25061
+ return null;
25062
+ }
25063
+ /**
25064
+ * Build a web URL for a Confluence page.
25065
+ */
25066
+ getConfluencePageUrl(pageId) {
25067
+ return `https://${this.host}/wiki/pages/viewpage.action?pageId=${pageId}`;
25068
+ }
25011
25069
  };
25012
25070
  function createJiraClient(jiraUserConfig) {
25013
25071
  const host = jiraUserConfig?.host ?? process.env.JIRA_HOST;
@@ -26108,6 +26166,111 @@ function createJiraTools(store, projectConfig) {
26108
26166
  };
26109
26167
  }
26110
26168
  ),
26169
+ // --- Confluence tools ---
26170
+ tool20(
26171
+ "link_to_confluence",
26172
+ "Link a Confluence page to any Marvin artifact. Validates the page exists and fetches its title.",
26173
+ {
26174
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'A-003', 'T-002')"),
26175
+ confluenceUrl: external_exports.string().describe("Confluence page URL")
26176
+ },
26177
+ async (args) => {
26178
+ const jira = createJiraClient(jiraUserConfig);
26179
+ if (!jira) return jiraNotConfiguredError();
26180
+ const artifact = store.get(args.artifactId);
26181
+ if (!artifact) {
26182
+ return {
26183
+ content: [
26184
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
26185
+ ],
26186
+ isError: true
26187
+ };
26188
+ }
26189
+ const pageId = JiraClient.extractPageId(args.confluenceUrl);
26190
+ if (!pageId) {
26191
+ return {
26192
+ content: [
26193
+ { type: "text", text: `Could not extract page ID from URL: ${args.confluenceUrl}` }
26194
+ ],
26195
+ isError: true
26196
+ };
26197
+ }
26198
+ const page = await jira.client.getConfluencePage(pageId);
26199
+ const existingTags = artifact.frontmatter.tags ?? [];
26200
+ store.update(args.artifactId, {
26201
+ confluenceUrl: args.confluenceUrl,
26202
+ confluencePageId: pageId,
26203
+ confluenceTitle: page.title,
26204
+ tags: [...existingTags.filter((t) => !t.startsWith("confluence:")), `confluence:${page.title}`]
26205
+ });
26206
+ return {
26207
+ content: [
26208
+ {
26209
+ type: "text",
26210
+ text: `Linked ${args.artifactId} to Confluence page "${page.title}" (ID: ${pageId}).`
26211
+ }
26212
+ ]
26213
+ };
26214
+ }
26215
+ ),
26216
+ tool20(
26217
+ "read_confluence_page",
26218
+ "Read the content of a Confluence page by URL or page ID. Returns the page title, metadata, and body as plain text.",
26219
+ {
26220
+ pageUrl: external_exports.string().optional().describe("Confluence page URL"),
26221
+ pageId: external_exports.string().optional().describe("Confluence page ID (alternative to URL)")
26222
+ },
26223
+ async (args) => {
26224
+ const jira = createJiraClient(jiraUserConfig);
26225
+ if (!jira) return jiraNotConfiguredError();
26226
+ const resolvedId = args.pageId ?? (args.pageUrl ? JiraClient.extractPageId(args.pageUrl) : null);
26227
+ if (!resolvedId) {
26228
+ return {
26229
+ content: [
26230
+ {
26231
+ type: "text",
26232
+ text: "Provide either pageUrl or pageId. Could not extract page ID from the given URL."
26233
+ }
26234
+ ],
26235
+ isError: true
26236
+ };
26237
+ }
26238
+ const page = await jira.client.getConfluencePage(resolvedId);
26239
+ let bodyText = "";
26240
+ if (page.body?.atlas_doc_format?.value) {
26241
+ try {
26242
+ const adf = JSON.parse(page.body.atlas_doc_format.value);
26243
+ bodyText = extractCommentText(adf);
26244
+ } catch {
26245
+ bodyText = page.body.atlas_doc_format.value;
26246
+ }
26247
+ }
26248
+ const parts = [
26249
+ `# ${page.title}`,
26250
+ "",
26251
+ `Page ID: ${page.id}`,
26252
+ `Status: ${page.status}`,
26253
+ `Version: ${page.version.number} (${page.version.createdAt.slice(0, 10)})`,
26254
+ "",
26255
+ "---",
26256
+ "",
26257
+ bodyText || "(empty page)"
26258
+ ];
26259
+ const allDocs = store.registeredTypes.flatMap((t) => store.list({ type: t }));
26260
+ const linkedArtifacts = allDocs.filter(
26261
+ (d) => d.frontmatter.confluencePageId === resolvedId || d.frontmatter.confluenceUrl === args.pageUrl
26262
+ );
26263
+ if (linkedArtifacts.length > 0) {
26264
+ parts.push("");
26265
+ parts.push("---");
26266
+ parts.push(`Linked Marvin artifacts: ${linkedArtifacts.map((d) => d.frontmatter.id).join(", ")}`);
26267
+ }
26268
+ return {
26269
+ content: [{ type: "text", text: parts.join("\n") }]
26270
+ };
26271
+ },
26272
+ { annotations: { readOnlyHint: true } }
26273
+ ),
26111
26274
  // --- Jira status fetch (read-only) ---
26112
26275
  tool20(
26113
26276
  "fetch_jira_status",
@@ -26418,6 +26581,8 @@ function formatIssueEntry(issue2) {
26418
26581
  var COMMON_TOOLS = `**Available tools:**
26419
26582
  - \`push_artifact_to_jira\` \u2014 create a Jira issue from any Marvin artifact and link it directly via \`jiraKey\` on the artifact.
26420
26583
  - \`link_to_jira\` \u2014 link an existing Jira issue to any Marvin artifact (sets \`jiraKey\` directly on the artifact).
26584
+ - \`link_to_confluence\` \u2014 link a Confluence page to any Marvin artifact. Validates the page exists and fetches its title.
26585
+ - \`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.
26421
26586
  - \`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.
26422
26587
  - \`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).
26423
26588
  - \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
@@ -31948,7 +32113,7 @@ function createProgram() {
31948
32113
  const program = new Command();
31949
32114
  program.name("marvin").description(
31950
32115
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
31951
- ).version("0.5.11");
32116
+ ).version("0.5.13");
31952
32117
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
31953
32118
  await initCommand();
31954
32119
  });