mcp-ticketer 0.3.0__py3-none-any.whl → 0.3.2__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/adapters/aitrackdown.py +12 -15
- mcp_ticketer/adapters/github.py +7 -4
- mcp_ticketer/adapters/jira.py +23 -22
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +88 -89
- mcp_ticketer/adapters/linear/client.py +71 -52
- mcp_ticketer/adapters/linear/mappers.py +88 -68
- mcp_ticketer/adapters/linear/queries.py +28 -7
- mcp_ticketer/adapters/linear/types.py +57 -50
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/cli/adapter_diagnostics.py +86 -51
- mcp_ticketer/cli/diagnostics.py +165 -72
- mcp_ticketer/cli/linear_commands.py +156 -113
- mcp_ticketer/cli/main.py +194 -100
- mcp_ticketer/cli/simple_health.py +73 -45
- mcp_ticketer/cli/utils.py +15 -10
- mcp_ticketer/core/config.py +23 -19
- mcp_ticketer/core/env_discovery.py +33 -4
- mcp_ticketer/core/env_loader.py +109 -86
- mcp_ticketer/core/exceptions.py +20 -18
- mcp_ticketer/core/models.py +9 -0
- mcp_ticketer/core/project_config.py +1 -1
- mcp_ticketer/mcp/server.py +294 -139
- mcp_ticketer/queue/health_monitor.py +152 -121
- mcp_ticketer/queue/manager.py +11 -4
- mcp_ticketer/queue/queue.py +15 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +190 -132
- mcp_ticketer/queue/worker.py +54 -25
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-0.3.2.dist-info}/METADATA +1 -1
- mcp_ticketer-0.3.2.dist-info/RECORD +59 -0
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-0.3.2.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-0.3.2.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-0.3.2.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py
CHANGED
|
@@ -12,15 +12,15 @@ from dotenv import load_dotenv
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
|
|
15
|
+
# Import adapters module to trigger registration
|
|
16
|
+
import mcp_ticketer.adapters # noqa: F401
|
|
17
|
+
|
|
15
18
|
from ..__version__ import __version__
|
|
16
19
|
from ..core import AdapterRegistry, Priority, TicketState
|
|
17
|
-
from ..core.models import
|
|
20
|
+
from ..core.models import Comment, SearchQuery
|
|
18
21
|
from ..queue import Queue, QueueStatus, WorkerManager
|
|
19
|
-
from ..queue.health_monitor import
|
|
22
|
+
from ..queue.health_monitor import HealthStatus, QueueHealthMonitor
|
|
20
23
|
from ..queue.ticket_registry import TicketRegistry
|
|
21
|
-
|
|
22
|
-
# Import adapters module to trigger registration
|
|
23
|
-
import mcp_ticketer.adapters # noqa: F401
|
|
24
24
|
from .configure import configure_wizard, set_adapter_config, show_current_config
|
|
25
25
|
from .diagnostics import run_diagnostics
|
|
26
26
|
from .discover import app as discover_app
|
|
@@ -150,8 +150,8 @@ def _discover_from_env_files() -> Optional[str]:
|
|
|
150
150
|
|
|
151
151
|
Returns:
|
|
152
152
|
Adapter name if discovered, None otherwise
|
|
153
|
+
|
|
153
154
|
"""
|
|
154
|
-
import os
|
|
155
155
|
import logging
|
|
156
156
|
from pathlib import Path
|
|
157
157
|
|
|
@@ -166,12 +166,12 @@ def _discover_from_env_files() -> Optional[str]:
|
|
|
166
166
|
try:
|
|
167
167
|
# Simple .env parsing (key=value format)
|
|
168
168
|
env_vars = {}
|
|
169
|
-
with open(env_path
|
|
169
|
+
with open(env_path) as f:
|
|
170
170
|
for line in f:
|
|
171
171
|
line = line.strip()
|
|
172
|
-
if line and not line.startswith(
|
|
173
|
-
key, value = line.split(
|
|
174
|
-
env_vars[key.strip()] = value.strip().strip(
|
|
172
|
+
if line and not line.startswith("#") and "=" in line:
|
|
173
|
+
key, value = line.split("=", 1)
|
|
174
|
+
env_vars[key.strip()] = value.strip().strip("\"'")
|
|
175
175
|
|
|
176
176
|
# Check for adapter-specific variables
|
|
177
177
|
if env_vars.get("LINEAR_API_KEY"):
|
|
@@ -195,6 +195,7 @@ def _save_adapter_to_config(adapter_name: str) -> None:
|
|
|
195
195
|
|
|
196
196
|
Args:
|
|
197
197
|
adapter_name: Name of the adapter to save as default
|
|
198
|
+
|
|
198
199
|
"""
|
|
199
200
|
import logging
|
|
200
201
|
|
|
@@ -325,6 +326,7 @@ def _prompt_for_adapter_selection(console: Console) -> str:
|
|
|
325
326
|
|
|
326
327
|
Returns:
|
|
327
328
|
Selected adapter type
|
|
329
|
+
|
|
328
330
|
"""
|
|
329
331
|
console.print("\n[bold blue]🚀 MCP Ticketer Setup[/bold blue]")
|
|
330
332
|
console.print("Choose which ticket system you want to connect to:\n")
|
|
@@ -335,26 +337,26 @@ def _prompt_for_adapter_selection(console: Console) -> str:
|
|
|
335
337
|
"name": "linear",
|
|
336
338
|
"title": "Linear",
|
|
337
339
|
"description": "Modern project management (linear.app)",
|
|
338
|
-
"requirements": "API key and team ID"
|
|
340
|
+
"requirements": "API key and team ID",
|
|
339
341
|
},
|
|
340
342
|
{
|
|
341
343
|
"name": "github",
|
|
342
344
|
"title": "GitHub Issues",
|
|
343
345
|
"description": "GitHub repository issues",
|
|
344
|
-
"requirements": "Personal access token, owner, and repo"
|
|
346
|
+
"requirements": "Personal access token, owner, and repo",
|
|
345
347
|
},
|
|
346
348
|
{
|
|
347
349
|
"name": "jira",
|
|
348
350
|
"title": "JIRA",
|
|
349
351
|
"description": "Atlassian JIRA project management",
|
|
350
|
-
"requirements": "Server URL, email, and API token"
|
|
352
|
+
"requirements": "Server URL, email, and API token",
|
|
351
353
|
},
|
|
352
354
|
{
|
|
353
355
|
"name": "aitrackdown",
|
|
354
356
|
"title": "Local Files (AITrackdown)",
|
|
355
357
|
"description": "Store tickets in local files (no external service)",
|
|
356
|
-
"requirements": "None - works offline"
|
|
357
|
-
}
|
|
358
|
+
"requirements": "None - works offline",
|
|
359
|
+
},
|
|
358
360
|
]
|
|
359
361
|
|
|
360
362
|
# Display options
|
|
@@ -366,17 +368,17 @@ def _prompt_for_adapter_selection(console: Console) -> str:
|
|
|
366
368
|
# Get user selection
|
|
367
369
|
while True:
|
|
368
370
|
try:
|
|
369
|
-
choice = typer.prompt(
|
|
370
|
-
"Select adapter (1-4)",
|
|
371
|
-
type=int,
|
|
372
|
-
default=1
|
|
373
|
-
)
|
|
371
|
+
choice = typer.prompt("Select adapter (1-4)", type=int, default=1)
|
|
374
372
|
if 1 <= choice <= len(adapters):
|
|
375
373
|
selected_adapter = adapters[choice - 1]
|
|
376
|
-
console.print(
|
|
374
|
+
console.print(
|
|
375
|
+
f"\n[green]✓ Selected: {selected_adapter['title']}[/green]"
|
|
376
|
+
)
|
|
377
377
|
return selected_adapter["name"]
|
|
378
378
|
else:
|
|
379
|
-
console.print(
|
|
379
|
+
console.print(
|
|
380
|
+
f"[red]Please enter a number between 1 and {len(adapters)}[/red]"
|
|
381
|
+
)
|
|
380
382
|
except (ValueError, typer.Abort):
|
|
381
383
|
console.print("[yellow]Setup cancelled.[/yellow]")
|
|
382
384
|
raise typer.Exit(0)
|
|
@@ -447,6 +449,7 @@ def setup(
|
|
|
447
449
|
|
|
448
450
|
# Setup for different project
|
|
449
451
|
mcp-ticketer setup --path /path/to/project
|
|
452
|
+
|
|
450
453
|
"""
|
|
451
454
|
# Call init with all parameters
|
|
452
455
|
init(
|
|
@@ -568,32 +571,56 @@ def init(
|
|
|
568
571
|
console.print(
|
|
569
572
|
"[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
|
|
570
573
|
)
|
|
571
|
-
discovered = discover_config(proj_path)
|
|
572
574
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
if primary:
|
|
576
|
-
adapter_type = primary.adapter_type
|
|
577
|
-
console.print(
|
|
578
|
-
f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
|
|
579
|
-
)
|
|
575
|
+
# First try our improved .env configuration loader
|
|
576
|
+
from ..mcp.server import _load_env_configuration
|
|
580
577
|
|
|
581
|
-
|
|
582
|
-
console.print(
|
|
583
|
-
f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
|
|
584
|
-
)
|
|
585
|
-
console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
|
|
578
|
+
env_config = _load_env_configuration()
|
|
586
579
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
580
|
+
if env_config:
|
|
581
|
+
adapter_type = env_config["adapter_type"]
|
|
582
|
+
console.print(
|
|
583
|
+
f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Show what was discovered
|
|
587
|
+
console.print("\n[dim]Configuration found in: .env files[/dim]")
|
|
588
|
+
console.print("[dim]Confidence: 100%[/dim]")
|
|
589
|
+
|
|
590
|
+
# Ask user to confirm auto-detected adapter
|
|
591
|
+
if not typer.confirm(
|
|
592
|
+
f"Use detected {adapter_type} adapter?",
|
|
593
|
+
default=True,
|
|
594
|
+
):
|
|
595
|
+
adapter_type = None # Will trigger interactive selection
|
|
596
|
+
else:
|
|
597
|
+
# Fallback to old discovery system for backward compatibility
|
|
598
|
+
discovered = discover_config(proj_path)
|
|
599
|
+
|
|
600
|
+
if discovered and discovered.adapters:
|
|
601
|
+
primary = discovered.get_primary_adapter()
|
|
602
|
+
if primary:
|
|
603
|
+
adapter_type = primary.adapter_type
|
|
604
|
+
console.print(
|
|
605
|
+
f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# Show what was discovered
|
|
609
|
+
console.print(
|
|
610
|
+
f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
|
|
611
|
+
)
|
|
612
|
+
console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
|
|
613
|
+
|
|
614
|
+
# Ask user to confirm auto-detected adapter
|
|
615
|
+
if not typer.confirm(
|
|
616
|
+
f"Use detected {adapter_type} adapter?",
|
|
617
|
+
default=True,
|
|
618
|
+
):
|
|
619
|
+
adapter_type = None # Will trigger interactive selection
|
|
620
|
+
else:
|
|
592
621
|
adapter_type = None # Will trigger interactive selection
|
|
593
622
|
else:
|
|
594
623
|
adapter_type = None # Will trigger interactive selection
|
|
595
|
-
else:
|
|
596
|
-
adapter_type = None # Will trigger interactive selection
|
|
597
624
|
|
|
598
625
|
# If no adapter determined, show interactive selection
|
|
599
626
|
if not adapter_type:
|
|
@@ -617,7 +644,7 @@ def init(
|
|
|
617
644
|
if adapter_type == "aitrackdown":
|
|
618
645
|
config["adapters"]["aitrackdown"] = {
|
|
619
646
|
"type": "aitrackdown",
|
|
620
|
-
"base_path": base_path or ".aitrackdown"
|
|
647
|
+
"base_path": base_path or ".aitrackdown",
|
|
621
648
|
}
|
|
622
649
|
|
|
623
650
|
elif adapter_type == "linear":
|
|
@@ -630,11 +657,12 @@ def init(
|
|
|
630
657
|
if not linear_api_key and not discovered:
|
|
631
658
|
console.print("\n[bold]Linear Configuration[/bold]")
|
|
632
659
|
console.print("You need a Linear API key to connect to Linear.")
|
|
633
|
-
console.print(
|
|
660
|
+
console.print(
|
|
661
|
+
"[dim]Get your API key at: https://linear.app/settings/api[/dim]\n"
|
|
662
|
+
)
|
|
634
663
|
|
|
635
664
|
linear_api_key = typer.prompt(
|
|
636
|
-
"Enter your Linear API key",
|
|
637
|
-
hide_input=True
|
|
665
|
+
"Enter your Linear API key", hide_input=True
|
|
638
666
|
)
|
|
639
667
|
|
|
640
668
|
if linear_api_key:
|
|
@@ -652,8 +680,12 @@ def init(
|
|
|
652
680
|
linear_config["team_id"] = linear_team_id
|
|
653
681
|
|
|
654
682
|
if not linear_config.get("api_key") or not linear_config.get("team_id"):
|
|
655
|
-
console.print(
|
|
656
|
-
|
|
683
|
+
console.print(
|
|
684
|
+
"[red]Error:[/red] Linear requires both API key and team ID"
|
|
685
|
+
)
|
|
686
|
+
console.print(
|
|
687
|
+
"Run 'mcp-ticketer init --adapter linear' with proper credentials"
|
|
688
|
+
)
|
|
657
689
|
raise typer.Exit(1)
|
|
658
690
|
|
|
659
691
|
linear_config["type"] = "linear"
|
|
@@ -681,18 +713,17 @@ def init(
|
|
|
681
713
|
|
|
682
714
|
if not token and not discovered:
|
|
683
715
|
console.print("\nYou need a JIRA API token.")
|
|
684
|
-
console.print(
|
|
685
|
-
|
|
686
|
-
token = typer.prompt(
|
|
687
|
-
"Enter your JIRA API token",
|
|
688
|
-
hide_input=True
|
|
716
|
+
console.print(
|
|
717
|
+
"[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
|
|
689
718
|
)
|
|
690
719
|
|
|
720
|
+
token = typer.prompt("Enter your JIRA API token", hide_input=True)
|
|
721
|
+
|
|
691
722
|
if not project and not discovered:
|
|
692
723
|
project = typer.prompt(
|
|
693
724
|
"Default JIRA project key (optional, press Enter to skip)",
|
|
694
725
|
default="",
|
|
695
|
-
show_default=False
|
|
726
|
+
show_default=False,
|
|
696
727
|
)
|
|
697
728
|
|
|
698
729
|
# Validate required fields
|
|
@@ -712,7 +743,7 @@ def init(
|
|
|
712
743
|
"server": server,
|
|
713
744
|
"email": email,
|
|
714
745
|
"api_token": token,
|
|
715
|
-
"type": "jira"
|
|
746
|
+
"type": "jira",
|
|
716
747
|
}
|
|
717
748
|
|
|
718
749
|
if project:
|
|
@@ -732,19 +763,24 @@ def init(
|
|
|
732
763
|
console.print("\n[bold]GitHub Configuration[/bold]")
|
|
733
764
|
console.print("Enter your GitHub repository details.\n")
|
|
734
765
|
|
|
735
|
-
owner = typer.prompt(
|
|
766
|
+
owner = typer.prompt(
|
|
767
|
+
"GitHub repository owner (username or organization)"
|
|
768
|
+
)
|
|
736
769
|
|
|
737
770
|
if not repo and not discovered:
|
|
738
771
|
repo = typer.prompt("GitHub repository name")
|
|
739
772
|
|
|
740
773
|
if not token and not discovered:
|
|
741
774
|
console.print("\nYou need a GitHub Personal Access Token.")
|
|
742
|
-
console.print(
|
|
743
|
-
|
|
775
|
+
console.print(
|
|
776
|
+
"[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
|
|
777
|
+
)
|
|
778
|
+
console.print(
|
|
779
|
+
"[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n"
|
|
780
|
+
)
|
|
744
781
|
|
|
745
782
|
token = typer.prompt(
|
|
746
|
-
"Enter your GitHub Personal Access Token",
|
|
747
|
-
hide_input=True
|
|
783
|
+
"Enter your GitHub Personal Access Token", hide_input=True
|
|
748
784
|
)
|
|
749
785
|
|
|
750
786
|
# Validate required fields
|
|
@@ -757,14 +793,16 @@ def init(
|
|
|
757
793
|
raise typer.Exit(1)
|
|
758
794
|
|
|
759
795
|
if not token:
|
|
760
|
-
console.print(
|
|
796
|
+
console.print(
|
|
797
|
+
"[red]Error:[/red] GitHub Personal Access Token is required"
|
|
798
|
+
)
|
|
761
799
|
raise typer.Exit(1)
|
|
762
800
|
|
|
763
801
|
config["adapters"]["github"] = {
|
|
764
802
|
"owner": owner,
|
|
765
803
|
"repo": repo,
|
|
766
804
|
"token": token,
|
|
767
|
-
"type": "github"
|
|
805
|
+
"type": "github",
|
|
768
806
|
}
|
|
769
807
|
|
|
770
808
|
# 5. Save to appropriate location
|
|
@@ -808,13 +846,16 @@ def init(
|
|
|
808
846
|
_show_next_steps(console, adapter_type, config_file_path)
|
|
809
847
|
|
|
810
848
|
|
|
811
|
-
def _show_next_steps(
|
|
849
|
+
def _show_next_steps(
|
|
850
|
+
console: Console, adapter_type: str, config_file_path: Path
|
|
851
|
+
) -> None:
|
|
812
852
|
"""Show helpful next steps after initialization.
|
|
813
853
|
|
|
814
854
|
Args:
|
|
815
855
|
console: Rich console for output
|
|
816
856
|
adapter_type: Type of adapter that was configured
|
|
817
857
|
config_file_path: Path to the configuration file
|
|
858
|
+
|
|
818
859
|
"""
|
|
819
860
|
console.print("\n[bold green]🎉 Setup Complete![/bold green]")
|
|
820
861
|
console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
|
|
@@ -826,7 +867,9 @@ def _show_next_steps(console: Console, adapter_type: str, config_file_path: Path
|
|
|
826
867
|
console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
|
|
827
868
|
|
|
828
869
|
if adapter_type != "aitrackdown":
|
|
829
|
-
console.print(
|
|
870
|
+
console.print(
|
|
871
|
+
f"\n3. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]"
|
|
872
|
+
)
|
|
830
873
|
|
|
831
874
|
if adapter_type == "linear":
|
|
832
875
|
console.print(" Check your Linear workspace for the new ticket")
|
|
@@ -1132,11 +1175,14 @@ def status_command():
|
|
|
1132
1175
|
|
|
1133
1176
|
@app.command()
|
|
1134
1177
|
def health(
|
|
1135
|
-
auto_repair: bool = typer.Option(
|
|
1136
|
-
|
|
1178
|
+
auto_repair: bool = typer.Option(
|
|
1179
|
+
False, "--auto-repair", help="Attempt automatic repair of issues"
|
|
1180
|
+
),
|
|
1181
|
+
verbose: bool = typer.Option(
|
|
1182
|
+
False, "--verbose", "-v", help="Show detailed health information"
|
|
1183
|
+
),
|
|
1137
1184
|
) -> None:
|
|
1138
1185
|
"""Check queue system health and detect issues immediately."""
|
|
1139
|
-
|
|
1140
1186
|
health_monitor = QueueHealthMonitor()
|
|
1141
1187
|
health = health_monitor.check_health()
|
|
1142
1188
|
|
|
@@ -1145,14 +1191,14 @@ def health(
|
|
|
1145
1191
|
HealthStatus.HEALTHY: "green",
|
|
1146
1192
|
HealthStatus.WARNING: "yellow",
|
|
1147
1193
|
HealthStatus.CRITICAL: "red",
|
|
1148
|
-
HealthStatus.FAILED: "red"
|
|
1194
|
+
HealthStatus.FAILED: "red",
|
|
1149
1195
|
}
|
|
1150
1196
|
|
|
1151
1197
|
status_icon = {
|
|
1152
1198
|
HealthStatus.HEALTHY: "✓",
|
|
1153
1199
|
HealthStatus.WARNING: "⚠️",
|
|
1154
1200
|
HealthStatus.CRITICAL: "🚨",
|
|
1155
|
-
HealthStatus.FAILED: "❌"
|
|
1201
|
+
HealthStatus.FAILED: "❌",
|
|
1156
1202
|
}
|
|
1157
1203
|
|
|
1158
1204
|
color = status_color.get(health["status"], "white")
|
|
@@ -1175,7 +1221,10 @@ def health(
|
|
|
1175
1221
|
console.print("\n[green]✓ No issues detected[/green]")
|
|
1176
1222
|
|
|
1177
1223
|
# Auto-repair if requested
|
|
1178
|
-
if auto_repair and health["status"] in [
|
|
1224
|
+
if auto_repair and health["status"] in [
|
|
1225
|
+
HealthStatus.CRITICAL,
|
|
1226
|
+
HealthStatus.WARNING,
|
|
1227
|
+
]:
|
|
1179
1228
|
console.print("\n[yellow]Attempting automatic repair...[/yellow]")
|
|
1180
1229
|
repair_result = health_monitor.auto_repair()
|
|
1181
1230
|
|
|
@@ -1189,7 +1238,9 @@ def health(
|
|
|
1189
1238
|
new_health = health_monitor.check_health()
|
|
1190
1239
|
new_color = status_color.get(new_health["status"], "white")
|
|
1191
1240
|
new_icon = status_icon.get(new_health["status"], "?")
|
|
1192
|
-
console.print(
|
|
1241
|
+
console.print(
|
|
1242
|
+
f"[{new_color}]{new_icon} Updated Health: {new_health['status'].upper()}[/{new_color}]"
|
|
1243
|
+
)
|
|
1193
1244
|
else:
|
|
1194
1245
|
console.print("[yellow]No repair actions available[/yellow]")
|
|
1195
1246
|
|
|
@@ -1220,7 +1271,6 @@ def create(
|
|
|
1220
1271
|
),
|
|
1221
1272
|
) -> None:
|
|
1222
1273
|
"""Create a new ticket with comprehensive health checks."""
|
|
1223
|
-
|
|
1224
1274
|
# IMMEDIATE HEALTH CHECK - Critical for reliability
|
|
1225
1275
|
health_monitor = QueueHealthMonitor()
|
|
1226
1276
|
health = health_monitor.check_health()
|
|
@@ -1243,13 +1293,21 @@ def create(
|
|
|
1243
1293
|
# Re-check health after repair
|
|
1244
1294
|
health = health_monitor.check_health()
|
|
1245
1295
|
if health["status"] == HealthStatus.CRITICAL:
|
|
1246
|
-
console.print(
|
|
1247
|
-
|
|
1296
|
+
console.print(
|
|
1297
|
+
"[red]❌ Auto-repair failed. Manual intervention required.[/red]"
|
|
1298
|
+
)
|
|
1299
|
+
console.print(
|
|
1300
|
+
"[red]Cannot safely create ticket. Please check system status.[/red]"
|
|
1301
|
+
)
|
|
1248
1302
|
raise typer.Exit(1)
|
|
1249
1303
|
else:
|
|
1250
|
-
console.print(
|
|
1304
|
+
console.print(
|
|
1305
|
+
"[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]"
|
|
1306
|
+
)
|
|
1251
1307
|
else:
|
|
1252
|
-
console.print(
|
|
1308
|
+
console.print(
|
|
1309
|
+
"[red]❌ No repair actions available. Manual intervention required.[/red]"
|
|
1310
|
+
)
|
|
1253
1311
|
raise typer.Exit(1)
|
|
1254
1312
|
|
|
1255
1313
|
elif health["status"] == HealthStatus.WARNING:
|
|
@@ -1293,7 +1351,9 @@ def create(
|
|
|
1293
1351
|
|
|
1294
1352
|
# WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
|
|
1295
1353
|
if adapter_name == "linear":
|
|
1296
|
-
console.print(
|
|
1354
|
+
console.print(
|
|
1355
|
+
"[yellow]⚠️[/yellow] Using direct operation for Linear adapter (bypassing queue)"
|
|
1356
|
+
)
|
|
1297
1357
|
try:
|
|
1298
1358
|
# Load configuration and create adapter directly
|
|
1299
1359
|
config = load_config()
|
|
@@ -1301,20 +1361,27 @@ def create(
|
|
|
1301
1361
|
|
|
1302
1362
|
# Import and create adapter
|
|
1303
1363
|
from ..core.registry import AdapterRegistry
|
|
1364
|
+
|
|
1304
1365
|
adapter = AdapterRegistry.get_adapter(adapter_name, adapter_config)
|
|
1305
1366
|
|
|
1306
1367
|
# Create task directly
|
|
1307
|
-
from ..core.models import
|
|
1368
|
+
from ..core.models import Priority, Task
|
|
1369
|
+
|
|
1308
1370
|
task = Task(
|
|
1309
1371
|
title=task_data["title"],
|
|
1310
1372
|
description=task_data.get("description"),
|
|
1311
|
-
priority=
|
|
1373
|
+
priority=(
|
|
1374
|
+
Priority(task_data["priority"])
|
|
1375
|
+
if task_data.get("priority")
|
|
1376
|
+
else Priority.MEDIUM
|
|
1377
|
+
),
|
|
1312
1378
|
tags=task_data.get("tags", []),
|
|
1313
|
-
assignee=task_data.get("assignee")
|
|
1379
|
+
assignee=task_data.get("assignee"),
|
|
1314
1380
|
)
|
|
1315
1381
|
|
|
1316
1382
|
# Create ticket synchronously
|
|
1317
1383
|
import asyncio
|
|
1384
|
+
|
|
1318
1385
|
result = asyncio.run(adapter.create(task))
|
|
1319
1386
|
|
|
1320
1387
|
console.print(f"[green]✓[/green] Ticket created successfully: {result.id}")
|
|
@@ -1322,7 +1389,11 @@ def create(
|
|
|
1322
1389
|
console.print(f" Priority: {result.priority}")
|
|
1323
1390
|
console.print(f" State: {result.state}")
|
|
1324
1391
|
# Get URL from metadata if available
|
|
1325
|
-
if
|
|
1392
|
+
if (
|
|
1393
|
+
result.metadata
|
|
1394
|
+
and "linear" in result.metadata
|
|
1395
|
+
and "url" in result.metadata["linear"]
|
|
1396
|
+
):
|
|
1326
1397
|
console.print(f" URL: {result.metadata['linear']['url']}")
|
|
1327
1398
|
|
|
1328
1399
|
return result.id
|
|
@@ -1337,12 +1408,14 @@ def create(
|
|
|
1337
1408
|
ticket_data=task_data,
|
|
1338
1409
|
adapter=adapter_name,
|
|
1339
1410
|
operation="create",
|
|
1340
|
-
project_dir=str(Path.cwd()) # Explicitly pass current project directory
|
|
1411
|
+
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
1341
1412
|
)
|
|
1342
1413
|
|
|
1343
1414
|
# Register in ticket registry for tracking
|
|
1344
1415
|
registry = TicketRegistry()
|
|
1345
|
-
registry.register_ticket_operation(
|
|
1416
|
+
registry.register_ticket_operation(
|
|
1417
|
+
queue_id, adapter_name, "create", title, task_data
|
|
1418
|
+
)
|
|
1346
1419
|
|
|
1347
1420
|
console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
|
|
1348
1421
|
console.print(f" Title: {title}")
|
|
@@ -1359,6 +1432,7 @@ def create(
|
|
|
1359
1432
|
|
|
1360
1433
|
# Give immediate feedback on processing
|
|
1361
1434
|
import time
|
|
1435
|
+
|
|
1362
1436
|
time.sleep(1) # Brief pause to let worker start
|
|
1363
1437
|
|
|
1364
1438
|
# Check if item is being processed
|
|
@@ -1368,15 +1442,23 @@ def create(
|
|
|
1368
1442
|
elif item and item.status == QueueStatus.PENDING:
|
|
1369
1443
|
console.print("[yellow]⏳ Item is queued for processing[/yellow]")
|
|
1370
1444
|
else:
|
|
1371
|
-
console.print(
|
|
1445
|
+
console.print(
|
|
1446
|
+
"[red]⚠️ Item status unclear - check with 'mcp-ticketer check {queue_id}'[/red]"
|
|
1447
|
+
)
|
|
1372
1448
|
else:
|
|
1373
1449
|
# Worker didn't start - this is a problem
|
|
1374
1450
|
pending_count = queue.get_pending_count()
|
|
1375
1451
|
if pending_count > 1: # More than just this item
|
|
1376
|
-
console.print(
|
|
1377
|
-
|
|
1452
|
+
console.print(
|
|
1453
|
+
f"[red]❌ Worker failed to start with {pending_count} pending items![/red]"
|
|
1454
|
+
)
|
|
1455
|
+
console.print(
|
|
1456
|
+
"[red]This is a critical issue. Try 'mcp-ticketer queue worker start' manually.[/red]"
|
|
1457
|
+
)
|
|
1378
1458
|
else:
|
|
1379
|
-
console.print(
|
|
1459
|
+
console.print(
|
|
1460
|
+
"[yellow]Worker not started (no other pending items)[/yellow]"
|
|
1461
|
+
)
|
|
1380
1462
|
|
|
1381
1463
|
|
|
1382
1464
|
@app.command("list")
|
|
@@ -1421,7 +1503,7 @@ def list_tickets(
|
|
|
1421
1503
|
|
|
1422
1504
|
for ticket in tickets:
|
|
1423
1505
|
# Handle assignee field - Epic doesn't have assignee, Task does
|
|
1424
|
-
assignee = getattr(ticket,
|
|
1506
|
+
assignee = getattr(ticket, "assignee", None) or "-"
|
|
1425
1507
|
|
|
1426
1508
|
table.add_row(
|
|
1427
1509
|
ticket.id or "N/A",
|
|
@@ -1503,7 +1585,7 @@ def comment(
|
|
|
1503
1585
|
comment = Comment(
|
|
1504
1586
|
ticket_id=ticket_id,
|
|
1505
1587
|
content=content,
|
|
1506
|
-
author="cli-user" # Could be made configurable
|
|
1588
|
+
author="cli-user", # Could be made configurable
|
|
1507
1589
|
)
|
|
1508
1590
|
|
|
1509
1591
|
result = await adapter_instance.add_comment(comment)
|
|
@@ -1511,7 +1593,7 @@ def comment(
|
|
|
1511
1593
|
|
|
1512
1594
|
try:
|
|
1513
1595
|
result = asyncio.run(_comment())
|
|
1514
|
-
console.print(
|
|
1596
|
+
console.print("[green]✓[/green] Comment added successfully")
|
|
1515
1597
|
if result.id:
|
|
1516
1598
|
console.print(f"Comment ID: {result.id}")
|
|
1517
1599
|
console.print(f"Content: {content}")
|
|
@@ -1569,7 +1651,7 @@ def update(
|
|
|
1569
1651
|
ticket_data=updates,
|
|
1570
1652
|
adapter=adapter_name,
|
|
1571
1653
|
operation="update",
|
|
1572
|
-
project_dir=str(Path.cwd()) # Explicitly pass current project directory
|
|
1654
|
+
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
1573
1655
|
)
|
|
1574
1656
|
|
|
1575
1657
|
console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
|
|
@@ -1637,7 +1719,7 @@ def transition(
|
|
|
1637
1719
|
},
|
|
1638
1720
|
adapter=adapter_name,
|
|
1639
1721
|
operation="transition",
|
|
1640
|
-
project_dir=str(Path.cwd()) # Explicitly pass current project directory
|
|
1722
|
+
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
1641
1723
|
)
|
|
1642
1724
|
|
|
1643
1725
|
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
@@ -1699,30 +1781,42 @@ app.add_typer(queue_app, name="queue")
|
|
|
1699
1781
|
# Add discover command to main app
|
|
1700
1782
|
app.add_typer(discover_app, name="discover")
|
|
1701
1783
|
|
|
1784
|
+
|
|
1702
1785
|
# Add diagnostics command
|
|
1703
1786
|
@app.command()
|
|
1704
1787
|
def diagnose(
|
|
1705
|
-
output_file: Optional[str] = typer.Option(
|
|
1706
|
-
|
|
1707
|
-
|
|
1788
|
+
output_file: Optional[str] = typer.Option(
|
|
1789
|
+
None, "--output", "-o", help="Save full report to file"
|
|
1790
|
+
),
|
|
1791
|
+
json_output: bool = typer.Option(
|
|
1792
|
+
False, "--json", help="Output report in JSON format"
|
|
1793
|
+
),
|
|
1794
|
+
simple: bool = typer.Option(
|
|
1795
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
1796
|
+
),
|
|
1708
1797
|
) -> None:
|
|
1709
1798
|
"""Run comprehensive system diagnostics and health check."""
|
|
1710
1799
|
if simple:
|
|
1711
1800
|
from .simple_health import simple_diagnose
|
|
1801
|
+
|
|
1712
1802
|
report = simple_diagnose()
|
|
1713
1803
|
if output_file:
|
|
1714
1804
|
import json
|
|
1715
|
-
|
|
1805
|
+
|
|
1806
|
+
with open(output_file, "w") as f:
|
|
1716
1807
|
json.dump(report, f, indent=2)
|
|
1717
1808
|
console.print(f"\n📄 Report saved to: {output_file}")
|
|
1718
1809
|
if json_output:
|
|
1719
1810
|
import json
|
|
1811
|
+
|
|
1720
1812
|
console.print("\n" + json.dumps(report, indent=2))
|
|
1721
1813
|
if report["issues"]:
|
|
1722
1814
|
raise typer.Exit(1)
|
|
1723
1815
|
else:
|
|
1724
1816
|
try:
|
|
1725
|
-
asyncio.run(
|
|
1817
|
+
asyncio.run(
|
|
1818
|
+
run_diagnostics(output_file=output_file, json_output=json_output)
|
|
1819
|
+
)
|
|
1726
1820
|
except typer.Exit:
|
|
1727
1821
|
# typer.Exit is expected - don't fall back to simple diagnostics
|
|
1728
1822
|
raise
|
|
@@ -1730,6 +1824,7 @@ def diagnose(
|
|
|
1730
1824
|
console.print(f"⚠️ Full diagnostics failed: {e}")
|
|
1731
1825
|
console.print("🔄 Falling back to simple diagnostics...")
|
|
1732
1826
|
from .simple_health import simple_diagnose
|
|
1827
|
+
|
|
1733
1828
|
report = simple_diagnose()
|
|
1734
1829
|
if report["issues"]:
|
|
1735
1830
|
raise typer.Exit(1)
|
|
@@ -1744,6 +1839,7 @@ def health() -> None:
|
|
|
1744
1839
|
if result != 0:
|
|
1745
1840
|
raise typer.Exit(result)
|
|
1746
1841
|
|
|
1842
|
+
|
|
1747
1843
|
# Create MCP configuration command group
|
|
1748
1844
|
mcp_app = typer.Typer(
|
|
1749
1845
|
name="mcp",
|
|
@@ -1794,9 +1890,6 @@ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
|
1794
1890
|
console.print(f"\nRetry Count: {item.retry_count}")
|
|
1795
1891
|
|
|
1796
1892
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
1893
|
@app.command()
|
|
1801
1894
|
def serve(
|
|
1802
1895
|
adapter: Optional[AdapterType] = typer.Option(
|
|
@@ -1834,6 +1927,7 @@ def serve(
|
|
|
1834
1927
|
else:
|
|
1835
1928
|
# Priority 2: .env files
|
|
1836
1929
|
from ..mcp.server import _load_env_configuration
|
|
1930
|
+
|
|
1837
1931
|
env_config = _load_env_configuration()
|
|
1838
1932
|
if env_config:
|
|
1839
1933
|
adapter_type = env_config["adapter_type"]
|