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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +394 -9
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +836 -105
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +772 -1
- mcp_ticketer/adapters/linear/adapter.py +2293 -108
- mcp_ticketer/adapters/linear/client.py +146 -12
- mcp_ticketer/adapters/linear/mappers.py +105 -11
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +18 -6
- mcp_ticketer/cli/codex_configure.py +175 -60
- mcp_ticketer/cli/configure.py +884 -146
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +31 -28
- mcp_ticketer/cli/discover.py +293 -21
- mcp_ticketer/cli/gemini_configure.py +18 -6
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +109 -2055
- mcp_ticketer/cli/mcp_configure.py +673 -99
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +13 -11
- mcp_ticketer/cli/ticket_commands.py +277 -36
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +35 -1
- mcp_ticketer/core/adapter.py +170 -5
- mcp_ticketer/core/config.py +38 -31
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +10 -4
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +32 -20
- mcp_ticketer/core/models.py +136 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +148 -14
- mcp_ticketer/core/registry.py +1 -1
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +187 -93
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +37 -9
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +15 -13
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
|
499
|
-
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
500
|
-
console.print(f"
|
|
501
|
-
console.print(
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
|
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)})
|
|
517
|
-
for comment in ticket_comments:
|
|
518
|
-
|
|
519
|
-
|
|
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]")
|