mrvn-cli 0.5.10 → 0.5.12

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
@@ -15773,7 +15773,11 @@ function collectSprintSummaryData(store, sprintId) {
15773
15773
  progress: getEffectiveProgress(doc.frontmatter),
15774
15774
  owner: doc.frontmatter.owner,
15775
15775
  workFocus: focusTag ? focusTag.slice(6) : void 0,
15776
- aboutArtifact: about
15776
+ aboutArtifact: about,
15777
+ jiraKey: doc.frontmatter.jiraKey,
15778
+ jiraUrl: doc.frontmatter.jiraUrl,
15779
+ confluenceUrl: doc.frontmatter.confluenceUrl,
15780
+ confluenceTitle: doc.frontmatter.confluenceTitle
15777
15781
  };
15778
15782
  allItemsById.set(item.id, item);
15779
15783
  if (about && sprintItemIds.has(about)) {
@@ -16284,6 +16288,31 @@ function formatDate(iso) {
16284
16288
  function typeLabel(type) {
16285
16289
  return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
16286
16290
  }
16291
+ 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>`;
16292
+ 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>`;
16293
+ function jiraIcon(jiraKey, jiraUrl) {
16294
+ if (!jiraKey) return "";
16295
+ const href = jiraUrl ?? "#";
16296
+ const title = escapeHtml(jiraKey);
16297
+ return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener" title="Jira: ${title}" class="integration-link jira-link">${JIRA_SVG}</a>`;
16298
+ }
16299
+ function confluenceIcon(confluenceUrl, confluenceTitle) {
16300
+ if (!confluenceUrl) return "";
16301
+ const title = confluenceTitle ? escapeHtml(confluenceTitle) : "Confluence";
16302
+ return `<a href="${escapeHtml(confluenceUrl)}" target="_blank" rel="noopener" title="${title}" class="integration-link confluence-link">${CONFLUENCE_SVG}</a>`;
16303
+ }
16304
+ function integrationIcons(frontmatter) {
16305
+ const jira = jiraIcon(
16306
+ frontmatter.jiraKey,
16307
+ frontmatter.jiraUrl
16308
+ );
16309
+ const confluence = confluenceIcon(
16310
+ frontmatter.confluenceUrl,
16311
+ frontmatter.confluenceTitle
16312
+ );
16313
+ if (!jira && !confluence) return "";
16314
+ return `<span class="integration-icons">${jira}${confluence}</span>`;
16315
+ }
16287
16316
  function renderMarkdown(md) {
16288
16317
  const lines = md.split("\n");
16289
16318
  const out = [];
@@ -18149,6 +18178,23 @@ tr:hover td {
18149
18178
  .owner-badge-dm { background: rgba(52, 211, 153, 0.18); color: #34d399; }
18150
18179
  .owner-badge-other { background: rgba(139, 143, 164, 0.12); color: var(--text-dim); }
18151
18180
 
18181
+ /* Integration icons (Jira, Confluence) */
18182
+ .integration-icons {
18183
+ display: inline-flex;
18184
+ align-items: center;
18185
+ gap: 0.2rem;
18186
+ vertical-align: middle;
18187
+ margin-left: 0.35rem;
18188
+ }
18189
+ .integration-link {
18190
+ display: inline-flex;
18191
+ align-items: center;
18192
+ opacity: 0.7;
18193
+ transition: opacity 0.15s;
18194
+ }
18195
+ .integration-link:hover { opacity: 1; }
18196
+ .integration-icon { vertical-align: middle; }
18197
+
18152
18198
  /* Group header rows (PO dashboard decisions/deps) */
18153
18199
  .group-header-row td {
18154
18200
  background: var(--bg-hover);
@@ -18173,7 +18219,7 @@ function documentsPage(data) {
18173
18219
  (doc) => `
18174
18220
  <tr>
18175
18221
  <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
18176
- <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a></td>
18222
+ <td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a>${integrationIcons(doc.frontmatter)}</td>
18177
18223
  <td>${statusBadge(doc.frontmatter.status)}</td>
18178
18224
  <td>${escapeHtml(doc.frontmatter.owner ?? "\u2014")}</td>
18179
18225
  <td>${doc.frontmatter.priority ? `<span class="priority-${doc.frontmatter.priority.toLowerCase()}">${escapeHtml(doc.frontmatter.priority)}</span>` : "\u2014"}</td>
@@ -18262,7 +18308,7 @@ function documentDetailPage(doc) {
18262
18308
  </div>
18263
18309
 
18264
18310
  <div class="page-header">
18265
- <h2>${escapeHtml(fm.title)}</h2>
18311
+ <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
18266
18312
  <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
18267
18313
  </div>
18268
18314
 
@@ -19684,7 +19730,7 @@ function renderItemRows(items, borderColor, showOwner, depth = 0) {
19684
19730
  const row = `
19685
19731
  <tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
19686
19732
  <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
19687
- <td>${escapeHtml(w.title)}</td>
19733
+ <td>${escapeHtml(w.title)}${jiraIcon(w.jiraKey, w.jiraUrl)}${confluenceIcon(w.confluenceUrl, w.confluenceTitle)}</td>
19688
19734
  ${ownerCell}
19689
19735
  <td>${statusBadge(w.status)}</td>
19690
19736
  <td>${progressCell}</td>
@@ -21309,7 +21355,7 @@ function boardPage(data, basePath = "/board") {
21309
21355
  <div class="board-card">
21310
21356
  <a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">
21311
21357
  <div class="bc-id">${escapeHtml(doc.frontmatter.id)}</div>
21312
- <div class="bc-title">${escapeHtml(doc.frontmatter.title)}</div>
21358
+ <div class="bc-title">${escapeHtml(doc.frontmatter.title)}${integrationIcons(doc.frontmatter)}</div>
21313
21359
  ${doc.frontmatter.owner ? `<div class="bc-owner">${escapeHtml(doc.frontmatter.owner)}</div>` : ""}
21314
21360
  </a>
21315
21361
  </div>`
@@ -24896,11 +24942,14 @@ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
24896
24942
  var JiraClient = class {
24897
24943
  baseUrl;
24898
24944
  baseUrlV3;
24945
+ confluenceBaseUrl;
24899
24946
  authHeader;
24947
+ host;
24900
24948
  constructor(config2) {
24901
- const host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
24902
- this.baseUrl = `https://${host}/rest/api/2`;
24903
- this.baseUrlV3 = `https://${host}/rest/api/3`;
24949
+ this.host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
24950
+ this.baseUrl = `https://${this.host}/rest/api/2`;
24951
+ this.baseUrlV3 = `https://${this.host}/rest/api/3`;
24952
+ this.confluenceBaseUrl = `https://${this.host}/wiki/api/v2`;
24904
24953
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
24905
24954
  }
24906
24955
  async request(path21, method = "GET", body) {
@@ -24988,6 +25037,30 @@ var JiraClient = class {
24988
25037
  { body }
24989
25038
  );
24990
25039
  }
25040
+ // --- Confluence methods ---
25041
+ async getConfluencePage(pageId) {
25042
+ return this.doRequest(
25043
+ `${this.confluenceBaseUrl}/pages/${encodeURIComponent(pageId)}?body-format=atlas_doc_format`,
25044
+ "GET"
25045
+ );
25046
+ }
25047
+ /**
25048
+ * Extract a Confluence page ID from various URL formats.
25049
+ * Returns null if the URL doesn't match any known pattern.
25050
+ */
25051
+ static extractPageId(url2) {
25052
+ const pagesMatch = url2.match(/\/pages\/(\d+)/);
25053
+ if (pagesMatch) return pagesMatch[1];
25054
+ const paramMatch = url2.match(/[?&]pageId=(\d+)/);
25055
+ if (paramMatch) return paramMatch[1];
25056
+ return null;
25057
+ }
25058
+ /**
25059
+ * Build a web URL for a Confluence page.
25060
+ */
25061
+ getConfluencePageUrl(pageId) {
25062
+ return `https://${this.host}/wiki/pages/viewpage.action?pageId=${pageId}`;
25063
+ }
24991
25064
  };
24992
25065
  function createJiraClient(jiraUserConfig) {
24993
25066
  const host = jiraUserConfig?.host ?? process.env.JIRA_HOST;
@@ -25745,6 +25818,58 @@ function createJiraTools(store, projectConfig) {
25745
25818
  },
25746
25819
  { annotations: { readOnlyHint: true } }
25747
25820
  ),
25821
+ // --- Jira search (read-only) ---
25822
+ tool20(
25823
+ "search_jira",
25824
+ "Search Jira issues via JQL query. Read-only \u2014 returns results without creating any local documents. Use this to preview before importing or to find issues for linking.",
25825
+ {
25826
+ jql: external_exports.string().describe(`JQL query (e.g. 'project = PROJ AND status = "In Progress"')`),
25827
+ maxResults: external_exports.number().optional().describe("Max issues to return (default 20)")
25828
+ },
25829
+ async (args) => {
25830
+ const jira = createJiraClient(jiraUserConfig);
25831
+ if (!jira) return jiraNotConfiguredError();
25832
+ const result = await jira.client.searchIssuesV3(
25833
+ args.jql,
25834
+ ["summary", "status", "issuetype", "priority", "assignee", "labels"],
25835
+ args.maxResults ?? 20
25836
+ );
25837
+ const allDocs = store.registeredTypes.flatMap((t) => store.list({ type: t }));
25838
+ const jiraKeyToArtifact = /* @__PURE__ */ new Map();
25839
+ for (const doc of allDocs) {
25840
+ const jk = doc.frontmatter.jiraKey;
25841
+ if (jk) jiraKeyToArtifact.set(jk, doc.frontmatter.id);
25842
+ }
25843
+ const issues = result.issues.map((issue2) => {
25844
+ const marvinId = jiraKeyToArtifact.get(issue2.key);
25845
+ return {
25846
+ key: issue2.key,
25847
+ summary: issue2.fields.summary,
25848
+ status: issue2.fields.status.name,
25849
+ issueType: issue2.fields.issuetype.name,
25850
+ priority: issue2.fields.priority?.name ?? "None",
25851
+ assignee: issue2.fields.assignee?.displayName ?? "unassigned",
25852
+ labels: issue2.fields.labels ?? [],
25853
+ marvinArtifact: marvinId ?? null
25854
+ };
25855
+ });
25856
+ const parts = [
25857
+ `Found ${result.total ?? issues.length} issues (showing ${issues.length}).`,
25858
+ ""
25859
+ ];
25860
+ for (const issue2 of issues) {
25861
+ const linked = issue2.marvinArtifact ? ` \u2192 ${issue2.marvinArtifact}` : " (not linked)";
25862
+ parts.push(`${issue2.key} \u2014 ${issue2.summary} [${issue2.status}]${linked}`);
25863
+ parts.push(` Type: ${issue2.issueType} | Priority: ${issue2.priority} | Assignee: ${issue2.assignee}`);
25864
+ }
25865
+ parts.push("");
25866
+ parts.push("This is read-only. Use link_to_jira to link issues to Marvin artifacts, or pull_jira_issue to import as JI-xxx documents.");
25867
+ return {
25868
+ content: [{ type: "text", text: parts.join("\n") }]
25869
+ };
25870
+ },
25871
+ { annotations: { readOnlyHint: true } }
25872
+ ),
25748
25873
  // --- Jira → Local tools ---
25749
25874
  tool20(
25750
25875
  "pull_jira_issue",
@@ -26036,6 +26161,111 @@ function createJiraTools(store, projectConfig) {
26036
26161
  };
26037
26162
  }
26038
26163
  ),
26164
+ // --- Confluence tools ---
26165
+ tool20(
26166
+ "link_to_confluence",
26167
+ "Link a Confluence page to any Marvin artifact. Validates the page exists and fetches its title.",
26168
+ {
26169
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'A-003', 'T-002')"),
26170
+ confluenceUrl: external_exports.string().describe("Confluence page URL")
26171
+ },
26172
+ async (args) => {
26173
+ const jira = createJiraClient(jiraUserConfig);
26174
+ if (!jira) return jiraNotConfiguredError();
26175
+ const artifact = store.get(args.artifactId);
26176
+ if (!artifact) {
26177
+ return {
26178
+ content: [
26179
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
26180
+ ],
26181
+ isError: true
26182
+ };
26183
+ }
26184
+ const pageId = JiraClient.extractPageId(args.confluenceUrl);
26185
+ if (!pageId) {
26186
+ return {
26187
+ content: [
26188
+ { type: "text", text: `Could not extract page ID from URL: ${args.confluenceUrl}` }
26189
+ ],
26190
+ isError: true
26191
+ };
26192
+ }
26193
+ const page = await jira.client.getConfluencePage(pageId);
26194
+ const existingTags = artifact.frontmatter.tags ?? [];
26195
+ store.update(args.artifactId, {
26196
+ confluenceUrl: args.confluenceUrl,
26197
+ confluencePageId: pageId,
26198
+ confluenceTitle: page.title,
26199
+ tags: [...existingTags.filter((t) => !t.startsWith("confluence:")), `confluence:${page.title}`]
26200
+ });
26201
+ return {
26202
+ content: [
26203
+ {
26204
+ type: "text",
26205
+ text: `Linked ${args.artifactId} to Confluence page "${page.title}" (ID: ${pageId}).`
26206
+ }
26207
+ ]
26208
+ };
26209
+ }
26210
+ ),
26211
+ tool20(
26212
+ "read_confluence_page",
26213
+ "Read the content of a Confluence page by URL or page ID. Returns the page title, metadata, and body as plain text.",
26214
+ {
26215
+ pageUrl: external_exports.string().optional().describe("Confluence page URL"),
26216
+ pageId: external_exports.string().optional().describe("Confluence page ID (alternative to URL)")
26217
+ },
26218
+ async (args) => {
26219
+ const jira = createJiraClient(jiraUserConfig);
26220
+ if (!jira) return jiraNotConfiguredError();
26221
+ const resolvedId = args.pageId ?? (args.pageUrl ? JiraClient.extractPageId(args.pageUrl) : null);
26222
+ if (!resolvedId) {
26223
+ return {
26224
+ content: [
26225
+ {
26226
+ type: "text",
26227
+ text: "Provide either pageUrl or pageId. Could not extract page ID from the given URL."
26228
+ }
26229
+ ],
26230
+ isError: true
26231
+ };
26232
+ }
26233
+ const page = await jira.client.getConfluencePage(resolvedId);
26234
+ let bodyText = "";
26235
+ if (page.body?.atlas_doc_format?.value) {
26236
+ try {
26237
+ const adf = JSON.parse(page.body.atlas_doc_format.value);
26238
+ bodyText = extractCommentText(adf);
26239
+ } catch {
26240
+ bodyText = page.body.atlas_doc_format.value;
26241
+ }
26242
+ }
26243
+ const parts = [
26244
+ `# ${page.title}`,
26245
+ "",
26246
+ `Page ID: ${page.id}`,
26247
+ `Status: ${page.status}`,
26248
+ `Version: ${page.version.number} (${page.version.createdAt.slice(0, 10)})`,
26249
+ "",
26250
+ "---",
26251
+ "",
26252
+ bodyText || "(empty page)"
26253
+ ];
26254
+ const allDocs = store.registeredTypes.flatMap((t) => store.list({ type: t }));
26255
+ const linkedArtifacts = allDocs.filter(
26256
+ (d) => d.frontmatter.confluencePageId === resolvedId || d.frontmatter.confluenceUrl === args.pageUrl
26257
+ );
26258
+ if (linkedArtifacts.length > 0) {
26259
+ parts.push("");
26260
+ parts.push("---");
26261
+ parts.push(`Linked Marvin artifacts: ${linkedArtifacts.map((d) => d.frontmatter.id).join(", ")}`);
26262
+ }
26263
+ return {
26264
+ content: [{ type: "text", text: parts.join("\n") }]
26265
+ };
26266
+ },
26267
+ { annotations: { readOnlyHint: true } }
26268
+ ),
26039
26269
  // --- Jira status fetch (read-only) ---
26040
26270
  tool20(
26041
26271
  "fetch_jira_status",
@@ -26346,9 +26576,12 @@ function formatIssueEntry(issue2) {
26346
26576
  var COMMON_TOOLS = `**Available tools:**
26347
26577
  - \`push_artifact_to_jira\` \u2014 create a Jira issue from any Marvin artifact and link it directly via \`jiraKey\` on the artifact.
26348
26578
  - \`link_to_jira\` \u2014 link an existing Jira issue to any Marvin artifact (sets \`jiraKey\` directly on the artifact).
26579
+ - \`link_to_confluence\` \u2014 link a Confluence page to any Marvin artifact. Validates the page exists and fetches its title.
26580
+ - \`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.
26349
26581
  - \`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.
26350
26582
  - \`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).
26351
26583
  - \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
26584
+ - \`search_jira\` \u2014 **read-only**: search Jira via JQL and return results with Marvin cross-references. No documents created \u2014 use to preview before importing or find issues for linking.
26352
26585
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import Jira issues as local JI-xxx documents (for Jira-originated items with no existing Marvin artifact).
26353
26586
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally imported JI-xxx documents.
26354
26587
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira.
@@ -31875,7 +32108,7 @@ function createProgram() {
31875
32108
  const program = new Command();
31876
32109
  program.name("marvin").description(
31877
32110
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
31878
- ).version("0.5.10");
32111
+ ).version("0.5.12");
31879
32112
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
31880
32113
  await initCommand();
31881
32114
  });