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,152 @@
1
+ """Pull request integration tools for tickets.
2
+
3
+ This module implements tools for linking tickets with pull requests and
4
+ creating PRs from tickets. Note that PR functionality may not be available
5
+ in all adapters.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from ..server_sdk import get_adapter, mcp
11
+
12
+
13
+ @mcp.tool()
14
+ async def ticket_create_pr(
15
+ ticket_id: str,
16
+ title: str,
17
+ description: str = "",
18
+ source_branch: str | None = None,
19
+ target_branch: str = "main",
20
+ ) -> dict[str, Any]:
21
+ """Create a pull request linked to a ticket.
22
+
23
+ Creates a new pull request and automatically links it to the specified
24
+ ticket. This functionality may not be available in all adapters.
25
+
26
+ Args:
27
+ ticket_id: Unique identifier of the ticket to link the PR to
28
+ title: Pull request title
29
+ description: Pull request description
30
+ source_branch: Source branch for the PR (if not specified, may use ticket ID)
31
+ target_branch: Target branch for the PR (default: main)
32
+
33
+ Returns:
34
+ Created PR details and link information, or error information
35
+
36
+ """
37
+ try:
38
+ adapter = get_adapter()
39
+
40
+ # Check if adapter supports PR operations
41
+ if not hasattr(adapter, "create_pull_request"):
42
+ return {
43
+ "status": "error",
44
+ "error": f"Pull request creation not supported by {type(adapter).__name__} adapter",
45
+ "ticket_id": ticket_id,
46
+ }
47
+
48
+ # Read ticket to validate it exists
49
+ ticket = await adapter.read(ticket_id)
50
+ if ticket is None:
51
+ return {
52
+ "status": "error",
53
+ "error": f"Ticket {ticket_id} not found",
54
+ }
55
+
56
+ # Use ticket ID as source branch if not specified
57
+ if source_branch is None:
58
+ source_branch = f"feature/{ticket_id}"
59
+
60
+ # Create PR via adapter
61
+ pr_data = await adapter.create_pull_request(
62
+ ticket_id=ticket_id,
63
+ title=title,
64
+ description=description,
65
+ source_branch=source_branch,
66
+ target_branch=target_branch,
67
+ )
68
+
69
+ return {
70
+ "status": "completed",
71
+ "ticket_id": ticket_id,
72
+ "pull_request": pr_data,
73
+ }
74
+
75
+ except AttributeError:
76
+ return {
77
+ "status": "error",
78
+ "error": "Pull request creation not supported by this adapter",
79
+ "ticket_id": ticket_id,
80
+ }
81
+ except Exception as e:
82
+ return {
83
+ "status": "error",
84
+ "error": f"Failed to create pull request: {str(e)}",
85
+ "ticket_id": ticket_id,
86
+ }
87
+
88
+
89
+ @mcp.tool()
90
+ async def ticket_link_pr(
91
+ ticket_id: str,
92
+ pr_url: str,
93
+ ) -> dict[str, Any]:
94
+ """Link an existing pull request to a ticket.
95
+
96
+ Associates an existing pull request (identified by URL) with a ticket.
97
+ This is typically done by adding the PR URL to the ticket's metadata
98
+ or as a comment.
99
+
100
+ Args:
101
+ ticket_id: Unique identifier of the ticket
102
+ pr_url: URL of the pull request to link
103
+
104
+ Returns:
105
+ Link confirmation and updated ticket details, or error information
106
+
107
+ """
108
+ try:
109
+ adapter = get_adapter()
110
+
111
+ # Read ticket to validate it exists
112
+ ticket = await adapter.read(ticket_id)
113
+ if ticket is None:
114
+ return {
115
+ "status": "error",
116
+ "error": f"Ticket {ticket_id} not found",
117
+ }
118
+
119
+ # Check if adapter has specialized PR linking
120
+ if hasattr(adapter, "link_pull_request"):
121
+ result = await adapter.link_pull_request(ticket_id=ticket_id, pr_url=pr_url)
122
+ return {
123
+ "status": "completed",
124
+ "ticket_id": ticket_id,
125
+ "pr_url": pr_url,
126
+ "result": result,
127
+ }
128
+
129
+ # Fallback: Add PR link as comment
130
+ from ....core.models import Comment
131
+
132
+ comment = Comment(
133
+ ticket_id=ticket_id,
134
+ content=f"Pull Request: {pr_url}",
135
+ )
136
+
137
+ created_comment = await adapter.add_comment(comment)
138
+
139
+ return {
140
+ "status": "completed",
141
+ "ticket_id": ticket_id,
142
+ "pr_url": pr_url,
143
+ "method": "comment",
144
+ "comment": created_comment.model_dump(),
145
+ }
146
+
147
+ except Exception as e:
148
+ return {
149
+ "status": "error",
150
+ "error": f"Failed to link pull request: {str(e)}",
151
+ "ticket_id": ticket_id,
152
+ }
@@ -0,0 +1,215 @@
1
+ """Search and query tools for finding tickets.
2
+
3
+ This module implements advanced search capabilities for tickets using
4
+ various filters and criteria.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ from ....core.models import Priority, SearchQuery, TicketState
11
+ from ..server_sdk import get_adapter, mcp
12
+
13
+
14
+ @mcp.tool()
15
+ async def ticket_search(
16
+ query: str | None = None,
17
+ state: str | None = None,
18
+ priority: str | None = None,
19
+ tags: list[str] | None = None,
20
+ assignee: str | None = None,
21
+ limit: int = 10,
22
+ ) -> dict[str, Any]:
23
+ """Search tickets using advanced filters.
24
+
25
+ Searches for tickets matching the specified criteria. All filters are
26
+ optional and can be combined.
27
+
28
+ Args:
29
+ query: Text search query to match against title and description
30
+ state: Filter by state - must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked
31
+ priority: Filter by priority - must be one of: low, medium, high, critical
32
+ tags: Filter by tags - tickets must have all specified tags
33
+ assignee: Filter by assigned user ID or email
34
+ limit: Maximum number of results to return (default: 10, max: 100)
35
+
36
+ Returns:
37
+ List of tickets matching search criteria, or error information
38
+
39
+ """
40
+ try:
41
+ adapter = get_adapter()
42
+
43
+ # Add warning for unscoped searches
44
+ if not query and not (state or priority or tags or assignee):
45
+ logging.warning(
46
+ "Unscoped search with no query or filters. "
47
+ "This will search ALL tickets across all projects. "
48
+ "Tip: Configure default_project or default_team for automatic scoping."
49
+ )
50
+
51
+ # Validate and build search query
52
+ state_enum = None
53
+ if state is not None:
54
+ try:
55
+ state_enum = TicketState(state.lower())
56
+ except ValueError:
57
+ return {
58
+ "status": "error",
59
+ "error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
60
+ }
61
+
62
+ priority_enum = None
63
+ if priority is not None:
64
+ try:
65
+ priority_enum = Priority(priority.lower())
66
+ except ValueError:
67
+ return {
68
+ "status": "error",
69
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
70
+ }
71
+
72
+ # Create search query
73
+ search_query = SearchQuery(
74
+ query=query,
75
+ state=state_enum,
76
+ priority=priority_enum,
77
+ tags=tags,
78
+ assignee=assignee,
79
+ limit=min(limit, 100), # Enforce max limit
80
+ )
81
+
82
+ # Execute search via adapter
83
+ results = await adapter.search(search_query)
84
+
85
+ return {
86
+ "status": "completed",
87
+ "tickets": [ticket.model_dump() for ticket in results],
88
+ "count": len(results),
89
+ "query": {
90
+ "text": query,
91
+ "state": state,
92
+ "priority": priority,
93
+ "tags": tags,
94
+ "assignee": assignee,
95
+ },
96
+ }
97
+ except Exception as e:
98
+ return {
99
+ "status": "error",
100
+ "error": f"Failed to search tickets: {str(e)}",
101
+ }
102
+
103
+
104
+ @mcp.tool()
105
+ async def ticket_search_hierarchy(
106
+ query: str,
107
+ include_children: bool = True,
108
+ max_depth: int = 3,
109
+ ) -> dict[str, Any]:
110
+ """Search tickets and include their hierarchy.
111
+
112
+ Performs a text search and returns matching tickets along with their
113
+ hierarchical context (parent epics/issues and child issues/tasks).
114
+
115
+ Args:
116
+ query: Text search query to match against title and description
117
+ include_children: Whether to include child tickets in results
118
+ max_depth: Maximum hierarchy depth to include (1-3, default: 3)
119
+
120
+ Returns:
121
+ List of tickets with hierarchy information, or error information
122
+
123
+ """
124
+ try:
125
+ adapter = get_adapter()
126
+
127
+ # Validate max_depth
128
+ if max_depth < 1 or max_depth > 3:
129
+ return {
130
+ "status": "error",
131
+ "error": "max_depth must be between 1 and 3",
132
+ }
133
+
134
+ # Create search query
135
+ search_query = SearchQuery(
136
+ query=query,
137
+ limit=50, # Reasonable limit for hierarchical search
138
+ )
139
+
140
+ # Execute search via adapter
141
+ results = await adapter.search(search_query)
142
+
143
+ # Build hierarchical results
144
+ hierarchical_results = []
145
+ for ticket in results:
146
+ ticket_data = {
147
+ "ticket": ticket.model_dump(),
148
+ "hierarchy": {},
149
+ }
150
+
151
+ # Get parent epic if applicable
152
+ parent_epic_id = getattr(ticket, "parent_epic", None)
153
+ if parent_epic_id and max_depth >= 2:
154
+ try:
155
+ parent_epic = await adapter.read(parent_epic_id)
156
+ if parent_epic:
157
+ ticket_data["hierarchy"][
158
+ "parent_epic"
159
+ ] = parent_epic.model_dump()
160
+ except Exception:
161
+ pass # Parent not found, continue
162
+
163
+ # Get parent issue if applicable (for tasks)
164
+ parent_issue_id = getattr(ticket, "parent_issue", None)
165
+ if parent_issue_id and max_depth >= 2:
166
+ try:
167
+ parent_issue = await adapter.read(parent_issue_id)
168
+ if parent_issue:
169
+ ticket_data["hierarchy"][
170
+ "parent_issue"
171
+ ] = parent_issue.model_dump()
172
+ except Exception:
173
+ pass # Parent not found, continue
174
+
175
+ # Get children if requested
176
+ if include_children and max_depth >= 2:
177
+ children = []
178
+
179
+ # Get child issues (for epics)
180
+ child_issue_ids = getattr(ticket, "child_issues", [])
181
+ for child_id in child_issue_ids:
182
+ try:
183
+ child = await adapter.read(child_id)
184
+ if child:
185
+ children.append(child.model_dump())
186
+ except Exception:
187
+ pass # Child not found, continue
188
+
189
+ # Get child tasks (for issues)
190
+ child_task_ids = getattr(ticket, "children", [])
191
+ for child_id in child_task_ids:
192
+ try:
193
+ child = await adapter.read(child_id)
194
+ if child:
195
+ children.append(child.model_dump())
196
+ except Exception:
197
+ pass # Child not found, continue
198
+
199
+ if children:
200
+ ticket_data["hierarchy"]["children"] = children
201
+
202
+ hierarchical_results.append(ticket_data)
203
+
204
+ return {
205
+ "status": "completed",
206
+ "results": hierarchical_results,
207
+ "count": len(hierarchical_results),
208
+ "query": query,
209
+ "max_depth": max_depth,
210
+ }
211
+ except Exception as e:
212
+ return {
213
+ "status": "error",
214
+ "error": f"Failed to search with hierarchy: {str(e)}",
215
+ }
@@ -0,0 +1,170 @@
1
+ """MCP tools for session and ticket association management."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from ....core.session_state import SessionStateManager
8
+ from ..server_sdk import mcp
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @mcp.tool()
14
+ async def attach_ticket(
15
+ action: str,
16
+ ticket_id: str | None = None,
17
+ ) -> dict[str, Any]:
18
+ """Associate current work session with a ticket.
19
+
20
+ This tool helps track which ticket your current work is related to.
21
+ The association persists for the session (30 minutes of inactivity).
22
+
23
+ **Important**: It's recommended to associate work with a ticket for proper
24
+ tracking and organization.
25
+
26
+ Actions:
27
+ - **set**: Associate work with a specific ticket
28
+ - **clear**: Remove current ticket association
29
+ - **none**: Opt out of ticket association for this session
30
+ - **status**: Check current ticket association
31
+
32
+ Args:
33
+ action: What to do with the ticket association (set/clear/none/status)
34
+ ticket_id: Ticket ID to associate (e.g., "PROJ-123", UUID), required for 'set'
35
+
36
+ Returns:
37
+ Success status and current session state
38
+
39
+ Examples:
40
+ # Associate with a ticket
41
+ attach_ticket(action="set", ticket_id="PROJ-123")
42
+
43
+ # Opt out for this session
44
+ attach_ticket(action="none")
45
+
46
+ # Check current status
47
+ attach_ticket(action="status")
48
+
49
+ """
50
+ try:
51
+ manager = SessionStateManager(project_path=Path.cwd())
52
+ state = manager.load_session()
53
+
54
+ if action == "set":
55
+ if not ticket_id:
56
+ return {
57
+ "success": False,
58
+ "error": "ticket_id is required when action='set'",
59
+ "guidance": "Please provide a ticket ID to associate with this session",
60
+ }
61
+
62
+ manager.set_current_ticket(ticket_id)
63
+ return {
64
+ "success": True,
65
+ "message": f"Work session now associated with ticket: {ticket_id}",
66
+ "current_ticket": ticket_id,
67
+ "session_id": state.session_id,
68
+ "opted_out": False,
69
+ }
70
+
71
+ elif action == "clear":
72
+ manager.set_current_ticket(None)
73
+ return {
74
+ "success": True,
75
+ "message": "Ticket association cleared",
76
+ "current_ticket": None,
77
+ "session_id": state.session_id,
78
+ "opted_out": False,
79
+ "guidance": "You can associate with a ticket anytime using attach_ticket(action='set', ticket_id='...')",
80
+ }
81
+
82
+ elif action == "none":
83
+ manager.opt_out_ticket()
84
+ return {
85
+ "success": True,
86
+ "message": "Opted out of ticket association for this session",
87
+ "current_ticket": None,
88
+ "session_id": state.session_id,
89
+ "opted_out": True,
90
+ "note": "This opt-out will reset after 30 minutes of inactivity",
91
+ }
92
+
93
+ elif action == "status":
94
+ current_ticket = manager.get_current_ticket()
95
+
96
+ if state.ticket_opted_out:
97
+ status_msg = "No ticket associated (opted out for this session)"
98
+ elif current_ticket:
99
+ status_msg = f"Currently associated with ticket: {current_ticket}"
100
+ else:
101
+ status_msg = "No ticket associated"
102
+
103
+ return {
104
+ "success": True,
105
+ "message": status_msg,
106
+ "current_ticket": current_ticket,
107
+ "session_id": state.session_id,
108
+ "opted_out": state.ticket_opted_out,
109
+ "guidance": (
110
+ (
111
+ "Associate with a ticket: attach_ticket(action='set', ticket_id='...')\n"
112
+ "Opt out: attach_ticket(action='none')"
113
+ )
114
+ if not current_ticket and not state.ticket_opted_out
115
+ else None
116
+ ),
117
+ }
118
+
119
+ else:
120
+ return {
121
+ "success": False,
122
+ "error": f"Invalid action: {action}",
123
+ "valid_actions": ["set", "clear", "none", "status"],
124
+ }
125
+
126
+ except Exception as e:
127
+ logger.error(f"Error in attach_ticket: {e}")
128
+ return {
129
+ "success": False,
130
+ "error": str(e),
131
+ }
132
+
133
+
134
+ @mcp.tool()
135
+ async def get_session_info() -> dict[str, Any]:
136
+ """Get current session information and ticket association status.
137
+
138
+ Returns session metadata including ID, current ticket, and activity status.
139
+
140
+ Returns:
141
+ Session information dictionary
142
+
143
+ Example:
144
+ {
145
+ "session_id": "abc-123",
146
+ "current_ticket": "PROJ-123",
147
+ "opted_out": false,
148
+ "last_activity": "2025-01-19T20:00:00"
149
+ }
150
+
151
+ """
152
+ try:
153
+ manager = SessionStateManager(project_path=Path.cwd())
154
+ state = manager.load_session()
155
+
156
+ return {
157
+ "success": True,
158
+ "session_id": state.session_id,
159
+ "current_ticket": state.current_ticket,
160
+ "opted_out": state.ticket_opted_out,
161
+ "last_activity": state.last_activity,
162
+ "session_timeout_minutes": 30,
163
+ }
164
+
165
+ except Exception as e:
166
+ logger.error(f"Error in get_session_info: {e}")
167
+ return {
168
+ "success": False,
169
+ "error": str(e),
170
+ }