mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.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.

Files changed (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,7 @@ import json
5
5
  import os
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
+ from typing import Any
8
9
 
9
10
  import typer
10
11
  from rich.console import Console
@@ -73,7 +74,7 @@ def save_config(config: dict) -> None:
73
74
 
74
75
  def get_adapter(
75
76
  override_adapter: str | None = None, override_config: dict | None = None
76
- ):
77
+ ) -> Any:
77
78
  """Get configured adapter instance."""
78
79
  config = load_config()
79
80
 
@@ -245,7 +246,7 @@ def create(
245
246
  console.print(
246
247
  "[red]Cannot safely create ticket. Please check system status.[/red]"
247
248
  )
248
- raise typer.Exit(1)
249
+ raise typer.Exit(1) from None
249
250
  else:
250
251
  console.print(
251
252
  "[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]"
@@ -254,7 +255,7 @@ def create(
254
255
  console.print(
255
256
  "[red]❌ No repair actions available. Manual intervention required.[/red]"
256
257
  )
257
- raise typer.Exit(1)
258
+ raise typer.Exit(1) from None
258
259
 
259
260
  elif health["status"] == HealthStatus.WARNING:
260
261
  console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
@@ -429,7 +430,7 @@ def list_tickets(
429
430
  ) -> None:
430
431
  """List tickets with optional filters."""
431
432
 
432
- async def _list():
433
+ async def _list() -> list[Any]:
433
434
  adapter_instance = get_adapter(
434
435
  override_adapter=adapter.value if adapter else None
435
436
  )
@@ -472,51 +473,122 @@ def list_tickets(
472
473
  @app.command()
473
474
  def show(
474
475
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
475
- comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
476
+ no_comments: bool = typer.Option(
477
+ False, "--no-comments", help="Hide comments (shown by default)"
478
+ ),
476
479
  adapter: AdapterType | None = typer.Option(
477
480
  None, "--adapter", help="Override default adapter"
478
481
  ),
479
482
  ) -> None:
480
- """Show detailed ticket information."""
483
+ """Show detailed ticket information with full context.
484
+
485
+ By default, displays ticket details along with all comments to provide
486
+ a holistic view of the ticket's history and context.
481
487
 
482
- async def _show():
488
+ Use --no-comments to display only ticket metadata without comments.
489
+ """
490
+
491
+ async def _show() -> tuple[Any, Any, Any]:
483
492
  adapter_instance = get_adapter(
484
493
  override_adapter=adapter.value if adapter else None
485
494
  )
486
495
  ticket = await adapter_instance.read(ticket_id)
487
496
  ticket_comments = None
488
- if comments and ticket:
489
- ticket_comments = await adapter_instance.get_comments(ticket_id)
490
- return ticket, ticket_comments
497
+ attachments = None
498
+
499
+ # Fetch comments by default (unless explicitly disabled)
500
+ if not no_comments and ticket:
501
+ try:
502
+ ticket_comments = await adapter_instance.get_comments(ticket_id)
503
+ except (NotImplementedError, AttributeError):
504
+ # Adapter doesn't support comments
505
+ pass
506
+
507
+ # Try to fetch attachments if available
508
+ if ticket and hasattr(adapter_instance, "list_attachments"):
509
+ try:
510
+ attachments = await adapter_instance.list_attachments(ticket_id)
511
+ except (NotImplementedError, AttributeError):
512
+ pass
513
+
514
+ return ticket, ticket_comments, attachments
491
515
 
492
- ticket, ticket_comments = asyncio.run(_show())
516
+ ticket, ticket_comments, attachments = asyncio.run(_show())
493
517
 
494
518
  if not ticket:
495
519
  console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
496
- raise typer.Exit(1)
520
+ raise typer.Exit(1) from None
497
521
 
498
- # Display ticket details
499
- console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
500
- console.print(f"Title: {ticket.title}")
501
- console.print(f"State: [green]{ticket.state}[/green]")
502
- console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
522
+ # Display ticket header with metadata
523
+ console.print(f"\n[bold cyan]┌─ Ticket: {ticket.id}[/bold cyan]")
524
+ console.print(f"[bold]│ {ticket.title}[/bold]")
525
+ console.print("└" + "─" * 60)
503
526
 
504
- if ticket.description:
505
- console.print("\n[dim]Description:[/dim]")
506
- console.print(ticket.description)
507
-
508
- if ticket.tags:
509
- console.print(f"\nTags: {', '.join(ticket.tags)}")
527
+ # Display metadata in organized sections
528
+ console.print("\n[bold]Status[/bold]")
529
+ console.print(f" State: [green]{ticket.state}[/green]")
530
+ console.print(f" Priority: [yellow]{ticket.priority}[/yellow]")
510
531
 
511
532
  if ticket.assignee:
512
- console.print(f"Assignee: {ticket.assignee}")
533
+ console.print(f" Assignee: {ticket.assignee}")
534
+
535
+ # Display timestamps if available
536
+ if ticket.created_at or ticket.updated_at:
537
+ console.print("\n[bold]Timeline[/bold]")
538
+ if ticket.created_at:
539
+ console.print(f" Created: {ticket.created_at}")
540
+ if ticket.updated_at:
541
+ console.print(f" Updated: {ticket.updated_at}")
513
542
 
514
- # Display comments if requested
543
+ # Display tags
544
+ if ticket.tags:
545
+ console.print("\n[bold]Tags[/bold]")
546
+ console.print(f" {', '.join(ticket.tags)}")
547
+
548
+ # Display description
549
+ if ticket.description:
550
+ console.print("\n[bold]Description[/bold]")
551
+ console.print(f" {ticket.description}")
552
+
553
+ # Display parent/child relationships
554
+ parent_info = []
555
+ if hasattr(ticket, "parent_epic") and ticket.parent_epic:
556
+ parent_info.append(f"Epic: {ticket.parent_epic}")
557
+ if hasattr(ticket, "parent_issue") and ticket.parent_issue:
558
+ parent_info.append(f"Parent Issue: {ticket.parent_issue}")
559
+
560
+ if parent_info:
561
+ console.print("\n[bold]Hierarchy[/bold]")
562
+ for info in parent_info:
563
+ console.print(f" {info}")
564
+
565
+ # Display attachments if available
566
+ if attachments and len(attachments) > 0:
567
+ console.print(f"\n[bold]Attachments ({len(attachments)})[/bold]")
568
+ for att in attachments:
569
+ att_title = att.get("title", "Untitled")
570
+ att_url = att.get("url", "")
571
+ console.print(f" 📎 {att_title}")
572
+ if att_url:
573
+ console.print(f" {att_url}")
574
+
575
+ # Display comments with enhanced formatting
515
576
  if ticket_comments:
516
- console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
517
- for comment in ticket_comments:
518
- console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
519
- console.print(comment.content)
577
+ console.print(f"\n[bold]Activity & Comments ({len(ticket_comments)})[/bold]")
578
+ for i, comment in enumerate(ticket_comments, 1):
579
+ # Format timestamp
580
+ timestamp = comment.created_at if comment.created_at else "Unknown time"
581
+ author = comment.author if comment.author else "Unknown author"
582
+
583
+ console.print(f"\n[dim] {i}. {timestamp}[/dim]")
584
+ console.print(f" [cyan]@{author}[/cyan]")
585
+ console.print(f" {comment.content}")
586
+
587
+ # Footer with hint
588
+ if no_comments:
589
+ console.print(
590
+ "\n[dim]💡 Tip: Remove --no-comments to see activity and comments[/dim]"
591
+ )
520
592
 
521
593
 
522
594
  @app.command()
@@ -529,7 +601,7 @@ def comment(
529
601
  ) -> None:
530
602
  """Add a comment to a ticket."""
531
603
 
532
- async def _comment():
604
+ async def _comment() -> Comment:
533
605
  adapter_instance = get_adapter(
534
606
  override_adapter=adapter.value if adapter else None
535
607
  )
@@ -552,7 +624,176 @@ def comment(
552
624
  console.print(f"Content: {content}")
553
625
  except Exception as e:
554
626
  console.print(f"[red]✗[/red] Failed to add comment: {e}")
555
- raise typer.Exit(1)
627
+ raise typer.Exit(1) from None
628
+
629
+
630
+ @app.command()
631
+ def attach(
632
+ ticket_id: str = typer.Argument(..., help="Ticket ID or URL"),
633
+ file_path: Path = typer.Argument(..., help="Path to file to attach", exists=True),
634
+ description: str | None = typer.Option(
635
+ None, "--description", "-d", help="Attachment description or comment"
636
+ ),
637
+ adapter: AdapterType | None = typer.Option(
638
+ None, "--adapter", help="Override default adapter"
639
+ ),
640
+ ) -> None:
641
+ """Attach a file to a ticket.
642
+
643
+ Examples:
644
+ mcp-ticketer ticket attach 1M-157 docs/analysis.md
645
+ mcp-ticketer ticket attach PROJ-123 screenshot.png -d "Error screenshot"
646
+ mcp-ticketer ticket attach https://linear.app/.../issue/ABC-123 diagram.pdf
647
+ """
648
+
649
+ async def _attach() -> dict[str, Any]:
650
+ import mimetypes
651
+
652
+ adapter_instance = get_adapter(
653
+ override_adapter=adapter.value if adapter else None
654
+ )
655
+
656
+ # Detect MIME type
657
+ mime_type, _ = mimetypes.guess_type(str(file_path))
658
+ if not mime_type:
659
+ mime_type = "application/octet-stream"
660
+
661
+ # Method 1: Try Linear-specific upload (if available)
662
+ if hasattr(adapter_instance, "upload_file") and hasattr(
663
+ adapter_instance, "attach_file_to_issue"
664
+ ):
665
+ try:
666
+ # Upload file to Linear's S3
667
+ file_url = await adapter_instance.upload_file(
668
+ file_path=str(file_path), mime_type=mime_type
669
+ )
670
+
671
+ # Attach to issue
672
+ attachment = await adapter_instance.attach_file_to_issue(
673
+ issue_id=ticket_id,
674
+ file_url=file_url,
675
+ title=file_path.name,
676
+ subtitle=description,
677
+ )
678
+
679
+ return {
680
+ "status": "completed",
681
+ "attachment": attachment,
682
+ "file_url": file_url,
683
+ "method": "linear_native_upload",
684
+ }
685
+ except Exception:
686
+ # If Linear upload fails, fall through to next method
687
+ pass
688
+
689
+ # Method 2: Try generic add_attachment (if available)
690
+ if hasattr(adapter_instance, "add_attachment"):
691
+ try:
692
+ attachment = await adapter_instance.add_attachment(
693
+ ticket_id=ticket_id,
694
+ file_path=str(file_path),
695
+ description=description or "",
696
+ )
697
+ return {
698
+ "status": "completed",
699
+ "attachment": attachment,
700
+ "method": "adapter_native",
701
+ }
702
+ except NotImplementedError:
703
+ pass
704
+
705
+ # Method 3: Fallback - Add file reference as comment
706
+ from ..core.models import Comment
707
+
708
+ comment_content = f"📎 File reference: {file_path.name}"
709
+ if description:
710
+ comment_content += f"\n\n{description}"
711
+
712
+ comment_obj = Comment(
713
+ ticket_id=ticket_id,
714
+ content=comment_content,
715
+ author="cli-user",
716
+ )
717
+
718
+ comment = await adapter_instance.add_comment(comment_obj)
719
+ return {
720
+ "status": "completed",
721
+ "comment": comment,
722
+ "method": "comment_reference",
723
+ "note": "Adapter doesn't support attachments - added file reference as comment",
724
+ }
725
+
726
+ # Validate file before attempting upload
727
+ if not file_path.exists():
728
+ console.print(f"[red]✗[/red] File not found: {file_path}")
729
+ raise typer.Exit(1) from None
730
+
731
+ if not file_path.is_file():
732
+ console.print(f"[red]✗[/red] Path is not a file: {file_path}")
733
+ raise typer.Exit(1) from None
734
+
735
+ # Display file info
736
+ file_size = file_path.stat().st_size
737
+ size_mb = file_size / (1024 * 1024)
738
+ console.print(f"\n[dim]Attaching file to ticket {ticket_id}...[/dim]")
739
+ console.print(f" File: {file_path.name} ({size_mb:.2f} MB)")
740
+
741
+ # Detect MIME type
742
+ import mimetypes
743
+
744
+ mime_type, _ = mimetypes.guess_type(str(file_path))
745
+ if mime_type:
746
+ console.print(f" Type: {mime_type}")
747
+
748
+ try:
749
+ result = asyncio.run(_attach())
750
+
751
+ if result["status"] == "completed":
752
+ console.print(
753
+ f"\n[green]✓[/green] File attached successfully to {ticket_id}"
754
+ )
755
+
756
+ # Display attachment details based on method used
757
+ method = result.get("method", "unknown")
758
+
759
+ if method == "linear_native_upload":
760
+ console.print(" Method: Linear native upload")
761
+ if "file_url" in result:
762
+ console.print(f" URL: {result['file_url']}")
763
+ if "attachment" in result and isinstance(result["attachment"], dict):
764
+ att = result["attachment"]
765
+ if "id" in att:
766
+ console.print(f" ID: {att['id']}")
767
+ if "title" in att:
768
+ console.print(f" Title: {att['title']}")
769
+
770
+ elif method == "adapter_native":
771
+ console.print(" Method: Adapter native")
772
+ if "attachment" in result:
773
+ att = result["attachment"]
774
+ if isinstance(att, dict):
775
+ if "id" in att:
776
+ console.print(f" ID: {att['id']}")
777
+ if "url" in att:
778
+ console.print(f" URL: {att['url']}")
779
+
780
+ elif method == "comment_reference":
781
+ console.print(" Method: Comment reference")
782
+ console.print(f" [dim]{result.get('note', '')}[/dim]")
783
+ if "comment" in result:
784
+ comment = result["comment"]
785
+ if isinstance(comment, dict) and "id" in comment:
786
+ console.print(f" Comment ID: {comment['id']}")
787
+
788
+ else:
789
+ # Error case
790
+ error_msg = result.get("error", "Unknown error")
791
+ console.print(f"\n[red]✗[/red] Failed to attach file: {error_msg}")
792
+ raise typer.Exit(1) from None
793
+
794
+ except Exception as e:
795
+ console.print(f"\n[red]✗[/red] Failed to attach file: {e}")
796
+ raise typer.Exit(1) from None
556
797
 
557
798
 
558
799
  @app.command()
@@ -585,7 +826,7 @@ def update(
585
826
 
586
827
  if not updates:
587
828
  console.print("[yellow]No updates specified[/yellow]")
588
- raise typer.Exit(1)
829
+ raise typer.Exit(1) from None
589
830
 
590
831
  # Get the adapter name
591
832
  config = load_config()
@@ -653,7 +894,7 @@ def transition(
653
894
  " - Flag syntax (recommended): mcp-ticketer ticket transition TICKET-ID --state STATE\n"
654
895
  " - Positional syntax: mcp-ticketer ticket transition TICKET-ID STATE"
655
896
  )
656
- raise typer.Exit(1)
897
+ raise typer.Exit(1) from None
657
898
 
658
899
  # Get the adapter name
659
900
  config = load_config()
@@ -700,7 +941,7 @@ def search(
700
941
  ) -> None:
701
942
  """Search tickets with advanced query."""
702
943
 
703
- async def _search():
944
+ async def _search() -> list[Any]:
704
945
  adapter_instance = get_adapter(
705
946
  override_adapter=adapter.value if adapter else None
706
947
  )
@@ -731,14 +972,14 @@ def search(
731
972
 
732
973
 
733
974
  @app.command()
734
- def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
975
+ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")) -> None:
735
976
  """Check status of a queued operation."""
736
977
  queue = Queue()
737
978
  item = queue.get_item(queue_id)
738
979
 
739
980
  if not item:
740
981
  console.print(f"[red]Queue item not found: {queue_id}[/red]")
741
- raise typer.Exit(1)
982
+ raise typer.Exit(1) from None
742
983
 
743
984
  # Display status
744
985
  console.print(f"\n[bold]Queue Item: {item.id}[/bold]")