mcp-ticketer 2.0.1__py3-none-any.whl → 2.2.13__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/__version__.py +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +122 -0
- mcp_ticketer/adapters/asana/adapter.py +121 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/adapter.py +1000 -92
- mcp_ticketer/adapters/linear/client.py +91 -1
- mcp_ticketer/adapters/linear/mappers.py +107 -0
- mcp_ticketer/adapters/linear/queries.py +112 -2
- mcp_ticketer/adapters/linear/types.py +50 -10
- mcp_ticketer/cli/configure.py +524 -89
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/main.py +10 -0
- mcp_ticketer/cli/mcp_configure.py +177 -49
- mcp_ticketer/cli/platform_installer.py +9 -0
- mcp_ticketer/cli/setup_command.py +157 -1
- mcp_ticketer/cli/ticket_commands.py +443 -81
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +28 -0
- mcp_ticketer/core/adapter.py +367 -1
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +345 -0
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +6 -1
- mcp_ticketer/core/state_matcher.py +36 -3
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/routing.py +68 -0
- mcp_ticketer/mcp/server/tools/__init__.py +7 -4
- mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
- mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
- mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
- mcp_ticketer/queue/queue.py +68 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
- mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer-2.0.1.dist-info/top_level.txt +0 -1
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -213,11 +213,25 @@ def create(
|
|
|
213
213
|
"--epic",
|
|
214
214
|
help="Parent epic/project ID (synonym for --project)",
|
|
215
215
|
),
|
|
216
|
+
wait: bool = typer.Option(
|
|
217
|
+
False,
|
|
218
|
+
"--wait",
|
|
219
|
+
"-w",
|
|
220
|
+
help="Wait for operation to complete (synchronous mode, returns actual ticket ID)",
|
|
221
|
+
),
|
|
222
|
+
timeout: float = typer.Option(
|
|
223
|
+
30.0,
|
|
224
|
+
"--timeout",
|
|
225
|
+
help="Timeout in seconds for --wait mode (default: 30)",
|
|
226
|
+
),
|
|
227
|
+
output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
216
228
|
adapter: AdapterType | None = typer.Option(
|
|
217
229
|
None, "--adapter", help="Override default adapter"
|
|
218
230
|
),
|
|
219
231
|
) -> None:
|
|
220
232
|
"""Create a new ticket with comprehensive health checks."""
|
|
233
|
+
from .utils import format_error_json, format_json_response, serialize_task
|
|
234
|
+
|
|
221
235
|
# IMMEDIATE HEALTH CHECK - Critical for reliability
|
|
222
236
|
health_monitor = QueueHealthMonitor()
|
|
223
237
|
health = health_monitor.check_health()
|
|
@@ -336,22 +350,35 @@ def create(
|
|
|
336
350
|
|
|
337
351
|
result = asyncio.run(adapter_instance.create(task))
|
|
338
352
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
353
|
+
if output_json:
|
|
354
|
+
data = serialize_task(result)
|
|
355
|
+
console.print(
|
|
356
|
+
format_json_response(
|
|
357
|
+
"success", data, message="Ticket created successfully"
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
console.print(
|
|
362
|
+
f"[green]✓[/green] Ticket created successfully: {result.id}"
|
|
363
|
+
)
|
|
364
|
+
console.print(f" Title: {result.title}")
|
|
365
|
+
console.print(f" Priority: {result.priority}")
|
|
366
|
+
console.print(f" State: {result.state}")
|
|
367
|
+
# Get URL from metadata if available
|
|
368
|
+
if (
|
|
369
|
+
result.metadata
|
|
370
|
+
and "linear" in result.metadata
|
|
371
|
+
and "url" in result.metadata["linear"]
|
|
372
|
+
):
|
|
373
|
+
console.print(f" URL: {result.metadata['linear']['url']}")
|
|
350
374
|
|
|
351
375
|
return result.id
|
|
352
376
|
|
|
353
377
|
except Exception as e:
|
|
354
|
-
|
|
378
|
+
if output_json:
|
|
379
|
+
console.print(format_error_json(e))
|
|
380
|
+
else:
|
|
381
|
+
console.print(f"[red]❌[/red] Failed to create ticket: {e}")
|
|
355
382
|
raise
|
|
356
383
|
|
|
357
384
|
# Use queue for other adapters
|
|
@@ -369,51 +396,113 @@ def create(
|
|
|
369
396
|
queue_id, adapter_name, "create", title, task_data
|
|
370
397
|
)
|
|
371
398
|
|
|
372
|
-
|
|
373
|
-
console.print(f" Title: {title}")
|
|
374
|
-
console.print(f" Priority: {priority}")
|
|
375
|
-
console.print(f" Adapter: {adapter_name}")
|
|
376
|
-
console.print(
|
|
377
|
-
"[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]"
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
# Start worker if needed with immediate feedback
|
|
399
|
+
# Start worker if needed - must happen before polling
|
|
381
400
|
manager = WorkerManager()
|
|
382
401
|
worker_started = manager.start_if_needed()
|
|
383
402
|
|
|
384
403
|
if worker_started:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
# Give immediate feedback on processing
|
|
388
|
-
import time
|
|
389
|
-
|
|
390
|
-
time.sleep(1) # Brief pause to let worker start
|
|
404
|
+
if not output_json:
|
|
405
|
+
console.print("[dim]Worker started to process request[/dim]")
|
|
391
406
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if
|
|
395
|
-
console.print("[green]✓ Item is being processed by worker[/green]")
|
|
396
|
-
elif item and item.status == QueueStatus.PENDING:
|
|
397
|
-
console.print("[yellow]⏳ Item is queued for processing[/yellow]")
|
|
398
|
-
else:
|
|
407
|
+
# SYNCHRONOUS MODE: Poll until completion if --wait flag is set
|
|
408
|
+
if wait:
|
|
409
|
+
if not output_json:
|
|
399
410
|
console.print(
|
|
400
|
-
"[
|
|
411
|
+
f"[yellow]⏳[/yellow] Waiting for operation to complete (timeout: {timeout}s)..."
|
|
401
412
|
)
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
# Poll the queue until operation completes
|
|
416
|
+
completed_item = queue.poll_until_complete(queue_id, timeout=timeout)
|
|
417
|
+
|
|
418
|
+
# Extract result data
|
|
419
|
+
result = completed_item.result
|
|
420
|
+
|
|
421
|
+
# Extract ticket ID from result
|
|
422
|
+
ticket_id = result.get("id") if result else queue_id
|
|
423
|
+
|
|
424
|
+
if output_json:
|
|
425
|
+
# Return actual ticket data in JSON format
|
|
426
|
+
data = result if result else {"queue_id": queue_id}
|
|
427
|
+
console.print(
|
|
428
|
+
format_json_response(
|
|
429
|
+
"success", data, message="Ticket created successfully"
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
else:
|
|
433
|
+
# Display ticket creation success with actual ID
|
|
434
|
+
console.print(
|
|
435
|
+
f"[green]✓[/green] Ticket created successfully: {ticket_id}"
|
|
436
|
+
)
|
|
437
|
+
console.print(f" Title: {title}")
|
|
438
|
+
console.print(f" Priority: {priority}")
|
|
439
|
+
|
|
440
|
+
# Display additional metadata if available
|
|
441
|
+
if result:
|
|
442
|
+
if "url" in result:
|
|
443
|
+
console.print(f" URL: {result['url']}")
|
|
444
|
+
if "state" in result:
|
|
445
|
+
console.print(f" State: {result['state']}")
|
|
446
|
+
|
|
447
|
+
return ticket_id
|
|
448
|
+
|
|
449
|
+
except TimeoutError as e:
|
|
450
|
+
if output_json:
|
|
451
|
+
console.print(format_error_json(str(e), queue_id=queue_id))
|
|
452
|
+
else:
|
|
453
|
+
console.print(f"[red]❌[/red] Operation timed out after {timeout}s")
|
|
454
|
+
console.print(f" Queue ID: {queue_id}")
|
|
455
|
+
console.print(
|
|
456
|
+
f" Use 'mcp-ticketer ticket check {queue_id}' to check status later"
|
|
457
|
+
)
|
|
458
|
+
raise typer.Exit(1) from None
|
|
459
|
+
|
|
460
|
+
except RuntimeError as e:
|
|
461
|
+
if output_json:
|
|
462
|
+
console.print(format_error_json(str(e), queue_id=queue_id))
|
|
463
|
+
else:
|
|
464
|
+
console.print(f"[red]❌[/red] Operation failed: {e}")
|
|
465
|
+
console.print(f" Queue ID: {queue_id}")
|
|
466
|
+
raise typer.Exit(1) from None
|
|
467
|
+
|
|
468
|
+
# ASYNCHRONOUS MODE (default): Return queue ID immediately
|
|
402
469
|
else:
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
470
|
+
if output_json:
|
|
471
|
+
data = {
|
|
472
|
+
"queue_id": queue_id,
|
|
473
|
+
"title": title,
|
|
474
|
+
"priority": priority.value if hasattr(priority, "value") else priority,
|
|
475
|
+
"adapter": adapter_name,
|
|
476
|
+
"status": "queued",
|
|
477
|
+
}
|
|
409
478
|
console.print(
|
|
410
|
-
"
|
|
479
|
+
format_json_response("success", data, message="Ticket creation queued")
|
|
411
480
|
)
|
|
412
481
|
else:
|
|
482
|
+
console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
|
|
483
|
+
console.print(f" Title: {title}")
|
|
484
|
+
console.print(f" Priority: {priority}")
|
|
485
|
+
console.print(f" Adapter: {adapter_name}")
|
|
413
486
|
console.print(
|
|
414
|
-
"[
|
|
487
|
+
"[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]"
|
|
415
488
|
)
|
|
416
489
|
|
|
490
|
+
# Give immediate feedback on processing
|
|
491
|
+
import time
|
|
492
|
+
|
|
493
|
+
time.sleep(1) # Brief pause to let worker start
|
|
494
|
+
|
|
495
|
+
# Check if item is being processed
|
|
496
|
+
item = queue.get_item(queue_id)
|
|
497
|
+
if item and item.status == QueueStatus.PROCESSING:
|
|
498
|
+
console.print("[green]✓ Item is being processed by worker[/green]")
|
|
499
|
+
elif item and item.status == QueueStatus.PENDING:
|
|
500
|
+
console.print("[yellow]⏳ Item is queued for processing[/yellow]")
|
|
501
|
+
else:
|
|
502
|
+
console.print(
|
|
503
|
+
"[red]⚠️ Item status unclear - check with 'mcp-ticketer ticket check {queue_id}'[/red]"
|
|
504
|
+
)
|
|
505
|
+
|
|
417
506
|
|
|
418
507
|
@app.command("list")
|
|
419
508
|
def list_tickets(
|
|
@@ -424,11 +513,13 @@ def list_tickets(
|
|
|
424
513
|
None, "--priority", "-p", help="Filter by priority"
|
|
425
514
|
),
|
|
426
515
|
limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
|
|
516
|
+
output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
427
517
|
adapter: AdapterType | None = typer.Option(
|
|
428
518
|
None, "--adapter", help="Override default adapter"
|
|
429
519
|
),
|
|
430
520
|
) -> None:
|
|
431
521
|
"""List tickets with optional filters."""
|
|
522
|
+
from .utils import format_json_response, serialize_task
|
|
432
523
|
|
|
433
524
|
async def _list() -> list[Any]:
|
|
434
525
|
adapter_instance = get_adapter(
|
|
@@ -444,10 +535,29 @@ def list_tickets(
|
|
|
444
535
|
tickets = asyncio.run(_list())
|
|
445
536
|
|
|
446
537
|
if not tickets:
|
|
447
|
-
|
|
538
|
+
if output_json:
|
|
539
|
+
console.print(
|
|
540
|
+
format_json_response(
|
|
541
|
+
"success", {"tickets": [], "count": 0, "has_more": False}
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
else:
|
|
545
|
+
console.print("[yellow]No tickets found[/yellow]")
|
|
448
546
|
return
|
|
449
547
|
|
|
450
|
-
#
|
|
548
|
+
# JSON output
|
|
549
|
+
if output_json:
|
|
550
|
+
tickets_data = [serialize_task(t) for t in tickets]
|
|
551
|
+
data = {
|
|
552
|
+
"tickets": tickets_data,
|
|
553
|
+
"count": len(tickets_data),
|
|
554
|
+
"has_more": len(tickets)
|
|
555
|
+
>= limit, # Heuristic: if we got exactly limit, there might be more
|
|
556
|
+
}
|
|
557
|
+
console.print(format_json_response("success", data))
|
|
558
|
+
return
|
|
559
|
+
|
|
560
|
+
# Original table output
|
|
451
561
|
table = Table(title="Tickets")
|
|
452
562
|
table.add_column("ID", style="cyan", no_wrap=True)
|
|
453
563
|
table.add_column("Title", style="white")
|
|
@@ -476,6 +586,7 @@ def show(
|
|
|
476
586
|
no_comments: bool = typer.Option(
|
|
477
587
|
False, "--no-comments", help="Hide comments (shown by default)"
|
|
478
588
|
),
|
|
589
|
+
output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
479
590
|
adapter: AdapterType | None = typer.Option(
|
|
480
591
|
None, "--adapter", help="Override default adapter"
|
|
481
592
|
),
|
|
@@ -486,7 +597,9 @@ def show(
|
|
|
486
597
|
a holistic view of the ticket's history and context.
|
|
487
598
|
|
|
488
599
|
Use --no-comments to display only ticket metadata without comments.
|
|
600
|
+
Use --json to output in machine-readable JSON format.
|
|
489
601
|
"""
|
|
602
|
+
from .utils import format_error_json, format_json_response, serialize_task
|
|
490
603
|
|
|
491
604
|
async def _show() -> tuple[Any, Any, Any]:
|
|
492
605
|
adapter_instance = get_adapter(
|
|
@@ -513,11 +626,53 @@ def show(
|
|
|
513
626
|
|
|
514
627
|
return ticket, ticket_comments, attachments
|
|
515
628
|
|
|
516
|
-
|
|
629
|
+
try:
|
|
630
|
+
ticket, ticket_comments, attachments = asyncio.run(_show())
|
|
517
631
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
632
|
+
if not ticket:
|
|
633
|
+
if output_json:
|
|
634
|
+
console.print(
|
|
635
|
+
format_error_json(
|
|
636
|
+
f"Ticket not found: {ticket_id}", ticket_id=ticket_id
|
|
637
|
+
)
|
|
638
|
+
)
|
|
639
|
+
else:
|
|
640
|
+
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
641
|
+
raise typer.Exit(1) from None
|
|
642
|
+
|
|
643
|
+
# JSON output
|
|
644
|
+
if output_json:
|
|
645
|
+
data = serialize_task(ticket)
|
|
646
|
+
|
|
647
|
+
# Add comments if available
|
|
648
|
+
if ticket_comments:
|
|
649
|
+
data["comments"] = [
|
|
650
|
+
{
|
|
651
|
+
"id": getattr(c, "id", None),
|
|
652
|
+
"text": c.content,
|
|
653
|
+
"author": c.author,
|
|
654
|
+
"created_at": (
|
|
655
|
+
c.created_at.isoformat()
|
|
656
|
+
if hasattr(c.created_at, "isoformat")
|
|
657
|
+
else str(c.created_at)
|
|
658
|
+
),
|
|
659
|
+
}
|
|
660
|
+
for c in ticket_comments
|
|
661
|
+
]
|
|
662
|
+
|
|
663
|
+
# Add attachments if available
|
|
664
|
+
if attachments:
|
|
665
|
+
data["attachments"] = attachments
|
|
666
|
+
|
|
667
|
+
console.print(format_json_response("success", data))
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
# Original formatted output continues below...
|
|
671
|
+
except Exception as e:
|
|
672
|
+
if output_json:
|
|
673
|
+
console.print(format_error_json(e, ticket_id=ticket_id))
|
|
674
|
+
raise typer.Exit(1) from None
|
|
675
|
+
raise
|
|
521
676
|
|
|
522
677
|
# Display ticket header with metadata
|
|
523
678
|
console.print(f"\n[bold cyan]┌─ Ticket: {ticket.id}[/bold cyan]")
|
|
@@ -595,11 +750,13 @@ def show(
|
|
|
595
750
|
def comment(
|
|
596
751
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
597
752
|
content: str = typer.Argument(..., help="Comment content"),
|
|
753
|
+
output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
598
754
|
adapter: AdapterType | None = typer.Option(
|
|
599
755
|
None, "--adapter", help="Override default adapter"
|
|
600
756
|
),
|
|
601
757
|
) -> None:
|
|
602
758
|
"""Add a comment to a ticket."""
|
|
759
|
+
from .utils import format_error_json, format_json_response
|
|
603
760
|
|
|
604
761
|
async def _comment() -> Comment:
|
|
605
762
|
adapter_instance = get_adapter(
|
|
@@ -618,12 +775,34 @@ def comment(
|
|
|
618
775
|
|
|
619
776
|
try:
|
|
620
777
|
result = asyncio.run(_comment())
|
|
621
|
-
|
|
622
|
-
if
|
|
623
|
-
|
|
624
|
-
|
|
778
|
+
|
|
779
|
+
if output_json:
|
|
780
|
+
data = {
|
|
781
|
+
"id": result.id,
|
|
782
|
+
"ticket_id": ticket_id,
|
|
783
|
+
"text": content,
|
|
784
|
+
"author": result.author,
|
|
785
|
+
"created_at": (
|
|
786
|
+
result.created_at.isoformat()
|
|
787
|
+
if hasattr(result.created_at, "isoformat")
|
|
788
|
+
else str(result.created_at)
|
|
789
|
+
),
|
|
790
|
+
}
|
|
791
|
+
console.print(
|
|
792
|
+
format_json_response(
|
|
793
|
+
"success", data, message="Comment added successfully"
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
else:
|
|
797
|
+
console.print("[green]✓[/green] Comment added successfully")
|
|
798
|
+
if result.id:
|
|
799
|
+
console.print(f"Comment ID: {result.id}")
|
|
800
|
+
console.print(f"Content: {content}")
|
|
625
801
|
except Exception as e:
|
|
626
|
-
|
|
802
|
+
if output_json:
|
|
803
|
+
console.print(format_error_json(e, ticket_id=ticket_id))
|
|
804
|
+
else:
|
|
805
|
+
console.print(f"[red]✗[/red] Failed to add comment: {e}")
|
|
627
806
|
raise typer.Exit(1) from None
|
|
628
807
|
|
|
629
808
|
|
|
@@ -807,11 +986,25 @@ def update(
|
|
|
807
986
|
None, "--priority", "-p", help="New priority"
|
|
808
987
|
),
|
|
809
988
|
assignee: str | None = typer.Option(None, "--assignee", "-a", help="New assignee"),
|
|
989
|
+
wait: bool = typer.Option(
|
|
990
|
+
False,
|
|
991
|
+
"--wait",
|
|
992
|
+
"-w",
|
|
993
|
+
help="Wait for operation to complete (synchronous mode)",
|
|
994
|
+
),
|
|
995
|
+
timeout: float = typer.Option(
|
|
996
|
+
30.0,
|
|
997
|
+
"--timeout",
|
|
998
|
+
help="Timeout in seconds for --wait mode (default: 30)",
|
|
999
|
+
),
|
|
1000
|
+
output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
810
1001
|
adapter: AdapterType | None = typer.Option(
|
|
811
1002
|
None, "--adapter", help="Override default adapter"
|
|
812
1003
|
),
|
|
813
1004
|
) -> None:
|
|
814
1005
|
"""Update ticket fields."""
|
|
1006
|
+
from .utils import format_json_response
|
|
1007
|
+
|
|
815
1008
|
updates = {}
|
|
816
1009
|
if title:
|
|
817
1010
|
updates["title"] = title
|
|
@@ -825,7 +1018,16 @@ def update(
|
|
|
825
1018
|
updates["assignee"] = assignee
|
|
826
1019
|
|
|
827
1020
|
if not updates:
|
|
828
|
-
|
|
1021
|
+
if output_json:
|
|
1022
|
+
console.print(
|
|
1023
|
+
format_json_response(
|
|
1024
|
+
"error",
|
|
1025
|
+
{"error": "No updates specified"},
|
|
1026
|
+
message="No updates specified",
|
|
1027
|
+
)
|
|
1028
|
+
)
|
|
1029
|
+
else:
|
|
1030
|
+
console.print("[yellow]No updates specified[/yellow]")
|
|
829
1031
|
raise typer.Exit(1) from None
|
|
830
1032
|
|
|
831
1033
|
# Get the adapter name
|
|
@@ -846,18 +1048,77 @@ def update(
|
|
|
846
1048
|
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
847
1049
|
)
|
|
848
1050
|
|
|
849
|
-
console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
|
|
850
|
-
for key, value in updates.items():
|
|
851
|
-
if key != "ticket_id":
|
|
852
|
-
console.print(f" {key}: {value}")
|
|
853
|
-
console.print(
|
|
854
|
-
"[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]"
|
|
855
|
-
)
|
|
856
|
-
|
|
857
1051
|
# Start worker if needed
|
|
858
1052
|
manager = WorkerManager()
|
|
859
1053
|
if manager.start_if_needed():
|
|
860
|
-
|
|
1054
|
+
if not output_json:
|
|
1055
|
+
console.print("[dim]Worker started to process request[/dim]")
|
|
1056
|
+
|
|
1057
|
+
# SYNCHRONOUS MODE: Poll until completion if --wait flag is set
|
|
1058
|
+
if wait:
|
|
1059
|
+
from .utils import format_error_json
|
|
1060
|
+
|
|
1061
|
+
if not output_json:
|
|
1062
|
+
console.print(
|
|
1063
|
+
f"[yellow]⏳[/yellow] Waiting for update to complete (timeout: {timeout}s)..."
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
try:
|
|
1067
|
+
# Poll the queue until operation completes
|
|
1068
|
+
completed_item = queue.poll_until_complete(queue_id, timeout=timeout)
|
|
1069
|
+
result = completed_item.result
|
|
1070
|
+
|
|
1071
|
+
if output_json:
|
|
1072
|
+
data = result if result else {"queue_id": queue_id, "id": ticket_id}
|
|
1073
|
+
console.print(
|
|
1074
|
+
format_json_response(
|
|
1075
|
+
"success", data, message="Ticket updated successfully"
|
|
1076
|
+
)
|
|
1077
|
+
)
|
|
1078
|
+
else:
|
|
1079
|
+
console.print(
|
|
1080
|
+
f"[green]✓[/green] Ticket updated successfully: {ticket_id}"
|
|
1081
|
+
)
|
|
1082
|
+
for key, value in updates.items():
|
|
1083
|
+
if key != "ticket_id":
|
|
1084
|
+
console.print(f" {key}: {value}")
|
|
1085
|
+
|
|
1086
|
+
except TimeoutError as e:
|
|
1087
|
+
if output_json:
|
|
1088
|
+
console.print(format_error_json(str(e), queue_id=queue_id))
|
|
1089
|
+
else:
|
|
1090
|
+
console.print(f"[red]❌[/red] Operation timed out after {timeout}s")
|
|
1091
|
+
console.print(f" Queue ID: {queue_id}")
|
|
1092
|
+
raise typer.Exit(1) from None
|
|
1093
|
+
|
|
1094
|
+
except RuntimeError as e:
|
|
1095
|
+
if output_json:
|
|
1096
|
+
console.print(format_error_json(str(e), queue_id=queue_id))
|
|
1097
|
+
else:
|
|
1098
|
+
console.print(f"[red]❌[/red] Operation failed: {e}")
|
|
1099
|
+
raise typer.Exit(1) from None
|
|
1100
|
+
|
|
1101
|
+
# ASYNCHRONOUS MODE (default)
|
|
1102
|
+
else:
|
|
1103
|
+
if output_json:
|
|
1104
|
+
updated_fields = [k for k in updates.keys() if k != "ticket_id"]
|
|
1105
|
+
data = {
|
|
1106
|
+
"id": ticket_id,
|
|
1107
|
+
"queue_id": queue_id,
|
|
1108
|
+
"updated_fields": updated_fields,
|
|
1109
|
+
**{k: v for k, v in updates.items() if k != "ticket_id"},
|
|
1110
|
+
}
|
|
1111
|
+
console.print(
|
|
1112
|
+
format_json_response("success", data, message="Ticket update queued")
|
|
1113
|
+
)
|
|
1114
|
+
else:
|
|
1115
|
+
console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
|
|
1116
|
+
for key, value in updates.items():
|
|
1117
|
+
if key != "ticket_id":
|
|
1118
|
+
console.print(f" {key}: {value}")
|
|
1119
|
+
console.print(
|
|
1120
|
+
"[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]"
|
|
1121
|
+
)
|
|
861
1122
|
|
|
862
1123
|
|
|
863
1124
|
@app.command()
|
|
@@ -869,6 +1130,18 @@ def transition(
|
|
|
869
1130
|
state: TicketState | None = typer.Option(
|
|
870
1131
|
None, "--state", "-s", help="Target state (recommended)"
|
|
871
1132
|
),
|
|
1133
|
+
wait: bool = typer.Option(
|
|
1134
|
+
False,
|
|
1135
|
+
"--wait",
|
|
1136
|
+
"-w",
|
|
1137
|
+
help="Wait for operation to complete (synchronous mode)",
|
|
1138
|
+
),
|
|
1139
|
+
timeout: float = typer.Option(
|
|
1140
|
+
30.0,
|
|
1141
|
+
"--timeout",
|
|
1142
|
+
help="Timeout in seconds for --wait mode (default: 30)",
|
|
1143
|
+
),
|
|
1144
|
+
output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
872
1145
|
adapter: AdapterType | None = typer.Option(
|
|
873
1146
|
None, "--adapter", help="Override default adapter"
|
|
874
1147
|
),
|
|
@@ -884,16 +1157,25 @@ def transition(
|
|
|
884
1157
|
mcp-ticketer ticket transition BTA-215 done
|
|
885
1158
|
|
|
886
1159
|
"""
|
|
1160
|
+
from .utils import format_json_response
|
|
1161
|
+
|
|
887
1162
|
# Determine which state to use (prefer flag over positional)
|
|
888
1163
|
target_state = state if state is not None else state_positional
|
|
889
1164
|
|
|
890
1165
|
if target_state is None:
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1166
|
+
if output_json:
|
|
1167
|
+
console.print(
|
|
1168
|
+
format_json_response(
|
|
1169
|
+
"error", {"error": "State is required"}, message="State is required"
|
|
1170
|
+
)
|
|
1171
|
+
)
|
|
1172
|
+
else:
|
|
1173
|
+
console.print("[red]Error: State is required[/red]")
|
|
1174
|
+
console.print(
|
|
1175
|
+
"Use either:\n"
|
|
1176
|
+
" - Flag syntax (recommended): mcp-ticketer ticket transition TICKET-ID --state STATE\n"
|
|
1177
|
+
" - Positional syntax: mcp-ticketer ticket transition TICKET-ID STATE"
|
|
1178
|
+
)
|
|
897
1179
|
raise typer.Exit(1) from None
|
|
898
1180
|
|
|
899
1181
|
# Get the adapter name
|
|
@@ -904,28 +1186,92 @@ def transition(
|
|
|
904
1186
|
|
|
905
1187
|
# Add to queue with explicit project directory
|
|
906
1188
|
queue = Queue()
|
|
1189
|
+
state_value = target_state.value if hasattr(target_state, "value") else target_state
|
|
907
1190
|
queue_id = queue.add(
|
|
908
1191
|
ticket_data={
|
|
909
1192
|
"ticket_id": ticket_id,
|
|
910
|
-
"state":
|
|
911
|
-
target_state.value if hasattr(target_state, "value") else target_state
|
|
912
|
-
),
|
|
1193
|
+
"state": state_value,
|
|
913
1194
|
},
|
|
914
1195
|
adapter=adapter_name,
|
|
915
1196
|
operation="transition",
|
|
916
1197
|
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
917
1198
|
)
|
|
918
1199
|
|
|
919
|
-
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
920
|
-
console.print(f" Ticket: {ticket_id} → {target_state}")
|
|
921
|
-
console.print(
|
|
922
|
-
"[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]"
|
|
923
|
-
)
|
|
924
|
-
|
|
925
1200
|
# Start worker if needed
|
|
926
1201
|
manager = WorkerManager()
|
|
927
1202
|
if manager.start_if_needed():
|
|
928
|
-
|
|
1203
|
+
if not output_json:
|
|
1204
|
+
console.print("[dim]Worker started to process request[/dim]")
|
|
1205
|
+
|
|
1206
|
+
# SYNCHRONOUS MODE: Poll until completion if --wait flag is set
|
|
1207
|
+
if wait:
|
|
1208
|
+
from .utils import format_error_json
|
|
1209
|
+
|
|
1210
|
+
if not output_json:
|
|
1211
|
+
console.print(
|
|
1212
|
+
f"[yellow]⏳[/yellow] Waiting for transition to complete (timeout: {timeout}s)..."
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
try:
|
|
1216
|
+
# Poll the queue until operation completes
|
|
1217
|
+
completed_item = queue.poll_until_complete(queue_id, timeout=timeout)
|
|
1218
|
+
result = completed_item.result
|
|
1219
|
+
|
|
1220
|
+
if output_json:
|
|
1221
|
+
data = (
|
|
1222
|
+
result
|
|
1223
|
+
if result
|
|
1224
|
+
else {
|
|
1225
|
+
"id": ticket_id,
|
|
1226
|
+
"new_state": state_value,
|
|
1227
|
+
"matched_state": state_value,
|
|
1228
|
+
"confidence": 1.0,
|
|
1229
|
+
}
|
|
1230
|
+
)
|
|
1231
|
+
console.print(
|
|
1232
|
+
format_json_response(
|
|
1233
|
+
"success", data, message="State transition completed"
|
|
1234
|
+
)
|
|
1235
|
+
)
|
|
1236
|
+
else:
|
|
1237
|
+
console.print(
|
|
1238
|
+
f"[green]✓[/green] State transition completed: {ticket_id} → {target_state}"
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
except TimeoutError as e:
|
|
1242
|
+
if output_json:
|
|
1243
|
+
console.print(format_error_json(str(e), queue_id=queue_id))
|
|
1244
|
+
else:
|
|
1245
|
+
console.print(f"[red]❌[/red] Operation timed out after {timeout}s")
|
|
1246
|
+
console.print(f" Queue ID: {queue_id}")
|
|
1247
|
+
raise typer.Exit(1) from None
|
|
1248
|
+
|
|
1249
|
+
except RuntimeError as e:
|
|
1250
|
+
if output_json:
|
|
1251
|
+
console.print(format_error_json(str(e), queue_id=queue_id))
|
|
1252
|
+
else:
|
|
1253
|
+
console.print(f"[red]❌[/red] Operation failed: {e}")
|
|
1254
|
+
raise typer.Exit(1) from None
|
|
1255
|
+
|
|
1256
|
+
# ASYNCHRONOUS MODE (default)
|
|
1257
|
+
else:
|
|
1258
|
+
if output_json:
|
|
1259
|
+
data = {
|
|
1260
|
+
"id": ticket_id,
|
|
1261
|
+
"queue_id": queue_id,
|
|
1262
|
+
"new_state": state_value,
|
|
1263
|
+
"matched_state": state_value,
|
|
1264
|
+
"confidence": 1.0,
|
|
1265
|
+
}
|
|
1266
|
+
console.print(
|
|
1267
|
+
format_json_response("success", data, message="State transition queued")
|
|
1268
|
+
)
|
|
1269
|
+
else:
|
|
1270
|
+
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
1271
|
+
console.print(f" Ticket: {ticket_id} → {target_state}")
|
|
1272
|
+
console.print(
|
|
1273
|
+
"[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]"
|
|
1274
|
+
)
|
|
929
1275
|
|
|
930
1276
|
|
|
931
1277
|
@app.command()
|
|
@@ -935,11 +1281,13 @@ def search(
|
|
|
935
1281
|
priority: Priority | None = typer.Option(None, "--priority", "-p"),
|
|
936
1282
|
assignee: str | None = typer.Option(None, "--assignee", "-a"),
|
|
937
1283
|
limit: int = typer.Option(10, "--limit", "-l"),
|
|
1284
|
+
output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
938
1285
|
adapter: AdapterType | None = typer.Option(
|
|
939
1286
|
None, "--adapter", help="Override default adapter"
|
|
940
1287
|
),
|
|
941
1288
|
) -> None:
|
|
942
1289
|
"""Search tickets with advanced query."""
|
|
1290
|
+
from .utils import format_json_response, serialize_task
|
|
943
1291
|
|
|
944
1292
|
async def _search() -> list[Any]:
|
|
945
1293
|
adapter_instance = get_adapter(
|
|
@@ -957,7 +1305,21 @@ def search(
|
|
|
957
1305
|
tickets = asyncio.run(_search())
|
|
958
1306
|
|
|
959
1307
|
if not tickets:
|
|
960
|
-
|
|
1308
|
+
if output_json:
|
|
1309
|
+
console.print(
|
|
1310
|
+
format_json_response(
|
|
1311
|
+
"success", {"tickets": [], "query": query, "count": 0}
|
|
1312
|
+
)
|
|
1313
|
+
)
|
|
1314
|
+
else:
|
|
1315
|
+
console.print("[yellow]No tickets found matching query[/yellow]")
|
|
1316
|
+
return
|
|
1317
|
+
|
|
1318
|
+
# JSON output
|
|
1319
|
+
if output_json:
|
|
1320
|
+
tickets_data = [serialize_task(t) for t in tickets]
|
|
1321
|
+
data = {"tickets": tickets_data, "query": query, "count": len(tickets_data)}
|
|
1322
|
+
console.print(format_json_response("success", data))
|
|
961
1323
|
return
|
|
962
1324
|
|
|
963
1325
|
# Display results
|