mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__py3-none-any.whl

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.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (109) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +796 -46
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,23 @@
1
+ """Ticket analysis and cleanup tools for PM monitoring.
2
+
3
+ This module provides comprehensive analysis capabilities for ticket health:
4
+ - Similarity detection: Find duplicate or related tickets using TF-IDF
5
+ - Staleness detection: Identify old, inactive tickets
6
+ - Orphaned detection: Find tickets missing hierarchy (epic/project)
7
+ - Cleanup reports: Comprehensive analysis with recommendations
8
+
9
+ These tools help product managers maintain ticket health and development practices.
10
+ """
11
+
12
+ from .orphaned import OrphanedResult, OrphanedTicketDetector
13
+ from .similarity import SimilarityResult, TicketSimilarityAnalyzer
14
+ from .staleness import StalenessResult, StaleTicketDetector
15
+
16
+ __all__ = [
17
+ "SimilarityResult",
18
+ "TicketSimilarityAnalyzer",
19
+ "StalenessResult",
20
+ "StaleTicketDetector",
21
+ "OrphanedResult",
22
+ "OrphanedTicketDetector",
23
+ ]
@@ -0,0 +1,218 @@
1
+ """Orphaned ticket detection - tickets without parent epic/project.
2
+
3
+ This module identifies tickets that are not properly organized in the hierarchy:
4
+ - Tickets without parent epic/milestone
5
+ - Tickets not assigned to any project/team
6
+ - Standalone issues that should be part of larger initiatives
7
+
8
+ Proper hierarchy ensures better organization and tracking of work.
9
+ """
10
+
11
+ from typing import TYPE_CHECKING
12
+
13
+ from pydantic import BaseModel
14
+
15
+ if TYPE_CHECKING:
16
+ from ..core.models import Task
17
+
18
+
19
+ class OrphanedResult(BaseModel):
20
+ """Result of orphaned ticket analysis.
21
+
22
+ Attributes:
23
+ ticket_id: ID of the orphaned ticket
24
+ ticket_title: Title of the orphaned ticket
25
+ ticket_type: Type of ticket (task, issue, epic)
26
+ orphan_type: Type of orphan condition (no_parent, no_epic, no_project)
27
+ suggested_action: Recommended action (assign_epic, assign_project, review)
28
+ reason: Human-readable explanation
29
+
30
+ """
31
+
32
+ ticket_id: str
33
+ ticket_title: str
34
+ ticket_type: str # "task", "issue", "epic"
35
+ orphan_type: str # "no_parent", "no_epic", "no_project"
36
+ suggested_action: str # "assign_epic", "assign_project", "review"
37
+ reason: str
38
+
39
+
40
+ class OrphanedTicketDetector:
41
+ """Detects orphaned tickets without proper hierarchy.
42
+
43
+ Analyzes tickets to find those missing proper parent relationships:
44
+ - Tasks without parent issues
45
+ - Issues without parent epics
46
+ - Tickets without project/team assignments
47
+
48
+ This helps identify organizational gaps in ticket management.
49
+ """
50
+
51
+ def find_orphaned_tickets(
52
+ self,
53
+ tickets: list["Task"],
54
+ epics: list["Task"] | None = None,
55
+ ) -> list[OrphanedResult]:
56
+ """Find tickets without parent epic/project associations.
57
+
58
+ Args:
59
+ tickets: List of tickets to analyze
60
+ epics: Optional list of epics for validation
61
+
62
+ Returns:
63
+ List of orphaned tickets with suggested actions
64
+
65
+ """
66
+ results = []
67
+
68
+ for ticket in tickets:
69
+ orphan_types = self._check_orphaned(ticket)
70
+
71
+ for orphan_type in orphan_types:
72
+ result = OrphanedResult(
73
+ ticket_id=ticket.id or "unknown",
74
+ ticket_title=ticket.title,
75
+ ticket_type=self._get_ticket_type(ticket),
76
+ orphan_type=orphan_type,
77
+ suggested_action=self._suggest_action(orphan_type),
78
+ reason=self._build_reason(orphan_type, ticket),
79
+ )
80
+ results.append(result)
81
+
82
+ return results
83
+
84
+ def _check_orphaned(self, ticket: "Task") -> list[str]:
85
+ """Check if ticket is orphaned in various ways.
86
+
87
+ Args:
88
+ ticket: Ticket to check
89
+
90
+ Returns:
91
+ List of orphan type strings
92
+
93
+ """
94
+ orphan_types = []
95
+ metadata = ticket.metadata or {}
96
+
97
+ # Check ticket type
98
+ ticket_type = self._get_ticket_type(ticket)
99
+
100
+ # For tasks, check parent_issue
101
+ if ticket_type == "task":
102
+ if not getattr(ticket, "parent_issue", None):
103
+ orphan_types.append("no_parent")
104
+ return orphan_types
105
+
106
+ # For issues, check parent_epic and project
107
+ if ticket_type == "issue":
108
+ # Check for parent epic
109
+ has_epic = any(
110
+ [
111
+ getattr(ticket, "parent_epic", None),
112
+ metadata.get("parent_id"),
113
+ metadata.get("parentId"),
114
+ metadata.get("epic_id"),
115
+ metadata.get("epicId"),
116
+ metadata.get("milestone_id"), # GitHub milestones
117
+ metadata.get("epic"), # JIRA epics
118
+ ]
119
+ )
120
+
121
+ if not has_epic:
122
+ orphan_types.append("no_epic")
123
+
124
+ # Check for project assignment
125
+ has_project = any(
126
+ [
127
+ metadata.get("project_id"),
128
+ metadata.get("projectId"),
129
+ metadata.get("team_id"),
130
+ metadata.get("teamId"),
131
+ metadata.get("board_id"), # JIRA boards
132
+ metadata.get("workspace_id"), # Asana workspaces
133
+ ]
134
+ )
135
+
136
+ if not has_project:
137
+ orphan_types.append("no_project")
138
+
139
+ # If neither epic nor project
140
+ if not has_epic and not has_project:
141
+ orphan_types.append("no_parent")
142
+
143
+ return orphan_types
144
+
145
+ def _get_ticket_type(self, ticket: "Task") -> str:
146
+ """Determine ticket type from metadata.
147
+
148
+ Args:
149
+ ticket: Ticket to analyze
150
+
151
+ Returns:
152
+ Ticket type string (task, issue, epic)
153
+
154
+ """
155
+ from ..core.models import TicketType
156
+
157
+ # Check explicit ticket_type field
158
+ ticket_type = getattr(ticket, "ticket_type", None)
159
+ if ticket_type:
160
+ if ticket_type == TicketType.EPIC:
161
+ return "epic"
162
+ elif ticket_type in (TicketType.TASK, TicketType.SUBTASK):
163
+ return "task"
164
+ elif ticket_type == TicketType.ISSUE:
165
+ return "issue"
166
+
167
+ # Fallback to metadata inspection
168
+ metadata = ticket.metadata or {}
169
+
170
+ if metadata.get("type") == "epic":
171
+ return "epic"
172
+ elif metadata.get("issue_type") == "Epic":
173
+ return "epic"
174
+ elif metadata.get("type") == "task":
175
+ return "task"
176
+ elif getattr(ticket, "parent_issue", None):
177
+ return "task" # Has parent issue, so it's a task
178
+ else:
179
+ return "issue" # Default to issue
180
+
181
+ def _suggest_action(self, orphan_type: str) -> str:
182
+ """Suggest action for orphaned ticket.
183
+
184
+ Args:
185
+ orphan_type: Type of orphan condition
186
+
187
+ Returns:
188
+ Suggested action string
189
+
190
+ """
191
+ if orphan_type == "no_parent":
192
+ return "review" # Needs manual review
193
+ elif orphan_type == "no_epic":
194
+ return "assign_epic"
195
+ elif orphan_type == "no_project":
196
+ return "assign_project"
197
+ else:
198
+ return "review"
199
+
200
+ def _build_reason(self, orphan_type: str, ticket: "Task") -> str:
201
+ """Build human-readable reason.
202
+
203
+ Args:
204
+ orphan_type: Type of orphan condition
205
+ ticket: The ticket being analyzed
206
+
207
+ Returns:
208
+ Human-readable explanation
209
+
210
+ """
211
+ ticket_type = self._get_ticket_type(ticket)
212
+
213
+ reasons = {
214
+ "no_parent": f"{ticket_type.capitalize()} has no parent epic or project assigned",
215
+ "no_epic": f"{ticket_type.capitalize()} is missing parent epic/milestone",
216
+ "no_project": f"{ticket_type.capitalize()} is not assigned to any project/team",
217
+ }
218
+ return reasons.get(orphan_type, "Orphaned ticket")
@@ -0,0 +1,224 @@
1
+ """Ticket similarity detection using TF-IDF and fuzzy matching.
2
+
3
+ This module provides similarity analysis between tickets to detect:
4
+ - Duplicate tickets that should be merged
5
+ - Related tickets that should be linked
6
+ - Similar work that could be consolidated
7
+
8
+ Uses TF-IDF vectorization with cosine similarity for content analysis,
9
+ and fuzzy string matching for title comparison.
10
+ """
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ from pydantic import BaseModel
15
+ from rapidfuzz import fuzz
16
+ from sklearn.feature_extraction.text import TfidfVectorizer
17
+ from sklearn.metrics.pairwise import cosine_similarity
18
+
19
+ if TYPE_CHECKING:
20
+ from ..core.models import Task
21
+
22
+
23
+ class SimilarityResult(BaseModel):
24
+ """Result of similarity analysis between two tickets.
25
+
26
+ Attributes:
27
+ ticket1_id: ID of first ticket
28
+ ticket1_title: Title of first ticket
29
+ ticket2_id: ID of second ticket
30
+ ticket2_title: Title of second ticket
31
+ similarity_score: Overall similarity score (0.0-1.0)
32
+ similarity_reasons: List of reasons for similarity
33
+ suggested_action: Recommended action (merge, link, ignore)
34
+ confidence: Confidence in the similarity (0.0-1.0)
35
+
36
+ """
37
+
38
+ ticket1_id: str
39
+ ticket1_title: str
40
+ ticket2_id: str
41
+ ticket2_title: str
42
+ similarity_score: float # 0.0-1.0
43
+ similarity_reasons: list[str]
44
+ suggested_action: str # "merge", "link", "ignore"
45
+ confidence: float
46
+
47
+
48
+ class TicketSimilarityAnalyzer:
49
+ """Analyzes tickets to find similar/duplicate entries.
50
+
51
+ Uses a combination of TF-IDF vectorization on titles and descriptions,
52
+ plus fuzzy string matching on titles to identify similar tickets.
53
+
54
+ Attributes:
55
+ threshold: Minimum similarity score to report (0.0-1.0)
56
+ title_weight: Weight given to title similarity (0.0-1.0)
57
+ description_weight: Weight given to description similarity (0.0-1.0)
58
+
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ threshold: float = 0.75,
64
+ title_weight: float = 0.7,
65
+ description_weight: float = 0.3,
66
+ ):
67
+ """Initialize the similarity analyzer.
68
+
69
+ Args:
70
+ threshold: Minimum similarity score to report (default: 0.75)
71
+ title_weight: Weight for title similarity (default: 0.7)
72
+ description_weight: Weight for description similarity (default: 0.3)
73
+
74
+ """
75
+ self.threshold = threshold
76
+ self.title_weight = title_weight
77
+ self.description_weight = description_weight
78
+
79
+ def find_similar_tickets(
80
+ self,
81
+ tickets: list["Task"],
82
+ target_ticket: "Task | None" = None,
83
+ limit: int = 10,
84
+ ) -> list[SimilarityResult]:
85
+ """Find similar tickets using TF-IDF + cosine similarity.
86
+
87
+ Args:
88
+ tickets: List of tickets to analyze
89
+ target_ticket: Find similar to this ticket (if None, find all pairs)
90
+ limit: Maximum results to return
91
+
92
+ Returns:
93
+ List of similarity results above threshold, sorted by score
94
+
95
+ """
96
+ if len(tickets) < 2:
97
+ return []
98
+
99
+ # Build corpus for TF-IDF
100
+ titles = [t.title for t in tickets]
101
+ descriptions = [t.description or "" for t in tickets]
102
+
103
+ # TF-IDF on titles
104
+ title_vectorizer = TfidfVectorizer(
105
+ min_df=1, stop_words="english", lowercase=True, ngram_range=(1, 2)
106
+ )
107
+ title_matrix = title_vectorizer.fit_transform(titles)
108
+
109
+ # TF-IDF on descriptions (if available)
110
+ desc_matrix = None
111
+ if any(descriptions):
112
+ desc_vectorizer = TfidfVectorizer(
113
+ min_df=1, stop_words="english", lowercase=True, ngram_range=(1, 2)
114
+ )
115
+ desc_matrix = desc_vectorizer.fit_transform(descriptions)
116
+
117
+ # Compute similarity matrices
118
+ title_similarity = cosine_similarity(title_matrix)
119
+
120
+ if desc_matrix is not None:
121
+ desc_similarity = cosine_similarity(desc_matrix)
122
+ # Weighted combination
123
+ combined_similarity = (
124
+ self.title_weight * title_similarity
125
+ + self.description_weight * desc_similarity
126
+ )
127
+ else:
128
+ # Only use title similarity if no descriptions
129
+ combined_similarity = title_similarity
130
+
131
+ results = []
132
+
133
+ if target_ticket:
134
+ # Find similar to specific ticket
135
+ target_idx = next(
136
+ (i for i, t in enumerate(tickets) if t.id == target_ticket.id),
137
+ None,
138
+ )
139
+ if target_idx is None:
140
+ return []
141
+
142
+ for i, ticket in enumerate(tickets):
143
+ if i == target_idx:
144
+ continue
145
+
146
+ score = float(combined_similarity[target_idx, i])
147
+ if score >= self.threshold:
148
+ results.append(self._create_result(target_ticket, ticket, score))
149
+ else:
150
+ # Find all similar pairs
151
+ for i in range(len(tickets)):
152
+ for j in range(i + 1, len(tickets)):
153
+ score = float(combined_similarity[i, j])
154
+ if score >= self.threshold:
155
+ results.append(
156
+ self._create_result(tickets[i], tickets[j], score)
157
+ )
158
+
159
+ # Sort by score and limit
160
+ results.sort(key=lambda x: x.similarity_score, reverse=True)
161
+ return results[:limit]
162
+
163
+ def _create_result(
164
+ self,
165
+ ticket1: "Task",
166
+ ticket2: "Task",
167
+ score: float,
168
+ ) -> SimilarityResult:
169
+ """Create similarity result with analysis.
170
+
171
+ Args:
172
+ ticket1: First ticket
173
+ ticket2: Second ticket
174
+ score: Similarity score
175
+
176
+ Returns:
177
+ SimilarityResult with detailed analysis
178
+
179
+ """
180
+ reasons = []
181
+
182
+ # Title similarity using fuzzy matching
183
+ title_sim = fuzz.ratio(ticket1.title, ticket2.title) / 100.0
184
+ if title_sim > 0.8:
185
+ reasons.append("very_similar_titles")
186
+ elif title_sim > 0.6:
187
+ reasons.append("similar_titles")
188
+
189
+ # Tag overlap
190
+ tags1 = set(ticket1.tags or [])
191
+ tags2 = set(ticket2.tags or [])
192
+ if tags1 and tags2:
193
+ overlap = len(tags1 & tags2) / len(tags1 | tags2)
194
+ if overlap > 0.5:
195
+ reasons.append(f"tag_overlap_{int(overlap*100)}%")
196
+
197
+ # Same state
198
+ if ticket1.state == ticket2.state:
199
+ reasons.append("same_state")
200
+
201
+ # Same assignee
202
+ assignee1 = getattr(ticket1, "assignee", None)
203
+ assignee2 = getattr(ticket2, "assignee", None)
204
+ if assignee1 and assignee2 and assignee1 == assignee2:
205
+ reasons.append("same_assignee")
206
+
207
+ # Determine action
208
+ if score > 0.9:
209
+ action = "merge" # Very likely duplicates
210
+ elif score > 0.75:
211
+ action = "link" # Related, should be linked
212
+ else:
213
+ action = "ignore" # Low confidence
214
+
215
+ return SimilarityResult(
216
+ ticket1_id=ticket1.id or "unknown",
217
+ ticket1_title=ticket1.title,
218
+ ticket2_id=ticket2.id or "unknown",
219
+ ticket2_title=ticket2.title,
220
+ similarity_score=score,
221
+ similarity_reasons=reasons,
222
+ suggested_action=action,
223
+ confidence=score,
224
+ )