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.
Files changed (41) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/tickets.py +365 -784
  3. claude_mpm/core/output_style_manager.py +24 -0
  4. claude_mpm/core/unified_agent_registry.py +46 -15
  5. claude_mpm/services/agents/deployment/agent_discovery_service.py +12 -3
  6. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +172 -233
  7. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +575 -0
  8. claude_mpm/services/agents/deployment/agent_operation_service.py +573 -0
  9. claude_mpm/services/agents/deployment/agent_record_service.py +419 -0
  10. claude_mpm/services/agents/deployment/agent_state_service.py +381 -0
  11. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +4 -2
  12. claude_mpm/services/infrastructure/__init__.py +31 -5
  13. claude_mpm/services/infrastructure/monitoring/__init__.py +43 -0
  14. claude_mpm/services/infrastructure/monitoring/aggregator.py +437 -0
  15. claude_mpm/services/infrastructure/monitoring/base.py +130 -0
  16. claude_mpm/services/infrastructure/monitoring/legacy.py +203 -0
  17. claude_mpm/services/infrastructure/monitoring/network.py +218 -0
  18. claude_mpm/services/infrastructure/monitoring/process.py +342 -0
  19. claude_mpm/services/infrastructure/monitoring/resources.py +243 -0
  20. claude_mpm/services/infrastructure/monitoring/service.py +367 -0
  21. claude_mpm/services/infrastructure/monitoring.py +67 -1030
  22. claude_mpm/services/project/analyzer.py +13 -4
  23. claude_mpm/services/project/analyzer_refactored.py +450 -0
  24. claude_mpm/services/project/analyzer_v2.py +566 -0
  25. claude_mpm/services/project/architecture_analyzer.py +461 -0
  26. claude_mpm/services/project/dependency_analyzer.py +462 -0
  27. claude_mpm/services/project/language_analyzer.py +265 -0
  28. claude_mpm/services/project/metrics_collector.py +410 -0
  29. claude_mpm/services/ticket_manager.py +5 -1
  30. claude_mpm/services/ticket_services/__init__.py +26 -0
  31. claude_mpm/services/ticket_services/crud_service.py +328 -0
  32. claude_mpm/services/ticket_services/formatter_service.py +290 -0
  33. claude_mpm/services/ticket_services/search_service.py +324 -0
  34. claude_mpm/services/ticket_services/validation_service.py +303 -0
  35. claude_mpm/services/ticket_services/workflow_service.py +244 -0
  36. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/METADATA +1 -1
  37. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/RECORD +41 -17
  38. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/WHEEL +0 -0
  39. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/entry_points.txt +0 -0
  40. {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.5.dist-info}/licenses/LICENSE +0 -0
  41. {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}"