mcp-ticketer 0.12.0__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 (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -473,51 +473,122 @@ def list_tickets(
473
473
  @app.command()
474
474
  def show(
475
475
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
476
- 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
+ ),
477
479
  adapter: AdapterType | None = typer.Option(
478
480
  None, "--adapter", help="Override default adapter"
479
481
  ),
480
482
  ) -> None:
481
- """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.
482
487
 
483
- async def _show() -> tuple[Any, Any]:
488
+ Use --no-comments to display only ticket metadata without comments.
489
+ """
490
+
491
+ async def _show() -> tuple[Any, Any, Any]:
484
492
  adapter_instance = get_adapter(
485
493
  override_adapter=adapter.value if adapter else None
486
494
  )
487
495
  ticket = await adapter_instance.read(ticket_id)
488
496
  ticket_comments = None
489
- if comments and ticket:
490
- ticket_comments = await adapter_instance.get_comments(ticket_id)
491
- 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
492
515
 
493
- ticket, ticket_comments = asyncio.run(_show())
516
+ ticket, ticket_comments, attachments = asyncio.run(_show())
494
517
 
495
518
  if not ticket:
496
519
  console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
497
520
  raise typer.Exit(1) from None
498
521
 
499
- # Display ticket details
500
- console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
501
- console.print(f"Title: {ticket.title}")
502
- console.print(f"State: [green]{ticket.state}[/green]")
503
- 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)
504
526
 
505
- if ticket.description:
506
- console.print("\n[dim]Description:[/dim]")
507
- console.print(ticket.description)
508
-
509
- if ticket.tags:
510
- 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]")
511
531
 
512
532
  if ticket.assignee:
513
- console.print(f"Assignee: {ticket.assignee}")
533
+ console.print(f" Assignee: {ticket.assignee}")
514
534
 
515
- # Display comments if requested
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}")
542
+
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
516
576
  if ticket_comments:
517
- console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
518
- for comment in ticket_comments:
519
- console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
520
- 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
+ )
521
592
 
522
593
 
523
594
  @app.command()
@@ -556,6 +627,175 @@ def comment(
556
627
  raise typer.Exit(1) from None
557
628
 
558
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
797
+
798
+
559
799
  @app.command()
560
800
  def update(
561
801
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
@@ -2,20 +2,38 @@
2
2
 
3
3
  from .adapter import BaseAdapter
4
4
  from .instructions import (
5
- InstructionsError,
6
- InstructionsNotFoundError,
7
- InstructionsValidationError,
8
- TicketInstructionsManager,
9
- get_instructions,
5
+ InstructionsError,
6
+ InstructionsNotFoundError,
7
+ InstructionsValidationError,
8
+ TicketInstructionsManager,
9
+ get_instructions,
10
+ )
11
+ from .models import (
12
+ Attachment,
13
+ Comment,
14
+ Epic,
15
+ Priority,
16
+ ProjectUpdate,
17
+ ProjectUpdateHealth,
18
+ Task,
19
+ TicketState,
20
+ TicketType,
10
21
  )
11
- from .models import Attachment, Comment, Epic, Priority, Task, TicketState, TicketType
12
22
  from .registry import AdapterRegistry
23
+ from .state_matcher import (
24
+ SemanticStateMatcher,
25
+ StateMatchResult,
26
+ ValidationResult,
27
+ get_state_matcher,
28
+ )
13
29
 
14
30
  __all__ = [
15
31
  "Epic",
16
32
  "Task",
17
33
  "Comment",
18
34
  "Attachment",
35
+ "ProjectUpdate",
36
+ "ProjectUpdateHealth",
19
37
  "TicketState",
20
38
  "Priority",
21
39
  "TicketType",
@@ -26,4 +44,8 @@ __all__ = [
26
44
  "InstructionsNotFoundError",
27
45
  "InstructionsValidationError",
28
46
  "get_instructions",
47
+ "SemanticStateMatcher",
48
+ "StateMatchResult",
49
+ "ValidationResult",
50
+ "get_state_matcher",
29
51
  ]