mcp-ticketer 0.12.0__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.

Files changed (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {mcp_ticketer-0.12.0.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)