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,285 @@
|
|
|
1
|
+
"""Queue-related CLI commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich import print as rprint
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from ..queue import Queue, QueueStatus, WorkerManager, Worker
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(
|
|
12
|
+
name="queue",
|
|
13
|
+
help="Queue management commands"
|
|
14
|
+
)
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("list")
|
|
19
|
+
def list_queue(
|
|
20
|
+
status: QueueStatus = typer.Option(
|
|
21
|
+
None,
|
|
22
|
+
"--status",
|
|
23
|
+
"-s",
|
|
24
|
+
help="Filter by status"
|
|
25
|
+
),
|
|
26
|
+
limit: int = typer.Option(
|
|
27
|
+
25,
|
|
28
|
+
"--limit",
|
|
29
|
+
"-l",
|
|
30
|
+
help="Maximum items to show"
|
|
31
|
+
)
|
|
32
|
+
):
|
|
33
|
+
"""List queue items."""
|
|
34
|
+
queue = Queue()
|
|
35
|
+
items = queue.list_items(status=status, limit=limit)
|
|
36
|
+
|
|
37
|
+
if not items:
|
|
38
|
+
console.print("[yellow]No items in queue[/yellow]")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Create table
|
|
42
|
+
table = Table(title=f"Queue Items ({len(items)} shown)")
|
|
43
|
+
table.add_column("Queue ID", style="cyan", no_wrap=True)
|
|
44
|
+
table.add_column("Operation", style="white")
|
|
45
|
+
table.add_column("Adapter", style="blue")
|
|
46
|
+
table.add_column("Status", style="green")
|
|
47
|
+
table.add_column("Created", style="yellow")
|
|
48
|
+
table.add_column("Retries", style="red")
|
|
49
|
+
|
|
50
|
+
for item in items:
|
|
51
|
+
# Format status with color
|
|
52
|
+
if item.status == QueueStatus.COMPLETED:
|
|
53
|
+
status_str = f"[green]{item.status}[/green]"
|
|
54
|
+
elif item.status == QueueStatus.FAILED:
|
|
55
|
+
status_str = f"[red]{item.status}[/red]"
|
|
56
|
+
elif item.status == QueueStatus.PROCESSING:
|
|
57
|
+
status_str = f"[yellow]{item.status}[/yellow]"
|
|
58
|
+
else:
|
|
59
|
+
status_str = item.status
|
|
60
|
+
|
|
61
|
+
# Format time
|
|
62
|
+
created_str = item.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
63
|
+
|
|
64
|
+
table.add_row(
|
|
65
|
+
item.id,
|
|
66
|
+
item.operation,
|
|
67
|
+
item.adapter,
|
|
68
|
+
status_str,
|
|
69
|
+
created_str,
|
|
70
|
+
str(item.retry_count)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
console.print(table)
|
|
74
|
+
|
|
75
|
+
# Show summary
|
|
76
|
+
stats = queue.get_stats()
|
|
77
|
+
console.print("\n[bold]Queue Summary:[/bold]")
|
|
78
|
+
console.print(f" Pending: {stats.get('pending', 0)}")
|
|
79
|
+
console.print(f" Processing: {stats.get('processing', 0)}")
|
|
80
|
+
console.print(f" Completed: {stats.get('completed', 0)}")
|
|
81
|
+
console.print(f" Failed: {stats.get('failed', 0)}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command("retry")
|
|
85
|
+
def retry_item(
|
|
86
|
+
queue_id: str = typer.Argument(..., help="Queue ID to retry")
|
|
87
|
+
):
|
|
88
|
+
"""Retry a failed queue item."""
|
|
89
|
+
queue = Queue()
|
|
90
|
+
item = queue.get_item(queue_id)
|
|
91
|
+
|
|
92
|
+
if not item:
|
|
93
|
+
console.print(f"[red]Queue item not found: {queue_id}[/red]")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
|
|
96
|
+
if item.status != QueueStatus.FAILED:
|
|
97
|
+
console.print(f"[yellow]Item {queue_id} is not failed (status: {item.status})[/yellow]")
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
# Reset to pending
|
|
101
|
+
queue.update_status(queue_id, QueueStatus.PENDING, error_message=None)
|
|
102
|
+
console.print(f"[green]✓[/green] Queue item {queue_id} reset for retry")
|
|
103
|
+
|
|
104
|
+
# Start worker if needed
|
|
105
|
+
manager = WorkerManager()
|
|
106
|
+
if manager.start_if_needed():
|
|
107
|
+
console.print("[dim]Worker started to process retry[/dim]")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command("clear")
|
|
111
|
+
def clear_queue(
|
|
112
|
+
status: QueueStatus = typer.Option(
|
|
113
|
+
None,
|
|
114
|
+
"--status",
|
|
115
|
+
"-s",
|
|
116
|
+
help="Clear only items with this status"
|
|
117
|
+
),
|
|
118
|
+
days: int = typer.Option(
|
|
119
|
+
7,
|
|
120
|
+
"--days",
|
|
121
|
+
"-d",
|
|
122
|
+
help="Clear items older than this many days"
|
|
123
|
+
),
|
|
124
|
+
confirm: bool = typer.Option(
|
|
125
|
+
False,
|
|
126
|
+
"--yes",
|
|
127
|
+
"-y",
|
|
128
|
+
help="Skip confirmation"
|
|
129
|
+
)
|
|
130
|
+
):
|
|
131
|
+
"""Clear old queue items."""
|
|
132
|
+
queue = Queue()
|
|
133
|
+
|
|
134
|
+
if not confirm:
|
|
135
|
+
if status:
|
|
136
|
+
msg = f"Clear all {status} items older than {days} days?"
|
|
137
|
+
else:
|
|
138
|
+
msg = f"Clear all completed/failed items older than {days} days?"
|
|
139
|
+
|
|
140
|
+
if not typer.confirm(msg):
|
|
141
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
142
|
+
raise typer.Exit(0)
|
|
143
|
+
|
|
144
|
+
queue.cleanup_old(days=days)
|
|
145
|
+
console.print(f"[green]✓[/green] Cleared old queue items")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# Worker commands
|
|
149
|
+
worker_app = typer.Typer(
|
|
150
|
+
name="worker",
|
|
151
|
+
help="Worker management commands"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@worker_app.command("start")
|
|
156
|
+
def start_worker():
|
|
157
|
+
"""Start the background worker."""
|
|
158
|
+
manager = WorkerManager()
|
|
159
|
+
|
|
160
|
+
if manager.is_running():
|
|
161
|
+
console.print("[yellow]Worker is already running[/yellow]")
|
|
162
|
+
status = manager.get_status()
|
|
163
|
+
console.print(f"PID: {status.get('pid')}")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
if manager.start():
|
|
167
|
+
console.print("[green]✓[/green] Worker started successfully")
|
|
168
|
+
status = manager.get_status()
|
|
169
|
+
console.print(f"PID: {status.get('pid')}")
|
|
170
|
+
else:
|
|
171
|
+
console.print("[red]✗[/red] Failed to start worker")
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@worker_app.command("stop")
|
|
176
|
+
def stop_worker():
|
|
177
|
+
"""Stop the background worker."""
|
|
178
|
+
manager = WorkerManager()
|
|
179
|
+
|
|
180
|
+
if not manager.is_running():
|
|
181
|
+
console.print("[yellow]Worker is not running[/yellow]")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if manager.stop():
|
|
185
|
+
console.print("[green]✓[/green] Worker stopped successfully")
|
|
186
|
+
else:
|
|
187
|
+
console.print("[red]✗[/red] Failed to stop worker")
|
|
188
|
+
raise typer.Exit(1)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@worker_app.command("restart")
|
|
192
|
+
def restart_worker():
|
|
193
|
+
"""Restart the background worker."""
|
|
194
|
+
manager = WorkerManager()
|
|
195
|
+
|
|
196
|
+
if manager.restart():
|
|
197
|
+
console.print("[green]✓[/green] Worker restarted successfully")
|
|
198
|
+
status = manager.get_status()
|
|
199
|
+
console.print(f"PID: {status.get('pid')}")
|
|
200
|
+
else:
|
|
201
|
+
console.print("[red]✗[/red] Failed to restart worker")
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@worker_app.command("status")
|
|
206
|
+
def worker_status():
|
|
207
|
+
"""Check worker status."""
|
|
208
|
+
manager = WorkerManager()
|
|
209
|
+
status = manager.get_status()
|
|
210
|
+
|
|
211
|
+
if status["running"]:
|
|
212
|
+
console.print(f"[green]● Worker is running[/green]")
|
|
213
|
+
console.print(f" PID: {status.get('pid')}")
|
|
214
|
+
|
|
215
|
+
if "cpu_percent" in status:
|
|
216
|
+
console.print(f" CPU: {status['cpu_percent']:.1f}%")
|
|
217
|
+
console.print(f" Memory: {status['memory_mb']:.1f} MB")
|
|
218
|
+
|
|
219
|
+
# Format uptime
|
|
220
|
+
if "create_time" in status:
|
|
221
|
+
uptime = datetime.now().timestamp() - status["create_time"]
|
|
222
|
+
hours = int(uptime // 3600)
|
|
223
|
+
minutes = int((uptime % 3600) // 60)
|
|
224
|
+
console.print(f" Uptime: {hours}h {minutes}m")
|
|
225
|
+
else:
|
|
226
|
+
console.print("[red]○ Worker is not running[/red]")
|
|
227
|
+
|
|
228
|
+
# Show queue stats
|
|
229
|
+
if "queue" in status:
|
|
230
|
+
console.print("\n[bold]Queue Status:[/bold]")
|
|
231
|
+
queue_stats = status["queue"]
|
|
232
|
+
console.print(f" Pending: {queue_stats.get('pending', 0)}")
|
|
233
|
+
console.print(f" Processing: {queue_stats.get('processing', 0)}")
|
|
234
|
+
console.print(f" Completed: {queue_stats.get('completed', 0)}")
|
|
235
|
+
console.print(f" Failed: {queue_stats.get('failed', 0)}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@worker_app.command("logs")
|
|
239
|
+
def worker_logs(
|
|
240
|
+
lines: int = typer.Option(
|
|
241
|
+
50,
|
|
242
|
+
"--lines",
|
|
243
|
+
"-n",
|
|
244
|
+
help="Number of lines to show"
|
|
245
|
+
),
|
|
246
|
+
follow: bool = typer.Option(
|
|
247
|
+
False,
|
|
248
|
+
"--follow",
|
|
249
|
+
"-f",
|
|
250
|
+
help="Follow log output"
|
|
251
|
+
)
|
|
252
|
+
):
|
|
253
|
+
"""View worker logs."""
|
|
254
|
+
import time
|
|
255
|
+
from pathlib import Path
|
|
256
|
+
|
|
257
|
+
log_file = Path.home() / ".mcp-ticketer" / "logs" / "worker.log"
|
|
258
|
+
|
|
259
|
+
if not log_file.exists():
|
|
260
|
+
console.print("[yellow]No log file found[/yellow]")
|
|
261
|
+
raise typer.Exit(1)
|
|
262
|
+
|
|
263
|
+
if follow:
|
|
264
|
+
# Follow mode - like tail -f
|
|
265
|
+
console.print("[dim]Following worker logs (Ctrl+C to stop)...[/dim]\n")
|
|
266
|
+
try:
|
|
267
|
+
with open(log_file, "r") as f:
|
|
268
|
+
# Go to end of file
|
|
269
|
+
f.seek(0, 2)
|
|
270
|
+
while True:
|
|
271
|
+
line = f.readline()
|
|
272
|
+
if line:
|
|
273
|
+
console.print(line, end="")
|
|
274
|
+
else:
|
|
275
|
+
time.sleep(0.1)
|
|
276
|
+
except KeyboardInterrupt:
|
|
277
|
+
console.print("\n[dim]Stopped following logs[/dim]")
|
|
278
|
+
else:
|
|
279
|
+
# Show last N lines
|
|
280
|
+
logs = Worker.get_logs(lines=lines)
|
|
281
|
+
console.print(logs)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Add worker subcommand to queue app
|
|
285
|
+
app.add_typer(worker_app, name="worker")
|