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,266 @@
1
+ """Stale ticket detection based on age and activity.
2
+
3
+ This module identifies tickets that may need closing or review based on:
4
+ - Age: How long since the ticket was created
5
+ - Inactivity: How long since the last update
6
+ - State: Tickets in certain states (open, waiting, blocked)
7
+ - Priority: Lower priority tickets are more likely to be stale
8
+
9
+ The staleness score combines these factors to identify candidates for cleanup.
10
+ """
11
+
12
+ from datetime import datetime
13
+ from typing import TYPE_CHECKING
14
+
15
+ from pydantic import BaseModel
16
+
17
+ if TYPE_CHECKING:
18
+ from ..core.models import Task, TicketState
19
+
20
+
21
+ class StalenessResult(BaseModel):
22
+ """Result of staleness analysis for a ticket.
23
+
24
+ Attributes:
25
+ ticket_id: ID of the stale ticket
26
+ ticket_title: Title of the stale ticket
27
+ ticket_state: Current state of the ticket
28
+ age_days: Days since ticket was created
29
+ days_since_update: Days since last update
30
+ days_since_comment: Days since last comment (if available)
31
+ staleness_score: Overall staleness score (0.0-1.0, higher = staler)
32
+ suggested_action: Recommended action (close, review, keep)
33
+ reason: Human-readable explanation
34
+
35
+ """
36
+
37
+ ticket_id: str
38
+ ticket_title: str
39
+ ticket_state: str
40
+ age_days: int
41
+ days_since_update: int
42
+ days_since_comment: int | None
43
+ staleness_score: float # 0.0-1.0
44
+ suggested_action: str # "close", "review", "keep"
45
+ reason: str
46
+
47
+
48
+ class StaleTicketDetector:
49
+ """Detects stale tickets based on age and activity.
50
+
51
+ Analyzes tickets to find those that are old, inactive, and may be
52
+ candidates for closing or review. Uses configurable thresholds for
53
+ age and activity, along with state and priority factors.
54
+
55
+ Attributes:
56
+ age_threshold: Minimum age in days to consider
57
+ activity_threshold: Days without activity to consider stale
58
+ check_states: List of states to check for staleness
59
+
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ age_threshold_days: int = 90,
65
+ activity_threshold_days: int = 30,
66
+ check_states: list["TicketState"] | None = None,
67
+ ):
68
+ """Initialize the stale ticket detector.
69
+
70
+ Args:
71
+ age_threshold_days: Minimum age to consider (default: 90)
72
+ activity_threshold_days: Days without activity (default: 30)
73
+ check_states: Ticket states to check (default: open, waiting, blocked)
74
+
75
+ """
76
+ from ..core.models import TicketState
77
+
78
+ self.age_threshold = age_threshold_days
79
+ self.activity_threshold = activity_threshold_days
80
+ self.check_states = check_states or [
81
+ TicketState.OPEN,
82
+ TicketState.WAITING,
83
+ TicketState.BLOCKED,
84
+ ]
85
+
86
+ def find_stale_tickets(
87
+ self,
88
+ tickets: list["Task"],
89
+ limit: int = 50,
90
+ ) -> list[StalenessResult]:
91
+ """Find stale tickets that may need attention.
92
+
93
+ Args:
94
+ tickets: List of tickets to analyze
95
+ limit: Maximum results
96
+
97
+ Returns:
98
+ List of staleness results, sorted by staleness score
99
+
100
+ """
101
+ now = datetime.now()
102
+ results = []
103
+
104
+ for ticket in tickets:
105
+ # Skip tickets not in check_states
106
+ if ticket.state not in self.check_states:
107
+ continue
108
+
109
+ # Calculate metrics
110
+ age_days = self._days_since(ticket.created_at, now)
111
+ days_since_update = self._days_since(ticket.updated_at, now)
112
+
113
+ # Check staleness criteria
114
+ is_old = age_days > self.age_threshold
115
+ is_inactive = days_since_update > self.activity_threshold
116
+
117
+ if is_old and is_inactive:
118
+ staleness_score = self._calculate_staleness_score(
119
+ age_days, days_since_update, ticket
120
+ )
121
+
122
+ result = StalenessResult(
123
+ ticket_id=ticket.id or "unknown",
124
+ ticket_title=ticket.title,
125
+ ticket_state=(
126
+ ticket.state.value
127
+ if hasattr(ticket.state, "value")
128
+ else str(ticket.state)
129
+ ),
130
+ age_days=age_days,
131
+ days_since_update=days_since_update,
132
+ days_since_comment=None, # Can be enhanced with comment data
133
+ staleness_score=staleness_score,
134
+ suggested_action=self._suggest_action(staleness_score),
135
+ reason=self._build_reason(age_days, days_since_update, ticket),
136
+ )
137
+ results.append(result)
138
+
139
+ # Sort by staleness score
140
+ results.sort(key=lambda x: x.staleness_score, reverse=True)
141
+ return results[:limit]
142
+
143
+ def _days_since(self, dt: datetime | None, now: datetime) -> int:
144
+ """Calculate days since a datetime.
145
+
146
+ Args:
147
+ dt: Datetime to calculate from
148
+ now: Current datetime
149
+
150
+ Returns:
151
+ Number of days since dt (0 if dt is None)
152
+
153
+ """
154
+ if dt is None:
155
+ return 0
156
+ # Handle timezone-aware and naive datetimes
157
+ if dt.tzinfo is not None and now.tzinfo is None:
158
+ now = now.replace(tzinfo=dt.tzinfo)
159
+ elif dt.tzinfo is None and now.tzinfo is not None:
160
+ dt = dt.replace(tzinfo=now.tzinfo)
161
+ return (now - dt).days
162
+
163
+ def _calculate_staleness_score(
164
+ self,
165
+ age_days: int,
166
+ days_since_update: int,
167
+ ticket: "Task",
168
+ ) -> float:
169
+ """Calculate staleness score (0.0-1.0, higher = staler).
170
+
171
+ Args:
172
+ age_days: Days since creation
173
+ days_since_update: Days since last update
174
+ ticket: The ticket being analyzed
175
+
176
+ Returns:
177
+ Staleness score between 0.0 and 1.0
178
+
179
+ """
180
+ from ..core.models import Priority, TicketState
181
+
182
+ # Base score from age and inactivity
183
+ age_factor = min(age_days / 365, 1.0) # Normalize to 1 year
184
+ activity_factor = min(days_since_update / 180, 1.0) # Normalize to 6 months
185
+
186
+ base_score = (age_factor + activity_factor) / 2
187
+
188
+ # Priority adjustment (low priority = more stale)
189
+ priority_weights = {
190
+ Priority.CRITICAL: 0.0,
191
+ Priority.HIGH: 0.3,
192
+ Priority.MEDIUM: 0.7,
193
+ Priority.LOW: 1.0,
194
+ }
195
+ priority_factor = priority_weights.get(ticket.priority, 0.5)
196
+
197
+ # State adjustment
198
+ state_weights = {
199
+ TicketState.BLOCKED: 0.8, # Blocked tickets are very stale
200
+ TicketState.WAITING: 0.9, # Waiting tickets are very stale
201
+ TicketState.OPEN: 0.6,
202
+ }
203
+ state_factor = state_weights.get(ticket.state, 0.5)
204
+
205
+ # Weighted combination
206
+ final_score = base_score * 0.5 + priority_factor * 0.3 + state_factor * 0.2
207
+
208
+ return min(final_score, 1.0)
209
+
210
+ def _suggest_action(self, score: float) -> str:
211
+ """Suggest action based on staleness score.
212
+
213
+ Args:
214
+ score: Staleness score
215
+
216
+ Returns:
217
+ Suggested action string
218
+
219
+ """
220
+ if score > 0.8:
221
+ return "close" # Very stale, likely won't be done
222
+ elif score > 0.6:
223
+ return "review" # Moderately stale, needs review
224
+ else:
225
+ return "keep" # Still relevant
226
+
227
+ def _build_reason(
228
+ self,
229
+ age_days: int,
230
+ days_since_update: int,
231
+ ticket: "Task",
232
+ ) -> str:
233
+ """Build human-readable reason string.
234
+
235
+ Args:
236
+ age_days: Days since creation
237
+ days_since_update: Days since last update
238
+ ticket: The ticket being analyzed
239
+
240
+ Returns:
241
+ Human-readable explanation
242
+
243
+ """
244
+ from ..core.models import Priority, TicketState
245
+
246
+ reasons = []
247
+
248
+ if age_days > 365:
249
+ reasons.append(f"created {age_days} days ago")
250
+ elif age_days > self.age_threshold:
251
+ reasons.append(f"old ({age_days} days)")
252
+
253
+ if days_since_update > 180:
254
+ reasons.append(f"no updates for {days_since_update} days")
255
+ elif days_since_update > self.activity_threshold:
256
+ reasons.append(f"inactive for {days_since_update} days")
257
+
258
+ if ticket.state == TicketState.BLOCKED:
259
+ reasons.append("blocked state")
260
+ elif ticket.state == TicketState.WAITING:
261
+ reasons.append("waiting state")
262
+
263
+ if ticket.priority == Priority.LOW:
264
+ reasons.append("low priority")
265
+
266
+ return ", ".join(reasons)
@@ -4,8 +4,9 @@ import asyncio
4
4
  import hashlib
5
5
  import json
6
6
  import time
7
+ from collections.abc import Callable
7
8
  from functools import wraps
8
- from typing import Any, Callable, Optional
9
+ from typing import Any
9
10
 
10
11
 
11
12
  class CacheEntry:
@@ -41,7 +42,7 @@ class MemoryCache:
41
42
  self._default_ttl = default_ttl
42
43
  self._lock = asyncio.Lock()
43
44
 
44
- async def get(self, key: str) -> Optional[Any]:
45
+ async def get(self, key: str) -> Any | None:
45
46
  """Get value from cache.
46
47
 
47
48
  Args:
@@ -60,7 +61,7 @@ class MemoryCache:
60
61
  del self._cache[key]
61
62
  return None
62
63
 
63
- async def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
64
+ async def set(self, key: str, value: Any, ttl: float | None = None) -> None:
64
65
  """Set value in cache.
65
66
 
66
67
  Args:
@@ -114,7 +115,7 @@ class MemoryCache:
114
115
  return len(self._cache)
115
116
 
116
117
  @staticmethod
117
- def generate_key(*args, **kwargs) -> str:
118
+ def generate_key(*args: Any, **kwargs: Any) -> str:
118
119
  """Generate cache key from arguments.
119
120
 
120
121
  Args:
@@ -134,11 +135,11 @@ class MemoryCache:
134
135
 
135
136
 
136
137
  def cache_decorator(
137
- ttl: Optional[float] = None,
138
+ ttl: float | None = None,
138
139
  key_prefix: str = "",
139
- cache_instance: Optional[MemoryCache] = None,
140
+ cache_instance: MemoryCache | None = None,
140
141
  ) -> Callable:
141
- """Decorator for caching async function results.
142
+ """Decorate async function to cache its results.
142
143
 
143
144
  Args:
144
145
  ttl: TTL for cached results
@@ -154,7 +155,7 @@ def cache_decorator(
154
155
 
155
156
  def decorator(func: Callable) -> Callable:
156
157
  @wraps(func)
157
- async def wrapper(*args, **kwargs):
158
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
158
159
  # Generate cache key
159
160
  base_key = MemoryCache.generate_key(*args, **kwargs)
160
161
  cache_key = f"{key_prefix}:{func.__name__}:{base_key}"