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/marvin.js CHANGED
@@ -14635,9 +14635,14 @@ function collectSprintSummaryData(store, sprintId) {
14635
14635
  };
14636
14636
  });
14637
14637
  const sprintTag = `sprint:${fm.id}`;
14638
- const workItemDocs = allDocs.filter(
14638
+ const sprintTaggedDocs = allDocs.filter(
14639
14639
  (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)
14640
14640
  );
14641
+ const sprintTaggedIds = new Set(sprintTaggedDocs.map((d) => d.frontmatter.id));
14642
+ const orphanContributions = allDocs.filter(
14643
+ (d) => d.frontmatter.type === "contribution" && !sprintTaggedIds.has(d.frontmatter.id) && d.frontmatter.aboutArtifact && sprintTaggedIds.has(d.frontmatter.aboutArtifact)
14644
+ );
14645
+ const workItemDocs = [...sprintTaggedDocs, ...orphanContributions];
14641
14646
  const primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
14642
14647
  const byStatus = {};
14643
14648
  const byType = {};
@@ -14656,7 +14661,7 @@ function collectSprintSummaryData(store, sprintId) {
14656
14661
  }
14657
14662
  const allItemsById = /* @__PURE__ */ new Map();
14658
14663
  const childrenByParent = /* @__PURE__ */ new Map();
14659
- const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
14664
+ const workItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
14660
14665
  for (const doc of workItemDocs) {
14661
14666
  const about = doc.frontmatter.aboutArtifact;
14662
14667
  const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
@@ -14670,10 +14675,12 @@ function collectSprintSummaryData(store, sprintId) {
14670
14675
  workFocus: focusTag ? focusTag.slice(6) : void 0,
14671
14676
  aboutArtifact: about,
14672
14677
  jiraKey: doc.frontmatter.jiraKey,
14673
- jiraUrl: doc.frontmatter.jiraUrl
14678
+ jiraUrl: doc.frontmatter.jiraUrl,
14679
+ confluenceUrl: doc.frontmatter.confluenceUrl,
14680
+ confluenceTitle: doc.frontmatter.confluenceTitle
14674
14681
  };
14675
14682
  allItemsById.set(item.id, item);
14676
- if (about && sprintItemIds.has(about)) {
14683
+ if (about && workItemIds.has(about)) {
14677
14684
  if (!childrenByParent.has(about)) childrenByParent.set(about, []);
14678
14685
  childrenByParent.get(about).push(item);
14679
14686
  }
@@ -19356,11 +19363,30 @@ function formatDate(iso) {
19356
19363
  function typeLabel(type) {
19357
19364
  return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
19358
19365
  }
19366
+ 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>`;
19367
+ 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>`;
19359
19368
  function jiraIcon(jiraKey, jiraUrl) {
19360
19369
  if (!jiraKey) return "";
19361
19370
  const href = jiraUrl ?? "#";
19362
19371
  const title = escapeHtml(jiraKey);
19363
- 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>`;
19372
+ return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener" title="Jira: ${title}" class="integration-link jira-link">${JIRA_SVG}</a>`;
19373
+ }
19374
+ function confluenceIcon(confluenceUrl, confluenceTitle) {
19375
+ if (!confluenceUrl) return "";
19376
+ const title = confluenceTitle ? escapeHtml(confluenceTitle) : "Confluence";
19377
+ return `<a href="${escapeHtml(confluenceUrl)}" target="_blank" rel="noopener" title="${title}" class="integration-link confluence-link">${CONFLUENCE_SVG}</a>`;
19378
+ }
19379
+ function integrationIcons(frontmatter) {
19380
+ const jira = jiraIcon(
19381
+ frontmatter.jiraKey,
19382
+ frontmatter.jiraUrl
19383
+ );
19384
+ const confluence = confluenceIcon(
19385
+ frontmatter.confluenceUrl,
19386
+ frontmatter.confluenceTitle
19387
+ );
19388
+ if (!jira && !confluence) return "";
19389
+ return `<span class="integration-icons">${jira}${confluence}</span>`;
19364
19390
  }
19365
19391
  function renderMarkdown(md) {
19366
19392
  const lines = md.split("\n");
@@ -21227,17 +21253,22 @@ tr:hover td {
21227
21253
  .owner-badge-dm { background: rgba(52, 211, 153, 0.18); color: #34d399; }
21228
21254
  .owner-badge-other { background: rgba(139, 143, 164, 0.12); color: var(--text-dim); }
21229
21255
 
21230
- /* Jira link icon */
21231
- .jira-link {
21256
+ /* Integration icons (Jira, Confluence) */
21257
+ .integration-icons {
21232
21258
  display: inline-flex;
21233
21259
  align-items: center;
21260
+ gap: 0.25rem;
21234
21261
  vertical-align: middle;
21235
- margin-left: 0.35rem;
21262
+ margin-left: 0.5rem;
21263
+ }
21264
+ .integration-link {
21265
+ display: inline-flex;
21266
+ align-items: center;
21236
21267
  opacity: 0.7;
21237
21268
  transition: opacity 0.15s;
21238
21269
  }
21239
- .jira-link:hover { opacity: 1; }
21240
- .jira-icon { vertical-align: middle; }
21270
+ .integration-link:hover { opacity: 1; }
21271
+ .integration-icon { vertical-align: middle; }
21241
21272
 
21242
21273
  /* Group header rows (PO dashboard decisions/deps) */
21243
21274
  .group-header-row td {
@@ -21263,7 +21294,7 @@ function documentsPage(data) {
21263
21294
  (doc) => `
21264
21295
  <tr>
21265
21296
  <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
21266
- <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a>${jiraIcon(doc.frontmatter.jiraKey, doc.frontmatter.jiraUrl)}</td>
21297
+ <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a>${integrationIcons(doc.frontmatter)}</td>
21267
21298
  <td>${statusBadge(doc.frontmatter.status)}</td>
21268
21299
  <td>${escapeHtml(doc.frontmatter.owner ?? "\u2014")}</td>
21269
21300
  <td>${doc.frontmatter.priority ? `<span class="priority-${doc.frontmatter.priority.toLowerCase()}">${escapeHtml(doc.frontmatter.priority)}</span>` : "\u2014"}</td>
@@ -21352,7 +21383,7 @@ function documentDetailPage(doc) {
21352
21383
  </div>
21353
21384
 
21354
21385
  <div class="page-header">
21355
- <h2>${escapeHtml(fm.title)}${jiraIcon(fm.jiraKey, fm.jiraUrl)}</h2>
21386
+ <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
21356
21387
  <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
21357
21388
  </div>
21358
21389
 
@@ -22645,7 +22676,7 @@ function renderItemRows(items, borderColor, showOwner, depth = 0) {
22645
22676
  const row = `
22646
22677
  <tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
22647
22678
  <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
22648
- <td>${escapeHtml(w.title)}${jiraIcon(w.jiraKey, w.jiraUrl)}</td>
22679
+ <td>${escapeHtml(w.title)}${jiraIcon(w.jiraKey, w.jiraUrl)}${confluenceIcon(w.confluenceUrl, w.confluenceTitle)}</td>
22649
22680
  ${ownerCell}
22650
22681
  <td>${statusBadge(w.status)}</td>
22651
22682
  <td>${progressCell}</td>
@@ -24270,7 +24301,7 @@ function boardPage(data, basePath = "/board") {
24270
24301
  <div class="board-card">
24271
24302
  <a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">
24272
24303
  <div class="bc-id">${escapeHtml(doc.frontmatter.id)}</div>
24273
- <div class="bc-title">${escapeHtml(doc.frontmatter.title)}${jiraIcon(doc.frontmatter.jiraKey, doc.frontmatter.jiraUrl)}</div>
24304
+ <div class="bc-title">${escapeHtml(doc.frontmatter.title)}${integrationIcons(doc.frontmatter)}</div>
24274
24305
  ${doc.frontmatter.owner ? `<div class="bc-owner">${escapeHtml(doc.frontmatter.owner)}</div>` : ""}
24275
24306
  </a>
24276
24307
  </div>`
@@ -25162,11 +25193,14 @@ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
25162
25193
  var JiraClient = class {
25163
25194
  baseUrl;
25164
25195
  baseUrlV3;
25196
+ confluenceBaseUrl;
25165
25197
  authHeader;
25198
+ host;
25166
25199
  constructor(config2) {
25167
- const host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
25168
- this.baseUrl = `https://${host}/rest/api/2`;
25169
- this.baseUrlV3 = `https://${host}/rest/api/3`;
25200
+ this.host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
25201
+ this.baseUrl = `https://${this.host}/rest/api/2`;
25202
+ this.baseUrlV3 = `https://${this.host}/rest/api/3`;
25203
+ this.confluenceBaseUrl = `https://${this.host}/wiki/api/v2`;
25170
25204
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
25171
25205
  }
25172
25206
  async request(path21, method = "GET", body) {
@@ -25254,6 +25288,30 @@ var JiraClient = class {
25254
25288
  { body }
25255
25289
  );
25256
25290
  }
25291
+ // --- Confluence methods ---
25292
+ async getConfluencePage(pageId) {
25293
+ return this.doRequest(
25294
+ `${this.confluenceBaseUrl}/pages/${encodeURIComponent(pageId)}?body-format=atlas_doc_format`,
25295
+ "GET"
25296
+ );
25297
+ }
25298
+ /**
25299
+ * Extract a Confluence page ID from various URL formats.
25300
+ * Returns null if the URL doesn't match any known pattern.
25301
+ */
25302
+ static extractPageId(url2) {
25303
+ const pagesMatch = url2.match(/\/pages\/(\d+)/);
25304
+ if (pagesMatch) return pagesMatch[1];
25305
+ const paramMatch = url2.match(/[?&]pageId=(\d+)/);
25306
+ if (paramMatch) return paramMatch[1];
25307
+ return null;
25308
+ }
25309
+ /**
25310
+ * Build a web URL for a Confluence page.
25311
+ */
25312
+ getConfluencePageUrl(pageId) {
25313
+ return `https://${this.host}/wiki/pages/viewpage.action?pageId=${pageId}`;
25314
+ }
25257
25315
  };
25258
25316
  function createJiraClient(jiraUserConfig) {
25259
25317
  const host = jiraUserConfig?.host ?? process.env.JIRA_HOST;
@@ -26354,6 +26412,111 @@ function createJiraTools(store, projectConfig) {
26354
26412
  };
26355
26413
  }
26356
26414
  ),
26415
+ // --- Confluence tools ---
26416
+ tool20(
26417
+ "link_to_confluence",
26418
+ "Link a Confluence page to any Marvin artifact. Validates the page exists and fetches its title.",
26419
+ {
26420
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'A-003', 'T-002')"),
26421
+ confluenceUrl: external_exports.string().describe("Confluence page URL")
26422
+ },
26423
+ async (args) => {
26424
+ const jira = createJiraClient(jiraUserConfig);
26425
+ if (!jira) return jiraNotConfiguredError();
26426
+ const artifact = store.get(args.artifactId);
26427
+ if (!artifact) {
26428
+ return {
26429
+ content: [
26430
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
26431
+ ],
26432
+ isError: true
26433
+ };
26434
+ }
26435
+ const pageId = JiraClient.extractPageId(args.confluenceUrl);
26436
+ if (!pageId) {
26437
+ return {
26438
+ content: [
26439
+ { type: "text", text: `Could not extract page ID from URL: ${args.confluenceUrl}` }
26440
+ ],
26441
+ isError: true
26442
+ };
26443
+ }
26444
+ const page = await jira.client.getConfluencePage(pageId);
26445
+ const existingTags = artifact.frontmatter.tags ?? [];
26446
+ store.update(args.artifactId, {
26447
+ confluenceUrl: args.confluenceUrl,
26448
+ confluencePageId: pageId,
26449
+ confluenceTitle: page.title,
26450
+ tags: [...existingTags.filter((t) => !t.startsWith("confluence:")), `confluence:${page.title}`]
26451
+ });
26452
+ return {
26453
+ content: [
26454
+ {
26455
+ type: "text",
26456
+ text: `Linked ${args.artifactId} to Confluence page "${page.title}" (ID: ${pageId}).`
26457
+ }
26458
+ ]
26459
+ };
26460
+ }
26461
+ ),
26462
+ tool20(
26463
+ "read_confluence_page",
26464
+ "Read the content of a Confluence page by URL or page ID. Returns the page title, metadata, and body as plain text.",
26465
+ {
26466
+ pageUrl: external_exports.string().optional().describe("Confluence page URL"),
26467
+ pageId: external_exports.string().optional().describe("Confluence page ID (alternative to URL)")
26468
+ },
26469
+ async (args) => {
26470
+ const jira = createJiraClient(jiraUserConfig);
26471
+ if (!jira) return jiraNotConfiguredError();
26472
+ const resolvedId = args.pageId ?? (args.pageUrl ? JiraClient.extractPageId(args.pageUrl) : null);
26473
+ if (!resolvedId) {
26474
+ return {
26475
+ content: [
26476
+ {
26477
+ type: "text",
26478
+ text: "Provide either pageUrl or pageId. Could not extract page ID from the given URL."
26479
+ }
26480
+ ],
26481
+ isError: true
26482
+ };
26483
+ }
26484
+ const page = await jira.client.getConfluencePage(resolvedId);
26485
+ let bodyText = "";
26486
+ if (page.body?.atlas_doc_format?.value) {
26487
+ try {
26488
+ const adf = JSON.parse(page.body.atlas_doc_format.value);
26489
+ bodyText = extractCommentText(adf);
26490
+ } catch {
26491
+ bodyText = page.body.atlas_doc_format.value;
26492
+ }
26493
+ }
26494
+ const parts = [
26495
+ `# ${page.title}`,
26496
+ "",
26497
+ `Page ID: ${page.id}`,
26498
+ `Status: ${page.status}`,
26499
+ `Version: ${page.version.number} (${page.version.createdAt.slice(0, 10)})`,
26500
+ "",
26501
+ "---",
26502
+ "",
26503
+ bodyText || "(empty page)"
26504
+ ];
26505
+ const allDocs = store.registeredTypes.flatMap((t) => store.list({ type: t }));
26506
+ const linkedArtifacts = allDocs.filter(
26507
+ (d) => d.frontmatter.confluencePageId === resolvedId || d.frontmatter.confluenceUrl === args.pageUrl
26508
+ );
26509
+ if (linkedArtifacts.length > 0) {
26510
+ parts.push("");
26511
+ parts.push("---");
26512
+ parts.push(`Linked Marvin artifacts: ${linkedArtifacts.map((d) => d.frontmatter.id).join(", ")}`);
26513
+ }
26514
+ return {
26515
+ content: [{ type: "text", text: parts.join("\n") }]
26516
+ };
26517
+ },
26518
+ { annotations: { readOnlyHint: true } }
26519
+ ),
26357
26520
  // --- Jira status fetch (read-only) ---
26358
26521
  tool20(
26359
26522
  "fetch_jira_status",
@@ -26664,6 +26827,8 @@ function formatIssueEntry(issue2) {
26664
26827
  var COMMON_TOOLS = `**Available tools:**
26665
26828
  - \`push_artifact_to_jira\` \u2014 create a Jira issue from any Marvin artifact and link it directly via \`jiraKey\` on the artifact.
26666
26829
  - \`link_to_jira\` \u2014 link an existing Jira issue to any Marvin artifact (sets \`jiraKey\` directly on the artifact).
26830
+ - \`link_to_confluence\` \u2014 link a Confluence page to any Marvin artifact. Validates the page exists and fetches its title.
26831
+ - \`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.
26667
26832
  - \`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.
26668
26833
  - \`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).
26669
26834
  - \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
@@ -31940,7 +32105,7 @@ function createProgram() {
31940
32105
  const program2 = new Command();
31941
32106
  program2.name("marvin").description(
31942
32107
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
31943
- ).version("0.5.11");
32108
+ ).version("0.5.13");
31944
32109
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
31945
32110
  await initCommand();
31946
32111
  });