mcp-ticketer 0.1.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 +27 -0
- mcp_ticketer/__version__.py +40 -0
- mcp_ticketer/adapters/__init__.py +8 -0
- mcp_ticketer/adapters/aitrackdown.py +396 -0
- mcp_ticketer/adapters/github.py +974 -0
- mcp_ticketer/adapters/jira.py +831 -0
- mcp_ticketer/adapters/linear.py +1355 -0
- mcp_ticketer/cache/__init__.py +5 -0
- mcp_ticketer/cache/memory.py +193 -0
- mcp_ticketer/cli/__init__.py +5 -0
- mcp_ticketer/cli/main.py +812 -0
- mcp_ticketer/cli/queue_commands.py +285 -0
- mcp_ticketer/cli/utils.py +523 -0
- mcp_ticketer/core/__init__.py +15 -0
- mcp_ticketer/core/adapter.py +211 -0
- mcp_ticketer/core/config.py +403 -0
- mcp_ticketer/core/http_client.py +430 -0
- mcp_ticketer/core/mappers.py +492 -0
- mcp_ticketer/core/models.py +111 -0
- mcp_ticketer/core/registry.py +128 -0
- mcp_ticketer/mcp/__init__.py +5 -0
- mcp_ticketer/mcp/server.py +459 -0
- mcp_ticketer/py.typed +0 -0
- mcp_ticketer/queue/__init__.py +7 -0
- mcp_ticketer/queue/__main__.py +6 -0
- mcp_ticketer/queue/manager.py +261 -0
- mcp_ticketer/queue/queue.py +357 -0
- mcp_ticketer/queue/run_worker.py +38 -0
- mcp_ticketer/queue/worker.py +425 -0
- mcp_ticketer-0.1.1.dist-info/METADATA +362 -0
- mcp_ticketer-0.1.1.dist-info/RECORD +35 -0
- mcp_ticketer-0.1.1.dist-info/WHEEL +5 -0
- mcp_ticketer-0.1.1.dist-info/entry_points.txt +3 -0
- mcp_ticketer-0.1.1.dist-info/licenses/LICENSE +21 -0
- mcp_ticketer-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""Centralized CLI utilities and common patterns."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional, Dict, Any, Callable, TypeVar, List
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from functools import wraps
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
15
|
+
|
|
16
|
+
from ..core import Task, TicketState, Priority, AdapterRegistry
|
|
17
|
+
from ..core.config import get_config, get_adapter_config
|
|
18
|
+
from ..queue import Queue, WorkerManager, QueueStatus
|
|
19
|
+
|
|
20
|
+
# Type variable for async functions
|
|
21
|
+
T = TypeVar('T')
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CommonPatterns:
|
|
27
|
+
"""Common CLI patterns and utilities."""
|
|
28
|
+
|
|
29
|
+
# Configuration file management
|
|
30
|
+
CONFIG_FILE = Path.home() / ".mcp-ticketer" / "config.json"
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def load_config() -> dict:
|
|
34
|
+
"""Load configuration from file."""
|
|
35
|
+
if CommonPatterns.CONFIG_FILE.exists():
|
|
36
|
+
with open(CommonPatterns.CONFIG_FILE, "r") as f:
|
|
37
|
+
return json.load(f)
|
|
38
|
+
return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def save_config(config: dict) -> None:
|
|
42
|
+
"""Save configuration to file."""
|
|
43
|
+
CommonPatterns.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
with open(CommonPatterns.CONFIG_FILE, "w") as f:
|
|
45
|
+
json.dump(config, f, indent=2)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def merge_config(updates: dict) -> dict:
|
|
49
|
+
"""Merge updates into existing config."""
|
|
50
|
+
config = CommonPatterns.load_config()
|
|
51
|
+
|
|
52
|
+
# Handle default_adapter
|
|
53
|
+
if "default_adapter" in updates:
|
|
54
|
+
config["default_adapter"] = updates["default_adapter"]
|
|
55
|
+
|
|
56
|
+
# Handle adapter-specific configurations
|
|
57
|
+
if "adapters" in updates:
|
|
58
|
+
if "adapters" not in config:
|
|
59
|
+
config["adapters"] = {}
|
|
60
|
+
for adapter_name, adapter_config in updates["adapters"].items():
|
|
61
|
+
if adapter_name not in config["adapters"]:
|
|
62
|
+
config["adapters"][adapter_name] = {}
|
|
63
|
+
config["adapters"][adapter_name].update(adapter_config)
|
|
64
|
+
|
|
65
|
+
return config
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def get_adapter(override_adapter: Optional[str] = None, override_config: Optional[dict] = None):
|
|
69
|
+
"""Get configured adapter instance with environment variable support."""
|
|
70
|
+
config = CommonPatterns.load_config()
|
|
71
|
+
|
|
72
|
+
# Use override adapter if provided, otherwise use default
|
|
73
|
+
if override_adapter:
|
|
74
|
+
adapter_type = override_adapter
|
|
75
|
+
# If we have a stored config for this adapter, use it
|
|
76
|
+
adapters_config = config.get("adapters", {})
|
|
77
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
78
|
+
# Override with provided config if any
|
|
79
|
+
if override_config:
|
|
80
|
+
adapter_config.update(override_config)
|
|
81
|
+
else:
|
|
82
|
+
# Use default adapter from config
|
|
83
|
+
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
84
|
+
# Get config for the default adapter
|
|
85
|
+
adapters_config = config.get("adapters", {})
|
|
86
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
87
|
+
|
|
88
|
+
# Fallback to legacy config format for backward compatibility
|
|
89
|
+
if not adapter_config and "config" in config:
|
|
90
|
+
adapter_config = config["config"]
|
|
91
|
+
|
|
92
|
+
# Add environment variables for authentication
|
|
93
|
+
adapter_config = CommonPatterns._add_env_auth(adapter_type, adapter_config)
|
|
94
|
+
|
|
95
|
+
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _add_env_auth(adapter_type: str, adapter_config: dict) -> dict:
|
|
99
|
+
"""Add environment variable authentication to adapter config."""
|
|
100
|
+
auth_mapping = {
|
|
101
|
+
"linear": [("api_key", "LINEAR_API_KEY")],
|
|
102
|
+
"github": [("api_key", "GITHUB_TOKEN"), ("token", "GITHUB_TOKEN")],
|
|
103
|
+
"jira": [("api_token", "JIRA_ACCESS_TOKEN"), ("email", "JIRA_ACCESS_USER")],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if adapter_type in auth_mapping:
|
|
107
|
+
for config_key, env_var in auth_mapping[adapter_type]:
|
|
108
|
+
if not adapter_config.get(config_key):
|
|
109
|
+
env_value = os.getenv(env_var)
|
|
110
|
+
if env_value:
|
|
111
|
+
adapter_config[config_key] = env_value
|
|
112
|
+
|
|
113
|
+
return adapter_config
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def queue_operation(
|
|
117
|
+
ticket_data: Dict[str, Any],
|
|
118
|
+
operation: str,
|
|
119
|
+
adapter_name: Optional[str] = None,
|
|
120
|
+
show_progress: bool = True
|
|
121
|
+
) -> str:
|
|
122
|
+
"""Queue an operation and optionally start the worker."""
|
|
123
|
+
if not adapter_name:
|
|
124
|
+
config = CommonPatterns.load_config()
|
|
125
|
+
adapter_name = config.get("default_adapter", "aitrackdown")
|
|
126
|
+
|
|
127
|
+
# Add to queue
|
|
128
|
+
queue = Queue()
|
|
129
|
+
queue_id = queue.add(
|
|
130
|
+
ticket_data=ticket_data,
|
|
131
|
+
adapter=adapter_name,
|
|
132
|
+
operation=operation
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if show_progress:
|
|
136
|
+
console.print(f"[green]✓[/green] Queued {operation}: {queue_id}")
|
|
137
|
+
console.print("[dim]Use 'mcp-ticketer check {queue_id}' to check progress[/dim]")
|
|
138
|
+
|
|
139
|
+
# Start worker if needed
|
|
140
|
+
manager = WorkerManager()
|
|
141
|
+
if manager.start_if_needed():
|
|
142
|
+
if show_progress:
|
|
143
|
+
console.print("[dim]Worker started to process request[/dim]")
|
|
144
|
+
|
|
145
|
+
return queue_id
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def display_ticket_table(tickets: List[Task], title: str = "Tickets") -> None:
|
|
149
|
+
"""Display tickets in a formatted table."""
|
|
150
|
+
if not tickets:
|
|
151
|
+
console.print("[yellow]No tickets found[/yellow]")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
table = Table(title=title)
|
|
155
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
156
|
+
table.add_column("Title", style="white")
|
|
157
|
+
table.add_column("State", style="green")
|
|
158
|
+
table.add_column("Priority", style="yellow")
|
|
159
|
+
table.add_column("Assignee", style="blue")
|
|
160
|
+
|
|
161
|
+
for ticket in tickets:
|
|
162
|
+
table.add_row(
|
|
163
|
+
ticket.id or "N/A",
|
|
164
|
+
ticket.title,
|
|
165
|
+
ticket.state,
|
|
166
|
+
ticket.priority,
|
|
167
|
+
ticket.assignee or "-",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
console.print(table)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def display_ticket_details(ticket: Task, comments: Optional[List] = None) -> None:
|
|
174
|
+
"""Display detailed ticket information."""
|
|
175
|
+
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
176
|
+
console.print(f"Title: {ticket.title}")
|
|
177
|
+
console.print(f"State: [green]{ticket.state}[/green]")
|
|
178
|
+
console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
|
|
179
|
+
|
|
180
|
+
if ticket.description:
|
|
181
|
+
console.print(f"\n[dim]Description:[/dim]")
|
|
182
|
+
console.print(ticket.description)
|
|
183
|
+
|
|
184
|
+
if ticket.tags:
|
|
185
|
+
console.print(f"\nTags: {', '.join(ticket.tags)}")
|
|
186
|
+
|
|
187
|
+
if ticket.assignee:
|
|
188
|
+
console.print(f"Assignee: {ticket.assignee}")
|
|
189
|
+
|
|
190
|
+
# Display comments if provided
|
|
191
|
+
if comments:
|
|
192
|
+
console.print(f"\n[bold]Comments ({len(comments)}):[/bold]")
|
|
193
|
+
for comment in comments:
|
|
194
|
+
console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
|
|
195
|
+
console.print(comment.content)
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def display_queue_status() -> None:
|
|
199
|
+
"""Display queue and worker status."""
|
|
200
|
+
queue = Queue()
|
|
201
|
+
manager = WorkerManager()
|
|
202
|
+
|
|
203
|
+
# Get queue stats
|
|
204
|
+
stats = queue.get_stats()
|
|
205
|
+
pending = stats.get(QueueStatus.PENDING.value, 0)
|
|
206
|
+
|
|
207
|
+
# Show queue status
|
|
208
|
+
console.print("[bold]Queue Status:[/bold]")
|
|
209
|
+
console.print(f" Pending: {pending}")
|
|
210
|
+
console.print(f" Processing: {stats.get(QueueStatus.PROCESSING.value, 0)}")
|
|
211
|
+
console.print(f" Completed: {stats.get(QueueStatus.COMPLETED.value, 0)}")
|
|
212
|
+
console.print(f" Failed: {stats.get(QueueStatus.FAILED.value, 0)}")
|
|
213
|
+
|
|
214
|
+
# Show worker status
|
|
215
|
+
worker_status = manager.get_status()
|
|
216
|
+
if worker_status["running"]:
|
|
217
|
+
console.print(f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})")
|
|
218
|
+
else:
|
|
219
|
+
console.print("\n[red]○ Worker is not running[/red]")
|
|
220
|
+
if pending > 0:
|
|
221
|
+
console.print("[yellow]Note: There are pending items. Start worker with 'mcp-ticketer queue start'[/yellow]")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def async_command(f: Callable[..., T]) -> Callable[..., T]:
|
|
225
|
+
"""Decorator to handle async CLI commands."""
|
|
226
|
+
@wraps(f)
|
|
227
|
+
def wrapper(*args, **kwargs):
|
|
228
|
+
return asyncio.run(f(*args, **kwargs))
|
|
229
|
+
return wrapper
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def with_adapter(f: Callable) -> Callable:
|
|
233
|
+
"""Decorator to inject adapter instance into CLI commands."""
|
|
234
|
+
@wraps(f)
|
|
235
|
+
def wrapper(adapter: Optional[str] = None, *args, **kwargs):
|
|
236
|
+
adapter_instance = CommonPatterns.get_adapter(override_adapter=adapter)
|
|
237
|
+
return f(adapter_instance, *args, **kwargs)
|
|
238
|
+
return wrapper
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def with_progress(message: str = "Processing..."):
|
|
242
|
+
"""Decorator to show progress spinner for long-running operations."""
|
|
243
|
+
def decorator(f: Callable) -> Callable:
|
|
244
|
+
@wraps(f)
|
|
245
|
+
def wrapper(*args, **kwargs):
|
|
246
|
+
with Progress(
|
|
247
|
+
SpinnerColumn(),
|
|
248
|
+
TextColumn("[progress.description]{task.description}"),
|
|
249
|
+
transient=True,
|
|
250
|
+
) as progress:
|
|
251
|
+
progress.add_task(description=message, total=None)
|
|
252
|
+
return f(*args, **kwargs)
|
|
253
|
+
return wrapper
|
|
254
|
+
return decorator
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def validate_required_fields(**field_map):
|
|
258
|
+
"""Decorator to validate required fields are provided."""
|
|
259
|
+
def decorator(f: Callable) -> Callable:
|
|
260
|
+
@wraps(f)
|
|
261
|
+
def wrapper(*args, **kwargs):
|
|
262
|
+
missing_fields = []
|
|
263
|
+
for field_name, display_name in field_map.items():
|
|
264
|
+
if field_name in kwargs and kwargs[field_name] is None:
|
|
265
|
+
missing_fields.append(display_name)
|
|
266
|
+
|
|
267
|
+
if missing_fields:
|
|
268
|
+
console.print(f"[red]Error:[/red] Missing required fields: {', '.join(missing_fields)}")
|
|
269
|
+
raise typer.Exit(1)
|
|
270
|
+
|
|
271
|
+
return f(*args, **kwargs)
|
|
272
|
+
return wrapper
|
|
273
|
+
return decorator
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def handle_adapter_errors(f: Callable) -> Callable:
|
|
277
|
+
"""Decorator to handle common adapter errors gracefully."""
|
|
278
|
+
@wraps(f)
|
|
279
|
+
def wrapper(*args, **kwargs):
|
|
280
|
+
try:
|
|
281
|
+
return f(*args, **kwargs)
|
|
282
|
+
except ConnectionError as e:
|
|
283
|
+
console.print(f"[red]Connection Error:[/red] {e}")
|
|
284
|
+
console.print("Check your network connection and adapter configuration")
|
|
285
|
+
raise typer.Exit(1)
|
|
286
|
+
except ValueError as e:
|
|
287
|
+
console.print(f"[red]Configuration Error:[/red] {e}")
|
|
288
|
+
console.print("Run 'mcp-ticketer init' to configure your adapter")
|
|
289
|
+
raise typer.Exit(1)
|
|
290
|
+
except Exception as e:
|
|
291
|
+
console.print(f"[red]Unexpected Error:[/red] {e}")
|
|
292
|
+
raise typer.Exit(1)
|
|
293
|
+
return wrapper
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class ConfigValidator:
|
|
297
|
+
"""Configuration validation utilities."""
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def validate_adapter_config(adapter_type: str, config: dict) -> List[str]:
|
|
301
|
+
"""Validate adapter configuration and return list of issues."""
|
|
302
|
+
issues = []
|
|
303
|
+
|
|
304
|
+
validation_rules = {
|
|
305
|
+
"github": [
|
|
306
|
+
("token", "GitHub token"),
|
|
307
|
+
("owner", "Repository owner"),
|
|
308
|
+
("repo", "Repository name"),
|
|
309
|
+
],
|
|
310
|
+
"jira": [
|
|
311
|
+
("server", "JIRA server URL"),
|
|
312
|
+
("email", "User email"),
|
|
313
|
+
("api_token", "API token"),
|
|
314
|
+
],
|
|
315
|
+
"linear": [
|
|
316
|
+
("api_key", "API key"),
|
|
317
|
+
("team_key", "Team key"),
|
|
318
|
+
],
|
|
319
|
+
"aitrackdown": [
|
|
320
|
+
("base_path", "Base path"),
|
|
321
|
+
],
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if adapter_type in validation_rules:
|
|
325
|
+
for field, display_name in validation_rules[adapter_type]:
|
|
326
|
+
if not config.get(field):
|
|
327
|
+
env_var = ConfigValidator._get_env_var(adapter_type, field)
|
|
328
|
+
if env_var and not os.getenv(env_var):
|
|
329
|
+
issues.append(f"Missing {display_name} (config.{field} or {env_var})")
|
|
330
|
+
|
|
331
|
+
return issues
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def _get_env_var(adapter_type: str, field: str) -> Optional[str]:
|
|
335
|
+
"""Get corresponding environment variable name for a config field."""
|
|
336
|
+
env_mapping = {
|
|
337
|
+
"github": {"token": "GITHUB_TOKEN", "owner": "GITHUB_OWNER", "repo": "GITHUB_REPO"},
|
|
338
|
+
"jira": {"api_token": "JIRA_API_TOKEN", "email": "JIRA_EMAIL", "server": "JIRA_SERVER"},
|
|
339
|
+
"linear": {"api_key": "LINEAR_API_KEY"},
|
|
340
|
+
}
|
|
341
|
+
return env_mapping.get(adapter_type, {}).get(field)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class CommandBuilder:
|
|
345
|
+
"""Builder for common CLI command patterns."""
|
|
346
|
+
|
|
347
|
+
def __init__(self):
|
|
348
|
+
self._validators = []
|
|
349
|
+
self._handlers = []
|
|
350
|
+
self._decorators = []
|
|
351
|
+
|
|
352
|
+
def with_adapter_validation(self):
|
|
353
|
+
"""Add adapter configuration validation."""
|
|
354
|
+
self._validators.append(self._validate_adapter)
|
|
355
|
+
return self
|
|
356
|
+
|
|
357
|
+
def with_async_support(self):
|
|
358
|
+
"""Add async support to command."""
|
|
359
|
+
self._decorators.append(async_command)
|
|
360
|
+
return self
|
|
361
|
+
|
|
362
|
+
def with_error_handling(self):
|
|
363
|
+
"""Add error handling decorator."""
|
|
364
|
+
self._decorators.append(handle_adapter_errors)
|
|
365
|
+
return self
|
|
366
|
+
|
|
367
|
+
def with_progress(self, message: str = "Processing..."):
|
|
368
|
+
"""Add progress spinner."""
|
|
369
|
+
self._decorators.append(with_progress(message))
|
|
370
|
+
return self
|
|
371
|
+
|
|
372
|
+
def build(self, func: Callable) -> Callable:
|
|
373
|
+
"""Build the decorated function."""
|
|
374
|
+
decorated_func = func
|
|
375
|
+
for decorator in reversed(self._decorators):
|
|
376
|
+
decorated_func = decorator(decorated_func)
|
|
377
|
+
return decorated_func
|
|
378
|
+
|
|
379
|
+
def _validate_adapter(self, *args, **kwargs):
|
|
380
|
+
"""Validate adapter configuration."""
|
|
381
|
+
config = CommonPatterns.load_config()
|
|
382
|
+
default_adapter = config.get("default_adapter", "aitrackdown")
|
|
383
|
+
adapter_config = config.get("adapters", {}).get(default_adapter, {})
|
|
384
|
+
|
|
385
|
+
issues = ConfigValidator.validate_adapter_config(default_adapter, adapter_config)
|
|
386
|
+
if issues:
|
|
387
|
+
console.print("[red]Configuration Issues:[/red]")
|
|
388
|
+
for issue in issues:
|
|
389
|
+
console.print(f" • {issue}")
|
|
390
|
+
console.print("Run 'mcp-ticketer init' to fix configuration")
|
|
391
|
+
raise typer.Exit(1)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def create_standard_ticket_command(operation: str):
|
|
395
|
+
"""Create a standard ticket operation command."""
|
|
396
|
+
def command_template(
|
|
397
|
+
ticket_id: Optional[str] = None,
|
|
398
|
+
title: Optional[str] = None,
|
|
399
|
+
description: Optional[str] = None,
|
|
400
|
+
priority: Optional[Priority] = None,
|
|
401
|
+
state: Optional[TicketState] = None,
|
|
402
|
+
assignee: Optional[str] = None,
|
|
403
|
+
tags: Optional[List[str]] = None,
|
|
404
|
+
adapter: Optional[str] = None,
|
|
405
|
+
):
|
|
406
|
+
"""Template for ticket commands."""
|
|
407
|
+
# Build ticket data
|
|
408
|
+
ticket_data = {}
|
|
409
|
+
if ticket_id:
|
|
410
|
+
ticket_data["ticket_id"] = ticket_id
|
|
411
|
+
if title:
|
|
412
|
+
ticket_data["title"] = title
|
|
413
|
+
if description:
|
|
414
|
+
ticket_data["description"] = description
|
|
415
|
+
if priority:
|
|
416
|
+
ticket_data["priority"] = priority.value if hasattr(priority, 'value') else priority
|
|
417
|
+
if state:
|
|
418
|
+
ticket_data["state"] = state.value if hasattr(state, 'value') else state
|
|
419
|
+
if assignee:
|
|
420
|
+
ticket_data["assignee"] = assignee
|
|
421
|
+
if tags:
|
|
422
|
+
ticket_data["tags"] = tags
|
|
423
|
+
|
|
424
|
+
# Queue the operation
|
|
425
|
+
queue_id = CommonPatterns.queue_operation(ticket_data, operation, adapter)
|
|
426
|
+
|
|
427
|
+
# Show confirmation
|
|
428
|
+
console.print(f"[green]✓[/green] Queued {operation}: {queue_id}")
|
|
429
|
+
return queue_id
|
|
430
|
+
|
|
431
|
+
return command_template
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# Reusable command components
|
|
435
|
+
class TicketCommands:
|
|
436
|
+
"""Reusable ticket command implementations."""
|
|
437
|
+
|
|
438
|
+
@staticmethod
|
|
439
|
+
@async_command
|
|
440
|
+
@handle_adapter_errors
|
|
441
|
+
async def list_tickets(
|
|
442
|
+
adapter_instance,
|
|
443
|
+
state: Optional[TicketState] = None,
|
|
444
|
+
priority: Optional[Priority] = None,
|
|
445
|
+
limit: int = 10
|
|
446
|
+
):
|
|
447
|
+
"""List tickets with filters."""
|
|
448
|
+
filters = {}
|
|
449
|
+
if state:
|
|
450
|
+
filters["state"] = state
|
|
451
|
+
if priority:
|
|
452
|
+
filters["priority"] = priority
|
|
453
|
+
|
|
454
|
+
tickets = await adapter_instance.list(limit=limit, filters=filters)
|
|
455
|
+
CommonPatterns.display_ticket_table(tickets)
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
@async_command
|
|
459
|
+
@handle_adapter_errors
|
|
460
|
+
async def show_ticket(
|
|
461
|
+
adapter_instance,
|
|
462
|
+
ticket_id: str,
|
|
463
|
+
show_comments: bool = False
|
|
464
|
+
):
|
|
465
|
+
"""Show ticket details."""
|
|
466
|
+
ticket = await adapter_instance.read(ticket_id)
|
|
467
|
+
if not ticket:
|
|
468
|
+
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
469
|
+
raise typer.Exit(1)
|
|
470
|
+
|
|
471
|
+
comments = None
|
|
472
|
+
if show_comments:
|
|
473
|
+
comments = await adapter_instance.get_comments(ticket_id)
|
|
474
|
+
|
|
475
|
+
CommonPatterns.display_ticket_details(ticket, comments)
|
|
476
|
+
|
|
477
|
+
@staticmethod
|
|
478
|
+
def create_ticket(
|
|
479
|
+
title: str,
|
|
480
|
+
description: Optional[str] = None,
|
|
481
|
+
priority: Priority = Priority.MEDIUM,
|
|
482
|
+
tags: Optional[List[str]] = None,
|
|
483
|
+
assignee: Optional[str] = None,
|
|
484
|
+
adapter: Optional[str] = None
|
|
485
|
+
) -> str:
|
|
486
|
+
"""Create a new ticket."""
|
|
487
|
+
ticket_data = {
|
|
488
|
+
"title": title,
|
|
489
|
+
"description": description,
|
|
490
|
+
"priority": priority.value if isinstance(priority, Priority) else priority,
|
|
491
|
+
"tags": tags or [],
|
|
492
|
+
"assignee": assignee,
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return CommonPatterns.queue_operation(ticket_data, "create", adapter)
|
|
496
|
+
|
|
497
|
+
@staticmethod
|
|
498
|
+
def update_ticket(
|
|
499
|
+
ticket_id: str,
|
|
500
|
+
updates: Dict[str, Any],
|
|
501
|
+
adapter: Optional[str] = None
|
|
502
|
+
) -> str:
|
|
503
|
+
"""Update a ticket."""
|
|
504
|
+
if not updates:
|
|
505
|
+
console.print("[yellow]No updates specified[/yellow]")
|
|
506
|
+
raise typer.Exit(1)
|
|
507
|
+
|
|
508
|
+
updates["ticket_id"] = ticket_id
|
|
509
|
+
return CommonPatterns.queue_operation(updates, "update", adapter)
|
|
510
|
+
|
|
511
|
+
@staticmethod
|
|
512
|
+
def transition_ticket(
|
|
513
|
+
ticket_id: str,
|
|
514
|
+
state: TicketState,
|
|
515
|
+
adapter: Optional[str] = None
|
|
516
|
+
) -> str:
|
|
517
|
+
"""Transition ticket state."""
|
|
518
|
+
ticket_data = {
|
|
519
|
+
"ticket_id": ticket_id,
|
|
520
|
+
"state": state.value if hasattr(state, 'value') else state
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return CommonPatterns.queue_operation(ticket_data, "transition", adapter)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Core models and abstractions for MCP Ticketer."""
|
|
2
|
+
|
|
3
|
+
from .models import Epic, Task, Comment, TicketState, Priority
|
|
4
|
+
from .adapter import BaseAdapter
|
|
5
|
+
from .registry import AdapterRegistry
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Epic",
|
|
9
|
+
"Task",
|
|
10
|
+
"Comment",
|
|
11
|
+
"TicketState",
|
|
12
|
+
"Priority",
|
|
13
|
+
"BaseAdapter",
|
|
14
|
+
"AdapterRegistry",
|
|
15
|
+
]
|