claude-mpm 4.1.4__py3-none-any.whl → 4.1.5__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/tickets.py +365 -784
- claude_mpm/core/output_style_manager.py +24 -0
- claude_mpm/core/unified_agent_registry.py +46 -15
- claude_mpm/services/agents/deployment/agent_discovery_service.py +12 -3
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +172 -233
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +575 -0
- claude_mpm/services/agents/deployment/agent_operation_service.py +573 -0
- claude_mpm/services/agents/deployment/agent_record_service.py +419 -0
- claude_mpm/services/agents/deployment/agent_state_service.py +381 -0
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +4 -2
- claude_mpm/services/infrastructure/__init__.py +31 -5
- claude_mpm/services/infrastructure/monitoring/__init__.py +43 -0
- claude_mpm/services/infrastructure/monitoring/aggregator.py +437 -0
- claude_mpm/services/infrastructure/monitoring/base.py +130 -0
- claude_mpm/services/infrastructure/monitoring/legacy.py +203 -0
- claude_mpm/services/infrastructure/monitoring/network.py +218 -0
- claude_mpm/services/infrastructure/monitoring/process.py +342 -0
- claude_mpm/services/infrastructure/monitoring/resources.py +243 -0
- claude_mpm/services/infrastructure/monitoring/service.py +367 -0
- claude_mpm/services/infrastructure/monitoring.py +67 -1030
- claude_mpm/services/project/analyzer.py +13 -4
- claude_mpm/services/project/analyzer_refactored.py +450 -0
- claude_mpm/services/project/analyzer_v2.py +566 -0
- claude_mpm/services/project/architecture_analyzer.py +461 -0
- claude_mpm/services/project/dependency_analyzer.py +462 -0
- claude_mpm/services/project/language_analyzer.py +265 -0
- claude_mpm/services/project/metrics_collector.py +410 -0
- claude_mpm/services/ticket_manager.py +5 -1
- claude_mpm/services/ticket_services/__init__.py +26 -0
- claude_mpm/services/ticket_services/crud_service.py +328 -0
- claude_mpm/services/ticket_services/formatter_service.py +290 -0
- claude_mpm/services/ticket_services/search_service.py +324 -0
- claude_mpm/services/ticket_services/validation_service.py +303 -0
- claude_mpm/services/ticket_services/workflow_service.py +244 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/RECORD +41 -17
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ticket services for clean separation of concerns.
|
|
3
|
+
|
|
4
|
+
WHY: Extract business logic from the god class TicketsCommand into
|
|
5
|
+
focused, testable service modules following SOLID principles.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Each service has a single responsibility
|
|
9
|
+
- Services use dependency injection for flexibility
|
|
10
|
+
- Clear interfaces for each service
|
|
11
|
+
- Services are stateless for better testability
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .crud_service import TicketCRUDService
|
|
15
|
+
from .formatter_service import TicketFormatterService
|
|
16
|
+
from .search_service import TicketSearchService
|
|
17
|
+
from .validation_service import TicketValidationService
|
|
18
|
+
from .workflow_service import TicketWorkflowService
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"TicketCRUDService",
|
|
22
|
+
"TicketFormatterService",
|
|
23
|
+
"TicketSearchService",
|
|
24
|
+
"TicketValidationService",
|
|
25
|
+
"TicketWorkflowService",
|
|
26
|
+
]
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CRUD operations service for tickets.
|
|
3
|
+
|
|
4
|
+
WHY: Centralizes all Create, Read, Update, Delete operations for tickets,
|
|
5
|
+
separating data access from presentation logic.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Uses TicketManager as backend (can be replaced with actual implementation)
|
|
9
|
+
- Returns standardized response objects
|
|
10
|
+
- Handles aitrackdown CLI fallback for operations not in TicketManager
|
|
11
|
+
- Provides consistent error handling
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from ...core.logger import get_logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TicketCRUDService:
|
|
22
|
+
"""Service for ticket CRUD operations."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, ticket_manager=None):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the CRUD service.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
ticket_manager: Optional ticket manager instance for testing
|
|
30
|
+
"""
|
|
31
|
+
self.logger = get_logger("services.ticket_crud")
|
|
32
|
+
self._ticket_manager = ticket_manager
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def ticket_manager(self):
|
|
36
|
+
"""Lazy load ticket manager."""
|
|
37
|
+
if self._ticket_manager is None:
|
|
38
|
+
try:
|
|
39
|
+
from ..ticket_manager import TicketManager
|
|
40
|
+
except ImportError:
|
|
41
|
+
from claude_mpm.services.ticket_manager import TicketManager
|
|
42
|
+
self._ticket_manager = TicketManager()
|
|
43
|
+
return self._ticket_manager
|
|
44
|
+
|
|
45
|
+
def create_ticket(
|
|
46
|
+
self,
|
|
47
|
+
title: str,
|
|
48
|
+
ticket_type: str = "task",
|
|
49
|
+
priority: str = "medium",
|
|
50
|
+
description: str = "",
|
|
51
|
+
tags: Optional[List[str]] = None,
|
|
52
|
+
parent_epic: Optional[str] = None,
|
|
53
|
+
parent_issue: Optional[str] = None,
|
|
54
|
+
) -> Dict[str, Any]:
|
|
55
|
+
"""
|
|
56
|
+
Create a new ticket.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dict with success status and ticket_id or error message
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
ticket_id = self.ticket_manager.create_ticket(
|
|
63
|
+
title=title,
|
|
64
|
+
ticket_type=ticket_type,
|
|
65
|
+
description=description,
|
|
66
|
+
priority=priority,
|
|
67
|
+
tags=tags or [],
|
|
68
|
+
source="claude-mpm-cli",
|
|
69
|
+
parent_epic=parent_epic,
|
|
70
|
+
parent_issue=parent_issue,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if ticket_id:
|
|
74
|
+
return {
|
|
75
|
+
"success": True,
|
|
76
|
+
"ticket_id": ticket_id,
|
|
77
|
+
"message": f"Created ticket: {ticket_id}",
|
|
78
|
+
}
|
|
79
|
+
return {"success": False, "error": "Failed to create ticket"}
|
|
80
|
+
except Exception as e:
|
|
81
|
+
self.logger.error(f"Error creating ticket: {e}")
|
|
82
|
+
return {"success": False, "error": str(e)}
|
|
83
|
+
|
|
84
|
+
def get_ticket(self, ticket_id: str) -> Optional[Dict[str, Any]]:
|
|
85
|
+
"""
|
|
86
|
+
Get a specific ticket by ID.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Ticket data or None if not found
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
return self.ticket_manager.get_ticket(ticket_id)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
self.logger.error(f"Error getting ticket {ticket_id}: {e}")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def list_tickets(
|
|
98
|
+
self,
|
|
99
|
+
limit: int = 20,
|
|
100
|
+
page: int = 1,
|
|
101
|
+
page_size: int = 20,
|
|
102
|
+
type_filter: str = "all",
|
|
103
|
+
status_filter: str = "all",
|
|
104
|
+
) -> Dict[str, Any]:
|
|
105
|
+
"""
|
|
106
|
+
List tickets with pagination and filtering.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dict with tickets list and pagination info
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
# Try aitrackdown CLI first for better pagination
|
|
113
|
+
tickets = self._list_via_aitrackdown(
|
|
114
|
+
limit, page, page_size, type_filter, status_filter
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if tickets is None:
|
|
118
|
+
# Fallback to TicketManager
|
|
119
|
+
tickets = self._list_via_manager(
|
|
120
|
+
limit, page, page_size, type_filter, status_filter
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"success": True,
|
|
125
|
+
"tickets": tickets,
|
|
126
|
+
"page": page,
|
|
127
|
+
"page_size": page_size,
|
|
128
|
+
"total_shown": len(tickets),
|
|
129
|
+
}
|
|
130
|
+
except Exception as e:
|
|
131
|
+
self.logger.error(f"Error listing tickets: {e}")
|
|
132
|
+
return {"success": False, "error": str(e), "tickets": []}
|
|
133
|
+
|
|
134
|
+
def _list_via_aitrackdown(
|
|
135
|
+
self,
|
|
136
|
+
limit: int,
|
|
137
|
+
page: int,
|
|
138
|
+
page_size: int,
|
|
139
|
+
type_filter: str,
|
|
140
|
+
status_filter: str,
|
|
141
|
+
) -> Optional[List[Dict]]:
|
|
142
|
+
"""List tickets using aitrackdown CLI."""
|
|
143
|
+
try:
|
|
144
|
+
cmd = ["aitrackdown", "status", "tasks"]
|
|
145
|
+
|
|
146
|
+
# Calculate offset for pagination
|
|
147
|
+
offset = (page - 1) * page_size
|
|
148
|
+
total_needed = offset + page_size
|
|
149
|
+
cmd.extend(["--limit", str(total_needed * 2)])
|
|
150
|
+
|
|
151
|
+
# Add filters
|
|
152
|
+
if type_filter != "all":
|
|
153
|
+
cmd.extend(["--type", type_filter])
|
|
154
|
+
if status_filter != "all":
|
|
155
|
+
cmd.extend(["--status", status_filter])
|
|
156
|
+
|
|
157
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
158
|
+
|
|
159
|
+
if result.stdout.strip():
|
|
160
|
+
all_tickets = json.loads(result.stdout)
|
|
161
|
+
if isinstance(all_tickets, list):
|
|
162
|
+
# Apply pagination
|
|
163
|
+
return all_tickets[offset : offset + page_size]
|
|
164
|
+
return []
|
|
165
|
+
except (
|
|
166
|
+
subprocess.CalledProcessError,
|
|
167
|
+
FileNotFoundError,
|
|
168
|
+
json.JSONDecodeError,
|
|
169
|
+
) as e:
|
|
170
|
+
self.logger.debug(f"aitrackdown not available: {e}")
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
def _list_via_manager(
|
|
174
|
+
self,
|
|
175
|
+
limit: int,
|
|
176
|
+
page: int,
|
|
177
|
+
page_size: int,
|
|
178
|
+
type_filter: str,
|
|
179
|
+
status_filter: str,
|
|
180
|
+
) -> List[Dict]:
|
|
181
|
+
"""List tickets using TicketManager."""
|
|
182
|
+
all_tickets = self.ticket_manager.list_recent_tickets(limit=limit * 2)
|
|
183
|
+
|
|
184
|
+
# Apply filters
|
|
185
|
+
filtered_tickets = []
|
|
186
|
+
for ticket in all_tickets:
|
|
187
|
+
if type_filter != "all":
|
|
188
|
+
ticket_type = ticket.get("metadata", {}).get("ticket_type", "unknown")
|
|
189
|
+
if ticket_type != type_filter:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if status_filter != "all":
|
|
193
|
+
if ticket.get("status") != status_filter:
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
filtered_tickets.append(ticket)
|
|
197
|
+
|
|
198
|
+
# Apply pagination
|
|
199
|
+
offset = (page - 1) * page_size
|
|
200
|
+
return filtered_tickets[offset : offset + page_size]
|
|
201
|
+
|
|
202
|
+
def update_ticket(
|
|
203
|
+
self,
|
|
204
|
+
ticket_id: str,
|
|
205
|
+
status: Optional[str] = None,
|
|
206
|
+
priority: Optional[str] = None,
|
|
207
|
+
description: Optional[str] = None,
|
|
208
|
+
tags: Optional[List[str]] = None,
|
|
209
|
+
assignees: Optional[List[str]] = None,
|
|
210
|
+
) -> Dict[str, Any]:
|
|
211
|
+
"""
|
|
212
|
+
Update a ticket's properties.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dict with success status and message
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
updates = {}
|
|
219
|
+
if status:
|
|
220
|
+
updates["status"] = status
|
|
221
|
+
if priority:
|
|
222
|
+
updates["priority"] = priority
|
|
223
|
+
if description:
|
|
224
|
+
updates["description"] = description
|
|
225
|
+
if tags:
|
|
226
|
+
updates["tags"] = tags
|
|
227
|
+
if assignees:
|
|
228
|
+
updates["assignees"] = assignees
|
|
229
|
+
|
|
230
|
+
if not updates:
|
|
231
|
+
return {"success": False, "error": "No updates specified"}
|
|
232
|
+
|
|
233
|
+
# Try TicketManager first
|
|
234
|
+
success = self.ticket_manager.update_task(ticket_id, **updates)
|
|
235
|
+
|
|
236
|
+
if success:
|
|
237
|
+
return {"success": True, "message": f"Updated ticket: {ticket_id}"}
|
|
238
|
+
|
|
239
|
+
# Fallback to aitrackdown CLI for status transitions
|
|
240
|
+
if status:
|
|
241
|
+
return self._update_via_aitrackdown(ticket_id, status, updates)
|
|
242
|
+
|
|
243
|
+
return {"success": False, "error": f"Failed to update ticket: {ticket_id}"}
|
|
244
|
+
except Exception as e:
|
|
245
|
+
self.logger.error(f"Error updating ticket {ticket_id}: {e}")
|
|
246
|
+
return {"success": False, "error": str(e)}
|
|
247
|
+
|
|
248
|
+
def _update_via_aitrackdown(
|
|
249
|
+
self, ticket_id: str, status: str, updates: Dict
|
|
250
|
+
) -> Dict[str, Any]:
|
|
251
|
+
"""Update ticket using aitrackdown CLI."""
|
|
252
|
+
try:
|
|
253
|
+
cmd = ["aitrackdown", "transition", ticket_id, status]
|
|
254
|
+
|
|
255
|
+
# Add comment with other updates
|
|
256
|
+
comment_parts = []
|
|
257
|
+
if updates.get("priority"):
|
|
258
|
+
comment_parts.append(f"Priority: {updates['priority']}")
|
|
259
|
+
if updates.get("assignees"):
|
|
260
|
+
comment_parts.append(f"Assigned to: {', '.join(updates['assignees'])}")
|
|
261
|
+
if updates.get("tags"):
|
|
262
|
+
comment_parts.append(f"Tags: {', '.join(updates['tags'])}")
|
|
263
|
+
|
|
264
|
+
if comment_parts:
|
|
265
|
+
comment = " | ".join(comment_parts)
|
|
266
|
+
cmd.extend(["--comment", comment])
|
|
267
|
+
|
|
268
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
269
|
+
return {"success": True, "message": f"Updated ticket: {ticket_id}"}
|
|
270
|
+
except subprocess.CalledProcessError as e:
|
|
271
|
+
self.logger.error(f"Failed to update via CLI: {e}")
|
|
272
|
+
return {"success": False, "error": f"Failed to update ticket: {ticket_id}"}
|
|
273
|
+
|
|
274
|
+
def close_ticket(
|
|
275
|
+
self, ticket_id: str, resolution: Optional[str] = None
|
|
276
|
+
) -> Dict[str, Any]:
|
|
277
|
+
"""
|
|
278
|
+
Close a ticket.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Dict with success status and message
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
# Try TicketManager first
|
|
285
|
+
success = self.ticket_manager.close_task(ticket_id, resolution=resolution)
|
|
286
|
+
|
|
287
|
+
if success:
|
|
288
|
+
return {"success": True, "message": f"Closed ticket: {ticket_id}"}
|
|
289
|
+
|
|
290
|
+
# Fallback to aitrackdown CLI
|
|
291
|
+
return self._close_via_aitrackdown(ticket_id, resolution)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
self.logger.error(f"Error closing ticket {ticket_id}: {e}")
|
|
294
|
+
return {"success": False, "error": str(e)}
|
|
295
|
+
|
|
296
|
+
def _close_via_aitrackdown(
|
|
297
|
+
self, ticket_id: str, resolution: Optional[str]
|
|
298
|
+
) -> Dict[str, Any]:
|
|
299
|
+
"""Close ticket using aitrackdown CLI."""
|
|
300
|
+
try:
|
|
301
|
+
cmd = ["aitrackdown", "close", ticket_id]
|
|
302
|
+
if resolution:
|
|
303
|
+
cmd.extend(["--comment", resolution])
|
|
304
|
+
|
|
305
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
306
|
+
return {"success": True, "message": f"Closed ticket: {ticket_id}"}
|
|
307
|
+
except subprocess.CalledProcessError:
|
|
308
|
+
return {"success": False, "error": f"Failed to close ticket: {ticket_id}"}
|
|
309
|
+
|
|
310
|
+
def delete_ticket(self, ticket_id: str, force: bool = False) -> Dict[str, Any]:
|
|
311
|
+
"""
|
|
312
|
+
Delete a ticket.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Dict with success status and message
|
|
316
|
+
"""
|
|
317
|
+
try:
|
|
318
|
+
cmd = ["aitrackdown", "delete", ticket_id]
|
|
319
|
+
if force:
|
|
320
|
+
cmd.append("--force")
|
|
321
|
+
|
|
322
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
323
|
+
return {"success": True, "message": f"Deleted ticket: {ticket_id}"}
|
|
324
|
+
except subprocess.CalledProcessError:
|
|
325
|
+
return {"success": False, "error": f"Failed to delete ticket: {ticket_id}"}
|
|
326
|
+
except Exception as e:
|
|
327
|
+
self.logger.error(f"Error deleting ticket {ticket_id}: {e}")
|
|
328
|
+
return {"success": False, "error": str(e)}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output formatting service for tickets.
|
|
3
|
+
|
|
4
|
+
WHY: Separates presentation logic from business logic, allowing for
|
|
5
|
+
consistent formatting across different output modes and contexts.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Supports multiple output formats (text, json, yaml, table)
|
|
9
|
+
- Uses emoji for visual status indicators
|
|
10
|
+
- Handles pagination display
|
|
11
|
+
- Provides consistent formatting patterns
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TicketFormatterService:
|
|
18
|
+
"""Service for formatting ticket output."""
|
|
19
|
+
|
|
20
|
+
# Status emoji mapping for visual indicators
|
|
21
|
+
STATUS_EMOJI = {
|
|
22
|
+
"open": "🔵",
|
|
23
|
+
"in_progress": "🟡",
|
|
24
|
+
"done": "🟢",
|
|
25
|
+
"closed": "⚫",
|
|
26
|
+
"blocked": "🔴",
|
|
27
|
+
"ready": "🟣",
|
|
28
|
+
"tested": "🟢",
|
|
29
|
+
"waiting": "🟠",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
DEFAULT_EMOJI = "⚪"
|
|
33
|
+
|
|
34
|
+
def format_ticket_created(
|
|
35
|
+
self, ticket_id: str, verbose: bool = False, **metadata
|
|
36
|
+
) -> List[str]:
|
|
37
|
+
"""
|
|
38
|
+
Format output for created ticket.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of formatted output lines
|
|
42
|
+
"""
|
|
43
|
+
lines = [f"✅ Created ticket: {ticket_id}"]
|
|
44
|
+
|
|
45
|
+
if verbose:
|
|
46
|
+
if metadata.get("type"):
|
|
47
|
+
lines.append(f" Type: {metadata['type']}")
|
|
48
|
+
if metadata.get("priority"):
|
|
49
|
+
lines.append(f" Priority: {metadata['priority']}")
|
|
50
|
+
if metadata.get("tags"):
|
|
51
|
+
lines.append(f" Tags: {', '.join(metadata['tags'])}")
|
|
52
|
+
if metadata.get("parent_epic"):
|
|
53
|
+
lines.append(f" Parent Epic: {metadata['parent_epic']}")
|
|
54
|
+
if metadata.get("parent_issue"):
|
|
55
|
+
lines.append(f" Parent Issue: {metadata['parent_issue']}")
|
|
56
|
+
|
|
57
|
+
return lines
|
|
58
|
+
|
|
59
|
+
def format_ticket_list(
|
|
60
|
+
self,
|
|
61
|
+
tickets: List[Dict[str, Any]],
|
|
62
|
+
page: int = 1,
|
|
63
|
+
page_size: int = 20,
|
|
64
|
+
verbose: bool = False,
|
|
65
|
+
) -> List[str]:
|
|
66
|
+
"""
|
|
67
|
+
Format a list of tickets for display.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of formatted output lines
|
|
71
|
+
"""
|
|
72
|
+
if not tickets:
|
|
73
|
+
return ["No tickets found matching criteria"]
|
|
74
|
+
|
|
75
|
+
total_shown = len(tickets)
|
|
76
|
+
lines = [f"Tickets (page {page}, showing {total_shown} tickets):", "-" * 80]
|
|
77
|
+
|
|
78
|
+
for ticket in tickets:
|
|
79
|
+
# Get status emoji
|
|
80
|
+
status = ticket.get("status", "unknown")
|
|
81
|
+
emoji = self.STATUS_EMOJI.get(status, self.DEFAULT_EMOJI)
|
|
82
|
+
|
|
83
|
+
lines.append(f"{emoji} [{ticket['id']}] {ticket['title']}")
|
|
84
|
+
|
|
85
|
+
if verbose:
|
|
86
|
+
ticket_type = ticket.get("metadata", {}).get("ticket_type", "task")
|
|
87
|
+
priority = ticket.get("priority", "medium")
|
|
88
|
+
lines.append(
|
|
89
|
+
f" Type: {ticket_type} | Status: {status} | Priority: {priority}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if ticket.get("tags"):
|
|
93
|
+
lines.append(f" Tags: {', '.join(ticket['tags'])}")
|
|
94
|
+
|
|
95
|
+
lines.append(f" Created: {ticket.get('created_at', 'Unknown')}")
|
|
96
|
+
lines.append("")
|
|
97
|
+
|
|
98
|
+
# Add pagination hints
|
|
99
|
+
if total_shown == page_size:
|
|
100
|
+
lines.extend(
|
|
101
|
+
[
|
|
102
|
+
"-" * 80,
|
|
103
|
+
f"📄 Page {page} | Showing {total_shown} tickets",
|
|
104
|
+
f"💡 Next page: claude-mpm tickets list --page {page + 1} --page-size {page_size}",
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
if page > 1:
|
|
108
|
+
lines.append(
|
|
109
|
+
f"💡 Previous page: claude-mpm tickets list --page {page - 1} --page-size {page_size}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return lines
|
|
113
|
+
|
|
114
|
+
def format_ticket_detail(
|
|
115
|
+
self, ticket: Dict[str, Any], verbose: bool = False
|
|
116
|
+
) -> List[str]:
|
|
117
|
+
"""
|
|
118
|
+
Format a single ticket's details for display.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of formatted output lines
|
|
122
|
+
"""
|
|
123
|
+
if not ticket:
|
|
124
|
+
return ["❌ Ticket not found"]
|
|
125
|
+
|
|
126
|
+
lines = [
|
|
127
|
+
f"Ticket: {ticket['id']}",
|
|
128
|
+
"=" * 80,
|
|
129
|
+
f"Title: {ticket['title']}",
|
|
130
|
+
f"Type: {ticket.get('metadata', {}).get('ticket_type', 'unknown')}",
|
|
131
|
+
f"Status: {ticket['status']}",
|
|
132
|
+
f"Priority: {ticket['priority']}",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
if ticket.get("tags"):
|
|
136
|
+
lines.append(f"Tags: {', '.join(ticket['tags'])}")
|
|
137
|
+
|
|
138
|
+
if ticket.get("assignees"):
|
|
139
|
+
lines.append(f"Assignees: {', '.join(ticket['assignees'])}")
|
|
140
|
+
|
|
141
|
+
# Show parent references
|
|
142
|
+
metadata = ticket.get("metadata", {})
|
|
143
|
+
if metadata.get("parent_epic"):
|
|
144
|
+
lines.append(f"Parent Epic: {metadata['parent_epic']}")
|
|
145
|
+
if metadata.get("parent_issue"):
|
|
146
|
+
lines.append(f"Parent Issue: {metadata['parent_issue']}")
|
|
147
|
+
|
|
148
|
+
lines.extend(
|
|
149
|
+
[
|
|
150
|
+
"",
|
|
151
|
+
"Description:",
|
|
152
|
+
"-" * 40,
|
|
153
|
+
ticket.get("description", "No description"),
|
|
154
|
+
"",
|
|
155
|
+
f"Created: {ticket['created_at']}",
|
|
156
|
+
f"Updated: {ticket['updated_at']}",
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if verbose and metadata:
|
|
161
|
+
lines.extend(["", "Metadata:", "-" * 40])
|
|
162
|
+
for key, value in metadata.items():
|
|
163
|
+
if key not in ["parent_epic", "parent_issue", "ticket_type"]:
|
|
164
|
+
lines.append(f" {key}: {value}")
|
|
165
|
+
|
|
166
|
+
return lines
|
|
167
|
+
|
|
168
|
+
def format_search_results(
|
|
169
|
+
self, tickets: List[Dict[str, Any]], query: str, show_snippets: bool = True
|
|
170
|
+
) -> List[str]:
|
|
171
|
+
"""
|
|
172
|
+
Format search results with optional context snippets.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List of formatted output lines
|
|
176
|
+
"""
|
|
177
|
+
if not tickets:
|
|
178
|
+
return [f"No tickets found matching '{query}'"]
|
|
179
|
+
|
|
180
|
+
lines = [f"Search results for '{query}' (showing {len(tickets)}):", "-" * 80]
|
|
181
|
+
|
|
182
|
+
for ticket in tickets:
|
|
183
|
+
status = ticket.get("status", "unknown")
|
|
184
|
+
emoji = self.STATUS_EMOJI.get(status, self.DEFAULT_EMOJI)
|
|
185
|
+
|
|
186
|
+
lines.append(f"{emoji} [{ticket['id']}] {ticket['title']}")
|
|
187
|
+
|
|
188
|
+
if show_snippets:
|
|
189
|
+
# Show snippet if query appears in description
|
|
190
|
+
desc = ticket.get("description", "")
|
|
191
|
+
if query.lower() in desc.lower():
|
|
192
|
+
snippet = self._get_search_snippet(desc, query)
|
|
193
|
+
lines.append(f" {snippet}")
|
|
194
|
+
|
|
195
|
+
return lines
|
|
196
|
+
|
|
197
|
+
def _get_search_snippet(
|
|
198
|
+
self, text: str, query: str, context_chars: int = 30
|
|
199
|
+
) -> str:
|
|
200
|
+
"""
|
|
201
|
+
Extract a snippet of text around the search query.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Formatted snippet with ellipsis if truncated
|
|
205
|
+
"""
|
|
206
|
+
lower_text = text.lower()
|
|
207
|
+
query_lower = query.lower()
|
|
208
|
+
|
|
209
|
+
if query_lower not in lower_text:
|
|
210
|
+
return ""
|
|
211
|
+
|
|
212
|
+
idx = lower_text.index(query_lower)
|
|
213
|
+
start = max(0, idx - context_chars)
|
|
214
|
+
end = min(len(text), idx + len(query) + context_chars)
|
|
215
|
+
|
|
216
|
+
snippet = text[start:end]
|
|
217
|
+
|
|
218
|
+
if start > 0:
|
|
219
|
+
snippet = "..." + snippet
|
|
220
|
+
if end < len(text):
|
|
221
|
+
snippet = snippet + "..."
|
|
222
|
+
|
|
223
|
+
return snippet
|
|
224
|
+
|
|
225
|
+
def format_operation_result(
|
|
226
|
+
self,
|
|
227
|
+
operation: str,
|
|
228
|
+
ticket_id: str,
|
|
229
|
+
success: bool,
|
|
230
|
+
message: Optional[str] = None,
|
|
231
|
+
) -> str:
|
|
232
|
+
"""
|
|
233
|
+
Format the result of a ticket operation.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Formatted result message
|
|
237
|
+
"""
|
|
238
|
+
if success:
|
|
239
|
+
emoji = "✅"
|
|
240
|
+
default_messages = {
|
|
241
|
+
"update": f"Updated ticket: {ticket_id}",
|
|
242
|
+
"close": f"Closed ticket: {ticket_id}",
|
|
243
|
+
"delete": f"Deleted ticket: {ticket_id}",
|
|
244
|
+
"comment": f"Added comment to ticket: {ticket_id}",
|
|
245
|
+
"workflow": f"Updated workflow state for {ticket_id}",
|
|
246
|
+
}
|
|
247
|
+
msg = message or default_messages.get(
|
|
248
|
+
operation, f"Operation completed: {ticket_id}"
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
emoji = "❌"
|
|
252
|
+
default_messages = {
|
|
253
|
+
"update": f"Failed to update ticket: {ticket_id}",
|
|
254
|
+
"close": f"Failed to close ticket: {ticket_id}",
|
|
255
|
+
"delete": f"Failed to delete ticket: {ticket_id}",
|
|
256
|
+
"comment": f"Failed to add comment to ticket: {ticket_id}",
|
|
257
|
+
"workflow": f"Failed to update workflow state for ticket: {ticket_id}",
|
|
258
|
+
}
|
|
259
|
+
msg = message or default_messages.get(
|
|
260
|
+
operation, f"Operation failed: {ticket_id}"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return f"{emoji} {msg}"
|
|
264
|
+
|
|
265
|
+
def format_error(self, error: str) -> str:
|
|
266
|
+
"""
|
|
267
|
+
Format an error message.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Formatted error message
|
|
271
|
+
"""
|
|
272
|
+
return f"❌ {error}"
|
|
273
|
+
|
|
274
|
+
def format_info(self, info: str) -> str:
|
|
275
|
+
"""
|
|
276
|
+
Format an informational message.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Formatted info message
|
|
280
|
+
"""
|
|
281
|
+
return f"ℹ️ {info}"
|
|
282
|
+
|
|
283
|
+
def format_warning(self, warning: str) -> str:
|
|
284
|
+
"""
|
|
285
|
+
Format a warning message.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Formatted warning message
|
|
289
|
+
"""
|
|
290
|
+
return f"⚠️ {warning}"
|