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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +796 -46
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +879 -129
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +973 -73
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +2732 -0
- mcp_ticketer/adapters/linear/client.py +344 -0
- mcp_ticketer/adapters/linear/mappers.py +420 -0
- mcp_ticketer/adapters/linear/queries.py +479 -0
- mcp_ticketer/adapters/linear/types.py +360 -0
- mcp_ticketer/adapters/linear.py +10 -2315
- mcp_ticketer/analysis/__init__.py +23 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +888 -151
- mcp_ticketer/cli/diagnostics.py +400 -157
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +616 -0
- mcp_ticketer/cli/main.py +203 -1165
- mcp_ticketer/cli/mcp_configure.py +474 -90
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +418 -0
- mcp_ticketer/cli/platform_installer.py +513 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +90 -65
- mcp_ticketer/cli/ticket_commands.py +1013 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +114 -66
- mcp_ticketer/core/__init__.py +24 -1
- mcp_ticketer/core/adapter.py +250 -16
- mcp_ticketer/core/config.py +145 -37
- mcp_ticketer/core/env_discovery.py +101 -22
- mcp_ticketer/core/env_loader.py +349 -0
- mcp_ticketer/core/exceptions.py +160 -0
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/models.py +280 -28
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +183 -49
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +56 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +95 -25
- mcp_ticketer/queue/queue.py +40 -21
- mcp_ticketer/queue/run_worker.py +6 -1
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +109 -49
- mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
- mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
- mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {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)
|
mcp_ticketer/cache/memory.py
CHANGED
|
@@ -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
|
|
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) ->
|
|
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:
|
|
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:
|
|
138
|
+
ttl: float | None = None,
|
|
138
139
|
key_prefix: str = "",
|
|
139
|
-
cache_instance:
|
|
140
|
+
cache_instance: MemoryCache | None = None,
|
|
140
141
|
) -> Callable:
|
|
141
|
-
"""
|
|
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}"
|