@crypto512/jicon-mcp 2.1.1 → 2.2.0

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.
Files changed (103) hide show
  1. package/README.md +288 -1
  2. package/TOOL_LIST.md +390 -87
  3. package/dist/config/constants.d.ts +12 -0
  4. package/dist/config/constants.d.ts.map +1 -1
  5. package/dist/config/constants.js +13 -0
  6. package/dist/config/constants.js.map +1 -1
  7. package/dist/config/loader.d.ts +8 -0
  8. package/dist/config/loader.d.ts.map +1 -1
  9. package/dist/config/loader.js +27 -1
  10. package/dist/config/loader.js.map +1 -1
  11. package/dist/config/types.d.ts +93 -0
  12. package/dist/config/types.d.ts.map +1 -1
  13. package/dist/config/types.js +26 -0
  14. package/dist/config/types.js.map +1 -1
  15. package/dist/confluence/tools.d.ts +8 -1
  16. package/dist/confluence/tools.d.ts.map +1 -1
  17. package/dist/confluence/tools.js +61 -51
  18. package/dist/confluence/tools.js.map +1 -1
  19. package/dist/credentials/client-factory.d.ts +64 -0
  20. package/dist/credentials/client-factory.d.ts.map +1 -0
  21. package/dist/credentials/client-factory.js +110 -0
  22. package/dist/credentials/client-factory.js.map +1 -0
  23. package/dist/credentials/context.d.ts +25 -0
  24. package/dist/credentials/context.d.ts.map +1 -0
  25. package/dist/credentials/context.js +35 -0
  26. package/dist/credentials/context.js.map +1 -0
  27. package/dist/credentials/extractor.d.ts +21 -0
  28. package/dist/credentials/extractor.d.ts.map +1 -0
  29. package/dist/credentials/extractor.js +46 -0
  30. package/dist/credentials/extractor.js.map +1 -0
  31. package/dist/credentials/index.d.ts +9 -0
  32. package/dist/credentials/index.d.ts.map +1 -0
  33. package/dist/credentials/index.js +8 -0
  34. package/dist/credentials/index.js.map +1 -0
  35. package/dist/credentials/types.d.ts +21 -0
  36. package/dist/credentials/types.d.ts.map +1 -0
  37. package/dist/credentials/types.js +13 -0
  38. package/dist/credentials/types.js.map +1 -0
  39. package/dist/index.js +98 -75
  40. package/dist/index.js.map +1 -1
  41. package/dist/jira/activity-tools.d.ts +54 -1
  42. package/dist/jira/activity-tools.d.ts.map +1 -1
  43. package/dist/jira/activity-tools.js +737 -79
  44. package/dist/jira/activity-tools.js.map +1 -1
  45. package/dist/jira/client.d.ts +34 -2
  46. package/dist/jira/client.d.ts.map +1 -1
  47. package/dist/jira/client.js +119 -63
  48. package/dist/jira/client.js.map +1 -1
  49. package/dist/jira/tools.d.ts +58 -1
  50. package/dist/jira/tools.d.ts.map +1 -1
  51. package/dist/jira/tools.js +226 -27
  52. package/dist/jira/tools.js.map +1 -1
  53. package/dist/jira/types.d.ts +22 -0
  54. package/dist/jira/types.d.ts.map +1 -1
  55. package/dist/permissions/tool-registry.d.ts +10 -10
  56. package/dist/permissions/tool-registry.d.ts.map +1 -1
  57. package/dist/permissions/tool-registry.js +8 -4
  58. package/dist/permissions/tool-registry.js.map +1 -1
  59. package/dist/tempo/tools.d.ts +33 -1
  60. package/dist/tempo/tools.d.ts.map +1 -1
  61. package/dist/tempo/tools.js +141 -17
  62. package/dist/tempo/tools.js.map +1 -1
  63. package/dist/transport/http.d.ts +18 -0
  64. package/dist/transport/http.d.ts.map +1 -0
  65. package/dist/transport/http.js +66 -0
  66. package/dist/transport/http.js.map +1 -0
  67. package/dist/transport/index.d.ts +17 -0
  68. package/dist/transport/index.d.ts.map +1 -0
  69. package/dist/transport/index.js +32 -0
  70. package/dist/transport/index.js.map +1 -0
  71. package/dist/transport/types.d.ts +15 -0
  72. package/dist/transport/types.d.ts.map +1 -0
  73. package/dist/transport/types.js +12 -0
  74. package/dist/transport/types.js.map +1 -0
  75. package/dist/types.d.ts +10 -0
  76. package/dist/types.d.ts.map +1 -1
  77. package/dist/types.js +0 -1
  78. package/dist/types.js.map +1 -1
  79. package/dist/utils/buffer-pipeline/index.js +2 -2
  80. package/dist/utils/buffer-pipeline/index.js.map +1 -1
  81. package/dist/utils/buffer-pipeline/schema.d.ts +99 -99
  82. package/dist/utils/buffer-pipeline/schema.d.ts.map +1 -1
  83. package/dist/utils/buffer-tools.d.ts +91 -83
  84. package/dist/utils/buffer-tools.d.ts.map +1 -1
  85. package/dist/utils/buffer-tools.js +312 -139
  86. package/dist/utils/buffer-tools.js.map +1 -1
  87. package/dist/utils/jicon-help.d.ts +3 -3
  88. package/dist/utils/jicon-help.d.ts.map +1 -1
  89. package/dist/utils/jicon-help.js +141 -10
  90. package/dist/utils/jicon-help.js.map +1 -1
  91. package/dist/utils/json-structure.d.ts +11 -0
  92. package/dist/utils/json-structure.d.ts.map +1 -1
  93. package/dist/utils/json-structure.js +61 -0
  94. package/dist/utils/json-structure.js.map +1 -1
  95. package/dist/utils/plantuml/tools.d.ts +4 -4
  96. package/dist/utils/plantuml/tools.d.ts.map +1 -1
  97. package/dist/utils/plantuml/tools.js +29 -8
  98. package/dist/utils/plantuml/tools.js.map +1 -1
  99. package/dist/utils/plantuml/types.d.ts +4 -4
  100. package/dist/utils/response-formatter.d.ts.map +1 -1
  101. package/dist/utils/response-formatter.js +8 -4
  102. package/dist/utils/response-formatter.js.map +1 -1
  103. package/package.json +5 -2
@@ -81,7 +81,17 @@ function truncateText(text, maxLength) {
81
81
  return text;
82
82
  return text.substring(0, maxLength - 3) + "...";
83
83
  }
84
- export function createActivityTools(client) {
84
+ /**
85
+ * Create activity tools with a client getter function
86
+ */
87
+ export function createActivityTools(getClient) {
88
+ const requireClient = () => {
89
+ const client = getClient();
90
+ if (!client) {
91
+ throw new Error("Jira is not configured. Provide JIRA_URL and JIRA_API_TOKEN environment variables, or pass credentials via X-Jira-Url and X-Jira-Token headers.");
92
+ }
93
+ return client;
94
+ };
85
95
  return {
86
96
  jira_get_activity_digest: {
87
97
  description: `Get recent activity across issues for LLM analysis. Returns DIRECTLY (not buffered).
@@ -126,7 +136,7 @@ Example: jira_get_activity_digest(projectKey="ACME", since="7d")`,
126
136
  // Add date filter
127
137
  const fullJql = `${scopeJql} AND updated >= "${dateFrom}" ORDER BY updated DESC`;
128
138
  // Search for issues with changelog
129
- const searchResult = await client.searchWithChangelog(fullJql, 100, [
139
+ const searchResult = await requireClient().searchWithChangelog(fullJql, 100, [
130
140
  "summary",
131
141
  "status",
132
142
  "priority",
@@ -134,29 +144,44 @@ Example: jira_get_activity_digest(projectKey="ACME", since="7d")`,
134
144
  ]);
135
145
  const activities = [];
136
146
  const issueKeys = searchResult.issues.map((i) => i.key);
137
- // Collect changelog activities
138
- const trackFields = [];
147
+ // Get localized field names for non-English Jira instances
148
+ // This maps system field ID (e.g., "status") to localized name (e.g., "Statut")
149
+ const localizedFieldNames = await requireClient().getLocalizedFieldNames();
150
+ // Build list of localized field names to track
151
+ const trackFieldsEnglish = [];
139
152
  if (args.includeStatusChanges !== false)
140
- trackFields.push("status");
153
+ trackFieldsEnglish.push("status");
141
154
  if (args.includePriorityChanges !== false)
142
- trackFields.push("priority");
155
+ trackFieldsEnglish.push("priority");
143
156
  if (args.includeAssigneeChanges !== false)
144
- trackFields.push("assignee");
145
- trackFields.push("resolution"); // Always track resolution
157
+ trackFieldsEnglish.push("assignee");
158
+ trackFieldsEnglish.push("resolution"); // Always track resolution
159
+ // Build localized field names for matching + reverse map for type lookup
160
+ const trackFields = [];
161
+ const localizedToEnglish = new Map();
162
+ for (const englishId of trackFieldsEnglish) {
163
+ const localizedName = localizedFieldNames.get(englishId) || englishId;
164
+ trackFields.push(localizedName);
165
+ localizedToEnglish.set(localizedName, englishId);
166
+ }
167
+ // Type mapping uses English field IDs
168
+ const typeMap = {
169
+ status: "status_change",
170
+ priority: "priority_change",
171
+ assignee: "assignee_change",
172
+ resolution: "resolution_change",
173
+ };
146
174
  for (const issue of searchResult.issues) {
147
175
  if (issue.changelog?.histories) {
148
176
  const changes = extractMeaningfulChanges(issue.changelog.histories, dateFrom, trackFields);
149
177
  for (const change of changes) {
150
- const typeMap = {
151
- status: "status_change",
152
- priority: "priority_change",
153
- assignee: "assignee_change",
154
- resolution: "resolution_change",
155
- };
178
+ // Convert localized field name back to English for type lookup
179
+ const fieldLower = change.field.toLowerCase();
180
+ const englishFieldId = localizedToEnglish.get(fieldLower) || fieldLower;
156
181
  activities.push({
157
182
  issueKey: issue.key,
158
183
  issueSummary: issue.fields.summary,
159
- type: typeMap[change.field.toLowerCase()] || "status_change",
184
+ type: typeMap[englishFieldId] || "status_change",
160
185
  timestamp: change.timestamp,
161
186
  author: change.author,
162
187
  details: {
@@ -170,7 +195,7 @@ Example: jira_get_activity_digest(projectKey="ACME", since="7d")`,
170
195
  }
171
196
  // Collect comments if requested
172
197
  if (args.includeComments !== false && issueKeys.length > 0) {
173
- const commentsMap = await client.getCommentsForIssues(issueKeys, dateFrom);
198
+ const commentsMap = await requireClient().getCommentsForIssues(issueKeys, dateFrom);
174
199
  const issueMap = new Map(searchResult.issues.map((i) => [i.key, i]));
175
200
  for (const [issueKey, comments] of commentsMap) {
176
201
  const issue = issueMap.get(issueKey);
@@ -223,13 +248,16 @@ Example: jira_get_activity_digest(projectKey="ACME", since="7d")`,
223
248
  comments: 0,
224
249
  statusToDone: 0,
225
250
  };
251
+ // Get status category map for language-independent status detection
252
+ const statusCategoryMap = await requireClient().getStatusCategoryMap();
226
253
  for (const activity of limitedActivities) {
227
254
  switch (activity.type) {
228
255
  case "status_change":
229
256
  stats.statusChanges++;
230
- if (activity.details.to?.toLowerCase().includes("done") ||
231
- activity.details.to?.toLowerCase().includes("closed") ||
232
- activity.details.to?.toLowerCase().includes("resolved")) {
257
+ // Use language-independent statusCategory.key lookup
258
+ const targetStatus = activity.details.to?.toLowerCase() || "";
259
+ const categoryKey = statusCategoryMap.get(targetStatus);
260
+ if (categoryKey === "done") {
233
261
  stats.statusToDone++;
234
262
  }
235
263
  break;
@@ -354,7 +382,7 @@ Example: jira_get_recent_comments(projectKey="ACME", since="7d")`,
354
382
  // Add date filter for updated issues (comments update the issue)
355
383
  const fullJql = `${scopeJql} AND updated >= "${dateFrom}" ORDER BY updated DESC`;
356
384
  // Search for issues
357
- const searchResult = await client.searchIssuesAll(fullJql, ["summary", "status"], 100);
385
+ const searchResult = await requireClient().searchIssuesAll(fullJql, ["summary", "status"], 100);
358
386
  if (searchResult.issues.length === 0) {
359
387
  return formatSuccessDirect({
360
388
  _directContent: true,
@@ -363,7 +391,7 @@ Example: jira_get_recent_comments(projectKey="ACME", since="7d")`,
363
391
  }
364
392
  // Get comments for all issues
365
393
  const issueKeys = searchResult.issues.map((i) => i.key);
366
- const commentsMap = await client.getCommentsForIssues(issueKeys, dateFrom);
394
+ const commentsMap = await requireClient().getCommentsForIssues(issueKeys, dateFrom);
367
395
  const issueMap = new Map(searchResult.issues.map((i) => [i.key, i]));
368
396
  // Collect and filter comments
369
397
  const allComments = [];
@@ -468,8 +496,18 @@ Example: jira_get_changelog(projectKey="ACME", since="7d", fields=["status", "pr
468
496
  try {
469
497
  const since = args.since || "7d";
470
498
  const { dateFrom, dateTo } = parseSince(since);
471
- const trackFields = args.fields || ["status", "priority", "assignee", "resolution"];
499
+ const userFields = args.fields || ["status", "priority", "assignee", "resolution"];
472
500
  const maxChanges = args.maxChanges ?? 50;
501
+ // Get localized field names for non-English Jira instances
502
+ const localizedFieldNames = await requireClient().getLocalizedFieldNames();
503
+ // Build list of localized field names to track
504
+ // User provides English field IDs, we convert to localized names
505
+ const trackFields = [];
506
+ for (const userField of userFields) {
507
+ const fieldLower = userField.toLowerCase();
508
+ const localizedName = localizedFieldNames.get(fieldLower) || fieldLower;
509
+ trackFields.push(localizedName);
510
+ }
473
511
  // Build scoping JQL
474
512
  const scopeJql = buildScopingJql({
475
513
  projectKey: args.projectKey,
@@ -479,12 +517,12 @@ Example: jira_get_changelog(projectKey="ACME", since="7d", fields=["status", "pr
479
517
  // Add date filter
480
518
  const fullJql = `${scopeJql} AND updated >= "${dateFrom}" ORDER BY updated DESC`;
481
519
  // Search for issues with changelog
482
- const searchResult = await client.searchWithChangelog(fullJql, 100, ["summary"]);
520
+ const searchResult = await requireClient().searchWithChangelog(fullJql, 100, ["summary"]);
483
521
  // Extract changes
484
522
  const allChanges = [];
485
523
  for (const issue of searchResult.issues) {
486
524
  if (issue.changelog?.histories) {
487
- const changes = extractMeaningfulChanges(issue.changelog.histories, dateFrom, trackFields.map((f) => f.toLowerCase()));
525
+ const changes = extractMeaningfulChanges(issue.changelog.histories, dateFrom, trackFields);
488
526
  for (const change of changes) {
489
527
  allChanges.push({
490
528
  issueKey: issue.key,
@@ -608,13 +646,13 @@ Example: jira_analyze_epic(issueKey="PROJ-100")`,
608
646
  "resolutiondate",
609
647
  ];
610
648
  // Get the root issue with full details
611
- const rootIssue = await client.getIssue(args.issueKey, analysisFields);
649
+ const rootIssue = await requireClient().getIssue(args.issueKey, analysisFields);
612
650
  const rootType = rootIssue.fields.issuetype?.name || "Issue";
613
651
  // Collect all child issues recursively
614
652
  const allIssues = [rootIssue];
615
653
  const visited = new Set([args.issueKey]);
616
654
  const collectChildren = async (parentKey) => {
617
- // 1. Subtasks
655
+ // 1. Subtasks (direct children via subtasks field)
618
656
  const parent = allIssues.find((i) => i.key === parentKey);
619
657
  const subtasks = parent?.fields.subtasks;
620
658
  if (subtasks) {
@@ -622,7 +660,7 @@ Example: jira_analyze_epic(issueKey="PROJ-100")`,
622
660
  if (!visited.has(sub.key)) {
623
661
  visited.add(sub.key);
624
662
  try {
625
- const issue = await client.getIssue(sub.key, analysisFields);
663
+ const issue = await requireClient().getIssue(sub.key, analysisFields);
626
664
  allIssues.push(issue);
627
665
  await collectChildren(sub.key);
628
666
  }
@@ -632,24 +670,38 @@ Example: jira_analyze_epic(issueKey="PROJ-100")`,
632
670
  }
633
671
  }
634
672
  }
635
- // 2. Epic children (try multiple patterns)
636
- const epicPatterns = [
637
- `"Epic Link" = ${parentKey}`,
638
- `parent = ${parentKey}`,
639
- `parentEpic = ${parentKey}`,
640
- ];
641
- for (const jql of epicPatterns) {
673
+ // 2. Children via link fields (Epic Link, Parent Link) and parent field
674
+ // Build JQL patterns in priority order
675
+ const childPatterns = [];
676
+ // Epic Link field (Epic → Story/Task/Bug)
677
+ const epicLinkFieldId = await requireClient().getEpicLinkFieldId();
678
+ if (epicLinkFieldId) {
679
+ childPatterns.push(`cf[${epicLinkFieldId.replace("customfield_", "")}] = ${parentKey}`);
680
+ }
681
+ // Parent Link field (Initiative → Epic in Jira Portfolio/Advanced Roadmaps)
682
+ const parentLinkFieldId = await requireClient().getParentLinkFieldId();
683
+ if (parentLinkFieldId) {
684
+ childPatterns.push(`cf[${parentLinkFieldId.replace("customfield_", "")}] = ${parentKey}`);
685
+ }
686
+ // Standard parent field (Team-managed/Next-Gen projects)
687
+ childPatterns.push(`parent = ${parentKey}`);
688
+ // Jira Software Cloud specific
689
+ childPatterns.push(`parentEpic = ${parentKey}`);
690
+ // Try each pattern, continue to next if no results or error
691
+ for (const jql of childPatterns) {
642
692
  try {
643
- const children = await client.searchIssues(jql, 100, analysisFields);
693
+ const children = await requireClient().searchIssues(jql, 100, analysisFields);
644
694
  for (const child of children.issues) {
645
695
  if (!visited.has(child.key)) {
646
696
  visited.add(child.key);
647
697
  allIssues.push(child);
698
+ // Recurse to handle deep hierarchies:
699
+ // Initiative → Epic → Story → Task → Subtask
648
700
  await collectChildren(child.key);
649
701
  }
650
702
  }
651
- if (children.issues.length > 0)
652
- break; // Found children with this pattern
703
+ // Don't break early - multiple patterns may find different children
704
+ // e.g., Epic Link finds Stories, Parent Link finds sub-Epics
653
705
  }
654
706
  catch {
655
707
  // Pattern not supported, try next
@@ -658,13 +710,16 @@ Example: jira_analyze_epic(issueKey="PROJ-100")`,
658
710
  };
659
711
  await collectChildren(args.issueKey);
660
712
  const byType = new Map();
661
- const statusToDone = (status) => {
662
- const lower = status.toLowerCase();
663
- return lower.includes("done") || lower.includes("closed") || lower.includes("resolved");
713
+ // Use statusCategory.key which is language-independent
714
+ // Keys: "new" (To Do), "indeterminate" (In Progress), "done" (Done), "undefined" (No category)
715
+ const getStatusCategory = (issue) => {
716
+ return issue.fields.status?.statusCategory?.key || "undefined";
664
717
  };
665
- const statusToInProgress = (status) => {
666
- const lower = status.toLowerCase();
667
- return lower.includes("progress") || lower.includes("review") || lower.includes("testing");
718
+ const statusToDone = (issue) => {
719
+ return getStatusCategory(issue) === "done";
720
+ };
721
+ const statusToInProgress = (issue) => {
722
+ return getStatusCategory(issue) === "indeterminate";
668
723
  };
669
724
  for (const issue of allIssues) {
670
725
  const type = issue.fields.issuetype?.name || "Unknown";
@@ -681,11 +736,11 @@ Example: jira_analyze_epic(issueKey="PROJ-100")`,
681
736
  }
682
737
  const metrics = byType.get(type);
683
738
  metrics.total++;
684
- const statusName = issue.fields.status?.name || "";
685
- if (statusToDone(statusName)) {
739
+ // Use language-independent statusCategory.key
740
+ if (statusToDone(issue)) {
686
741
  metrics.done++;
687
742
  }
688
- else if (statusToInProgress(statusName)) {
743
+ else if (statusToInProgress(issue)) {
689
744
  metrics.inProgress++;
690
745
  }
691
746
  else {
@@ -732,31 +787,59 @@ Example: jira_analyze_epic(issueKey="PROJ-100")`,
732
787
  totals.timeEstimatedSeconds += estimated;
733
788
  totals.timeRemainingSeconds += remaining;
734
789
  }
735
- // Extract linked Confluence pages and related issues
790
+ // Extract linked Confluence pages and cross-project dependencies
736
791
  const confluenceLinks = [];
737
- const relatedIssues = [];
738
- const issueLinks = rootIssue.fields.issuelinks;
739
- if (issueLinks) {
792
+ const crossProjectDeps = [];
793
+ // Helper to extract project key from issue key (e.g., "CORE-123" -> "CORE")
794
+ const extractProject = (issueKey) => {
795
+ const match = issueKey.match(/^([A-Z][A-Z0-9]*)-\d+$/);
796
+ return match ? match[1] : "?";
797
+ };
798
+ // Track seen target keys to deduplicate
799
+ const seenTargets = new Set();
800
+ // Collect links from ALL issues in hierarchy (not just root)
801
+ for (const issue of allIssues) {
802
+ const issueLinks = issue.fields.issuelinks;
803
+ if (!issueLinks)
804
+ continue;
740
805
  for (const link of issueLinks) {
806
+ // Process outward links
741
807
  if (link.outwardIssue) {
742
- relatedIssues.push({
743
- key: link.outwardIssue.key,
744
- summary: link.outwardIssue.fields?.summary || "",
745
- relationship: `${link.type.outward}`,
746
- });
808
+ const targetKey = link.outwardIssue.key;
809
+ // Skip if target is in hierarchy (already shown as child) or already seen
810
+ if (!visited.has(targetKey) && !seenTargets.has(targetKey)) {
811
+ seenTargets.add(targetKey);
812
+ crossProjectDeps.push({
813
+ source: issue.key,
814
+ relationship: link.type.outward,
815
+ target: targetKey,
816
+ project: extractProject(targetKey),
817
+ summary: link.outwardIssue.fields?.summary || "",
818
+ status: link.outwardIssue.fields?.status?.name || "Unknown",
819
+ });
820
+ }
747
821
  }
822
+ // Process inward links
748
823
  if (link.inwardIssue) {
749
- relatedIssues.push({
750
- key: link.inwardIssue.key,
751
- summary: link.inwardIssue.fields?.summary || "",
752
- relationship: `${link.type.inward}`,
753
- });
824
+ const targetKey = link.inwardIssue.key;
825
+ // Skip if target is in hierarchy or already seen
826
+ if (!visited.has(targetKey) && !seenTargets.has(targetKey)) {
827
+ seenTargets.add(targetKey);
828
+ crossProjectDeps.push({
829
+ source: issue.key,
830
+ relationship: link.type.inward,
831
+ target: targetKey,
832
+ project: extractProject(targetKey),
833
+ summary: link.inwardIssue.fields?.summary || "",
834
+ status: link.inwardIssue.fields?.status?.name || "Unknown",
835
+ });
836
+ }
754
837
  }
755
838
  }
756
839
  }
757
840
  // Check for web links (Confluence pages)
758
841
  try {
759
- const remoteLinks = await client.getIssueRemoteLinks(args.issueKey);
842
+ const remoteLinks = await requireClient().getIssueRemoteLinks(args.issueKey);
760
843
  for (const link of remoteLinks) {
761
844
  if (link.object?.url && (link.object.url.includes("confluence") ||
762
845
  link.object.url.includes("/wiki/"))) {
@@ -776,7 +859,7 @@ Example: jira_analyze_epic(issueKey="PROJ-100")`,
776
859
  const sinceDate = new Date();
777
860
  sinceDate.setDate(sinceDate.getDate() - commentDays);
778
861
  const since = sinceDate.toISOString().split("T")[0];
779
- const commentsMap = await client.getCommentsForIssues(allIssues.map((i) => i.key), since);
862
+ const commentsMap = await requireClient().getCommentsForIssues(allIssues.map((i) => i.key), since);
780
863
  for (const [issueKey, comments] of commentsMap) {
781
864
  const sorted = [...comments].sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
782
865
  for (const comment of sorted.slice(0, maxCommentsPerIssue)) {
@@ -886,27 +969,29 @@ Example: jira_analyze_epic(issueKey="PROJ-100")`,
886
969
  }
887
970
  lines.push("");
888
971
  }
889
- // Linked resources
890
- if (confluenceLinks.length > 0 || relatedIssues.length > 0) {
891
- lines.push("## Linked Resources");
972
+ // Confluence pages
973
+ if (confluenceLinks.length > 0) {
974
+ lines.push("## Linked Confluence Pages");
892
975
  lines.push("");
893
- if (confluenceLinks.length > 0) {
894
- lines.push("**Confluence Pages**:");
895
- for (const link of confluenceLinks) {
896
- lines.push(`- [${link.title}](${link.url})`);
897
- }
898
- lines.push("");
976
+ for (const link of confluenceLinks) {
977
+ lines.push(`- [${link.title}](${link.url})`);
899
978
  }
900
- if (relatedIssues.length > 0) {
901
- lines.push("**Related Issues**:");
902
- for (const rel of relatedIssues.slice(0, 10)) {
903
- lines.push(`- ${rel.key}: ${rel.summary} (${rel.relationship})`);
904
- }
905
- if (relatedIssues.length > 10) {
906
- lines.push(`- *...and ${relatedIssues.length - 10} more*`);
907
- }
979
+ lines.push("");
980
+ }
981
+ // Cross-project dependencies (links to issues outside hierarchy)
982
+ if (crossProjectDeps.length > 0) {
983
+ lines.push("## Cross-Project Dependencies");
984
+ lines.push("");
985
+ lines.push("| Source | Relationship | Target | Project | Status |");
986
+ lines.push("|--------|--------------|--------|---------|--------|");
987
+ for (const dep of crossProjectDeps.slice(0, 20)) {
988
+ lines.push(`| ${dep.source} | ${dep.relationship} | ${dep.target} | ${dep.project} | ${dep.status} |`);
989
+ }
990
+ if (crossProjectDeps.length > 20) {
908
991
  lines.push("");
992
+ lines.push(`*...and ${crossProjectDeps.length - 20} more dependencies*`);
909
993
  }
994
+ lines.push("");
910
995
  }
911
996
  // Recent comments
912
997
  if (recentComments.length > 0) {
@@ -966,6 +1051,579 @@ Example: jira_analyze_epic(issueKey="PROJ-100")`,
966
1051
  }
967
1052
  },
968
1053
  },
1054
+ jira_epic_summary: {
1055
+ description: `Lightweight Epic/Initiative summary with hierarchy and status breakdown. Returns DIRECTLY (not buffered).
1056
+
1057
+ Provides quick overview including:
1058
+ - Issue list with type, summary, status, assignee
1059
+ - Status breakdown by issue type
1060
+ - Cross-project dependencies (links to issues outside hierarchy)
1061
+
1062
+ Lighter than jira_analyze_epic (no time metrics, no comments, no Confluence links).
1063
+
1064
+ REQUIRES: issueKey (Epic, Initiative, or parent issue)
1065
+ RETURNS: Markdown summary (directly to LLM, not buffered)
1066
+
1067
+ Example: jira_epic_summary(issueKey="PROJ-100")`,
1068
+ inputSchema: z.object({
1069
+ issueKey: z.string().describe("Epic or parent issue key (e.g., PROJ-123)"),
1070
+ includeLinks: z.boolean().optional().describe("Show cross-project dependencies (default: true)"),
1071
+ maxDepth: z.number().optional().describe("Maximum recursion depth (default: 3)"),
1072
+ }),
1073
+ handler: async (args) => {
1074
+ try {
1075
+ const includeLinks = args.includeLinks !== false;
1076
+ const maxDepth = args.maxDepth ?? 3;
1077
+ // Minimal fields for summary
1078
+ const summaryFields = [
1079
+ "summary",
1080
+ "status",
1081
+ "issuetype",
1082
+ "assignee",
1083
+ "subtasks",
1084
+ "issuelinks",
1085
+ ];
1086
+ // Get the root issue
1087
+ const rootIssue = await requireClient().getIssue(args.issueKey, summaryFields);
1088
+ const rootType = rootIssue.fields.issuetype?.name || "Issue";
1089
+ // Collect hierarchy
1090
+ const allIssues = [rootIssue];
1091
+ const visited = new Set([args.issueKey]);
1092
+ const collectChildren = async (parentKey, depth) => {
1093
+ if (depth >= maxDepth)
1094
+ return;
1095
+ // 1. Subtasks
1096
+ const parent = allIssues.find((i) => i.key === parentKey);
1097
+ const subtasks = parent?.fields.subtasks;
1098
+ if (subtasks) {
1099
+ for (const sub of subtasks) {
1100
+ if (!visited.has(sub.key)) {
1101
+ visited.add(sub.key);
1102
+ try {
1103
+ const issue = await requireClient().getIssue(sub.key, summaryFields);
1104
+ allIssues.push(issue);
1105
+ await collectChildren(sub.key, depth + 1);
1106
+ }
1107
+ catch {
1108
+ // Skip inaccessible issues
1109
+ }
1110
+ }
1111
+ }
1112
+ }
1113
+ // 2. Children via link fields
1114
+ const childPatterns = [];
1115
+ const epicLinkFieldId = await requireClient().getEpicLinkFieldId();
1116
+ if (epicLinkFieldId) {
1117
+ childPatterns.push(`cf[${epicLinkFieldId.replace("customfield_", "")}] = ${parentKey}`);
1118
+ }
1119
+ const parentLinkFieldId = await requireClient().getParentLinkFieldId();
1120
+ if (parentLinkFieldId) {
1121
+ childPatterns.push(`cf[${parentLinkFieldId.replace("customfield_", "")}] = ${parentKey}`);
1122
+ }
1123
+ childPatterns.push(`parent = ${parentKey}`);
1124
+ childPatterns.push(`parentEpic = ${parentKey}`);
1125
+ for (const jql of childPatterns) {
1126
+ try {
1127
+ const children = await requireClient().searchIssues(jql, 100, summaryFields);
1128
+ for (const child of children.issues) {
1129
+ if (!visited.has(child.key)) {
1130
+ visited.add(child.key);
1131
+ allIssues.push(child);
1132
+ await collectChildren(child.key, depth + 1);
1133
+ }
1134
+ }
1135
+ }
1136
+ catch {
1137
+ // Pattern not supported
1138
+ }
1139
+ }
1140
+ };
1141
+ await collectChildren(args.issueKey, 0);
1142
+ // Status category helper
1143
+ const getStatusCategory = (issue) => {
1144
+ return issue.fields.status?.statusCategory?.key || "undefined";
1145
+ };
1146
+ // Calculate metrics by type
1147
+ const byType = new Map();
1148
+ for (const issue of allIssues) {
1149
+ const type = issue.fields.issuetype?.name || "Unknown";
1150
+ if (!byType.has(type)) {
1151
+ byType.set(type, { total: 0, done: 0, inProgress: 0, todo: 0 });
1152
+ }
1153
+ const metrics = byType.get(type);
1154
+ metrics.total++;
1155
+ const category = getStatusCategory(issue);
1156
+ if (category === "done") {
1157
+ metrics.done++;
1158
+ }
1159
+ else if (category === "indeterminate") {
1160
+ metrics.inProgress++;
1161
+ }
1162
+ else {
1163
+ metrics.todo++;
1164
+ }
1165
+ }
1166
+ // Calculate totals
1167
+ let totalDone = 0, totalInProgress = 0, totalTodo = 0;
1168
+ for (const metrics of byType.values()) {
1169
+ totalDone += metrics.done;
1170
+ totalInProgress += metrics.inProgress;
1171
+ totalTodo += metrics.todo;
1172
+ }
1173
+ const totalIssues = allIssues.length;
1174
+ const totalCompletion = totalIssues > 0 ? Math.round((totalDone / totalIssues) * 100) : 0;
1175
+ // Collect cross-project dependencies
1176
+ const crossProjectDeps = [];
1177
+ if (includeLinks) {
1178
+ const extractProject = (issueKey) => {
1179
+ const match = issueKey.match(/^([A-Z][A-Z0-9]*)-\d+$/);
1180
+ return match ? match[1] : "?";
1181
+ };
1182
+ const seenTargets = new Set();
1183
+ for (const issue of allIssues) {
1184
+ const issueLinks = issue.fields.issuelinks;
1185
+ if (!issueLinks)
1186
+ continue;
1187
+ for (const link of issueLinks) {
1188
+ if (link.outwardIssue) {
1189
+ const targetKey = link.outwardIssue.key;
1190
+ if (!visited.has(targetKey) && !seenTargets.has(targetKey)) {
1191
+ seenTargets.add(targetKey);
1192
+ crossProjectDeps.push({
1193
+ source: issue.key,
1194
+ relationship: link.type.outward,
1195
+ target: targetKey,
1196
+ project: extractProject(targetKey),
1197
+ status: link.outwardIssue.fields?.status?.name || "Unknown",
1198
+ });
1199
+ }
1200
+ }
1201
+ if (link.inwardIssue) {
1202
+ const targetKey = link.inwardIssue.key;
1203
+ if (!visited.has(targetKey) && !seenTargets.has(targetKey)) {
1204
+ seenTargets.add(targetKey);
1205
+ crossProjectDeps.push({
1206
+ source: issue.key,
1207
+ relationship: link.type.inward,
1208
+ target: targetKey,
1209
+ project: extractProject(targetKey),
1210
+ status: link.inwardIssue.fields?.status?.name || "Unknown",
1211
+ });
1212
+ }
1213
+ }
1214
+ }
1215
+ }
1216
+ }
1217
+ // Build markdown output
1218
+ const lines = [
1219
+ `# ${rootType} Summary: ${args.issueKey}`,
1220
+ `**${rootIssue.fields.summary}**`,
1221
+ "",
1222
+ `**Status**: ${rootIssue.fields.status?.name || "Unknown"} | **Completion**: ${totalCompletion}%`,
1223
+ "",
1224
+ ];
1225
+ // Issue list
1226
+ lines.push(`## Hierarchy (${totalIssues} issues)`);
1227
+ lines.push("");
1228
+ lines.push("| Key | Type | Summary | Status | Assignee |");
1229
+ lines.push("|-----|------|---------|--------|----------|");
1230
+ for (const issue of allIssues.slice(0, 50)) {
1231
+ const type = issue.fields.issuetype?.name || "?";
1232
+ const summary = truncateText(issue.fields.summary || "", 50);
1233
+ const status = issue.fields.status?.name || "?";
1234
+ const assignee = issue.fields.assignee?.displayName || "Unassigned";
1235
+ lines.push(`| ${issue.key} | ${type} | ${summary} | ${status} | ${assignee} |`);
1236
+ }
1237
+ if (allIssues.length > 50) {
1238
+ lines.push("");
1239
+ lines.push(`*...and ${allIssues.length - 50} more issues*`);
1240
+ }
1241
+ lines.push("");
1242
+ // Status breakdown
1243
+ lines.push("## Status Breakdown");
1244
+ lines.push("");
1245
+ lines.push("| Type | Total | Done | In Progress | To Do | Completion |");
1246
+ lines.push("|------|-------|------|-------------|-------|------------|");
1247
+ for (const [type, metrics] of byType) {
1248
+ const completion = metrics.total > 0 ? Math.round((metrics.done / metrics.total) * 100) : 0;
1249
+ lines.push(`| ${type} | ${metrics.total} | ${metrics.done} | ${metrics.inProgress} | ${metrics.todo} | ${completion}% |`);
1250
+ }
1251
+ lines.push(`| **TOTAL** | **${totalIssues}** | **${totalDone}** | **${totalInProgress}** | **${totalTodo}** | **${totalCompletion}%** |`);
1252
+ lines.push("");
1253
+ // Cross-project dependencies
1254
+ if (crossProjectDeps.length > 0) {
1255
+ lines.push(`## Cross-Project Dependencies (${crossProjectDeps.length})`);
1256
+ lines.push("");
1257
+ lines.push("| Source | Relationship | Target | Project | Status |");
1258
+ lines.push("|--------|--------------|--------|---------|--------|");
1259
+ for (const dep of crossProjectDeps.slice(0, 20)) {
1260
+ lines.push(`| ${dep.source} | ${dep.relationship} | ${dep.target} | ${dep.project} | ${dep.status} |`);
1261
+ }
1262
+ if (crossProjectDeps.length > 20) {
1263
+ lines.push("");
1264
+ lines.push(`*...and ${crossProjectDeps.length - 20} more dependencies*`);
1265
+ }
1266
+ lines.push("");
1267
+ }
1268
+ const output = lines.join("\n");
1269
+ return formatSuccessDirect({
1270
+ _directContent: true,
1271
+ content: output,
1272
+ });
1273
+ }
1274
+ catch (error) {
1275
+ return formatError(isApiError(error) ? error : new Error(String(error)));
1276
+ }
1277
+ },
1278
+ },
1279
+ jira_analyze_sprint: {
1280
+ description: `Comprehensive Sprint analysis for LLM insights. Returns DIRECTLY (not buffered).
1281
+
1282
+ Provides in-depth sprint breakdown for LLM analysis including:
1283
+ - Sprint metadata: dates, duration, days remaining
1284
+ - Progress: completion percentage, status breakdown
1285
+ - Issue breakdown by type and status
1286
+ - Time metrics: estimated vs spent vs remaining
1287
+ - Team distribution: who's working on what
1288
+ - Risk indicators: stale, blocked, high-priority unstarted issues
1289
+ - Recent comments: technical discussions
1290
+
1291
+ Ideal for:
1292
+ - "How is Sprint 42 progressing?"
1293
+ - "What are the risks in the current sprint?"
1294
+ - "Show me sprint status with team breakdown"
1295
+
1296
+ REQUIRES: sprintId (from jira_get_sprints)
1297
+ RETURNS: Markdown-formatted sprint analysis with pre-computed metrics
1298
+
1299
+ Example: jira_analyze_sprint(sprintId=123)`,
1300
+ inputSchema: z.object({
1301
+ sprintId: z.number().describe("Sprint ID (from jira_get_sprints)"),
1302
+ includeComments: z.boolean().optional().describe("Include recent comments (default: true)"),
1303
+ commentDays: z.number().optional().describe("Days of comments to include (default: 7)"),
1304
+ includeRisks: z.boolean().optional().describe("Calculate risk indicators (default: true)"),
1305
+ }),
1306
+ handler: async (args) => {
1307
+ try {
1308
+ const includeComments = args.includeComments !== false;
1309
+ const commentDays = args.commentDays ?? 7;
1310
+ const includeRisks = args.includeRisks !== false;
1311
+ // Fetch sprint info
1312
+ const sprint = await requireClient().getSprint(args.sprintId);
1313
+ // Fetch sprint issues with required fields
1314
+ const sprintIssuesResult = await requireClient().getSprintIssues(args.sprintId);
1315
+ const issues = sprintIssuesResult.issues;
1316
+ // Re-fetch issues with full fields (getSprintIssues may not return all fields)
1317
+ const issueKeys = issues.map(i => i.key);
1318
+ const analysisFields = [
1319
+ "summary",
1320
+ "status",
1321
+ "priority",
1322
+ "assignee",
1323
+ "issuetype",
1324
+ "labels",
1325
+ "components",
1326
+ "timeoriginalestimate",
1327
+ "timeestimate",
1328
+ "timespent",
1329
+ "issuelinks",
1330
+ "created",
1331
+ "updated",
1332
+ ];
1333
+ // Fetch full issue details in batches
1334
+ const fullIssues = [];
1335
+ const batchSize = 50;
1336
+ for (let i = 0; i < issueKeys.length; i += batchSize) {
1337
+ const batchKeys = issueKeys.slice(i, i + batchSize);
1338
+ const jql = `key in (${batchKeys.join(", ")})`;
1339
+ const result = await requireClient().searchIssuesAll(jql, analysisFields, batchKeys.length);
1340
+ fullIssues.push(...result.issues);
1341
+ }
1342
+ // Calculate sprint timing
1343
+ const now = new Date();
1344
+ const startDate = sprint.startDate ? new Date(sprint.startDate) : null;
1345
+ const endDate = sprint.endDate ? new Date(sprint.endDate) : null;
1346
+ let sprintDuration = 0;
1347
+ let daysPassed = 0;
1348
+ let daysRemaining = 0;
1349
+ if (startDate && endDate) {
1350
+ sprintDuration = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
1351
+ if (sprint.state === "active") {
1352
+ daysPassed = Math.ceil((now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
1353
+ daysRemaining = Math.max(0, Math.ceil((endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)));
1354
+ }
1355
+ else if (sprint.state === "closed") {
1356
+ daysPassed = sprintDuration;
1357
+ daysRemaining = 0;
1358
+ }
1359
+ }
1360
+ // Status category helper (language-independent)
1361
+ const getStatusCategory = (issue) => {
1362
+ const key = issue.fields.status?.statusCategory?.key || "new";
1363
+ if (key === "done")
1364
+ return "done";
1365
+ if (key === "indeterminate")
1366
+ return "indeterminate";
1367
+ return "new";
1368
+ };
1369
+ // Calculate status metrics
1370
+ const statusCounts = { done: 0, inProgress: 0, todo: 0 };
1371
+ const byType = new Map();
1372
+ const byAssignee = new Map();
1373
+ let totalTimeSpent = 0;
1374
+ let totalTimeEstimate = 0;
1375
+ let totalTimeRemaining = 0;
1376
+ for (const issue of fullIssues) {
1377
+ const category = getStatusCategory(issue);
1378
+ const typeName = issue.fields.issuetype?.name || "Unknown";
1379
+ const assigneeName = issue.fields.assignee?.displayName || "Unassigned";
1380
+ // Status counts
1381
+ if (category === "done") {
1382
+ statusCounts.done++;
1383
+ }
1384
+ else if (category === "indeterminate") {
1385
+ statusCounts.inProgress++;
1386
+ }
1387
+ else {
1388
+ statusCounts.todo++;
1389
+ }
1390
+ // By type
1391
+ if (!byType.has(typeName)) {
1392
+ byType.set(typeName, { total: 0, done: 0, inProgress: 0, todo: 0 });
1393
+ }
1394
+ const typeMetrics = byType.get(typeName);
1395
+ typeMetrics.total++;
1396
+ if (category === "done")
1397
+ typeMetrics.done++;
1398
+ else if (category === "indeterminate")
1399
+ typeMetrics.inProgress++;
1400
+ else
1401
+ typeMetrics.todo++;
1402
+ // By assignee
1403
+ if (!byAssignee.has(assigneeName)) {
1404
+ byAssignee.set(assigneeName, { total: 0, done: 0, inProgress: 0 });
1405
+ }
1406
+ const assigneeMetrics = byAssignee.get(assigneeName);
1407
+ assigneeMetrics.total++;
1408
+ if (category === "done")
1409
+ assigneeMetrics.done++;
1410
+ else if (category === "indeterminate")
1411
+ assigneeMetrics.inProgress++;
1412
+ // Time tracking
1413
+ const spent = issue.fields.timespent || 0;
1414
+ const estimated = issue.fields.timeoriginalestimate || 0;
1415
+ const remaining = issue.fields.timeestimate || 0;
1416
+ totalTimeSpent += spent;
1417
+ totalTimeEstimate += estimated;
1418
+ totalTimeRemaining += remaining;
1419
+ }
1420
+ const totalIssues = fullIssues.length;
1421
+ const completionPercent = totalIssues > 0 ? Math.round((statusCounts.done / totalIssues) * 100) : 0;
1422
+ // Risk detection
1423
+ const risks = [];
1424
+ if (includeRisks) {
1425
+ const threeDaysAgo = new Date();
1426
+ threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
1427
+ for (const issue of fullIssues) {
1428
+ const category = getStatusCategory(issue);
1429
+ const priorityName = issue.fields.priority?.name?.toLowerCase() || "";
1430
+ const updated = new Date(issue.fields.updated);
1431
+ // Stale: In Progress for 3+ days without update
1432
+ if (category === "indeterminate" && updated < threeDaysAgo) {
1433
+ const staleDays = Math.ceil((now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24));
1434
+ risks.push({
1435
+ issueKey: issue.key,
1436
+ summary: issue.fields.summary,
1437
+ riskType: "stale",
1438
+ detail: `In Progress for ${staleDays} days without update`,
1439
+ });
1440
+ }
1441
+ // High priority unstarted
1442
+ if (category === "new" && (priorityName.includes("high") || priorityName.includes("highest") || priorityName.includes("critical"))) {
1443
+ risks.push({
1444
+ issueKey: issue.key,
1445
+ summary: issue.fields.summary,
1446
+ riskType: "high-priority-unstarted",
1447
+ detail: `${issue.fields.priority?.name} priority, still To Do`,
1448
+ });
1449
+ }
1450
+ // Blocked: has "is blocked by" link to issue outside sprint
1451
+ const issueLinks = issue.fields.issuelinks;
1452
+ if (issueLinks) {
1453
+ for (const link of issueLinks) {
1454
+ // Check for "is blocked by" relationships
1455
+ if (link.type.inward?.toLowerCase().includes("blocked") && link.inwardIssue) {
1456
+ const blockingKey = link.inwardIssue.key;
1457
+ // Check if blocking issue is outside this sprint
1458
+ if (!issueKeys.includes(blockingKey)) {
1459
+ const blockingStatus = link.inwardIssue.fields?.status?.name || "Unknown";
1460
+ risks.push({
1461
+ issueKey: issue.key,
1462
+ summary: issue.fields.summary,
1463
+ riskType: "blocked",
1464
+ detail: `Blocked by ${blockingKey} (${blockingStatus}) - external to sprint`,
1465
+ });
1466
+ }
1467
+ }
1468
+ }
1469
+ }
1470
+ }
1471
+ }
1472
+ // Collect recent comments if requested
1473
+ const recentComments = [];
1474
+ if (includeComments && issueKeys.length > 0) {
1475
+ const sinceDate = new Date();
1476
+ sinceDate.setDate(sinceDate.getDate() - commentDays);
1477
+ const since = sinceDate.toISOString().split("T")[0];
1478
+ const commentsMap = await requireClient().getCommentsForIssues(issueKeys, since);
1479
+ for (const [issueKey, comments] of commentsMap) {
1480
+ const sorted = [...comments].sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
1481
+ for (const comment of sorted.slice(0, 3)) {
1482
+ recentComments.push({
1483
+ issueKey,
1484
+ author: comment.author.displayName,
1485
+ date: comment.created,
1486
+ body: truncateText(comment.body, 200),
1487
+ });
1488
+ }
1489
+ }
1490
+ // Sort all comments by date
1491
+ recentComments.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
1492
+ }
1493
+ // Build markdown output
1494
+ const lines = [
1495
+ `# Sprint Analysis: ${sprint.name}`,
1496
+ "",
1497
+ ];
1498
+ // Sprint metadata
1499
+ lines.push(`**State**: ${sprint.state.charAt(0).toUpperCase() + sprint.state.slice(1)}`);
1500
+ if (startDate && endDate) {
1501
+ lines.push(`**Period**: ${formatDisplayDate(sprint.startDate)} - ${formatDisplayDate(sprint.endDate)} (${sprintDuration} days)`);
1502
+ if (sprint.state === "active") {
1503
+ lines.push(`**Progress**: Day ${daysPassed} of ${sprintDuration} (${daysRemaining} days remaining)`);
1504
+ }
1505
+ }
1506
+ lines.push("");
1507
+ // Sprint progress
1508
+ lines.push("---");
1509
+ lines.push("## Sprint Progress");
1510
+ lines.push("");
1511
+ lines.push(`**Completion**: ${completionPercent}% (${statusCounts.done} of ${totalIssues} issues done)`);
1512
+ lines.push("");
1513
+ lines.push("| Status | Count | % |");
1514
+ lines.push("|--------|-------|---|");
1515
+ lines.push(`| Done | ${statusCounts.done} | ${totalIssues > 0 ? Math.round((statusCounts.done / totalIssues) * 100) : 0}% |`);
1516
+ lines.push(`| In Progress | ${statusCounts.inProgress} | ${totalIssues > 0 ? Math.round((statusCounts.inProgress / totalIssues) * 100) : 0}% |`);
1517
+ lines.push(`| To Do | ${statusCounts.todo} | ${totalIssues > 0 ? Math.round((statusCounts.todo / totalIssues) * 100) : 0}% |`);
1518
+ lines.push("");
1519
+ // Issue breakdown by type
1520
+ lines.push("## Issue Breakdown by Type");
1521
+ lines.push("");
1522
+ lines.push("| Type | Total | Done | In Progress | To Do |");
1523
+ lines.push("|------|-------|------|-------------|-------|");
1524
+ for (const [type, metrics] of byType) {
1525
+ lines.push(`| ${type} | ${metrics.total} | ${metrics.done} | ${metrics.inProgress} | ${metrics.todo} |`);
1526
+ }
1527
+ lines.push("");
1528
+ // Time metrics
1529
+ if (totalTimeSpent > 0 || totalTimeEstimate > 0) {
1530
+ lines.push("## Time Metrics");
1531
+ lines.push("");
1532
+ lines.push(`- **Estimated**: ${formatTimeSpent(totalTimeEstimate)}`);
1533
+ lines.push(`- **Spent**: ${formatTimeSpent(totalTimeSpent)}`);
1534
+ lines.push(`- **Remaining**: ${formatTimeSpent(totalTimeRemaining)}`);
1535
+ if (totalTimeEstimate > 0) {
1536
+ const burnRate = Math.round((totalTimeSpent / totalTimeEstimate) * 100);
1537
+ lines.push(`- **Burn Rate**: ${burnRate}% spent, ${completionPercent}% done`);
1538
+ if (burnRate > completionPercent + 10) {
1539
+ lines.push(`- **⚠️ Warning**: Time burn rate (${burnRate}%) exceeds completion rate (${completionPercent}%)`);
1540
+ }
1541
+ }
1542
+ lines.push("");
1543
+ }
1544
+ // Team distribution
1545
+ lines.push("## Team Distribution");
1546
+ lines.push("");
1547
+ lines.push("| Assignee | Total | Done | In Progress |");
1548
+ lines.push("|----------|-------|------|-------------|");
1549
+ // Sort by total issues descending
1550
+ const sortedAssignees = Array.from(byAssignee.entries()).sort((a, b) => b[1].total - a[1].total);
1551
+ for (const [assignee, metrics] of sortedAssignees.slice(0, 15)) {
1552
+ lines.push(`| ${assignee} | ${metrics.total} | ${metrics.done} | ${metrics.inProgress} |`);
1553
+ }
1554
+ if (sortedAssignees.length > 15) {
1555
+ lines.push(`| *...and ${sortedAssignees.length - 15} more* | | | |`);
1556
+ }
1557
+ lines.push("");
1558
+ // Risk indicators
1559
+ if (includeRisks && risks.length > 0) {
1560
+ lines.push("## Risk Indicators");
1561
+ lines.push("");
1562
+ lines.push(`⚠️ **${risks.length} issue${risks.length > 1 ? "s" : ""} at risk:**`);
1563
+ for (const risk of risks.slice(0, 10)) {
1564
+ lines.push(`- **${risk.issueKey}**: ${risk.detail}`);
1565
+ }
1566
+ if (risks.length > 10) {
1567
+ lines.push(`- *...and ${risks.length - 10} more risks*`);
1568
+ }
1569
+ lines.push("");
1570
+ }
1571
+ else if (includeRisks) {
1572
+ lines.push("## Risk Indicators");
1573
+ lines.push("");
1574
+ lines.push("✓ No significant risks detected.");
1575
+ lines.push("");
1576
+ }
1577
+ // Recent comments
1578
+ if (recentComments.length > 0) {
1579
+ lines.push(`## Recent Discussions (last ${commentDays} days)`);
1580
+ lines.push("");
1581
+ for (const comment of recentComments.slice(0, 10)) {
1582
+ const dateStr = formatDisplayDate(comment.date);
1583
+ const body = comment.body.replace(/\n/g, " ").trim();
1584
+ lines.push(`- **${comment.issueKey}** @${comment.author} (${dateStr}): "${body}"`);
1585
+ }
1586
+ if (recentComments.length > 10) {
1587
+ lines.push(`- *...and ${recentComments.length - 10} more comments*`);
1588
+ }
1589
+ lines.push("");
1590
+ }
1591
+ // Summary
1592
+ lines.push("---");
1593
+ lines.push("## Summary");
1594
+ lines.push("");
1595
+ lines.push(`Sprint "${sprint.name}" is **${completionPercent}% complete** with ${statusCounts.inProgress} issues in progress and ${statusCounts.todo} issues remaining.`);
1596
+ if (daysRemaining > 0) {
1597
+ lines.push(`**${daysRemaining} days remaining** in the sprint.`);
1598
+ }
1599
+ if (risks.length > 0) {
1600
+ lines.push(`**${risks.length} issue${risks.length > 1 ? "s" : ""}** need attention.`);
1601
+ }
1602
+ if (totalTimeEstimate > 0) {
1603
+ const burnRate = Math.round((totalTimeSpent / totalTimeEstimate) * 100);
1604
+ lines.push(`Time tracking shows **${burnRate}% of estimate used** for **${completionPercent}% completion**.`);
1605
+ }
1606
+ const output = lines.join("\n");
1607
+ // Check output size
1608
+ const maxSize = getMaxOutputSize();
1609
+ if (output.length > maxSize) {
1610
+ return formatSuccessDirect({
1611
+ _directContent: true,
1612
+ content: output.substring(0, maxSize - 200),
1613
+ truncated: true,
1614
+ hint: "Use includeComments=false or smaller commentDays for more concise output",
1615
+ });
1616
+ }
1617
+ return formatSuccessDirect({
1618
+ _directContent: true,
1619
+ content: output,
1620
+ });
1621
+ }
1622
+ catch (error) {
1623
+ return formatError(isApiError(error) ? error : new Error(String(error)));
1624
+ }
1625
+ },
1626
+ },
969
1627
  };
970
1628
  }
971
1629
  //# sourceMappingURL=activity-tools.js.map