mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__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 +394 -9
- 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 +836 -105
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +772 -1
- mcp_ticketer/adapters/linear/adapter.py +2293 -108
- mcp_ticketer/adapters/linear/client.py +146 -12
- mcp_ticketer/adapters/linear/mappers.py +105 -11
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +18 -6
- mcp_ticketer/cli/codex_configure.py +175 -60
- mcp_ticketer/cli/configure.py +884 -146
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +31 -28
- mcp_ticketer/cli/discover.py +293 -21
- mcp_ticketer/cli/gemini_configure.py +18 -6
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +109 -2055
- mcp_ticketer/cli/mcp_configure.py +673 -99
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +13 -11
- mcp_ticketer/cli/ticket_commands.py +277 -36
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +35 -1
- mcp_ticketer/core/adapter.py +170 -5
- mcp_ticketer/core/config.py +38 -31
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +10 -4
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +32 -20
- mcp_ticketer/core/models.py +136 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +148 -14
- mcp_ticketer/core/registry.py +1 -1
- 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 +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +187 -93
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +37 -9
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +15 -13
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Session state management for tracking current ticket associations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Session timeout: 30 minutes of inactivity
|
|
14
|
+
SESSION_TIMEOUT_MINUTES = 30
|
|
15
|
+
SESSION_STATE_FILE = ".mcp-ticketer/session.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SessionState:
|
|
20
|
+
"""Track session-specific state for ticket associations."""
|
|
21
|
+
|
|
22
|
+
session_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
23
|
+
current_ticket: str | None = None # Current ticket ID
|
|
24
|
+
ticket_opted_out: bool = False # User explicitly chose "none"
|
|
25
|
+
last_activity: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict[str, Any]:
|
|
28
|
+
"""Serialize to dictionary."""
|
|
29
|
+
return {
|
|
30
|
+
"session_id": self.session_id,
|
|
31
|
+
"current_ticket": self.current_ticket,
|
|
32
|
+
"ticket_opted_out": self.ticket_opted_out,
|
|
33
|
+
"last_activity": self.last_activity,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_dict(cls, data: dict[str, Any]) -> "SessionState":
|
|
38
|
+
"""Deserialize from dictionary."""
|
|
39
|
+
return cls(
|
|
40
|
+
session_id=data.get("session_id", str(uuid.uuid4())),
|
|
41
|
+
current_ticket=data.get("current_ticket"),
|
|
42
|
+
ticket_opted_out=data.get("ticket_opted_out", False),
|
|
43
|
+
last_activity=data.get("last_activity", datetime.now().isoformat()),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def is_expired(self) -> bool:
|
|
47
|
+
"""Check if session has expired due to inactivity."""
|
|
48
|
+
try:
|
|
49
|
+
last_activity = datetime.fromisoformat(self.last_activity)
|
|
50
|
+
timeout = timedelta(minutes=SESSION_TIMEOUT_MINUTES)
|
|
51
|
+
return datetime.now() - last_activity > timeout
|
|
52
|
+
except (ValueError, TypeError):
|
|
53
|
+
# Invalid timestamp, consider expired
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
def touch(self) -> None:
|
|
57
|
+
"""Update last activity timestamp."""
|
|
58
|
+
self.last_activity = datetime.now().isoformat()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SessionStateManager:
|
|
62
|
+
"""Manage session state persistence and lifecycle."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, project_path: Path | None = None):
|
|
65
|
+
"""Initialize session state manager.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
project_path: Project root directory (defaults to current directory)
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
self.project_path = project_path or Path.cwd()
|
|
72
|
+
self.state_file = self.project_path / SESSION_STATE_FILE
|
|
73
|
+
|
|
74
|
+
def load_session(self) -> SessionState:
|
|
75
|
+
"""Load session state from file.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
SessionState instance (creates new if expired or not found)
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
if not self.state_file.exists():
|
|
82
|
+
logger.debug("No session state file found, creating new session")
|
|
83
|
+
return SessionState()
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(self.state_file) as f:
|
|
87
|
+
data = json.load(f)
|
|
88
|
+
|
|
89
|
+
state = SessionState.from_dict(data)
|
|
90
|
+
|
|
91
|
+
# Check if session expired
|
|
92
|
+
if state.is_expired():
|
|
93
|
+
logger.info(
|
|
94
|
+
f"Session {state.session_id} expired after "
|
|
95
|
+
f"{SESSION_TIMEOUT_MINUTES} minutes, creating new session"
|
|
96
|
+
)
|
|
97
|
+
return SessionState()
|
|
98
|
+
|
|
99
|
+
# Touch to update activity
|
|
100
|
+
state.touch()
|
|
101
|
+
return state
|
|
102
|
+
|
|
103
|
+
except (json.JSONDecodeError, FileNotFoundError, KeyError) as e:
|
|
104
|
+
logger.warning(f"Failed to load session state: {e}, creating new session")
|
|
105
|
+
return SessionState()
|
|
106
|
+
|
|
107
|
+
def save_session(self, state: SessionState) -> None:
|
|
108
|
+
"""Save session state to file.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
state: SessionState to persist
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
# Ensure directory exists
|
|
116
|
+
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
# Touch before saving
|
|
119
|
+
state.touch()
|
|
120
|
+
|
|
121
|
+
# Write state
|
|
122
|
+
with open(self.state_file, "w") as f:
|
|
123
|
+
json.dump(state.to_dict(), f, indent=2)
|
|
124
|
+
|
|
125
|
+
logger.debug(f"Saved session state: session_id={state.session_id}")
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Failed to save session state: {e}")
|
|
129
|
+
|
|
130
|
+
def clear_session(self) -> None:
|
|
131
|
+
"""Clear session state (delete file)."""
|
|
132
|
+
try:
|
|
133
|
+
if self.state_file.exists():
|
|
134
|
+
self.state_file.unlink()
|
|
135
|
+
logger.info("Session state cleared")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"Failed to clear session state: {e}")
|
|
138
|
+
|
|
139
|
+
def get_current_ticket(self) -> str | None:
|
|
140
|
+
"""Get current ticket for this session (convenience method).
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Current ticket ID or None
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
state = self.load_session()
|
|
147
|
+
|
|
148
|
+
# If user opted out, return None
|
|
149
|
+
if state.ticket_opted_out:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
return state.current_ticket
|
|
153
|
+
|
|
154
|
+
def set_current_ticket(self, ticket_id: str | None) -> None:
|
|
155
|
+
"""Set current ticket for this session (convenience method).
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
ticket_id: Ticket ID to set (None to clear)
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
state = self.load_session()
|
|
162
|
+
state.current_ticket = ticket_id
|
|
163
|
+
state.ticket_opted_out = False # Clear opt-out when setting ticket
|
|
164
|
+
self.save_session(state)
|
|
165
|
+
|
|
166
|
+
def opt_out_ticket(self) -> None:
|
|
167
|
+
"""Mark that user doesn't want to associate work with a ticket (convenience method)."""
|
|
168
|
+
state = self.load_session()
|
|
169
|
+
state.current_ticket = None
|
|
170
|
+
state.ticket_opted_out = True
|
|
171
|
+
self.save_session(state)
|