mcp-ticketer 0.3.7__py3-none-any.whl → 0.4.0__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/cli/main.py +108 -33
- mcp_ticketer/cli/platform_commands.py +133 -0
- mcp_ticketer/cli/ticket_commands.py +765 -0
- {mcp_ticketer-0.3.7.dist-info → mcp_ticketer-0.4.0.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.3.7.dist-info → mcp_ticketer-0.4.0.dist-info}/RECORD +10 -8
- {mcp_ticketer-0.3.7.dist-info → mcp_ticketer-0.4.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.7.dist-info → mcp_ticketer-0.4.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.7.dist-info → mcp_ticketer-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.7.dist-info → mcp_ticketer-0.4.0.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
mcp_ticketer/cli/main.py
CHANGED
|
@@ -24,9 +24,10 @@ from ..queue.ticket_registry import TicketRegistry
|
|
|
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
|
|
27
|
-
from .linear_commands import app as linear_app
|
|
28
27
|
from .migrate_config import migrate_config_command
|
|
28
|
+
from .platform_commands import app as platform_app
|
|
29
29
|
from .queue_commands import app as queue_app
|
|
30
|
+
from .ticket_commands import app as ticket_app
|
|
30
31
|
|
|
31
32
|
# Load environment variables from .env files
|
|
32
33
|
# Priority: .env.local (highest) > .env (base)
|
|
@@ -1142,9 +1143,14 @@ def migrate_config(
|
|
|
1142
1143
|
migrate_config_command(dry_run=dry_run)
|
|
1143
1144
|
|
|
1144
1145
|
|
|
1145
|
-
@app.command("status")
|
|
1146
|
-
def
|
|
1147
|
-
"""Show queue and worker status.
|
|
1146
|
+
@app.command("queue-status", deprecated=True, hidden=True)
|
|
1147
|
+
def old_queue_status_command():
|
|
1148
|
+
"""Show queue and worker status.
|
|
1149
|
+
|
|
1150
|
+
DEPRECATED: Use 'mcp-ticketer queue status' instead.
|
|
1151
|
+
"""
|
|
1152
|
+
console.print("[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer queue status' instead.[/yellow]\n")
|
|
1153
|
+
|
|
1148
1154
|
queue = Queue()
|
|
1149
1155
|
manager = WorkerManager()
|
|
1150
1156
|
|
|
@@ -1169,12 +1175,12 @@ def status_command():
|
|
|
1169
1175
|
console.print("\n[red]○ Worker is not running[/red]")
|
|
1170
1176
|
if pending > 0:
|
|
1171
1177
|
console.print(
|
|
1172
|
-
"[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]"
|
|
1178
|
+
"[yellow]Note: There are pending items. Start worker with 'mcp-ticketer queue worker start'[/yellow]"
|
|
1173
1179
|
)
|
|
1174
1180
|
|
|
1175
1181
|
|
|
1176
|
-
@app.command()
|
|
1177
|
-
def
|
|
1182
|
+
@app.command("queue-health", deprecated=True, hidden=True)
|
|
1183
|
+
def old_queue_health_command(
|
|
1178
1184
|
auto_repair: bool = typer.Option(
|
|
1179
1185
|
False, "--auto-repair", help="Attempt automatic repair of issues"
|
|
1180
1186
|
),
|
|
@@ -1182,7 +1188,11 @@ def health(
|
|
|
1182
1188
|
False, "--verbose", "-v", help="Show detailed health information"
|
|
1183
1189
|
),
|
|
1184
1190
|
) -> None:
|
|
1185
|
-
"""Check queue system health and detect issues immediately.
|
|
1191
|
+
"""Check queue system health and detect issues immediately.
|
|
1192
|
+
|
|
1193
|
+
DEPRECATED: Use 'mcp-ticketer queue health' instead.
|
|
1194
|
+
"""
|
|
1195
|
+
console.print("[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer queue health' instead.[/yellow]\n")
|
|
1186
1196
|
health_monitor = QueueHealthMonitor()
|
|
1187
1197
|
health = health_monitor.check_health()
|
|
1188
1198
|
|
|
@@ -1251,7 +1261,7 @@ def health(
|
|
|
1251
1261
|
raise typer.Exit(2)
|
|
1252
1262
|
|
|
1253
1263
|
|
|
1254
|
-
@app.command()
|
|
1264
|
+
@app.command(deprecated=True, hidden=True)
|
|
1255
1265
|
def create(
|
|
1256
1266
|
title: str = typer.Argument(..., help="Ticket title"),
|
|
1257
1267
|
description: Optional[str] = typer.Option(
|
|
@@ -1280,7 +1290,12 @@ def create(
|
|
|
1280
1290
|
None, "--adapter", help="Override default adapter"
|
|
1281
1291
|
),
|
|
1282
1292
|
) -> None:
|
|
1283
|
-
"""Create a new ticket with comprehensive health checks.
|
|
1293
|
+
"""Create a new ticket with comprehensive health checks.
|
|
1294
|
+
|
|
1295
|
+
DEPRECATED: Use 'mcp-ticketer ticket create' instead.
|
|
1296
|
+
"""
|
|
1297
|
+
console.print("[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket create' instead.[/yellow]\n")
|
|
1298
|
+
|
|
1284
1299
|
# IMMEDIATE HEALTH CHECK - Critical for reliability
|
|
1285
1300
|
health_monitor = QueueHealthMonitor()
|
|
1286
1301
|
health = health_monitor.check_health()
|
|
@@ -1476,7 +1491,7 @@ def create(
|
|
|
1476
1491
|
)
|
|
1477
1492
|
|
|
1478
1493
|
|
|
1479
|
-
@app.command("list")
|
|
1494
|
+
@app.command("list", deprecated=True, hidden=True)
|
|
1480
1495
|
def list_tickets(
|
|
1481
1496
|
state: Optional[TicketState] = typer.Option(
|
|
1482
1497
|
None, "--state", "-s", help="Filter by state"
|
|
@@ -1489,7 +1504,11 @@ def list_tickets(
|
|
|
1489
1504
|
None, "--adapter", help="Override default adapter"
|
|
1490
1505
|
),
|
|
1491
1506
|
) -> None:
|
|
1492
|
-
"""List tickets with optional filters.
|
|
1507
|
+
"""List tickets with optional filters.
|
|
1508
|
+
|
|
1509
|
+
DEPRECATED: Use 'mcp-ticketer ticket list' instead.
|
|
1510
|
+
"""
|
|
1511
|
+
console.print("[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket list' instead.[/yellow]\n")
|
|
1493
1512
|
|
|
1494
1513
|
async def _list():
|
|
1495
1514
|
adapter_instance = get_adapter(
|
|
@@ -1531,7 +1550,7 @@ def list_tickets(
|
|
|
1531
1550
|
console.print(table)
|
|
1532
1551
|
|
|
1533
1552
|
|
|
1534
|
-
@app.command()
|
|
1553
|
+
@app.command(deprecated=True, hidden=True)
|
|
1535
1554
|
def show(
|
|
1536
1555
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1537
1556
|
comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
|
|
@@ -1539,7 +1558,11 @@ def show(
|
|
|
1539
1558
|
None, "--adapter", help="Override default adapter"
|
|
1540
1559
|
),
|
|
1541
1560
|
) -> None:
|
|
1542
|
-
"""Show detailed ticket information.
|
|
1561
|
+
"""Show detailed ticket information.
|
|
1562
|
+
|
|
1563
|
+
DEPRECATED: Use 'mcp-ticketer ticket show' instead.
|
|
1564
|
+
"""
|
|
1565
|
+
console.print("[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket show' instead.[/yellow]\n")
|
|
1543
1566
|
|
|
1544
1567
|
async def _show():
|
|
1545
1568
|
adapter_instance = get_adapter(
|
|
@@ -1581,7 +1604,7 @@ def show(
|
|
|
1581
1604
|
console.print(comment.content)
|
|
1582
1605
|
|
|
1583
1606
|
|
|
1584
|
-
@app.command()
|
|
1607
|
+
@app.command(deprecated=True, hidden=True)
|
|
1585
1608
|
def comment(
|
|
1586
1609
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1587
1610
|
content: str = typer.Argument(..., help="Comment content"),
|
|
@@ -1589,7 +1612,11 @@ def comment(
|
|
|
1589
1612
|
None, "--adapter", help="Override default adapter"
|
|
1590
1613
|
),
|
|
1591
1614
|
) -> None:
|
|
1592
|
-
"""Add a comment to a ticket.
|
|
1615
|
+
"""Add a comment to a ticket.
|
|
1616
|
+
|
|
1617
|
+
DEPRECATED: Use 'mcp-ticketer ticket comment' instead.
|
|
1618
|
+
"""
|
|
1619
|
+
console.print("[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket comment' instead.[/yellow]\n")
|
|
1593
1620
|
|
|
1594
1621
|
async def _comment():
|
|
1595
1622
|
adapter_instance = get_adapter(
|
|
@@ -1617,7 +1644,7 @@ def comment(
|
|
|
1617
1644
|
raise typer.Exit(1)
|
|
1618
1645
|
|
|
1619
1646
|
|
|
1620
|
-
@app.command()
|
|
1647
|
+
@app.command(deprecated=True, hidden=True)
|
|
1621
1648
|
def update(
|
|
1622
1649
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1623
1650
|
title: Optional[str] = typer.Option(None, "--title", help="New title"),
|
|
@@ -1634,7 +1661,11 @@ def update(
|
|
|
1634
1661
|
None, "--adapter", help="Override default adapter"
|
|
1635
1662
|
),
|
|
1636
1663
|
) -> None:
|
|
1637
|
-
"""Update ticket fields.
|
|
1664
|
+
"""Update ticket fields.
|
|
1665
|
+
|
|
1666
|
+
DEPRECATED: Use 'mcp-ticketer ticket update' instead.
|
|
1667
|
+
"""
|
|
1668
|
+
console.print("[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket update' instead.[/yellow]\n")
|
|
1638
1669
|
updates = {}
|
|
1639
1670
|
if title:
|
|
1640
1671
|
updates["title"] = title
|
|
@@ -1681,7 +1712,7 @@ def update(
|
|
|
1681
1712
|
console.print("[dim]Worker started to process request[/dim]")
|
|
1682
1713
|
|
|
1683
1714
|
|
|
1684
|
-
@app.command()
|
|
1715
|
+
@app.command(deprecated=True, hidden=True)
|
|
1685
1716
|
def transition(
|
|
1686
1717
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1687
1718
|
state_positional: Optional[TicketState] = typer.Argument(
|
|
@@ -1696,15 +1727,19 @@ def transition(
|
|
|
1696
1727
|
) -> None:
|
|
1697
1728
|
"""Change ticket state with validation.
|
|
1698
1729
|
|
|
1730
|
+
DEPRECATED: Use 'mcp-ticketer ticket transition' instead.
|
|
1731
|
+
|
|
1699
1732
|
Examples:
|
|
1700
1733
|
# Recommended syntax with flag:
|
|
1701
|
-
mcp-ticketer transition BTA-215 --state done
|
|
1702
|
-
mcp-ticketer transition BTA-215 -s in_progress
|
|
1734
|
+
mcp-ticketer ticket transition BTA-215 --state done
|
|
1735
|
+
mcp-ticketer ticket transition BTA-215 -s in_progress
|
|
1703
1736
|
|
|
1704
1737
|
# Legacy positional syntax (still supported):
|
|
1705
|
-
mcp-ticketer transition BTA-215 done
|
|
1738
|
+
mcp-ticketer ticket transition BTA-215 done
|
|
1706
1739
|
|
|
1707
1740
|
"""
|
|
1741
|
+
console.print("[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket transition' instead.[/yellow]\n")
|
|
1742
|
+
|
|
1708
1743
|
# Determine which state to use (prefer flag over positional)
|
|
1709
1744
|
target_state = state if state is not None else state_positional
|
|
1710
1745
|
|
|
@@ -1747,7 +1782,7 @@ def transition(
|
|
|
1747
1782
|
console.print("[dim]Worker started to process request[/dim]")
|
|
1748
1783
|
|
|
1749
1784
|
|
|
1750
|
-
@app.command()
|
|
1785
|
+
@app.command(deprecated=True, hidden=True)
|
|
1751
1786
|
def search(
|
|
1752
1787
|
query: Optional[str] = typer.Argument(None, help="Search query"),
|
|
1753
1788
|
state: Optional[TicketState] = typer.Option(None, "--state", "-s"),
|
|
@@ -1758,7 +1793,11 @@ def search(
|
|
|
1758
1793
|
None, "--adapter", help="Override default adapter"
|
|
1759
1794
|
),
|
|
1760
1795
|
) -> None:
|
|
1761
|
-
"""Search tickets with advanced query.
|
|
1796
|
+
"""Search tickets with advanced query.
|
|
1797
|
+
|
|
1798
|
+
DEPRECATED: Use 'mcp-ticketer ticket search' instead.
|
|
1799
|
+
"""
|
|
1800
|
+
console.print("[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket search' instead.[/yellow]\n")
|
|
1762
1801
|
|
|
1763
1802
|
async def _search():
|
|
1764
1803
|
adapter_instance = get_adapter(
|
|
@@ -1790,6 +1829,12 @@ def search(
|
|
|
1790
1829
|
console.print()
|
|
1791
1830
|
|
|
1792
1831
|
|
|
1832
|
+
# Add ticket command group to main app
|
|
1833
|
+
app.add_typer(ticket_app, name="ticket")
|
|
1834
|
+
|
|
1835
|
+
# Add platform command group to main app
|
|
1836
|
+
app.add_typer(platform_app, name="platform")
|
|
1837
|
+
|
|
1793
1838
|
# Add queue command to main app
|
|
1794
1839
|
app.add_typer(queue_app, name="queue")
|
|
1795
1840
|
|
|
@@ -1798,8 +1843,8 @@ app.add_typer(discover_app, name="discover")
|
|
|
1798
1843
|
|
|
1799
1844
|
|
|
1800
1845
|
# Add diagnostics command
|
|
1801
|
-
@app.command()
|
|
1802
|
-
def
|
|
1846
|
+
@app.command("diagnose")
|
|
1847
|
+
def diagnose_command(
|
|
1803
1848
|
output_file: Optional[str] = typer.Option(
|
|
1804
1849
|
None, "--output", "-o", help="Save full report to file"
|
|
1805
1850
|
),
|
|
@@ -1810,7 +1855,7 @@ def diagnose(
|
|
|
1810
1855
|
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
1811
1856
|
),
|
|
1812
1857
|
) -> None:
|
|
1813
|
-
"""Run comprehensive system diagnostics and health check."""
|
|
1858
|
+
"""Run comprehensive system diagnostics and health check (alias: doctor)."""
|
|
1814
1859
|
if simple:
|
|
1815
1860
|
from .simple_health import simple_diagnose
|
|
1816
1861
|
|
|
@@ -1845,9 +1890,36 @@ def diagnose(
|
|
|
1845
1890
|
raise typer.Exit(1)
|
|
1846
1891
|
|
|
1847
1892
|
|
|
1848
|
-
@app.command()
|
|
1849
|
-
def
|
|
1850
|
-
|
|
1893
|
+
@app.command("doctor")
|
|
1894
|
+
def doctor_alias(
|
|
1895
|
+
output_file: Optional[str] = typer.Option(
|
|
1896
|
+
None, "--output", "-o", help="Save full report to file"
|
|
1897
|
+
),
|
|
1898
|
+
json_output: bool = typer.Option(
|
|
1899
|
+
False, "--json", help="Output report in JSON format"
|
|
1900
|
+
),
|
|
1901
|
+
simple: bool = typer.Option(
|
|
1902
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
1903
|
+
),
|
|
1904
|
+
) -> None:
|
|
1905
|
+
"""Run comprehensive system diagnostics and health check (alias for diagnose)."""
|
|
1906
|
+
# Call the diagnose_command function with the same parameters
|
|
1907
|
+
diagnose_command(output_file=output_file, json_output=json_output, simple=simple)
|
|
1908
|
+
|
|
1909
|
+
|
|
1910
|
+
@app.command("status")
|
|
1911
|
+
def status_command() -> None:
|
|
1912
|
+
"""Quick health check - shows system status summary (alias: health)."""
|
|
1913
|
+
from .simple_health import simple_health_check
|
|
1914
|
+
|
|
1915
|
+
result = simple_health_check()
|
|
1916
|
+
if result != 0:
|
|
1917
|
+
raise typer.Exit(result)
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
@app.command("health")
|
|
1921
|
+
def health_alias() -> None:
|
|
1922
|
+
"""Quick health check - shows system status summary (alias for status)."""
|
|
1851
1923
|
from .simple_health import simple_health_check
|
|
1852
1924
|
|
|
1853
1925
|
result = simple_health_check()
|
|
@@ -1863,9 +1935,13 @@ mcp_app = typer.Typer(
|
|
|
1863
1935
|
)
|
|
1864
1936
|
|
|
1865
1937
|
|
|
1866
|
-
@app.command()
|
|
1938
|
+
@app.command(deprecated=True, hidden=True)
|
|
1867
1939
|
def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
1868
|
-
"""Check status of a queued operation.
|
|
1940
|
+
"""Check status of a queued operation.
|
|
1941
|
+
|
|
1942
|
+
DEPRECATED: Use 'mcp-ticketer ticket check' instead.
|
|
1943
|
+
"""
|
|
1944
|
+
console.print("[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket check' instead.[/yellow]\n")
|
|
1869
1945
|
queue = Queue()
|
|
1870
1946
|
item = queue.get_item(queue_id)
|
|
1871
1947
|
|
|
@@ -2141,7 +2217,6 @@ def mcp_auggie(
|
|
|
2141
2217
|
|
|
2142
2218
|
|
|
2143
2219
|
# Add command groups to main app (must be after all subcommands are defined)
|
|
2144
|
-
app.add_typer(linear_app, name="linear")
|
|
2145
2220
|
app.add_typer(mcp_app, name="mcp")
|
|
2146
2221
|
|
|
2147
2222
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Platform-specific command groups."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
# Import platform-specific command modules
|
|
6
|
+
from .linear_commands import app as linear_app
|
|
7
|
+
|
|
8
|
+
# Create main platform command group
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
name="platform",
|
|
11
|
+
help="Platform-specific commands (Linear, JIRA, GitHub, AITrackdown)",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Register Linear commands
|
|
15
|
+
app.add_typer(linear_app, name="linear")
|
|
16
|
+
|
|
17
|
+
# Create placeholder apps for other platforms
|
|
18
|
+
|
|
19
|
+
# JIRA platform commands (placeholder)
|
|
20
|
+
jira_app = typer.Typer(
|
|
21
|
+
name="jira",
|
|
22
|
+
help="JIRA-specific workspace and project management",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@jira_app.command("projects")
|
|
27
|
+
def jira_list_projects():
|
|
28
|
+
"""List JIRA projects (placeholder - not yet implemented)."""
|
|
29
|
+
from rich.console import Console
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
console.print(
|
|
33
|
+
"[yellow]JIRA platform commands are not yet implemented.[/yellow]"
|
|
34
|
+
)
|
|
35
|
+
console.print(
|
|
36
|
+
"Use the generic ticket commands for JIRA operations:\n"
|
|
37
|
+
" mcp-ticketer ticket create 'My ticket'\n"
|
|
38
|
+
" mcp-ticketer ticket list"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@jira_app.command("configure")
|
|
43
|
+
def jira_configure():
|
|
44
|
+
"""Configure JIRA adapter (placeholder - not yet implemented)."""
|
|
45
|
+
from rich.console import Console
|
|
46
|
+
|
|
47
|
+
console = Console()
|
|
48
|
+
console.print(
|
|
49
|
+
"[yellow]JIRA platform commands are not yet implemented.[/yellow]"
|
|
50
|
+
)
|
|
51
|
+
console.print(
|
|
52
|
+
"Use 'mcp-ticketer init --adapter jira' to configure JIRA adapter."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# GitHub platform commands (placeholder)
|
|
57
|
+
github_app = typer.Typer(
|
|
58
|
+
name="github",
|
|
59
|
+
help="GitHub-specific repository and issue management",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@github_app.command("repos")
|
|
64
|
+
def github_list_repos():
|
|
65
|
+
"""List GitHub repositories (placeholder - not yet implemented)."""
|
|
66
|
+
from rich.console import Console
|
|
67
|
+
|
|
68
|
+
console = Console()
|
|
69
|
+
console.print(
|
|
70
|
+
"[yellow]GitHub platform commands are not yet implemented.[/yellow]"
|
|
71
|
+
)
|
|
72
|
+
console.print(
|
|
73
|
+
"Use the generic ticket commands for GitHub operations:\n"
|
|
74
|
+
" mcp-ticketer ticket create 'My issue'\n"
|
|
75
|
+
" mcp-ticketer ticket list"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@github_app.command("configure")
|
|
80
|
+
def github_configure():
|
|
81
|
+
"""Configure GitHub adapter (placeholder - not yet implemented)."""
|
|
82
|
+
from rich.console import Console
|
|
83
|
+
|
|
84
|
+
console = Console()
|
|
85
|
+
console.print(
|
|
86
|
+
"[yellow]GitHub platform commands are not yet implemented.[/yellow]"
|
|
87
|
+
)
|
|
88
|
+
console.print(
|
|
89
|
+
"Use 'mcp-ticketer init --adapter github' to configure GitHub adapter."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# AITrackdown platform commands (placeholder)
|
|
94
|
+
aitrackdown_app = typer.Typer(
|
|
95
|
+
name="aitrackdown",
|
|
96
|
+
help="AITrackdown-specific local file management",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@aitrackdown_app.command("info")
|
|
101
|
+
def aitrackdown_info():
|
|
102
|
+
"""Show AITrackdown storage information (placeholder - not yet implemented)."""
|
|
103
|
+
from rich.console import Console
|
|
104
|
+
|
|
105
|
+
console = Console()
|
|
106
|
+
console.print(
|
|
107
|
+
"[yellow]AITrackdown platform commands are not yet implemented.[/yellow]"
|
|
108
|
+
)
|
|
109
|
+
console.print(
|
|
110
|
+
"Use the generic ticket commands for AITrackdown operations:\n"
|
|
111
|
+
" mcp-ticketer ticket create 'My ticket'\n"
|
|
112
|
+
" mcp-ticketer ticket list"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@aitrackdown_app.command("configure")
|
|
117
|
+
def aitrackdown_configure():
|
|
118
|
+
"""Configure AITrackdown adapter (placeholder - not yet implemented)."""
|
|
119
|
+
from rich.console import Console
|
|
120
|
+
|
|
121
|
+
console = Console()
|
|
122
|
+
console.print(
|
|
123
|
+
"[yellow]AITrackdown platform commands are not yet implemented.[/yellow]"
|
|
124
|
+
)
|
|
125
|
+
console.print(
|
|
126
|
+
"Use 'mcp-ticketer init --adapter aitrackdown' to configure AITrackdown adapter."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Register all platform command groups
|
|
131
|
+
app.add_typer(jira_app, name="jira")
|
|
132
|
+
app.add_typer(github_app, name="github")
|
|
133
|
+
app.add_typer(aitrackdown_app, name="aitrackdown")
|
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
"""Ticket management commands."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from ..core import AdapterRegistry, Priority, TicketState
|
|
15
|
+
from ..core.models import Comment, SearchQuery
|
|
16
|
+
from ..queue import Queue, QueueStatus, WorkerManager
|
|
17
|
+
from ..queue.health_monitor import HealthStatus, QueueHealthMonitor
|
|
18
|
+
from ..queue.ticket_registry import TicketRegistry
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Moved from main.py to avoid circular import
|
|
22
|
+
class AdapterType(str, Enum):
|
|
23
|
+
"""Available adapter types."""
|
|
24
|
+
|
|
25
|
+
AITRACKDOWN = "aitrackdown"
|
|
26
|
+
LINEAR = "linear"
|
|
27
|
+
JIRA = "jira"
|
|
28
|
+
GITHUB = "github"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
app = typer.Typer(
|
|
32
|
+
name="ticket",
|
|
33
|
+
help="Ticket management operations (create, list, update, search, etc.)",
|
|
34
|
+
)
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Configuration functions (moved from main.py to avoid circular import)
|
|
39
|
+
def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
40
|
+
"""Load configuration from project-local config file."""
|
|
41
|
+
import logging
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
base_dir = project_dir or Path.cwd()
|
|
45
|
+
project_config = base_dir / ".mcp-ticketer" / "config.json"
|
|
46
|
+
|
|
47
|
+
if project_config.exists():
|
|
48
|
+
try:
|
|
49
|
+
with open(project_config) as f:
|
|
50
|
+
config = json.load(f)
|
|
51
|
+
logger.info(f"Loaded configuration from: {project_config}")
|
|
52
|
+
return config
|
|
53
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
54
|
+
logger.warning(f"Could not load project config: {e}, using defaults")
|
|
55
|
+
console.print(f"[yellow]Warning: Could not load project config: {e}[/yellow]")
|
|
56
|
+
|
|
57
|
+
logger.info("No project-local config found, defaulting to aitrackdown adapter")
|
|
58
|
+
return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def save_config(config: dict) -> None:
|
|
62
|
+
"""Save configuration to project-local config file."""
|
|
63
|
+
import logging
|
|
64
|
+
|
|
65
|
+
logger = logging.getLogger(__name__)
|
|
66
|
+
project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
67
|
+
project_config.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
with open(project_config, "w") as f:
|
|
69
|
+
json.dump(config, f, indent=2)
|
|
70
|
+
logger.info(f"Saved configuration to: {project_config}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_adapter(override_adapter: Optional[str] = None, override_config: Optional[dict] = None):
|
|
74
|
+
"""Get configured adapter instance."""
|
|
75
|
+
config = load_config()
|
|
76
|
+
|
|
77
|
+
if override_adapter:
|
|
78
|
+
adapter_type = override_adapter
|
|
79
|
+
adapters_config = config.get("adapters", {})
|
|
80
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
81
|
+
if override_config:
|
|
82
|
+
adapter_config.update(override_config)
|
|
83
|
+
else:
|
|
84
|
+
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
85
|
+
adapters_config = config.get("adapters", {})
|
|
86
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
87
|
+
|
|
88
|
+
if not adapter_config and "config" in config:
|
|
89
|
+
adapter_config = config["config"]
|
|
90
|
+
|
|
91
|
+
# Add environment variables for authentication
|
|
92
|
+
if adapter_type == "linear":
|
|
93
|
+
if not adapter_config.get("api_key"):
|
|
94
|
+
adapter_config["api_key"] = os.getenv("LINEAR_API_KEY")
|
|
95
|
+
elif adapter_type == "github":
|
|
96
|
+
if not adapter_config.get("api_key") and not adapter_config.get("token"):
|
|
97
|
+
adapter_config["api_key"] = os.getenv("GITHUB_TOKEN")
|
|
98
|
+
elif adapter_type == "jira":
|
|
99
|
+
if not adapter_config.get("api_token"):
|
|
100
|
+
adapter_config["api_token"] = os.getenv("JIRA_ACCESS_TOKEN")
|
|
101
|
+
if not adapter_config.get("email"):
|
|
102
|
+
adapter_config["email"] = os.getenv("JIRA_ACCESS_USER")
|
|
103
|
+
|
|
104
|
+
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _discover_from_env_files() -> Optional[str]:
|
|
108
|
+
"""Discover adapter configuration from .env or .env.local files.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Adapter name if discovered, None otherwise
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
import logging
|
|
115
|
+
from pathlib import Path
|
|
116
|
+
|
|
117
|
+
logger = logging.getLogger(__name__)
|
|
118
|
+
|
|
119
|
+
# Check .env.local first, then .env
|
|
120
|
+
env_files = [".env.local", ".env"]
|
|
121
|
+
|
|
122
|
+
for env_file in env_files:
|
|
123
|
+
env_path = Path.cwd() / env_file
|
|
124
|
+
if env_path.exists():
|
|
125
|
+
try:
|
|
126
|
+
# Simple .env parsing (key=value format)
|
|
127
|
+
env_vars = {}
|
|
128
|
+
with open(env_path) as f:
|
|
129
|
+
for line in f:
|
|
130
|
+
line = line.strip()
|
|
131
|
+
if line and not line.startswith("#") and "=" in line:
|
|
132
|
+
key, value = line.split("=", 1)
|
|
133
|
+
env_vars[key.strip()] = value.strip().strip("\"'")
|
|
134
|
+
|
|
135
|
+
# Check for adapter-specific variables
|
|
136
|
+
if env_vars.get("LINEAR_API_KEY"):
|
|
137
|
+
logger.info(f"Discovered Linear configuration in {env_file}")
|
|
138
|
+
return "linear"
|
|
139
|
+
elif env_vars.get("GITHUB_TOKEN"):
|
|
140
|
+
logger.info(f"Discovered GitHub configuration in {env_file}")
|
|
141
|
+
return "github"
|
|
142
|
+
elif env_vars.get("JIRA_SERVER"):
|
|
143
|
+
logger.info(f"Discovered JIRA configuration in {env_file}")
|
|
144
|
+
return "jira"
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.warning(f"Could not read {env_file}: {e}")
|
|
148
|
+
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _save_adapter_to_config(adapter_name: str) -> None:
|
|
153
|
+
"""Save adapter configuration to config file.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
adapter_name: Name of the adapter to save as default
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
import logging
|
|
160
|
+
|
|
161
|
+
from .main import save_config
|
|
162
|
+
|
|
163
|
+
logger = logging.getLogger(__name__)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
config = load_config()
|
|
167
|
+
config["default_adapter"] = adapter_name
|
|
168
|
+
|
|
169
|
+
# Ensure adapters section exists
|
|
170
|
+
if "adapters" not in config:
|
|
171
|
+
config["adapters"] = {}
|
|
172
|
+
|
|
173
|
+
# Add basic adapter config if not exists
|
|
174
|
+
if adapter_name not in config["adapters"]:
|
|
175
|
+
if adapter_name == "aitrackdown":
|
|
176
|
+
config["adapters"][adapter_name] = {"base_path": ".aitrackdown"}
|
|
177
|
+
else:
|
|
178
|
+
config["adapters"][adapter_name] = {"type": adapter_name}
|
|
179
|
+
|
|
180
|
+
save_config(config)
|
|
181
|
+
logger.info(f"Saved {adapter_name} as default adapter")
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning(f"Could not save adapter configuration: {e}")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.command()
|
|
188
|
+
def create(
|
|
189
|
+
title: str = typer.Argument(..., help="Ticket title"),
|
|
190
|
+
description: Optional[str] = typer.Option(
|
|
191
|
+
None, "--description", "-d", help="Ticket description"
|
|
192
|
+
),
|
|
193
|
+
priority: Priority = typer.Option(
|
|
194
|
+
Priority.MEDIUM, "--priority", "-p", help="Priority level"
|
|
195
|
+
),
|
|
196
|
+
tags: Optional[list[str]] = typer.Option(
|
|
197
|
+
None, "--tag", "-t", help="Tags (can be specified multiple times)"
|
|
198
|
+
),
|
|
199
|
+
assignee: Optional[str] = typer.Option(
|
|
200
|
+
None, "--assignee", "-a", help="Assignee username"
|
|
201
|
+
),
|
|
202
|
+
project: Optional[str] = typer.Option(
|
|
203
|
+
None,
|
|
204
|
+
"--project",
|
|
205
|
+
help="Parent project/epic ID (synonym for --epic)",
|
|
206
|
+
),
|
|
207
|
+
epic: Optional[str] = typer.Option(
|
|
208
|
+
None,
|
|
209
|
+
"--epic",
|
|
210
|
+
help="Parent epic/project ID (synonym for --project)",
|
|
211
|
+
),
|
|
212
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
213
|
+
None, "--adapter", help="Override default adapter"
|
|
214
|
+
),
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Create a new ticket with comprehensive health checks."""
|
|
217
|
+
# IMMEDIATE HEALTH CHECK - Critical for reliability
|
|
218
|
+
health_monitor = QueueHealthMonitor()
|
|
219
|
+
health = health_monitor.check_health()
|
|
220
|
+
|
|
221
|
+
# Display health status
|
|
222
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
223
|
+
console.print("[red]🚨 CRITICAL: Queue system has serious issues![/red]")
|
|
224
|
+
for alert in health["alerts"]:
|
|
225
|
+
if alert["level"] == "critical":
|
|
226
|
+
console.print(f"[red] • {alert['message']}[/red]")
|
|
227
|
+
|
|
228
|
+
# Attempt auto-repair
|
|
229
|
+
console.print("[yellow]Attempting automatic repair...[/yellow]")
|
|
230
|
+
repair_result = health_monitor.auto_repair()
|
|
231
|
+
|
|
232
|
+
if repair_result["actions_taken"]:
|
|
233
|
+
for action in repair_result["actions_taken"]:
|
|
234
|
+
console.print(f"[yellow] ✓ {action}[/yellow]")
|
|
235
|
+
|
|
236
|
+
# Re-check health after repair
|
|
237
|
+
health = health_monitor.check_health()
|
|
238
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
239
|
+
console.print(
|
|
240
|
+
"[red]❌ Auto-repair failed. Manual intervention required.[/red]"
|
|
241
|
+
)
|
|
242
|
+
console.print(
|
|
243
|
+
"[red]Cannot safely create ticket. Please check system status.[/red]"
|
|
244
|
+
)
|
|
245
|
+
raise typer.Exit(1)
|
|
246
|
+
else:
|
|
247
|
+
console.print(
|
|
248
|
+
"[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]"
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
console.print(
|
|
252
|
+
"[red]❌ No repair actions available. Manual intervention required.[/red]"
|
|
253
|
+
)
|
|
254
|
+
raise typer.Exit(1)
|
|
255
|
+
|
|
256
|
+
elif health["status"] == HealthStatus.WARNING:
|
|
257
|
+
console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
|
|
258
|
+
for alert in health["alerts"]:
|
|
259
|
+
if alert["level"] == "warning":
|
|
260
|
+
console.print(f"[yellow] • {alert['message']}[/yellow]")
|
|
261
|
+
console.print("[yellow]Proceeding with ticket creation...[/yellow]")
|
|
262
|
+
|
|
263
|
+
# Get the adapter name with priority: 1) argument, 2) config, 3) .env files, 4) default
|
|
264
|
+
if adapter:
|
|
265
|
+
# Priority 1: Command-line argument - save to config for future use
|
|
266
|
+
adapter_name = adapter.value
|
|
267
|
+
_save_adapter_to_config(adapter_name)
|
|
268
|
+
else:
|
|
269
|
+
# Priority 2: Check existing config
|
|
270
|
+
config = load_config()
|
|
271
|
+
adapter_name = config.get("default_adapter")
|
|
272
|
+
|
|
273
|
+
if not adapter_name or adapter_name == "aitrackdown":
|
|
274
|
+
# Priority 3: Check .env files and save if found
|
|
275
|
+
env_adapter = _discover_from_env_files()
|
|
276
|
+
if env_adapter:
|
|
277
|
+
adapter_name = env_adapter
|
|
278
|
+
_save_adapter_to_config(adapter_name)
|
|
279
|
+
else:
|
|
280
|
+
# Priority 4: Default
|
|
281
|
+
adapter_name = "aitrackdown"
|
|
282
|
+
|
|
283
|
+
# Resolve project/epic synonym - prefer whichever is provided
|
|
284
|
+
parent_epic_id = project or epic
|
|
285
|
+
|
|
286
|
+
# Create task data
|
|
287
|
+
# Import Priority for type checking
|
|
288
|
+
from ..core.models import Priority as PriorityEnum
|
|
289
|
+
|
|
290
|
+
task_data = {
|
|
291
|
+
"title": title,
|
|
292
|
+
"description": description,
|
|
293
|
+
"priority": priority.value if isinstance(priority, PriorityEnum) else priority,
|
|
294
|
+
"tags": tags or [],
|
|
295
|
+
"assignee": assignee,
|
|
296
|
+
"parent_epic": parent_epic_id,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
|
|
300
|
+
if adapter_name == "linear":
|
|
301
|
+
console.print(
|
|
302
|
+
"[yellow]⚠️[/yellow] Using direct operation for Linear adapter (bypassing queue)"
|
|
303
|
+
)
|
|
304
|
+
try:
|
|
305
|
+
# Load configuration and create adapter directly
|
|
306
|
+
config = load_config()
|
|
307
|
+
adapter_config = config.get("adapters", {}).get(adapter_name, {})
|
|
308
|
+
|
|
309
|
+
# Import and create adapter
|
|
310
|
+
from ..core.registry import AdapterRegistry
|
|
311
|
+
|
|
312
|
+
adapter_instance = AdapterRegistry.get_adapter(adapter_name, adapter_config)
|
|
313
|
+
|
|
314
|
+
# Create task directly
|
|
315
|
+
from ..core.models import Priority, Task
|
|
316
|
+
|
|
317
|
+
task = Task(
|
|
318
|
+
title=task_data["title"],
|
|
319
|
+
description=task_data.get("description"),
|
|
320
|
+
priority=(
|
|
321
|
+
Priority(task_data["priority"])
|
|
322
|
+
if task_data.get("priority")
|
|
323
|
+
else Priority.MEDIUM
|
|
324
|
+
),
|
|
325
|
+
tags=task_data.get("tags", []),
|
|
326
|
+
assignee=task_data.get("assignee"),
|
|
327
|
+
parent_epic=task_data.get("parent_epic"),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Create ticket synchronously
|
|
331
|
+
import asyncio
|
|
332
|
+
|
|
333
|
+
result = asyncio.run(adapter_instance.create(task))
|
|
334
|
+
|
|
335
|
+
console.print(f"[green]✓[/green] Ticket created successfully: {result.id}")
|
|
336
|
+
console.print(f" Title: {result.title}")
|
|
337
|
+
console.print(f" Priority: {result.priority}")
|
|
338
|
+
console.print(f" State: {result.state}")
|
|
339
|
+
# Get URL from metadata if available
|
|
340
|
+
if (
|
|
341
|
+
result.metadata
|
|
342
|
+
and "linear" in result.metadata
|
|
343
|
+
and "url" in result.metadata["linear"]
|
|
344
|
+
):
|
|
345
|
+
console.print(f" URL: {result.metadata['linear']['url']}")
|
|
346
|
+
|
|
347
|
+
return result.id
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
console.print(f"[red]❌[/red] Failed to create ticket: {e}")
|
|
351
|
+
raise
|
|
352
|
+
|
|
353
|
+
# Use queue for other adapters
|
|
354
|
+
queue = Queue()
|
|
355
|
+
queue_id = queue.add(
|
|
356
|
+
ticket_data=task_data,
|
|
357
|
+
adapter=adapter_name,
|
|
358
|
+
operation="create",
|
|
359
|
+
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Register in ticket registry for tracking
|
|
363
|
+
registry = TicketRegistry()
|
|
364
|
+
registry.register_ticket_operation(
|
|
365
|
+
queue_id, adapter_name, "create", title, task_data
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
|
|
369
|
+
console.print(f" Title: {title}")
|
|
370
|
+
console.print(f" Priority: {priority}")
|
|
371
|
+
console.print(f" Adapter: {adapter_name}")
|
|
372
|
+
console.print("[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]")
|
|
373
|
+
|
|
374
|
+
# Start worker if needed with immediate feedback
|
|
375
|
+
manager = WorkerManager()
|
|
376
|
+
worker_started = manager.start_if_needed()
|
|
377
|
+
|
|
378
|
+
if worker_started:
|
|
379
|
+
console.print("[dim]Worker started to process request[/dim]")
|
|
380
|
+
|
|
381
|
+
# Give immediate feedback on processing
|
|
382
|
+
import time
|
|
383
|
+
|
|
384
|
+
time.sleep(1) # Brief pause to let worker start
|
|
385
|
+
|
|
386
|
+
# Check if item is being processed
|
|
387
|
+
item = queue.get_item(queue_id)
|
|
388
|
+
if item and item.status == QueueStatus.PROCESSING:
|
|
389
|
+
console.print("[green]✓ Item is being processed by worker[/green]")
|
|
390
|
+
elif item and item.status == QueueStatus.PENDING:
|
|
391
|
+
console.print("[yellow]⏳ Item is queued for processing[/yellow]")
|
|
392
|
+
else:
|
|
393
|
+
console.print(
|
|
394
|
+
"[red]⚠️ Item status unclear - check with 'mcp-ticketer ticket check {queue_id}'[/red]"
|
|
395
|
+
)
|
|
396
|
+
else:
|
|
397
|
+
# Worker didn't start - this is a problem
|
|
398
|
+
pending_count = queue.get_pending_count()
|
|
399
|
+
if pending_count > 1: # More than just this item
|
|
400
|
+
console.print(
|
|
401
|
+
f"[red]❌ Worker failed to start with {pending_count} pending items![/red]"
|
|
402
|
+
)
|
|
403
|
+
console.print(
|
|
404
|
+
"[red]This is a critical issue. Try 'mcp-ticketer queue worker start' manually.[/red]"
|
|
405
|
+
)
|
|
406
|
+
else:
|
|
407
|
+
console.print(
|
|
408
|
+
"[yellow]Worker not started (no other pending items)[/yellow]"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@app.command("list")
|
|
413
|
+
def list_tickets(
|
|
414
|
+
state: Optional[TicketState] = typer.Option(
|
|
415
|
+
None, "--state", "-s", help="Filter by state"
|
|
416
|
+
),
|
|
417
|
+
priority: Optional[Priority] = typer.Option(
|
|
418
|
+
None, "--priority", "-p", help="Filter by priority"
|
|
419
|
+
),
|
|
420
|
+
limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
|
|
421
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
422
|
+
None, "--adapter", help="Override default adapter"
|
|
423
|
+
),
|
|
424
|
+
) -> None:
|
|
425
|
+
"""List tickets with optional filters."""
|
|
426
|
+
|
|
427
|
+
async def _list():
|
|
428
|
+
adapter_instance = get_adapter(
|
|
429
|
+
override_adapter=adapter.value if adapter else None
|
|
430
|
+
)
|
|
431
|
+
filters = {}
|
|
432
|
+
if state:
|
|
433
|
+
filters["state"] = state
|
|
434
|
+
if priority:
|
|
435
|
+
filters["priority"] = priority
|
|
436
|
+
return await adapter_instance.list(limit=limit, filters=filters)
|
|
437
|
+
|
|
438
|
+
tickets = asyncio.run(_list())
|
|
439
|
+
|
|
440
|
+
if not tickets:
|
|
441
|
+
console.print("[yellow]No tickets found[/yellow]")
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
# Create table
|
|
445
|
+
table = Table(title="Tickets")
|
|
446
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
447
|
+
table.add_column("Title", style="white")
|
|
448
|
+
table.add_column("State", style="green")
|
|
449
|
+
table.add_column("Priority", style="yellow")
|
|
450
|
+
table.add_column("Assignee", style="blue")
|
|
451
|
+
|
|
452
|
+
for ticket in tickets:
|
|
453
|
+
# Handle assignee field - Epic doesn't have assignee, Task does
|
|
454
|
+
assignee = getattr(ticket, "assignee", None) or "-"
|
|
455
|
+
|
|
456
|
+
table.add_row(
|
|
457
|
+
ticket.id or "N/A",
|
|
458
|
+
ticket.title,
|
|
459
|
+
ticket.state,
|
|
460
|
+
ticket.priority,
|
|
461
|
+
assignee,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
console.print(table)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@app.command()
|
|
468
|
+
def show(
|
|
469
|
+
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
470
|
+
comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
|
|
471
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
472
|
+
None, "--adapter", help="Override default adapter"
|
|
473
|
+
),
|
|
474
|
+
) -> None:
|
|
475
|
+
"""Show detailed ticket information."""
|
|
476
|
+
|
|
477
|
+
async def _show():
|
|
478
|
+
adapter_instance = get_adapter(
|
|
479
|
+
override_adapter=adapter.value if adapter else None
|
|
480
|
+
)
|
|
481
|
+
ticket = await adapter_instance.read(ticket_id)
|
|
482
|
+
ticket_comments = None
|
|
483
|
+
if comments and ticket:
|
|
484
|
+
ticket_comments = await adapter_instance.get_comments(ticket_id)
|
|
485
|
+
return ticket, ticket_comments
|
|
486
|
+
|
|
487
|
+
ticket, ticket_comments = asyncio.run(_show())
|
|
488
|
+
|
|
489
|
+
if not ticket:
|
|
490
|
+
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
491
|
+
raise typer.Exit(1)
|
|
492
|
+
|
|
493
|
+
# Display ticket details
|
|
494
|
+
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
495
|
+
console.print(f"Title: {ticket.title}")
|
|
496
|
+
console.print(f"State: [green]{ticket.state}[/green]")
|
|
497
|
+
console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
|
|
498
|
+
|
|
499
|
+
if ticket.description:
|
|
500
|
+
console.print("\n[dim]Description:[/dim]")
|
|
501
|
+
console.print(ticket.description)
|
|
502
|
+
|
|
503
|
+
if ticket.tags:
|
|
504
|
+
console.print(f"\nTags: {', '.join(ticket.tags)}")
|
|
505
|
+
|
|
506
|
+
if ticket.assignee:
|
|
507
|
+
console.print(f"Assignee: {ticket.assignee}")
|
|
508
|
+
|
|
509
|
+
# Display comments if requested
|
|
510
|
+
if ticket_comments:
|
|
511
|
+
console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
|
|
512
|
+
for comment in ticket_comments:
|
|
513
|
+
console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
|
|
514
|
+
console.print(comment.content)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@app.command()
|
|
518
|
+
def comment(
|
|
519
|
+
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
520
|
+
content: str = typer.Argument(..., help="Comment content"),
|
|
521
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
522
|
+
None, "--adapter", help="Override default adapter"
|
|
523
|
+
),
|
|
524
|
+
) -> None:
|
|
525
|
+
"""Add a comment to a ticket."""
|
|
526
|
+
|
|
527
|
+
async def _comment():
|
|
528
|
+
adapter_instance = get_adapter(
|
|
529
|
+
override_adapter=adapter.value if adapter else None
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Create comment
|
|
533
|
+
comment_obj = Comment(
|
|
534
|
+
ticket_id=ticket_id,
|
|
535
|
+
content=content,
|
|
536
|
+
author="cli-user", # Could be made configurable
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
result = await adapter_instance.add_comment(comment_obj)
|
|
540
|
+
return result
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
result = asyncio.run(_comment())
|
|
544
|
+
console.print("[green]✓[/green] Comment added successfully")
|
|
545
|
+
if result.id:
|
|
546
|
+
console.print(f"Comment ID: {result.id}")
|
|
547
|
+
console.print(f"Content: {content}")
|
|
548
|
+
except Exception as e:
|
|
549
|
+
console.print(f"[red]✗[/red] Failed to add comment: {e}")
|
|
550
|
+
raise typer.Exit(1)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@app.command()
|
|
554
|
+
def update(
|
|
555
|
+
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
556
|
+
title: Optional[str] = typer.Option(None, "--title", help="New title"),
|
|
557
|
+
description: Optional[str] = typer.Option(
|
|
558
|
+
None, "--description", "-d", help="New description"
|
|
559
|
+
),
|
|
560
|
+
priority: Optional[Priority] = typer.Option(
|
|
561
|
+
None, "--priority", "-p", help="New priority"
|
|
562
|
+
),
|
|
563
|
+
assignee: Optional[str] = typer.Option(
|
|
564
|
+
None, "--assignee", "-a", help="New assignee"
|
|
565
|
+
),
|
|
566
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
567
|
+
None, "--adapter", help="Override default adapter"
|
|
568
|
+
),
|
|
569
|
+
) -> None:
|
|
570
|
+
"""Update ticket fields."""
|
|
571
|
+
updates = {}
|
|
572
|
+
if title:
|
|
573
|
+
updates["title"] = title
|
|
574
|
+
if description:
|
|
575
|
+
updates["description"] = description
|
|
576
|
+
if priority:
|
|
577
|
+
updates["priority"] = (
|
|
578
|
+
priority.value if isinstance(priority, Priority) else priority
|
|
579
|
+
)
|
|
580
|
+
if assignee:
|
|
581
|
+
updates["assignee"] = assignee
|
|
582
|
+
|
|
583
|
+
if not updates:
|
|
584
|
+
console.print("[yellow]No updates specified[/yellow]")
|
|
585
|
+
raise typer.Exit(1)
|
|
586
|
+
|
|
587
|
+
# Get the adapter name
|
|
588
|
+
config = load_config()
|
|
589
|
+
adapter_name = (
|
|
590
|
+
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# Add ticket_id to updates
|
|
594
|
+
updates["ticket_id"] = ticket_id
|
|
595
|
+
|
|
596
|
+
# Add to queue with explicit project directory
|
|
597
|
+
queue = Queue()
|
|
598
|
+
queue_id = queue.add(
|
|
599
|
+
ticket_data=updates,
|
|
600
|
+
adapter=adapter_name,
|
|
601
|
+
operation="update",
|
|
602
|
+
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
|
|
606
|
+
for key, value in updates.items():
|
|
607
|
+
if key != "ticket_id":
|
|
608
|
+
console.print(f" {key}: {value}")
|
|
609
|
+
console.print("[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]")
|
|
610
|
+
|
|
611
|
+
# Start worker if needed
|
|
612
|
+
manager = WorkerManager()
|
|
613
|
+
if manager.start_if_needed():
|
|
614
|
+
console.print("[dim]Worker started to process request[/dim]")
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@app.command()
|
|
618
|
+
def transition(
|
|
619
|
+
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
620
|
+
state_positional: Optional[TicketState] = typer.Argument(
|
|
621
|
+
None, help="Target state (positional - deprecated, use --state instead)"
|
|
622
|
+
),
|
|
623
|
+
state: Optional[TicketState] = typer.Option(
|
|
624
|
+
None, "--state", "-s", help="Target state (recommended)"
|
|
625
|
+
),
|
|
626
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
627
|
+
None, "--adapter", help="Override default adapter"
|
|
628
|
+
),
|
|
629
|
+
) -> None:
|
|
630
|
+
"""Change ticket state with validation.
|
|
631
|
+
|
|
632
|
+
Examples:
|
|
633
|
+
# Recommended syntax with flag:
|
|
634
|
+
mcp-ticketer ticket transition BTA-215 --state done
|
|
635
|
+
mcp-ticketer ticket transition BTA-215 -s in_progress
|
|
636
|
+
|
|
637
|
+
# Legacy positional syntax (still supported):
|
|
638
|
+
mcp-ticketer ticket transition BTA-215 done
|
|
639
|
+
|
|
640
|
+
"""
|
|
641
|
+
# Determine which state to use (prefer flag over positional)
|
|
642
|
+
target_state = state if state is not None else state_positional
|
|
643
|
+
|
|
644
|
+
if target_state is None:
|
|
645
|
+
console.print("[red]Error: State is required[/red]")
|
|
646
|
+
console.print(
|
|
647
|
+
"Use either:\n"
|
|
648
|
+
" - Flag syntax (recommended): mcp-ticketer ticket transition TICKET-ID --state STATE\n"
|
|
649
|
+
" - Positional syntax: mcp-ticketer ticket transition TICKET-ID STATE"
|
|
650
|
+
)
|
|
651
|
+
raise typer.Exit(1)
|
|
652
|
+
|
|
653
|
+
# Get the adapter name
|
|
654
|
+
config = load_config()
|
|
655
|
+
adapter_name = (
|
|
656
|
+
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
# Add to queue with explicit project directory
|
|
660
|
+
queue = Queue()
|
|
661
|
+
queue_id = queue.add(
|
|
662
|
+
ticket_data={
|
|
663
|
+
"ticket_id": ticket_id,
|
|
664
|
+
"state": (
|
|
665
|
+
target_state.value if hasattr(target_state, "value") else target_state
|
|
666
|
+
),
|
|
667
|
+
},
|
|
668
|
+
adapter=adapter_name,
|
|
669
|
+
operation="transition",
|
|
670
|
+
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
674
|
+
console.print(f" Ticket: {ticket_id} → {target_state}")
|
|
675
|
+
console.print("[dim]Use 'mcp-ticketer ticket check {queue_id}' to check progress[/dim]")
|
|
676
|
+
|
|
677
|
+
# Start worker if needed
|
|
678
|
+
manager = WorkerManager()
|
|
679
|
+
if manager.start_if_needed():
|
|
680
|
+
console.print("[dim]Worker started to process request[/dim]")
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
@app.command()
|
|
684
|
+
def search(
|
|
685
|
+
query: Optional[str] = typer.Argument(None, help="Search query"),
|
|
686
|
+
state: Optional[TicketState] = typer.Option(None, "--state", "-s"),
|
|
687
|
+
priority: Optional[Priority] = typer.Option(None, "--priority", "-p"),
|
|
688
|
+
assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
|
|
689
|
+
limit: int = typer.Option(10, "--limit", "-l"),
|
|
690
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
691
|
+
None, "--adapter", help="Override default adapter"
|
|
692
|
+
),
|
|
693
|
+
) -> None:
|
|
694
|
+
"""Search tickets with advanced query."""
|
|
695
|
+
|
|
696
|
+
async def _search():
|
|
697
|
+
adapter_instance = get_adapter(
|
|
698
|
+
override_adapter=adapter.value if adapter else None
|
|
699
|
+
)
|
|
700
|
+
search_query = SearchQuery(
|
|
701
|
+
query=query,
|
|
702
|
+
state=state,
|
|
703
|
+
priority=priority,
|
|
704
|
+
assignee=assignee,
|
|
705
|
+
limit=limit,
|
|
706
|
+
)
|
|
707
|
+
return await adapter_instance.search(search_query)
|
|
708
|
+
|
|
709
|
+
tickets = asyncio.run(_search())
|
|
710
|
+
|
|
711
|
+
if not tickets:
|
|
712
|
+
console.print("[yellow]No tickets found matching query[/yellow]")
|
|
713
|
+
return
|
|
714
|
+
|
|
715
|
+
# Display results
|
|
716
|
+
console.print(f"\n[bold]Found {len(tickets)} ticket(s)[/bold]\n")
|
|
717
|
+
|
|
718
|
+
for ticket in tickets:
|
|
719
|
+
console.print(f"[cyan]{ticket.id}[/cyan]: {ticket.title}")
|
|
720
|
+
console.print(f" State: {ticket.state} | Priority: {ticket.priority}")
|
|
721
|
+
if ticket.assignee:
|
|
722
|
+
console.print(f" Assignee: {ticket.assignee}")
|
|
723
|
+
console.print()
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
@app.command()
|
|
727
|
+
def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
728
|
+
"""Check status of a queued operation."""
|
|
729
|
+
queue = Queue()
|
|
730
|
+
item = queue.get_item(queue_id)
|
|
731
|
+
|
|
732
|
+
if not item:
|
|
733
|
+
console.print(f"[red]Queue item not found: {queue_id}[/red]")
|
|
734
|
+
raise typer.Exit(1)
|
|
735
|
+
|
|
736
|
+
# Display status
|
|
737
|
+
console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
|
|
738
|
+
console.print(f"Operation: {item.operation}")
|
|
739
|
+
console.print(f"Adapter: {item.adapter}")
|
|
740
|
+
|
|
741
|
+
# Status with color
|
|
742
|
+
if item.status == QueueStatus.COMPLETED:
|
|
743
|
+
console.print(f"Status: [green]{item.status}[/green]")
|
|
744
|
+
elif item.status == QueueStatus.FAILED:
|
|
745
|
+
console.print(f"Status: [red]{item.status}[/red]")
|
|
746
|
+
elif item.status == QueueStatus.PROCESSING:
|
|
747
|
+
console.print(f"Status: [yellow]{item.status}[/yellow]")
|
|
748
|
+
else:
|
|
749
|
+
console.print(f"Status: {item.status}")
|
|
750
|
+
|
|
751
|
+
# Timestamps
|
|
752
|
+
console.print(f"Created: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
753
|
+
if item.processed_at:
|
|
754
|
+
console.print(f"Processed: {item.processed_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
755
|
+
|
|
756
|
+
# Error or result
|
|
757
|
+
if item.error_message:
|
|
758
|
+
console.print(f"\n[red]Error:[/red] {item.error_message}")
|
|
759
|
+
elif item.result:
|
|
760
|
+
console.print("\n[green]Result:[/green]")
|
|
761
|
+
for key, value in item.result.items():
|
|
762
|
+
console.print(f" {key}: {value}")
|
|
763
|
+
|
|
764
|
+
if item.retry_count > 0:
|
|
765
|
+
console.print(f"\nRetry Count: {item.retry_count}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-ticketer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Universal ticket management interface for AI agents with MCP support
|
|
5
5
|
Author-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
6
6
|
Maintainer-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
mcp_ticketer/__init__.py,sha256=Xx4WaprO5PXhVPbYi1L6tBmwmJMkYS-lMyG4ieN6QP0,717
|
|
2
|
-
mcp_ticketer/__version__.py,sha256=
|
|
2
|
+
mcp_ticketer/__version__.py,sha256=CR9K0foaKbPjgF5Yl6n14vC3Mv6RP3UcC71ofaagTzE,1117
|
|
3
3
|
mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
mcp_ticketer/adapters/__init__.py,sha256=B5DFllWn23hkhmrLykNO5uMMSdcFuuPHXyLw_jyFzuE,358
|
|
5
5
|
mcp_ticketer/adapters/aitrackdown.py,sha256=Ecw2SQAGVQs5yMH6m2pj61LxCJsuy-g2bvF8uwTpLUE,22588
|
|
@@ -24,11 +24,13 @@ mcp_ticketer/cli/diagnostics.py,sha256=jHF68ydW3RNVGumBnHUjUmq6YOjQD2UDkx0O7M__x
|
|
|
24
24
|
mcp_ticketer/cli/discover.py,sha256=AF_qlQc1Oo0UkWayoF5pmRChS5J3fJjH6f2YZzd_k8w,13188
|
|
25
25
|
mcp_ticketer/cli/gemini_configure.py,sha256=ZNSA1lBW-itVToza-JxW95Po7daVXKiZAh7lp6pmXMU,9343
|
|
26
26
|
mcp_ticketer/cli/linear_commands.py,sha256=_8f8ze_1MbiUweU6RFHpldgfHLirysIdPjHr2_S0YhI,17319
|
|
27
|
-
mcp_ticketer/cli/main.py,sha256=
|
|
27
|
+
mcp_ticketer/cli/main.py,sha256=veWHcq44L58SxegU3iU--r1jz9LM1OWv4fUG47K3BGw,77798
|
|
28
28
|
mcp_ticketer/cli/mcp_configure.py,sha256=RzV50UjXgOmvMp-9S0zS39psuvjffVByaMrqrUaAGAM,9594
|
|
29
29
|
mcp_ticketer/cli/migrate_config.py,sha256=MYsr_C5ZxsGg0P13etWTWNrJ_lc6ElRCkzfQADYr3DM,5956
|
|
30
|
+
mcp_ticketer/cli/platform_commands.py,sha256=koLWUFW-q1oRxJMVI_V35cDZ8OOH3F3bIdD8RRTTRpM,3694
|
|
30
31
|
mcp_ticketer/cli/queue_commands.py,sha256=mm-3H6jmkUGJDyU_E46o9iRpek8tvFCm77F19OtHiZI,7884
|
|
31
32
|
mcp_ticketer/cli/simple_health.py,sha256=GlOLRRFoifCna995NoHuKpb3xmFkLi2b3Ke1hyeDvq4,7950
|
|
33
|
+
mcp_ticketer/cli/ticket_commands.py,sha256=AD9D-EidPA3vk_-MpY3B4QGKhezCQqbKCn8vRsg57Rg,26637
|
|
32
34
|
mcp_ticketer/cli/utils.py,sha256=IOycMmhwtCDpL3RN_wVEZkYMg9FHyDr4mg5M9Bxzfbo,23041
|
|
33
35
|
mcp_ticketer/core/__init__.py,sha256=eXovsaJymQRP2AwOBuOy6mFtI3I68D7gGenZ5V-IMqo,349
|
|
34
36
|
mcp_ticketer/core/adapter.py,sha256=q64LxOInIno7EIbmuxItf8KEsd-g9grCs__Z4uwZHto,10273
|
|
@@ -54,9 +56,9 @@ mcp_ticketer/queue/queue.py,sha256=PIB_8gOE4rCb5_tBNKw9qD6YhSgH3Ei3IzVrUSY3F_o,1
|
|
|
54
56
|
mcp_ticketer/queue/run_worker.py,sha256=WhoeamL8LKZ66TM8W1PkMPwjF2w_EDFMP-mevs6C1TM,1019
|
|
55
57
|
mcp_ticketer/queue/ticket_registry.py,sha256=FE6W_D8NA-66cJQ6VqghChF3JasYW845JVfEZdiqLbA,15449
|
|
56
58
|
mcp_ticketer/queue/worker.py,sha256=AF6W1bdxWnHiJd6-iBWqTHkZ4lFflsS65CAtgFPR0FA,20983
|
|
57
|
-
mcp_ticketer-0.
|
|
58
|
-
mcp_ticketer-0.
|
|
59
|
-
mcp_ticketer-0.
|
|
60
|
-
mcp_ticketer-0.
|
|
61
|
-
mcp_ticketer-0.
|
|
62
|
-
mcp_ticketer-0.
|
|
59
|
+
mcp_ticketer-0.4.0.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
|
|
60
|
+
mcp_ticketer-0.4.0.dist-info/METADATA,sha256=kU9H9axMFLLYInrAxYQPW-4rlfBEd9USR_kOHdlfWQY,13219
|
|
61
|
+
mcp_ticketer-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
62
|
+
mcp_ticketer-0.4.0.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
|
|
63
|
+
mcp_ticketer-0.4.0.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
|
|
64
|
+
mcp_ticketer-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|