@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.
- package/README.md +288 -1
- package/TOOL_LIST.md +390 -87
- package/dist/config/constants.d.ts +12 -0
- package/dist/config/constants.d.ts.map +1 -1
- package/dist/config/constants.js +13 -0
- package/dist/config/constants.js.map +1 -1
- package/dist/config/loader.d.ts +8 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +27 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts +93 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +26 -0
- package/dist/config/types.js.map +1 -1
- package/dist/confluence/tools.d.ts +8 -1
- package/dist/confluence/tools.d.ts.map +1 -1
- package/dist/confluence/tools.js +61 -51
- package/dist/confluence/tools.js.map +1 -1
- package/dist/credentials/client-factory.d.ts +64 -0
- package/dist/credentials/client-factory.d.ts.map +1 -0
- package/dist/credentials/client-factory.js +110 -0
- package/dist/credentials/client-factory.js.map +1 -0
- package/dist/credentials/context.d.ts +25 -0
- package/dist/credentials/context.d.ts.map +1 -0
- package/dist/credentials/context.js +35 -0
- package/dist/credentials/context.js.map +1 -0
- package/dist/credentials/extractor.d.ts +21 -0
- package/dist/credentials/extractor.d.ts.map +1 -0
- package/dist/credentials/extractor.js +46 -0
- package/dist/credentials/extractor.js.map +1 -0
- package/dist/credentials/index.d.ts +9 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +8 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/credentials/types.d.ts +21 -0
- package/dist/credentials/types.d.ts.map +1 -0
- package/dist/credentials/types.js +13 -0
- package/dist/credentials/types.js.map +1 -0
- package/dist/index.js +98 -75
- package/dist/index.js.map +1 -1
- package/dist/jira/activity-tools.d.ts +54 -1
- package/dist/jira/activity-tools.d.ts.map +1 -1
- package/dist/jira/activity-tools.js +737 -79
- package/dist/jira/activity-tools.js.map +1 -1
- package/dist/jira/client.d.ts +34 -2
- package/dist/jira/client.d.ts.map +1 -1
- package/dist/jira/client.js +119 -63
- package/dist/jira/client.js.map +1 -1
- package/dist/jira/tools.d.ts +58 -1
- package/dist/jira/tools.d.ts.map +1 -1
- package/dist/jira/tools.js +226 -27
- package/dist/jira/tools.js.map +1 -1
- package/dist/jira/types.d.ts +22 -0
- package/dist/jira/types.d.ts.map +1 -1
- package/dist/permissions/tool-registry.d.ts +10 -10
- package/dist/permissions/tool-registry.d.ts.map +1 -1
- package/dist/permissions/tool-registry.js +8 -4
- package/dist/permissions/tool-registry.js.map +1 -1
- package/dist/tempo/tools.d.ts +33 -1
- package/dist/tempo/tools.d.ts.map +1 -1
- package/dist/tempo/tools.js +141 -17
- package/dist/tempo/tools.js.map +1 -1
- package/dist/transport/http.d.ts +18 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +66 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/index.d.ts +17 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +32 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/types.d.ts +15 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +12 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/buffer-pipeline/index.js +2 -2
- package/dist/utils/buffer-pipeline/index.js.map +1 -1
- package/dist/utils/buffer-pipeline/schema.d.ts +99 -99
- package/dist/utils/buffer-pipeline/schema.d.ts.map +1 -1
- package/dist/utils/buffer-tools.d.ts +91 -83
- package/dist/utils/buffer-tools.d.ts.map +1 -1
- package/dist/utils/buffer-tools.js +312 -139
- package/dist/utils/buffer-tools.js.map +1 -1
- package/dist/utils/jicon-help.d.ts +3 -3
- package/dist/utils/jicon-help.d.ts.map +1 -1
- package/dist/utils/jicon-help.js +141 -10
- package/dist/utils/jicon-help.js.map +1 -1
- package/dist/utils/json-structure.d.ts +11 -0
- package/dist/utils/json-structure.d.ts.map +1 -1
- package/dist/utils/json-structure.js +61 -0
- package/dist/utils/json-structure.js.map +1 -1
- package/dist/utils/plantuml/tools.d.ts +4 -4
- package/dist/utils/plantuml/tools.d.ts.map +1 -1
- package/dist/utils/plantuml/tools.js +29 -8
- package/dist/utils/plantuml/tools.js.map +1 -1
- package/dist/utils/plantuml/types.d.ts +4 -4
- package/dist/utils/response-formatter.d.ts.map +1 -1
- package/dist/utils/response-formatter.js +8 -4
- package/dist/utils/response-formatter.js.map +1 -1
- 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
|
-
|
|
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
|
|
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
|
-
//
|
|
138
|
-
|
|
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
|
-
|
|
153
|
+
trackFieldsEnglish.push("status");
|
|
141
154
|
if (args.includePriorityChanges !== false)
|
|
142
|
-
|
|
155
|
+
trackFieldsEnglish.push("priority");
|
|
143
156
|
if (args.includeAssigneeChanges !== false)
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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[
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
|
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
|
-
|
|
652
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
685
|
-
if (statusToDone(
|
|
739
|
+
// Use language-independent statusCategory.key
|
|
740
|
+
if (statusToDone(issue)) {
|
|
686
741
|
metrics.done++;
|
|
687
742
|
}
|
|
688
|
-
else if (statusToInProgress(
|
|
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
|
|
790
|
+
// Extract linked Confluence pages and cross-project dependencies
|
|
736
791
|
const confluenceLinks = [];
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
890
|
-
if (confluenceLinks.length > 0
|
|
891
|
-
lines.push("## Linked
|
|
972
|
+
// Confluence pages
|
|
973
|
+
if (confluenceLinks.length > 0) {
|
|
974
|
+
lines.push("## Linked Confluence Pages");
|
|
892
975
|
lines.push("");
|
|
893
|
-
|
|
894
|
-
lines.push(
|
|
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
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|