mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -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/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- 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 +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +91 -54
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1544
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -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 +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- 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/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- 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 +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -2030
- mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
mcp_ticketer/cli/main.py
CHANGED
|
@@ -5,28 +5,35 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
11
|
from dotenv import load_dotenv
|
|
12
12
|
from rich.console import Console
|
|
13
|
-
from rich.table import Table
|
|
14
|
-
|
|
15
|
-
from ..__version__ import __version__
|
|
16
|
-
from ..core import AdapterRegistry, Priority, TicketState
|
|
17
|
-
from ..core.models import SearchQuery, Comment
|
|
18
|
-
from ..queue import Queue, QueueStatus, WorkerManager
|
|
19
|
-
from ..queue.health_monitor import QueueHealthMonitor, HealthStatus
|
|
20
|
-
from ..queue.ticket_registry import TicketRegistry
|
|
21
13
|
|
|
22
14
|
# Import adapters module to trigger registration
|
|
23
15
|
import mcp_ticketer.adapters # noqa: F401
|
|
16
|
+
|
|
17
|
+
from ..__version__ import __version__
|
|
18
|
+
from ..core import AdapterRegistry
|
|
24
19
|
from .configure import configure_wizard, set_adapter_config, show_current_config
|
|
25
20
|
from .diagnostics import run_diagnostics
|
|
26
21
|
from .discover import app as discover_app
|
|
27
|
-
from .
|
|
22
|
+
from .init_command import init
|
|
23
|
+
from .install_mcp_server import (
|
|
24
|
+
install_mcp_server,
|
|
25
|
+
list_mcp_servers,
|
|
26
|
+
uninstall_mcp_server,
|
|
27
|
+
)
|
|
28
|
+
from .instruction_commands import app as instruction_app
|
|
29
|
+
from .mcp_server_commands import mcp_app
|
|
28
30
|
from .migrate_config import migrate_config_command
|
|
31
|
+
from .platform_commands import app as platform_app
|
|
32
|
+
from .platform_installer import install, remove, uninstall
|
|
33
|
+
from .project_update_commands import app as project_update_app
|
|
29
34
|
from .queue_commands import app as queue_app
|
|
35
|
+
from .setup_command import setup
|
|
36
|
+
from .ticket_commands import app as ticket_app
|
|
30
37
|
|
|
31
38
|
# Load environment variables from .env files
|
|
32
39
|
# Priority: .env.local (highest) > .env (base)
|
|
@@ -48,11 +55,11 @@ app = typer.Typer(
|
|
|
48
55
|
console = Console()
|
|
49
56
|
|
|
50
57
|
|
|
51
|
-
def version_callback(value: bool):
|
|
58
|
+
def version_callback(value: bool) -> None:
|
|
52
59
|
"""Print version and exit."""
|
|
53
60
|
if value:
|
|
54
61
|
console.print(f"mcp-ticketer version {__version__}")
|
|
55
|
-
raise typer.Exit()
|
|
62
|
+
raise typer.Exit() from None
|
|
56
63
|
|
|
57
64
|
|
|
58
65
|
@app.callback()
|
|
@@ -65,7 +72,7 @@ def main_callback(
|
|
|
65
72
|
is_eager=True,
|
|
66
73
|
help="Show version and exit",
|
|
67
74
|
),
|
|
68
|
-
):
|
|
75
|
+
) -> None:
|
|
69
76
|
"""MCP Ticketer - Universal ticket management interface."""
|
|
70
77
|
pass
|
|
71
78
|
|
|
@@ -83,7 +90,7 @@ class AdapterType(str, Enum):
|
|
|
83
90
|
GITHUB = "github"
|
|
84
91
|
|
|
85
92
|
|
|
86
|
-
def load_config(project_dir:
|
|
93
|
+
def load_config(project_dir: Path | None = None) -> dict:
|
|
87
94
|
"""Load configuration from project-local config file ONLY.
|
|
88
95
|
|
|
89
96
|
SECURITY: This method ONLY reads from the current project directory
|
|
@@ -91,6 +98,7 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
|
91
98
|
from user home directory or system-wide locations.
|
|
92
99
|
|
|
93
100
|
Args:
|
|
101
|
+
----
|
|
94
102
|
project_dir: Optional project directory to load config from
|
|
95
103
|
|
|
96
104
|
Resolution order:
|
|
@@ -98,6 +106,7 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
|
98
106
|
2. Default to aitrackdown adapter
|
|
99
107
|
|
|
100
108
|
Returns:
|
|
109
|
+
-------
|
|
101
110
|
Configuration dictionary with adapter and config keys.
|
|
102
111
|
Defaults to aitrackdown if no local config exists.
|
|
103
112
|
|
|
@@ -145,13 +154,14 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
|
145
154
|
return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
|
|
146
155
|
|
|
147
156
|
|
|
148
|
-
def _discover_from_env_files() ->
|
|
157
|
+
def _discover_from_env_files() -> str | None:
|
|
149
158
|
"""Discover adapter configuration from .env or .env.local files.
|
|
150
159
|
|
|
151
160
|
Returns:
|
|
161
|
+
-------
|
|
152
162
|
Adapter name if discovered, None otherwise
|
|
163
|
+
|
|
153
164
|
"""
|
|
154
|
-
import os
|
|
155
165
|
import logging
|
|
156
166
|
from pathlib import Path
|
|
157
167
|
|
|
@@ -166,12 +176,12 @@ def _discover_from_env_files() -> Optional[str]:
|
|
|
166
176
|
try:
|
|
167
177
|
# Simple .env parsing (key=value format)
|
|
168
178
|
env_vars = {}
|
|
169
|
-
with open(env_path
|
|
179
|
+
with open(env_path) as f:
|
|
170
180
|
for line in f:
|
|
171
181
|
line = line.strip()
|
|
172
|
-
if line and not line.startswith(
|
|
173
|
-
key, value = line.split(
|
|
174
|
-
env_vars[key.strip()] = value.strip().strip(
|
|
182
|
+
if line and not line.startswith("#") and "=" in line:
|
|
183
|
+
key, value = line.split("=", 1)
|
|
184
|
+
env_vars[key.strip()] = value.strip().strip("\"'")
|
|
175
185
|
|
|
176
186
|
# Check for adapter-specific variables
|
|
177
187
|
if env_vars.get("LINEAR_API_KEY"):
|
|
@@ -194,7 +204,9 @@ def _save_adapter_to_config(adapter_name: str) -> None:
|
|
|
194
204
|
"""Save adapter configuration to config file.
|
|
195
205
|
|
|
196
206
|
Args:
|
|
207
|
+
----
|
|
197
208
|
adapter_name: Name of the adapter to save as default
|
|
209
|
+
|
|
198
210
|
"""
|
|
199
211
|
import logging
|
|
200
212
|
|
|
@@ -243,9 +255,11 @@ def merge_config(updates: dict) -> dict:
|
|
|
243
255
|
"""Merge updates into existing config.
|
|
244
256
|
|
|
245
257
|
Args:
|
|
258
|
+
----
|
|
246
259
|
updates: Configuration updates to merge
|
|
247
260
|
|
|
248
261
|
Returns:
|
|
262
|
+
-------
|
|
249
263
|
Updated configuration
|
|
250
264
|
|
|
251
265
|
"""
|
|
@@ -268,11 +282,12 @@ def merge_config(updates: dict) -> dict:
|
|
|
268
282
|
|
|
269
283
|
|
|
270
284
|
def get_adapter(
|
|
271
|
-
override_adapter:
|
|
272
|
-
):
|
|
285
|
+
override_adapter: str | None = None, override_config: dict | None = None
|
|
286
|
+
) -> Any:
|
|
273
287
|
"""Get configured adapter instance.
|
|
274
288
|
|
|
275
289
|
Args:
|
|
290
|
+
----
|
|
276
291
|
override_adapter: Override the default adapter type
|
|
277
292
|
override_config: Override configuration for the adapter
|
|
278
293
|
|
|
@@ -300,7 +315,6 @@ def get_adapter(
|
|
|
300
315
|
adapter_config = config["config"]
|
|
301
316
|
|
|
302
317
|
# Add environment variables for authentication
|
|
303
|
-
import os
|
|
304
318
|
|
|
305
319
|
if adapter_type == "linear":
|
|
306
320
|
if not adapter_config.get("api_key"):
|
|
@@ -317,639 +331,20 @@ def get_adapter(
|
|
|
317
331
|
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
318
332
|
|
|
319
333
|
|
|
320
|
-
def _prompt_for_adapter_selection(console: Console) -> str:
|
|
321
|
-
"""Interactive prompt for adapter selection.
|
|
322
|
-
|
|
323
|
-
Args:
|
|
324
|
-
console: Rich console for output
|
|
325
|
-
|
|
326
|
-
Returns:
|
|
327
|
-
Selected adapter type
|
|
328
|
-
"""
|
|
329
|
-
console.print("\n[bold blue]🚀 MCP Ticketer Setup[/bold blue]")
|
|
330
|
-
console.print("Choose which ticket system you want to connect to:\n")
|
|
331
|
-
|
|
332
|
-
# Define adapter options with descriptions
|
|
333
|
-
adapters = [
|
|
334
|
-
{
|
|
335
|
-
"name": "linear",
|
|
336
|
-
"title": "Linear",
|
|
337
|
-
"description": "Modern project management (linear.app)",
|
|
338
|
-
"requirements": "API key and team ID"
|
|
339
|
-
},
|
|
340
|
-
{
|
|
341
|
-
"name": "github",
|
|
342
|
-
"title": "GitHub Issues",
|
|
343
|
-
"description": "GitHub repository issues",
|
|
344
|
-
"requirements": "Personal access token, owner, and repo"
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
"name": "jira",
|
|
348
|
-
"title": "JIRA",
|
|
349
|
-
"description": "Atlassian JIRA project management",
|
|
350
|
-
"requirements": "Server URL, email, and API token"
|
|
351
|
-
},
|
|
352
|
-
{
|
|
353
|
-
"name": "aitrackdown",
|
|
354
|
-
"title": "Local Files (AITrackdown)",
|
|
355
|
-
"description": "Store tickets in local files (no external service)",
|
|
356
|
-
"requirements": "None - works offline"
|
|
357
|
-
}
|
|
358
|
-
]
|
|
359
|
-
|
|
360
|
-
# Display options
|
|
361
|
-
for i, adapter in enumerate(adapters, 1):
|
|
362
|
-
console.print(f"[cyan]{i}.[/cyan] [bold]{adapter['title']}[/bold]")
|
|
363
|
-
console.print(f" {adapter['description']}")
|
|
364
|
-
console.print(f" [dim]Requirements: {adapter['requirements']}[/dim]\n")
|
|
365
|
-
|
|
366
|
-
# Get user selection
|
|
367
|
-
while True:
|
|
368
|
-
try:
|
|
369
|
-
choice = typer.prompt(
|
|
370
|
-
"Select adapter (1-4)",
|
|
371
|
-
type=int,
|
|
372
|
-
default=1
|
|
373
|
-
)
|
|
374
|
-
if 1 <= choice <= len(adapters):
|
|
375
|
-
selected_adapter = adapters[choice - 1]
|
|
376
|
-
console.print(f"\n[green]✓ Selected: {selected_adapter['title']}[/green]")
|
|
377
|
-
return selected_adapter["name"]
|
|
378
|
-
else:
|
|
379
|
-
console.print(f"[red]Please enter a number between 1 and {len(adapters)}[/red]")
|
|
380
|
-
except (ValueError, typer.Abort):
|
|
381
|
-
console.print("[yellow]Setup cancelled.[/yellow]")
|
|
382
|
-
raise typer.Exit(0)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
@app.command()
|
|
386
|
-
def setup(
|
|
387
|
-
adapter: Optional[str] = typer.Option(
|
|
388
|
-
None,
|
|
389
|
-
"--adapter",
|
|
390
|
-
"-a",
|
|
391
|
-
help="Adapter type to use (interactive prompt if not specified)",
|
|
392
|
-
),
|
|
393
|
-
project_path: Optional[str] = typer.Option(
|
|
394
|
-
None, "--path", help="Project path (default: current directory)"
|
|
395
|
-
),
|
|
396
|
-
global_config: bool = typer.Option(
|
|
397
|
-
False,
|
|
398
|
-
"--global",
|
|
399
|
-
"-g",
|
|
400
|
-
help="Save to global config instead of project-specific",
|
|
401
|
-
),
|
|
402
|
-
base_path: Optional[str] = typer.Option(
|
|
403
|
-
None,
|
|
404
|
-
"--base-path",
|
|
405
|
-
"-p",
|
|
406
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
407
|
-
),
|
|
408
|
-
api_key: Optional[str] = typer.Option(
|
|
409
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
410
|
-
),
|
|
411
|
-
team_id: Optional[str] = typer.Option(
|
|
412
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
413
|
-
),
|
|
414
|
-
jira_server: Optional[str] = typer.Option(
|
|
415
|
-
None,
|
|
416
|
-
"--jira-server",
|
|
417
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
418
|
-
),
|
|
419
|
-
jira_email: Optional[str] = typer.Option(
|
|
420
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
421
|
-
),
|
|
422
|
-
jira_project: Optional[str] = typer.Option(
|
|
423
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
424
|
-
),
|
|
425
|
-
github_owner: Optional[str] = typer.Option(
|
|
426
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
427
|
-
),
|
|
428
|
-
github_repo: Optional[str] = typer.Option(
|
|
429
|
-
None, "--github-repo", help="GitHub repository name"
|
|
430
|
-
),
|
|
431
|
-
github_token: Optional[str] = typer.Option(
|
|
432
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
433
|
-
),
|
|
434
|
-
) -> None:
|
|
435
|
-
"""Interactive setup wizard for MCP Ticketer (alias for init).
|
|
436
|
-
|
|
437
|
-
This command provides a user-friendly setup experience with prompts
|
|
438
|
-
to guide you through configuring MCP Ticketer for your preferred
|
|
439
|
-
ticket management system. It's identical to 'init' and 'install'.
|
|
440
|
-
|
|
441
|
-
Examples:
|
|
442
|
-
# Run interactive setup
|
|
443
|
-
mcp-ticketer setup
|
|
444
|
-
|
|
445
|
-
# Setup with specific adapter
|
|
446
|
-
mcp-ticketer setup --adapter linear
|
|
447
|
-
|
|
448
|
-
# Setup for different project
|
|
449
|
-
mcp-ticketer setup --path /path/to/project
|
|
450
|
-
"""
|
|
451
|
-
# Call init with all parameters
|
|
452
|
-
init(
|
|
453
|
-
adapter=adapter,
|
|
454
|
-
project_path=project_path,
|
|
455
|
-
global_config=global_config,
|
|
456
|
-
base_path=base_path,
|
|
457
|
-
api_key=api_key,
|
|
458
|
-
team_id=team_id,
|
|
459
|
-
jira_server=jira_server,
|
|
460
|
-
jira_email=jira_email,
|
|
461
|
-
jira_project=jira_project,
|
|
462
|
-
github_owner=github_owner,
|
|
463
|
-
github_repo=github_repo,
|
|
464
|
-
github_token=github_token,
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
@app.command()
|
|
469
|
-
def init(
|
|
470
|
-
adapter: Optional[str] = typer.Option(
|
|
471
|
-
None,
|
|
472
|
-
"--adapter",
|
|
473
|
-
"-a",
|
|
474
|
-
help="Adapter type to use (interactive prompt if not specified)",
|
|
475
|
-
),
|
|
476
|
-
project_path: Optional[str] = typer.Option(
|
|
477
|
-
None, "--path", help="Project path (default: current directory)"
|
|
478
|
-
),
|
|
479
|
-
global_config: bool = typer.Option(
|
|
480
|
-
False,
|
|
481
|
-
"--global",
|
|
482
|
-
"-g",
|
|
483
|
-
help="Save to global config instead of project-specific",
|
|
484
|
-
),
|
|
485
|
-
base_path: Optional[str] = typer.Option(
|
|
486
|
-
None,
|
|
487
|
-
"--base-path",
|
|
488
|
-
"-p",
|
|
489
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
490
|
-
),
|
|
491
|
-
api_key: Optional[str] = typer.Option(
|
|
492
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
493
|
-
),
|
|
494
|
-
team_id: Optional[str] = typer.Option(
|
|
495
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
496
|
-
),
|
|
497
|
-
jira_server: Optional[str] = typer.Option(
|
|
498
|
-
None,
|
|
499
|
-
"--jira-server",
|
|
500
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
501
|
-
),
|
|
502
|
-
jira_email: Optional[str] = typer.Option(
|
|
503
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
504
|
-
),
|
|
505
|
-
jira_project: Optional[str] = typer.Option(
|
|
506
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
507
|
-
),
|
|
508
|
-
github_owner: Optional[str] = typer.Option(
|
|
509
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
510
|
-
),
|
|
511
|
-
github_repo: Optional[str] = typer.Option(
|
|
512
|
-
None, "--github-repo", help="GitHub repository name"
|
|
513
|
-
),
|
|
514
|
-
github_token: Optional[str] = typer.Option(
|
|
515
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
516
|
-
),
|
|
517
|
-
) -> None:
|
|
518
|
-
"""Initialize mcp-ticketer for the current project.
|
|
519
|
-
|
|
520
|
-
This command sets up MCP Ticketer configuration with interactive prompts
|
|
521
|
-
to guide you through the process. It auto-detects adapter configuration
|
|
522
|
-
from .env files or prompts for interactive setup if no configuration is found.
|
|
523
|
-
|
|
524
|
-
Creates .mcp-ticketer/config.json in the current directory with
|
|
525
|
-
auto-detected or specified adapter configuration.
|
|
526
|
-
|
|
527
|
-
Note: 'setup' and 'install' are synonyms for this command.
|
|
528
|
-
|
|
529
|
-
Examples:
|
|
530
|
-
# Interactive setup (same as 'setup' and 'install')
|
|
531
|
-
mcp-ticketer init
|
|
532
|
-
|
|
533
|
-
# Force specific adapter
|
|
534
|
-
mcp-ticketer init --adapter linear
|
|
535
|
-
|
|
536
|
-
# Initialize for different project
|
|
537
|
-
mcp-ticketer init --path /path/to/project
|
|
538
|
-
|
|
539
|
-
# Save globally (not recommended)
|
|
540
|
-
mcp-ticketer init --global
|
|
541
|
-
|
|
542
|
-
"""
|
|
543
|
-
from pathlib import Path
|
|
544
|
-
|
|
545
|
-
from ..core.env_discovery import discover_config
|
|
546
|
-
from ..core.project_config import ConfigResolver
|
|
547
|
-
|
|
548
|
-
# Determine project path
|
|
549
|
-
proj_path = Path(project_path) if project_path else Path.cwd()
|
|
550
|
-
|
|
551
|
-
# Check if already initialized (unless using --global)
|
|
552
|
-
if not global_config:
|
|
553
|
-
config_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
554
|
-
|
|
555
|
-
if config_path.exists():
|
|
556
|
-
if not typer.confirm(
|
|
557
|
-
f"Configuration already exists at {config_path}. Overwrite?",
|
|
558
|
-
default=False,
|
|
559
|
-
):
|
|
560
|
-
console.print("[yellow]Initialization cancelled.[/yellow]")
|
|
561
|
-
raise typer.Exit(0)
|
|
562
|
-
|
|
563
|
-
# 1. Try auto-discovery if no adapter specified
|
|
564
|
-
discovered = None
|
|
565
|
-
adapter_type = adapter
|
|
566
|
-
|
|
567
|
-
if not adapter_type:
|
|
568
|
-
console.print(
|
|
569
|
-
"[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
|
|
570
|
-
)
|
|
571
|
-
discovered = discover_config(proj_path)
|
|
572
|
-
|
|
573
|
-
if discovered and discovered.adapters:
|
|
574
|
-
primary = discovered.get_primary_adapter()
|
|
575
|
-
if primary:
|
|
576
|
-
adapter_type = primary.adapter_type
|
|
577
|
-
console.print(
|
|
578
|
-
f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
|
|
579
|
-
)
|
|
580
|
-
|
|
581
|
-
# Show what was discovered
|
|
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]")
|
|
586
|
-
|
|
587
|
-
# Ask user to confirm auto-detected adapter
|
|
588
|
-
if not typer.confirm(
|
|
589
|
-
f"Use detected {adapter_type} adapter?",
|
|
590
|
-
default=True,
|
|
591
|
-
):
|
|
592
|
-
adapter_type = None # Will trigger interactive selection
|
|
593
|
-
else:
|
|
594
|
-
adapter_type = None # Will trigger interactive selection
|
|
595
|
-
else:
|
|
596
|
-
adapter_type = None # Will trigger interactive selection
|
|
597
|
-
|
|
598
|
-
# If no adapter determined, show interactive selection
|
|
599
|
-
if not adapter_type:
|
|
600
|
-
adapter_type = _prompt_for_adapter_selection(console)
|
|
601
|
-
|
|
602
|
-
# 2. Create configuration based on adapter type
|
|
603
|
-
config = {"default_adapter": adapter_type, "adapters": {}}
|
|
604
|
-
|
|
605
|
-
# 3. If discovered and matches adapter_type, use discovered config
|
|
606
|
-
if discovered and adapter_type != "aitrackdown":
|
|
607
|
-
discovered_adapter = discovered.get_adapter_by_type(adapter_type)
|
|
608
|
-
if discovered_adapter:
|
|
609
|
-
adapter_config = discovered_adapter.config.copy()
|
|
610
|
-
# Ensure the config has the correct 'type' field
|
|
611
|
-
adapter_config["type"] = adapter_type
|
|
612
|
-
# Remove 'adapter' field if present (legacy)
|
|
613
|
-
adapter_config.pop("adapter", None)
|
|
614
|
-
config["adapters"][adapter_type] = adapter_config
|
|
615
|
-
|
|
616
|
-
# 4. Handle manual configuration for specific adapters
|
|
617
|
-
if adapter_type == "aitrackdown":
|
|
618
|
-
config["adapters"]["aitrackdown"] = {
|
|
619
|
-
"type": "aitrackdown",
|
|
620
|
-
"base_path": base_path or ".aitrackdown"
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
elif adapter_type == "linear":
|
|
624
|
-
# If not auto-discovered, build from CLI params or prompt
|
|
625
|
-
if adapter_type not in config["adapters"]:
|
|
626
|
-
linear_config = {}
|
|
627
|
-
|
|
628
|
-
# API Key
|
|
629
|
-
linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
|
|
630
|
-
if not linear_api_key and not discovered:
|
|
631
|
-
console.print("\n[bold]Linear Configuration[/bold]")
|
|
632
|
-
console.print("You need a Linear API key to connect to Linear.")
|
|
633
|
-
console.print("[dim]Get your API key at: https://linear.app/settings/api[/dim]\n")
|
|
634
|
-
|
|
635
|
-
linear_api_key = typer.prompt(
|
|
636
|
-
"Enter your Linear API key",
|
|
637
|
-
hide_input=True
|
|
638
|
-
)
|
|
639
|
-
|
|
640
|
-
if linear_api_key:
|
|
641
|
-
linear_config["api_key"] = linear_api_key
|
|
642
|
-
|
|
643
|
-
# Team ID
|
|
644
|
-
linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
|
|
645
|
-
if not linear_team_id and not discovered:
|
|
646
|
-
console.print("\nYou need your Linear team ID.")
|
|
647
|
-
console.print("[dim]Find it in Linear settings or team URL[/dim]\n")
|
|
648
|
-
|
|
649
|
-
linear_team_id = typer.prompt("Enter your Linear team ID")
|
|
650
|
-
|
|
651
|
-
if linear_team_id:
|
|
652
|
-
linear_config["team_id"] = linear_team_id
|
|
653
|
-
|
|
654
|
-
if not linear_config.get("api_key") or not linear_config.get("team_id"):
|
|
655
|
-
console.print("[red]Error:[/red] Linear requires both API key and team ID")
|
|
656
|
-
console.print("Run 'mcp-ticketer init --adapter linear' with proper credentials")
|
|
657
|
-
raise typer.Exit(1)
|
|
658
|
-
|
|
659
|
-
linear_config["type"] = "linear"
|
|
660
|
-
config["adapters"]["linear"] = linear_config
|
|
661
|
-
|
|
662
|
-
elif adapter_type == "jira":
|
|
663
|
-
# If not auto-discovered, build from CLI params or prompt
|
|
664
|
-
if adapter_type not in config["adapters"]:
|
|
665
|
-
server = jira_server or os.getenv("JIRA_SERVER")
|
|
666
|
-
email = jira_email or os.getenv("JIRA_EMAIL")
|
|
667
|
-
token = api_key or os.getenv("JIRA_API_TOKEN")
|
|
668
|
-
project = jira_project or os.getenv("JIRA_PROJECT_KEY")
|
|
669
|
-
|
|
670
|
-
# Interactive prompts for missing values
|
|
671
|
-
if not server and not discovered:
|
|
672
|
-
console.print("\n[bold]JIRA Configuration[/bold]")
|
|
673
|
-
console.print("Enter your JIRA server details.\n")
|
|
674
|
-
|
|
675
|
-
server = typer.prompt(
|
|
676
|
-
"JIRA server URL (e.g., https://company.atlassian.net)"
|
|
677
|
-
)
|
|
678
|
-
|
|
679
|
-
if not email and not discovered:
|
|
680
|
-
email = typer.prompt("Your JIRA email address")
|
|
681
|
-
|
|
682
|
-
if not token and not discovered:
|
|
683
|
-
console.print("\nYou need a JIRA API token.")
|
|
684
|
-
console.print("[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n")
|
|
685
|
-
|
|
686
|
-
token = typer.prompt(
|
|
687
|
-
"Enter your JIRA API token",
|
|
688
|
-
hide_input=True
|
|
689
|
-
)
|
|
690
|
-
|
|
691
|
-
if not project and not discovered:
|
|
692
|
-
project = typer.prompt(
|
|
693
|
-
"Default JIRA project key (optional, press Enter to skip)",
|
|
694
|
-
default="",
|
|
695
|
-
show_default=False
|
|
696
|
-
)
|
|
697
|
-
|
|
698
|
-
# Validate required fields
|
|
699
|
-
if not server:
|
|
700
|
-
console.print("[red]Error:[/red] JIRA server URL is required")
|
|
701
|
-
raise typer.Exit(1)
|
|
702
|
-
|
|
703
|
-
if not email:
|
|
704
|
-
console.print("[red]Error:[/red] JIRA email is required")
|
|
705
|
-
raise typer.Exit(1)
|
|
706
|
-
|
|
707
|
-
if not token:
|
|
708
|
-
console.print("[red]Error:[/red] JIRA API token is required")
|
|
709
|
-
raise typer.Exit(1)
|
|
710
|
-
|
|
711
|
-
jira_config = {
|
|
712
|
-
"server": server,
|
|
713
|
-
"email": email,
|
|
714
|
-
"api_token": token,
|
|
715
|
-
"type": "jira"
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if project:
|
|
719
|
-
jira_config["project_key"] = project
|
|
720
|
-
|
|
721
|
-
config["adapters"]["jira"] = jira_config
|
|
722
|
-
|
|
723
|
-
elif adapter_type == "github":
|
|
724
|
-
# If not auto-discovered, build from CLI params or prompt
|
|
725
|
-
if adapter_type not in config["adapters"]:
|
|
726
|
-
owner = github_owner or os.getenv("GITHUB_OWNER")
|
|
727
|
-
repo = github_repo or os.getenv("GITHUB_REPO")
|
|
728
|
-
token = github_token or os.getenv("GITHUB_TOKEN")
|
|
729
|
-
|
|
730
|
-
# Interactive prompts for missing values
|
|
731
|
-
if not owner and not discovered:
|
|
732
|
-
console.print("\n[bold]GitHub Configuration[/bold]")
|
|
733
|
-
console.print("Enter your GitHub repository details.\n")
|
|
734
|
-
|
|
735
|
-
owner = typer.prompt("GitHub repository owner (username or organization)")
|
|
736
|
-
|
|
737
|
-
if not repo and not discovered:
|
|
738
|
-
repo = typer.prompt("GitHub repository name")
|
|
739
|
-
|
|
740
|
-
if not token and not discovered:
|
|
741
|
-
console.print("\nYou need a GitHub Personal Access Token.")
|
|
742
|
-
console.print("[dim]Create one at: https://github.com/settings/tokens/new[/dim]")
|
|
743
|
-
console.print("[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n")
|
|
744
|
-
|
|
745
|
-
token = typer.prompt(
|
|
746
|
-
"Enter your GitHub Personal Access Token",
|
|
747
|
-
hide_input=True
|
|
748
|
-
)
|
|
749
|
-
|
|
750
|
-
# Validate required fields
|
|
751
|
-
if not owner:
|
|
752
|
-
console.print("[red]Error:[/red] GitHub repository owner is required")
|
|
753
|
-
raise typer.Exit(1)
|
|
754
|
-
|
|
755
|
-
if not repo:
|
|
756
|
-
console.print("[red]Error:[/red] GitHub repository name is required")
|
|
757
|
-
raise typer.Exit(1)
|
|
758
|
-
|
|
759
|
-
if not token:
|
|
760
|
-
console.print("[red]Error:[/red] GitHub Personal Access Token is required")
|
|
761
|
-
raise typer.Exit(1)
|
|
762
|
-
|
|
763
|
-
config["adapters"]["github"] = {
|
|
764
|
-
"owner": owner,
|
|
765
|
-
"repo": repo,
|
|
766
|
-
"token": token,
|
|
767
|
-
"type": "github"
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
# 5. Save to appropriate location
|
|
771
|
-
if global_config:
|
|
772
|
-
# Save to ~/.mcp-ticketer/config.json
|
|
773
|
-
resolver = ConfigResolver(project_path=proj_path)
|
|
774
|
-
config_file_path = resolver.GLOBAL_CONFIG_PATH
|
|
775
|
-
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
776
|
-
|
|
777
|
-
with open(config_file_path, "w") as f:
|
|
778
|
-
json.dump(config, f, indent=2)
|
|
779
|
-
|
|
780
|
-
console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
|
|
781
|
-
console.print(f"[dim]Global configuration saved to {config_file_path}[/dim]")
|
|
782
|
-
else:
|
|
783
|
-
# Save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
|
|
784
|
-
config_file_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
785
|
-
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
786
|
-
|
|
787
|
-
with open(config_file_path, "w") as f:
|
|
788
|
-
json.dump(config, f, indent=2)
|
|
789
|
-
|
|
790
|
-
console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
|
|
791
|
-
console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
|
|
792
|
-
|
|
793
|
-
# Add .mcp-ticketer to .gitignore if not already there
|
|
794
|
-
gitignore_path = proj_path / ".gitignore"
|
|
795
|
-
if gitignore_path.exists():
|
|
796
|
-
gitignore_content = gitignore_path.read_text()
|
|
797
|
-
if ".mcp-ticketer" not in gitignore_content:
|
|
798
|
-
with open(gitignore_path, "a") as f:
|
|
799
|
-
f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
|
|
800
|
-
console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
|
|
801
|
-
else:
|
|
802
|
-
# Create .gitignore if it doesn't exist
|
|
803
|
-
with open(gitignore_path, "w") as f:
|
|
804
|
-
f.write("# MCP Ticketer\n.mcp-ticketer/\n")
|
|
805
|
-
console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
|
|
806
|
-
|
|
807
|
-
# Show next steps
|
|
808
|
-
_show_next_steps(console, adapter_type, config_file_path)
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
def _show_next_steps(console: Console, adapter_type: str, config_file_path: Path) -> None:
|
|
812
|
-
"""Show helpful next steps after initialization.
|
|
813
|
-
|
|
814
|
-
Args:
|
|
815
|
-
console: Rich console for output
|
|
816
|
-
adapter_type: Type of adapter that was configured
|
|
817
|
-
config_file_path: Path to the configuration file
|
|
818
|
-
"""
|
|
819
|
-
console.print("\n[bold green]🎉 Setup Complete![/bold green]")
|
|
820
|
-
console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
|
|
821
|
-
|
|
822
|
-
console.print("[bold]Next Steps:[/bold]")
|
|
823
|
-
console.print("1. [cyan]Test your configuration:[/cyan]")
|
|
824
|
-
console.print(" mcp-ticketer diagnose")
|
|
825
|
-
console.print("\n2. [cyan]Create a test ticket:[/cyan]")
|
|
826
|
-
console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
|
|
827
|
-
|
|
828
|
-
if adapter_type != "aitrackdown":
|
|
829
|
-
console.print(f"\n3. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]")
|
|
830
|
-
|
|
831
|
-
if adapter_type == "linear":
|
|
832
|
-
console.print(" Check your Linear workspace for the new ticket")
|
|
833
|
-
elif adapter_type == "github":
|
|
834
|
-
console.print(" Check your GitHub repository's Issues tab")
|
|
835
|
-
elif adapter_type == "jira":
|
|
836
|
-
console.print(" Check your JIRA project for the new ticket")
|
|
837
|
-
else:
|
|
838
|
-
console.print("\n3. [cyan]Check local ticket storage:[/cyan]")
|
|
839
|
-
console.print(" ls .aitrackdown/")
|
|
840
|
-
|
|
841
|
-
console.print("\n4. [cyan]Configure MCP clients (optional):[/cyan]")
|
|
842
|
-
console.print(" mcp-ticketer mcp claude # For Claude Code")
|
|
843
|
-
console.print(" mcp-ticketer mcp auggie # For Auggie")
|
|
844
|
-
console.print(" mcp-ticketer mcp gemini # For Gemini CLI")
|
|
845
|
-
|
|
846
|
-
console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
|
|
847
|
-
console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
@app.command()
|
|
851
|
-
def install(
|
|
852
|
-
adapter: Optional[str] = typer.Option(
|
|
853
|
-
None,
|
|
854
|
-
"--adapter",
|
|
855
|
-
"-a",
|
|
856
|
-
help="Adapter type to use (auto-detected from .env if not specified)",
|
|
857
|
-
),
|
|
858
|
-
project_path: Optional[str] = typer.Option(
|
|
859
|
-
None, "--path", help="Project path (default: current directory)"
|
|
860
|
-
),
|
|
861
|
-
global_config: bool = typer.Option(
|
|
862
|
-
False,
|
|
863
|
-
"--global",
|
|
864
|
-
"-g",
|
|
865
|
-
help="Save to global config instead of project-specific",
|
|
866
|
-
),
|
|
867
|
-
base_path: Optional[str] = typer.Option(
|
|
868
|
-
None,
|
|
869
|
-
"--base-path",
|
|
870
|
-
"-p",
|
|
871
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
872
|
-
),
|
|
873
|
-
api_key: Optional[str] = typer.Option(
|
|
874
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
875
|
-
),
|
|
876
|
-
team_id: Optional[str] = typer.Option(
|
|
877
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
878
|
-
),
|
|
879
|
-
jira_server: Optional[str] = typer.Option(
|
|
880
|
-
None,
|
|
881
|
-
"--jira-server",
|
|
882
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
883
|
-
),
|
|
884
|
-
jira_email: Optional[str] = typer.Option(
|
|
885
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
886
|
-
),
|
|
887
|
-
jira_project: Optional[str] = typer.Option(
|
|
888
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
889
|
-
),
|
|
890
|
-
github_owner: Optional[str] = typer.Option(
|
|
891
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
892
|
-
),
|
|
893
|
-
github_repo: Optional[str] = typer.Option(
|
|
894
|
-
None, "--github-repo", help="GitHub repository name"
|
|
895
|
-
),
|
|
896
|
-
github_token: Optional[str] = typer.Option(
|
|
897
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
898
|
-
),
|
|
899
|
-
) -> None:
|
|
900
|
-
"""Initialize mcp-ticketer for the current project (alias for init).
|
|
901
|
-
|
|
902
|
-
This command is synonymous with 'init' and 'setup' - all three provide
|
|
903
|
-
identical functionality with interactive prompts to guide you through
|
|
904
|
-
configuring MCP Ticketer for your preferred ticket management system.
|
|
905
|
-
|
|
906
|
-
Examples:
|
|
907
|
-
# Interactive setup (same as 'init' and 'setup')
|
|
908
|
-
mcp-ticketer install
|
|
909
|
-
|
|
910
|
-
# Force specific adapter
|
|
911
|
-
mcp-ticketer install --adapter linear
|
|
912
|
-
|
|
913
|
-
# Initialize for different project
|
|
914
|
-
mcp-ticketer install --path /path/to/project
|
|
915
|
-
|
|
916
|
-
# Save globally (not recommended)
|
|
917
|
-
mcp-ticketer install --global
|
|
918
|
-
|
|
919
|
-
"""
|
|
920
|
-
# Call init with all parameters
|
|
921
|
-
init(
|
|
922
|
-
adapter=adapter,
|
|
923
|
-
project_path=project_path,
|
|
924
|
-
global_config=global_config,
|
|
925
|
-
base_path=base_path,
|
|
926
|
-
api_key=api_key,
|
|
927
|
-
team_id=team_id,
|
|
928
|
-
jira_server=jira_server,
|
|
929
|
-
jira_email=jira_email,
|
|
930
|
-
jira_project=jira_project,
|
|
931
|
-
github_owner=github_owner,
|
|
932
|
-
github_repo=github_repo,
|
|
933
|
-
github_token=github_token,
|
|
934
|
-
)
|
|
935
|
-
|
|
936
|
-
|
|
937
334
|
@app.command("set")
|
|
938
335
|
def set_config(
|
|
939
|
-
adapter:
|
|
336
|
+
adapter: AdapterType | None = typer.Option(
|
|
940
337
|
None, "--adapter", "-a", help="Set default adapter"
|
|
941
338
|
),
|
|
942
|
-
team_key:
|
|
339
|
+
team_key: str | None = typer.Option(
|
|
943
340
|
None, "--team-key", help="Linear team key (e.g., BTA)"
|
|
944
341
|
),
|
|
945
|
-
team_id:
|
|
946
|
-
owner:
|
|
947
|
-
|
|
948
|
-
),
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
project: Optional[str] = typer.Option(None, "--project", help="JIRA project key"),
|
|
952
|
-
base_path: Optional[str] = typer.Option(
|
|
342
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
|
|
343
|
+
owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
|
|
344
|
+
repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
|
|
345
|
+
server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
|
|
346
|
+
project: str | None = typer.Option(None, "--project", help="JIRA project key"),
|
|
347
|
+
base_path: str | None = typer.Option(
|
|
953
348
|
None, "--base-path", help="AITrackdown base path"
|
|
954
349
|
),
|
|
955
350
|
) -> None:
|
|
@@ -1039,16 +434,12 @@ def set_config(
|
|
|
1039
434
|
@app.command("configure")
|
|
1040
435
|
def configure_command(
|
|
1041
436
|
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
1042
|
-
adapter:
|
|
437
|
+
adapter: str | None = typer.Option(
|
|
1043
438
|
None, "--adapter", help="Set default adapter type"
|
|
1044
439
|
),
|
|
1045
|
-
api_key:
|
|
1046
|
-
project_id:
|
|
1047
|
-
|
|
1048
|
-
),
|
|
1049
|
-
team_id: Optional[str] = typer.Option(
|
|
1050
|
-
None, "--team-id", help="Set team ID (Linear)"
|
|
1051
|
-
),
|
|
440
|
+
api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
|
|
441
|
+
project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
|
|
442
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
|
|
1052
443
|
global_scope: bool = typer.Option(
|
|
1053
444
|
False,
|
|
1054
445
|
"--global",
|
|
@@ -1082,6 +473,26 @@ def configure_command(
|
|
|
1082
473
|
configure_wizard()
|
|
1083
474
|
|
|
1084
475
|
|
|
476
|
+
@app.command("config")
|
|
477
|
+
def config_alias(
|
|
478
|
+
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
479
|
+
adapter: str | None = typer.Option(
|
|
480
|
+
None, "--adapter", help="Set default adapter type"
|
|
481
|
+
),
|
|
482
|
+
api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
|
|
483
|
+
project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
|
|
484
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
|
|
485
|
+
global_scope: bool = typer.Option(
|
|
486
|
+
False,
|
|
487
|
+
"--global",
|
|
488
|
+
"-g",
|
|
489
|
+
help="Save to global config instead of project-specific",
|
|
490
|
+
),
|
|
491
|
+
) -> None:
|
|
492
|
+
"""Alias for configure command - shorter syntax."""
|
|
493
|
+
configure_command(show, adapter, api_key, project_id, team_id, global_scope)
|
|
494
|
+
|
|
495
|
+
|
|
1085
496
|
@app.command("migrate-config")
|
|
1086
497
|
def migrate_config(
|
|
1087
498
|
dry_run: bool = typer.Option(
|
|
@@ -1099,630 +510,74 @@ def migrate_config(
|
|
|
1099
510
|
migrate_config_command(dry_run=dry_run)
|
|
1100
511
|
|
|
1101
512
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
"""Show queue and worker status."""
|
|
1105
|
-
queue = Queue()
|
|
1106
|
-
manager = WorkerManager()
|
|
1107
|
-
|
|
1108
|
-
# Get queue stats
|
|
1109
|
-
stats = queue.get_stats()
|
|
1110
|
-
pending = stats.get(QueueStatus.PENDING.value, 0)
|
|
1111
|
-
|
|
1112
|
-
# Show queue status
|
|
1113
|
-
console.print("[bold]Queue Status:[/bold]")
|
|
1114
|
-
console.print(f" Pending: {pending}")
|
|
1115
|
-
console.print(f" Processing: {stats.get(QueueStatus.PROCESSING.value, 0)}")
|
|
1116
|
-
console.print(f" Completed: {stats.get(QueueStatus.COMPLETED.value, 0)}")
|
|
1117
|
-
console.print(f" Failed: {stats.get(QueueStatus.FAILED.value, 0)}")
|
|
1118
|
-
|
|
1119
|
-
# Show worker status
|
|
1120
|
-
worker_status = manager.get_status()
|
|
1121
|
-
if worker_status["running"]:
|
|
1122
|
-
console.print(
|
|
1123
|
-
f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})"
|
|
1124
|
-
)
|
|
1125
|
-
else:
|
|
1126
|
-
console.print("\n[red]○ Worker is not running[/red]")
|
|
1127
|
-
if pending > 0:
|
|
1128
|
-
console.print(
|
|
1129
|
-
"[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]"
|
|
1130
|
-
)
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
@app.command()
|
|
1134
|
-
def health(
|
|
1135
|
-
auto_repair: bool = typer.Option(False, "--auto-repair", help="Attempt automatic repair of issues"),
|
|
1136
|
-
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed health information")
|
|
1137
|
-
) -> None:
|
|
1138
|
-
"""Check queue system health and detect issues immediately."""
|
|
1139
|
-
|
|
1140
|
-
health_monitor = QueueHealthMonitor()
|
|
1141
|
-
health = health_monitor.check_health()
|
|
1142
|
-
|
|
1143
|
-
# Display overall status
|
|
1144
|
-
status_color = {
|
|
1145
|
-
HealthStatus.HEALTHY: "green",
|
|
1146
|
-
HealthStatus.WARNING: "yellow",
|
|
1147
|
-
HealthStatus.CRITICAL: "red",
|
|
1148
|
-
HealthStatus.FAILED: "red"
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
status_icon = {
|
|
1152
|
-
HealthStatus.HEALTHY: "✓",
|
|
1153
|
-
HealthStatus.WARNING: "⚠️",
|
|
1154
|
-
HealthStatus.CRITICAL: "🚨",
|
|
1155
|
-
HealthStatus.FAILED: "❌"
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
color = status_color.get(health["status"], "white")
|
|
1159
|
-
icon = status_icon.get(health["status"], "?")
|
|
1160
|
-
|
|
1161
|
-
console.print(f"[{color}]{icon} Queue Health: {health['status'].upper()}[/{color}]")
|
|
1162
|
-
console.print(f"Last checked: {health['timestamp']}")
|
|
1163
|
-
|
|
1164
|
-
# Display alerts
|
|
1165
|
-
if health["alerts"]:
|
|
1166
|
-
console.print("\n[bold]Issues Found:[/bold]")
|
|
1167
|
-
for alert in health["alerts"]:
|
|
1168
|
-
alert_color = status_color.get(alert["level"], "white")
|
|
1169
|
-
console.print(f"[{alert_color}] • {alert['message']}[/{alert_color}]")
|
|
1170
|
-
|
|
1171
|
-
if verbose and alert.get("details"):
|
|
1172
|
-
for key, value in alert["details"].items():
|
|
1173
|
-
console.print(f" {key}: {value}")
|
|
1174
|
-
else:
|
|
1175
|
-
console.print("\n[green]✓ No issues detected[/green]")
|
|
1176
|
-
|
|
1177
|
-
# Auto-repair if requested
|
|
1178
|
-
if auto_repair and health["status"] in [HealthStatus.CRITICAL, HealthStatus.WARNING]:
|
|
1179
|
-
console.print("\n[yellow]Attempting automatic repair...[/yellow]")
|
|
1180
|
-
repair_result = health_monitor.auto_repair()
|
|
1181
|
-
|
|
1182
|
-
if repair_result["actions_taken"]:
|
|
1183
|
-
console.print("[green]Repair actions taken:[/green]")
|
|
1184
|
-
for action in repair_result["actions_taken"]:
|
|
1185
|
-
console.print(f"[green] ✓ {action}[/green]")
|
|
1186
|
-
|
|
1187
|
-
# Re-check health
|
|
1188
|
-
console.print("\n[yellow]Re-checking health after repair...[/yellow]")
|
|
1189
|
-
new_health = health_monitor.check_health()
|
|
1190
|
-
new_color = status_color.get(new_health["status"], "white")
|
|
1191
|
-
new_icon = status_icon.get(new_health["status"], "?")
|
|
1192
|
-
console.print(f"[{new_color}]{new_icon} Updated Health: {new_health['status'].upper()}[/{new_color}]")
|
|
1193
|
-
else:
|
|
1194
|
-
console.print("[yellow]No repair actions available[/yellow]")
|
|
1195
|
-
|
|
1196
|
-
# Exit with appropriate code
|
|
1197
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
1198
|
-
raise typer.Exit(1)
|
|
1199
|
-
elif health["status"] == HealthStatus.WARNING:
|
|
1200
|
-
raise typer.Exit(2)
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
@app.command()
|
|
1204
|
-
def create(
|
|
1205
|
-
title: str = typer.Argument(..., help="Ticket title"),
|
|
1206
|
-
description: Optional[str] = typer.Option(
|
|
1207
|
-
None, "--description", "-d", help="Ticket description"
|
|
1208
|
-
),
|
|
1209
|
-
priority: Priority = typer.Option(
|
|
1210
|
-
Priority.MEDIUM, "--priority", "-p", help="Priority level"
|
|
1211
|
-
),
|
|
1212
|
-
tags: Optional[list[str]] = typer.Option(
|
|
1213
|
-
None, "--tag", "-t", help="Tags (can be specified multiple times)"
|
|
1214
|
-
),
|
|
1215
|
-
assignee: Optional[str] = typer.Option(
|
|
1216
|
-
None, "--assignee", "-a", help="Assignee username"
|
|
1217
|
-
),
|
|
1218
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1219
|
-
None, "--adapter", help="Override default adapter"
|
|
1220
|
-
),
|
|
1221
|
-
) -> None:
|
|
1222
|
-
"""Create a new ticket with comprehensive health checks."""
|
|
1223
|
-
|
|
1224
|
-
# IMMEDIATE HEALTH CHECK - Critical for reliability
|
|
1225
|
-
health_monitor = QueueHealthMonitor()
|
|
1226
|
-
health = health_monitor.check_health()
|
|
1227
|
-
|
|
1228
|
-
# Display health status
|
|
1229
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
1230
|
-
console.print("[red]🚨 CRITICAL: Queue system has serious issues![/red]")
|
|
1231
|
-
for alert in health["alerts"]:
|
|
1232
|
-
if alert["level"] == "critical":
|
|
1233
|
-
console.print(f"[red] • {alert['message']}[/red]")
|
|
1234
|
-
|
|
1235
|
-
# Attempt auto-repair
|
|
1236
|
-
console.print("[yellow]Attempting automatic repair...[/yellow]")
|
|
1237
|
-
repair_result = health_monitor.auto_repair()
|
|
1238
|
-
|
|
1239
|
-
if repair_result["actions_taken"]:
|
|
1240
|
-
for action in repair_result["actions_taken"]:
|
|
1241
|
-
console.print(f"[yellow] ✓ {action}[/yellow]")
|
|
1242
|
-
|
|
1243
|
-
# Re-check health after repair
|
|
1244
|
-
health = health_monitor.check_health()
|
|
1245
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
1246
|
-
console.print("[red]❌ Auto-repair failed. Manual intervention required.[/red]")
|
|
1247
|
-
console.print("[red]Cannot safely create ticket. Please check system status.[/red]")
|
|
1248
|
-
raise typer.Exit(1)
|
|
1249
|
-
else:
|
|
1250
|
-
console.print("[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]")
|
|
1251
|
-
else:
|
|
1252
|
-
console.print("[red]❌ No repair actions available. Manual intervention required.[/red]")
|
|
1253
|
-
raise typer.Exit(1)
|
|
1254
|
-
|
|
1255
|
-
elif health["status"] == HealthStatus.WARNING:
|
|
1256
|
-
console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
|
|
1257
|
-
for alert in health["alerts"]:
|
|
1258
|
-
if alert["level"] == "warning":
|
|
1259
|
-
console.print(f"[yellow] • {alert['message']}[/yellow]")
|
|
1260
|
-
console.print("[yellow]Proceeding with ticket creation...[/yellow]")
|
|
1261
|
-
|
|
1262
|
-
# Get the adapter name with priority: 1) argument, 2) config, 3) .env files, 4) default
|
|
1263
|
-
if adapter:
|
|
1264
|
-
# Priority 1: Command-line argument - save to config for future use
|
|
1265
|
-
adapter_name = adapter.value
|
|
1266
|
-
_save_adapter_to_config(adapter_name)
|
|
1267
|
-
else:
|
|
1268
|
-
# Priority 2: Check existing config
|
|
1269
|
-
config = load_config()
|
|
1270
|
-
adapter_name = config.get("default_adapter")
|
|
1271
|
-
|
|
1272
|
-
if not adapter_name or adapter_name == "aitrackdown":
|
|
1273
|
-
# Priority 3: Check .env files and save if found
|
|
1274
|
-
env_adapter = _discover_from_env_files()
|
|
1275
|
-
if env_adapter:
|
|
1276
|
-
adapter_name = env_adapter
|
|
1277
|
-
_save_adapter_to_config(adapter_name)
|
|
1278
|
-
else:
|
|
1279
|
-
# Priority 4: Default
|
|
1280
|
-
adapter_name = "aitrackdown"
|
|
1281
|
-
|
|
1282
|
-
# Create task data
|
|
1283
|
-
# Import Priority for type checking
|
|
1284
|
-
from ..core.models import Priority as PriorityEnum
|
|
1285
|
-
|
|
1286
|
-
task_data = {
|
|
1287
|
-
"title": title,
|
|
1288
|
-
"description": description,
|
|
1289
|
-
"priority": priority.value if isinstance(priority, PriorityEnum) else priority,
|
|
1290
|
-
"tags": tags or [],
|
|
1291
|
-
"assignee": assignee,
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
# WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
|
|
1295
|
-
if adapter_name == "linear":
|
|
1296
|
-
console.print("[yellow]⚠️[/yellow] Using direct operation for Linear adapter (bypassing queue)")
|
|
1297
|
-
try:
|
|
1298
|
-
# Load configuration and create adapter directly
|
|
1299
|
-
config = load_config()
|
|
1300
|
-
adapter_config = config.get("adapters", {}).get(adapter_name, {})
|
|
1301
|
-
|
|
1302
|
-
# Import and create adapter
|
|
1303
|
-
from ..core.registry import AdapterRegistry
|
|
1304
|
-
adapter = AdapterRegistry.get_adapter(adapter_name, adapter_config)
|
|
1305
|
-
|
|
1306
|
-
# Create task directly
|
|
1307
|
-
from ..core.models import Task, Priority
|
|
1308
|
-
task = Task(
|
|
1309
|
-
title=task_data["title"],
|
|
1310
|
-
description=task_data.get("description"),
|
|
1311
|
-
priority=Priority(task_data["priority"]) if task_data.get("priority") else Priority.MEDIUM,
|
|
1312
|
-
tags=task_data.get("tags", []),
|
|
1313
|
-
assignee=task_data.get("assignee")
|
|
1314
|
-
)
|
|
1315
|
-
|
|
1316
|
-
# Create ticket synchronously
|
|
1317
|
-
import asyncio
|
|
1318
|
-
result = asyncio.run(adapter.create(task))
|
|
1319
|
-
|
|
1320
|
-
console.print(f"[green]✓[/green] Ticket created successfully: {result.id}")
|
|
1321
|
-
console.print(f" Title: {result.title}")
|
|
1322
|
-
console.print(f" Priority: {result.priority}")
|
|
1323
|
-
console.print(f" State: {result.state}")
|
|
1324
|
-
# Get URL from metadata if available
|
|
1325
|
-
if result.metadata and 'linear' in result.metadata and 'url' in result.metadata['linear']:
|
|
1326
|
-
console.print(f" URL: {result.metadata['linear']['url']}")
|
|
1327
|
-
|
|
1328
|
-
return result.id
|
|
1329
|
-
|
|
1330
|
-
except Exception as e:
|
|
1331
|
-
console.print(f"[red]❌[/red] Failed to create ticket: {e}")
|
|
1332
|
-
raise
|
|
1333
|
-
|
|
1334
|
-
# Use queue for other adapters
|
|
1335
|
-
queue = Queue()
|
|
1336
|
-
queue_id = queue.add(
|
|
1337
|
-
ticket_data=task_data,
|
|
1338
|
-
adapter=adapter_name,
|
|
1339
|
-
operation="create",
|
|
1340
|
-
project_dir=str(Path.cwd()) # Explicitly pass current project directory
|
|
1341
|
-
)
|
|
1342
|
-
|
|
1343
|
-
# Register in ticket registry for tracking
|
|
1344
|
-
registry = TicketRegistry()
|
|
1345
|
-
registry.register_ticket_operation(queue_id, adapter_name, "create", title, task_data)
|
|
1346
|
-
|
|
1347
|
-
console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
|
|
1348
|
-
console.print(f" Title: {title}")
|
|
1349
|
-
console.print(f" Priority: {priority}")
|
|
1350
|
-
console.print(f" Adapter: {adapter_name}")
|
|
1351
|
-
console.print("[dim]Use 'mcp-ticketer check {queue_id}' to check progress[/dim]")
|
|
1352
|
-
|
|
1353
|
-
# Start worker if needed with immediate feedback
|
|
1354
|
-
manager = WorkerManager()
|
|
1355
|
-
worker_started = manager.start_if_needed()
|
|
1356
|
-
|
|
1357
|
-
if worker_started:
|
|
1358
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
1359
|
-
|
|
1360
|
-
# Give immediate feedback on processing
|
|
1361
|
-
import time
|
|
1362
|
-
time.sleep(1) # Brief pause to let worker start
|
|
1363
|
-
|
|
1364
|
-
# Check if item is being processed
|
|
1365
|
-
item = queue.get_item(queue_id)
|
|
1366
|
-
if item and item.status == QueueStatus.PROCESSING:
|
|
1367
|
-
console.print("[green]✓ Item is being processed by worker[/green]")
|
|
1368
|
-
elif item and item.status == QueueStatus.PENDING:
|
|
1369
|
-
console.print("[yellow]⏳ Item is queued for processing[/yellow]")
|
|
1370
|
-
else:
|
|
1371
|
-
console.print("[red]⚠️ Item status unclear - check with 'mcp-ticketer check {queue_id}'[/red]")
|
|
1372
|
-
else:
|
|
1373
|
-
# Worker didn't start - this is a problem
|
|
1374
|
-
pending_count = queue.get_pending_count()
|
|
1375
|
-
if pending_count > 1: # More than just this item
|
|
1376
|
-
console.print(f"[red]❌ Worker failed to start with {pending_count} pending items![/red]")
|
|
1377
|
-
console.print("[red]This is a critical issue. Try 'mcp-ticketer queue worker start' manually.[/red]")
|
|
1378
|
-
else:
|
|
1379
|
-
console.print("[yellow]Worker not started (no other pending items)[/yellow]")
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
@app.command("list")
|
|
1383
|
-
def list_tickets(
|
|
1384
|
-
state: Optional[TicketState] = typer.Option(
|
|
1385
|
-
None, "--state", "-s", help="Filter by state"
|
|
1386
|
-
),
|
|
1387
|
-
priority: Optional[Priority] = typer.Option(
|
|
1388
|
-
None, "--priority", "-p", help="Filter by priority"
|
|
1389
|
-
),
|
|
1390
|
-
limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
|
|
1391
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1392
|
-
None, "--adapter", help="Override default adapter"
|
|
1393
|
-
),
|
|
1394
|
-
) -> None:
|
|
1395
|
-
"""List tickets with optional filters."""
|
|
1396
|
-
|
|
1397
|
-
async def _list():
|
|
1398
|
-
adapter_instance = get_adapter(
|
|
1399
|
-
override_adapter=adapter.value if adapter else None
|
|
1400
|
-
)
|
|
1401
|
-
filters = {}
|
|
1402
|
-
if state:
|
|
1403
|
-
filters["state"] = state
|
|
1404
|
-
if priority:
|
|
1405
|
-
filters["priority"] = priority
|
|
1406
|
-
return await adapter_instance.list(limit=limit, filters=filters)
|
|
1407
|
-
|
|
1408
|
-
tickets = asyncio.run(_list())
|
|
1409
|
-
|
|
1410
|
-
if not tickets:
|
|
1411
|
-
console.print("[yellow]No tickets found[/yellow]")
|
|
1412
|
-
return
|
|
1413
|
-
|
|
1414
|
-
# Create table
|
|
1415
|
-
table = Table(title="Tickets")
|
|
1416
|
-
table.add_column("ID", style="cyan", no_wrap=True)
|
|
1417
|
-
table.add_column("Title", style="white")
|
|
1418
|
-
table.add_column("State", style="green")
|
|
1419
|
-
table.add_column("Priority", style="yellow")
|
|
1420
|
-
table.add_column("Assignee", style="blue")
|
|
1421
|
-
|
|
1422
|
-
for ticket in tickets:
|
|
1423
|
-
# Handle assignee field - Epic doesn't have assignee, Task does
|
|
1424
|
-
assignee = getattr(ticket, 'assignee', None) or "-"
|
|
1425
|
-
|
|
1426
|
-
table.add_row(
|
|
1427
|
-
ticket.id or "N/A",
|
|
1428
|
-
ticket.title,
|
|
1429
|
-
ticket.state,
|
|
1430
|
-
ticket.priority,
|
|
1431
|
-
assignee,
|
|
1432
|
-
)
|
|
513
|
+
# Add ticket command group to main app
|
|
514
|
+
app.add_typer(ticket_app, name="ticket")
|
|
1433
515
|
|
|
1434
|
-
|
|
516
|
+
# Add platform command group to main app
|
|
517
|
+
app.add_typer(platform_app, name="platform")
|
|
1435
518
|
|
|
519
|
+
# Add queue command to main app
|
|
520
|
+
app.add_typer(queue_app, name="queue")
|
|
1436
521
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1440
|
-
comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
|
|
1441
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1442
|
-
None, "--adapter", help="Override default adapter"
|
|
1443
|
-
),
|
|
1444
|
-
) -> None:
|
|
1445
|
-
"""Show detailed ticket information."""
|
|
522
|
+
# Add discover command to main app
|
|
523
|
+
app.add_typer(discover_app, name="discover")
|
|
1446
524
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
override_adapter=adapter.value if adapter else None
|
|
1450
|
-
)
|
|
1451
|
-
ticket = await adapter_instance.read(ticket_id)
|
|
1452
|
-
ticket_comments = None
|
|
1453
|
-
if comments and ticket:
|
|
1454
|
-
ticket_comments = await adapter_instance.get_comments(ticket_id)
|
|
1455
|
-
return ticket, ticket_comments
|
|
1456
|
-
|
|
1457
|
-
ticket, ticket_comments = asyncio.run(_show())
|
|
1458
|
-
|
|
1459
|
-
if not ticket:
|
|
1460
|
-
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
1461
|
-
raise typer.Exit(1)
|
|
1462
|
-
|
|
1463
|
-
# Display ticket details
|
|
1464
|
-
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
1465
|
-
console.print(f"Title: {ticket.title}")
|
|
1466
|
-
console.print(f"State: [green]{ticket.state}[/green]")
|
|
1467
|
-
console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
|
|
1468
|
-
|
|
1469
|
-
if ticket.description:
|
|
1470
|
-
console.print("\n[dim]Description:[/dim]")
|
|
1471
|
-
console.print(ticket.description)
|
|
1472
|
-
|
|
1473
|
-
if ticket.tags:
|
|
1474
|
-
console.print(f"\nTags: {', '.join(ticket.tags)}")
|
|
1475
|
-
|
|
1476
|
-
if ticket.assignee:
|
|
1477
|
-
console.print(f"Assignee: {ticket.assignee}")
|
|
1478
|
-
|
|
1479
|
-
# Display comments if requested
|
|
1480
|
-
if ticket_comments:
|
|
1481
|
-
console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
|
|
1482
|
-
for comment in ticket_comments:
|
|
1483
|
-
console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
|
|
1484
|
-
console.print(comment.content)
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
@app.command()
|
|
1488
|
-
def comment(
|
|
1489
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1490
|
-
content: str = typer.Argument(..., help="Comment content"),
|
|
1491
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1492
|
-
None, "--adapter", help="Override default adapter"
|
|
1493
|
-
),
|
|
1494
|
-
) -> None:
|
|
1495
|
-
"""Add a comment to a ticket."""
|
|
525
|
+
# Add instructions command to main app
|
|
526
|
+
app.add_typer(instruction_app, name="instructions")
|
|
1496
527
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
override_adapter=adapter.value if adapter else None
|
|
1500
|
-
)
|
|
528
|
+
# Add project-update command group to main app
|
|
529
|
+
app.add_typer(project_update_app, name="project-update")
|
|
1501
530
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
content=content,
|
|
1506
|
-
author="cli-user" # Could be made configurable
|
|
1507
|
-
)
|
|
531
|
+
# Add setup and init commands to main app
|
|
532
|
+
app.command()(setup)
|
|
533
|
+
app.command()(init)
|
|
1508
534
|
|
|
1509
|
-
|
|
1510
|
-
|
|
535
|
+
# Add platform installer commands to main app
|
|
536
|
+
app.command()(install)
|
|
537
|
+
app.command()(remove)
|
|
538
|
+
app.command()(uninstall)
|
|
1511
539
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
console.print(f"Comment ID: {result.id}")
|
|
1517
|
-
console.print(f"Content: {content}")
|
|
1518
|
-
except Exception as e:
|
|
1519
|
-
console.print(f"[red]✗[/red] Failed to add comment: {e}")
|
|
1520
|
-
raise typer.Exit(1)
|
|
540
|
+
# Add MCP server installer commands
|
|
541
|
+
app.command(name="install-mcp-server")(install_mcp_server)
|
|
542
|
+
app.command(name="list-mcp-servers")(list_mcp_servers)
|
|
543
|
+
app.command(name="uninstall-mcp-server")(uninstall_mcp_server)
|
|
1521
544
|
|
|
1522
545
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
None, "--description", "-d", help="New description"
|
|
1529
|
-
),
|
|
1530
|
-
priority: Optional[Priority] = typer.Option(
|
|
1531
|
-
None, "--priority", "-p", help="New priority"
|
|
1532
|
-
),
|
|
1533
|
-
assignee: Optional[str] = typer.Option(
|
|
1534
|
-
None, "--assignee", "-a", help="New assignee"
|
|
1535
|
-
),
|
|
1536
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1537
|
-
None, "--adapter", help="Override default adapter"
|
|
1538
|
-
),
|
|
1539
|
-
) -> None:
|
|
1540
|
-
"""Update ticket fields."""
|
|
1541
|
-
updates = {}
|
|
1542
|
-
if title:
|
|
1543
|
-
updates["title"] = title
|
|
1544
|
-
if description:
|
|
1545
|
-
updates["description"] = description
|
|
1546
|
-
if priority:
|
|
1547
|
-
updates["priority"] = (
|
|
1548
|
-
priority.value if isinstance(priority, Priority) else priority
|
|
1549
|
-
)
|
|
1550
|
-
if assignee:
|
|
1551
|
-
updates["assignee"] = assignee
|
|
1552
|
-
|
|
1553
|
-
if not updates:
|
|
1554
|
-
console.print("[yellow]No updates specified[/yellow]")
|
|
1555
|
-
raise typer.Exit(1)
|
|
1556
|
-
|
|
1557
|
-
# Get the adapter name
|
|
1558
|
-
config = load_config()
|
|
1559
|
-
adapter_name = (
|
|
1560
|
-
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
1561
|
-
)
|
|
1562
|
-
|
|
1563
|
-
# Add ticket_id to updates
|
|
1564
|
-
updates["ticket_id"] = ticket_id
|
|
1565
|
-
|
|
1566
|
-
# Add to queue with explicit project directory
|
|
1567
|
-
queue = Queue()
|
|
1568
|
-
queue_id = queue.add(
|
|
1569
|
-
ticket_data=updates,
|
|
1570
|
-
adapter=adapter_name,
|
|
1571
|
-
operation="update",
|
|
1572
|
-
project_dir=str(Path.cwd()) # Explicitly pass current project directory
|
|
1573
|
-
)
|
|
1574
|
-
|
|
1575
|
-
console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
|
|
1576
|
-
for key, value in updates.items():
|
|
1577
|
-
if key != "ticket_id":
|
|
1578
|
-
console.print(f" {key}: {value}")
|
|
1579
|
-
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
1580
|
-
|
|
1581
|
-
# Start worker if needed
|
|
1582
|
-
manager = WorkerManager()
|
|
1583
|
-
if manager.start_if_needed():
|
|
1584
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
@app.command()
|
|
1588
|
-
def transition(
|
|
1589
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1590
|
-
state_positional: Optional[TicketState] = typer.Argument(
|
|
1591
|
-
None, help="Target state (positional - deprecated, use --state instead)"
|
|
546
|
+
# Add diagnostics command
|
|
547
|
+
@app.command("doctor")
|
|
548
|
+
def doctor_command(
|
|
549
|
+
output_file: str | None = typer.Option(
|
|
550
|
+
None, "--output", "-o", help="Save full report to file"
|
|
1592
551
|
),
|
|
1593
|
-
|
|
1594
|
-
|
|
552
|
+
json_output: bool = typer.Option(
|
|
553
|
+
False, "--json", help="Output report in JSON format"
|
|
1595
554
|
),
|
|
1596
|
-
|
|
1597
|
-
|
|
555
|
+
simple: bool = typer.Option(
|
|
556
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
1598
557
|
),
|
|
1599
558
|
) -> None:
|
|
1600
|
-
"""
|
|
1601
|
-
|
|
1602
|
-
Examples:
|
|
1603
|
-
# Recommended syntax with flag:
|
|
1604
|
-
mcp-ticketer transition BTA-215 --state done
|
|
1605
|
-
mcp-ticketer transition BTA-215 -s in_progress
|
|
1606
|
-
|
|
1607
|
-
# Legacy positional syntax (still supported):
|
|
1608
|
-
mcp-ticketer transition BTA-215 done
|
|
1609
|
-
|
|
1610
|
-
"""
|
|
1611
|
-
# Determine which state to use (prefer flag over positional)
|
|
1612
|
-
target_state = state if state is not None else state_positional
|
|
1613
|
-
|
|
1614
|
-
if target_state is None:
|
|
1615
|
-
console.print("[red]Error: State is required[/red]")
|
|
1616
|
-
console.print(
|
|
1617
|
-
"Use either:\n"
|
|
1618
|
-
" - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
|
|
1619
|
-
" - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
|
|
1620
|
-
)
|
|
1621
|
-
raise typer.Exit(1)
|
|
1622
|
-
|
|
1623
|
-
# Get the adapter name
|
|
1624
|
-
config = load_config()
|
|
1625
|
-
adapter_name = (
|
|
1626
|
-
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
1627
|
-
)
|
|
1628
|
-
|
|
1629
|
-
# Add to queue with explicit project directory
|
|
1630
|
-
queue = Queue()
|
|
1631
|
-
queue_id = queue.add(
|
|
1632
|
-
ticket_data={
|
|
1633
|
-
"ticket_id": ticket_id,
|
|
1634
|
-
"state": (
|
|
1635
|
-
target_state.value if hasattr(target_state, "value") else target_state
|
|
1636
|
-
),
|
|
1637
|
-
},
|
|
1638
|
-
adapter=adapter_name,
|
|
1639
|
-
operation="transition",
|
|
1640
|
-
project_dir=str(Path.cwd()) # Explicitly pass current project directory
|
|
1641
|
-
)
|
|
1642
|
-
|
|
1643
|
-
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
1644
|
-
console.print(f" Ticket: {ticket_id} → {target_state}")
|
|
1645
|
-
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
1646
|
-
|
|
1647
|
-
# Start worker if needed
|
|
1648
|
-
manager = WorkerManager()
|
|
1649
|
-
if manager.start_if_needed():
|
|
1650
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
@app.command()
|
|
1654
|
-
def search(
|
|
1655
|
-
query: Optional[str] = typer.Argument(None, help="Search query"),
|
|
1656
|
-
state: Optional[TicketState] = typer.Option(None, "--state", "-s"),
|
|
1657
|
-
priority: Optional[Priority] = typer.Option(None, "--priority", "-p"),
|
|
1658
|
-
assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
|
|
1659
|
-
limit: int = typer.Option(10, "--limit", "-l"),
|
|
1660
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1661
|
-
None, "--adapter", help="Override default adapter"
|
|
1662
|
-
),
|
|
1663
|
-
) -> None:
|
|
1664
|
-
"""Search tickets with advanced query."""
|
|
1665
|
-
|
|
1666
|
-
async def _search():
|
|
1667
|
-
adapter_instance = get_adapter(
|
|
1668
|
-
override_adapter=adapter.value if adapter else None
|
|
1669
|
-
)
|
|
1670
|
-
search_query = SearchQuery(
|
|
1671
|
-
query=query,
|
|
1672
|
-
state=state,
|
|
1673
|
-
priority=priority,
|
|
1674
|
-
assignee=assignee,
|
|
1675
|
-
limit=limit,
|
|
1676
|
-
)
|
|
1677
|
-
return await adapter_instance.search(search_query)
|
|
1678
|
-
|
|
1679
|
-
tickets = asyncio.run(_search())
|
|
1680
|
-
|
|
1681
|
-
if not tickets:
|
|
1682
|
-
console.print("[yellow]No tickets found matching query[/yellow]")
|
|
1683
|
-
return
|
|
1684
|
-
|
|
1685
|
-
# Display results
|
|
1686
|
-
console.print(f"\n[bold]Found {len(tickets)} ticket(s)[/bold]\n")
|
|
1687
|
-
|
|
1688
|
-
for ticket in tickets:
|
|
1689
|
-
console.print(f"[cyan]{ticket.id}[/cyan]: {ticket.title}")
|
|
1690
|
-
console.print(f" State: {ticket.state} | Priority: {ticket.priority}")
|
|
1691
|
-
if ticket.assignee:
|
|
1692
|
-
console.print(f" Assignee: {ticket.assignee}")
|
|
1693
|
-
console.print()
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
# Add queue command to main app
|
|
1697
|
-
app.add_typer(queue_app, name="queue")
|
|
1698
|
-
|
|
1699
|
-
# Add discover command to main app
|
|
1700
|
-
app.add_typer(discover_app, name="discover")
|
|
1701
|
-
|
|
1702
|
-
# Add diagnostics command
|
|
1703
|
-
@app.command()
|
|
1704
|
-
def diagnose(
|
|
1705
|
-
output_file: Optional[str] = typer.Option(None, "--output", "-o", help="Save full report to file"),
|
|
1706
|
-
json_output: bool = typer.Option(False, "--json", help="Output report in JSON format"),
|
|
1707
|
-
simple: bool = typer.Option(False, "--simple", help="Use simple diagnostics (no heavy dependencies)"),
|
|
1708
|
-
) -> None:
|
|
1709
|
-
"""Run comprehensive system diagnostics and health check."""
|
|
559
|
+
"""Run comprehensive system diagnostics and health check (alias: diagnose)."""
|
|
1710
560
|
if simple:
|
|
1711
561
|
from .simple_health import simple_diagnose
|
|
562
|
+
|
|
1712
563
|
report = simple_diagnose()
|
|
1713
564
|
if output_file:
|
|
1714
565
|
import json
|
|
1715
|
-
|
|
566
|
+
|
|
567
|
+
with open(output_file, "w") as f:
|
|
1716
568
|
json.dump(report, f, indent=2)
|
|
1717
569
|
console.print(f"\n📄 Report saved to: {output_file}")
|
|
1718
570
|
if json_output:
|
|
1719
571
|
import json
|
|
572
|
+
|
|
1720
573
|
console.print("\n" + json.dumps(report, indent=2))
|
|
1721
574
|
if report["issues"]:
|
|
1722
|
-
raise typer.Exit(1)
|
|
575
|
+
raise typer.Exit(1) from None
|
|
1723
576
|
else:
|
|
1724
577
|
try:
|
|
1725
|
-
asyncio.run(
|
|
578
|
+
asyncio.run(
|
|
579
|
+
run_diagnostics(output_file=output_file, json_output=json_output)
|
|
580
|
+
)
|
|
1726
581
|
except typer.Exit:
|
|
1727
582
|
# typer.Exit is expected - don't fall back to simple diagnostics
|
|
1728
583
|
raise
|
|
@@ -1730,314 +585,55 @@ def diagnose(
|
|
|
1730
585
|
console.print(f"⚠️ Full diagnostics failed: {e}")
|
|
1731
586
|
console.print("🔄 Falling back to simple diagnostics...")
|
|
1732
587
|
from .simple_health import simple_diagnose
|
|
588
|
+
|
|
1733
589
|
report = simple_diagnose()
|
|
1734
590
|
if report["issues"]:
|
|
1735
|
-
raise typer.Exit(1)
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
@app.command()
|
|
1739
|
-
def health() -> None:
|
|
1740
|
-
"""Quick health check - shows system status summary."""
|
|
1741
|
-
from .simple_health import simple_health_check
|
|
1742
|
-
|
|
1743
|
-
result = simple_health_check()
|
|
1744
|
-
if result != 0:
|
|
1745
|
-
raise typer.Exit(result)
|
|
1746
|
-
|
|
1747
|
-
# Create MCP configuration command group
|
|
1748
|
-
mcp_app = typer.Typer(
|
|
1749
|
-
name="mcp",
|
|
1750
|
-
help="Configure MCP integration for AI clients (Claude, Gemini, Codex, Auggie)",
|
|
1751
|
-
add_completion=False,
|
|
1752
|
-
)
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
@app.command()
|
|
1756
|
-
def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
1757
|
-
"""Check status of a queued operation."""
|
|
1758
|
-
queue = Queue()
|
|
1759
|
-
item = queue.get_item(queue_id)
|
|
1760
|
-
|
|
1761
|
-
if not item:
|
|
1762
|
-
console.print(f"[red]Queue item not found: {queue_id}[/red]")
|
|
1763
|
-
raise typer.Exit(1)
|
|
1764
|
-
|
|
1765
|
-
# Display status
|
|
1766
|
-
console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
|
|
1767
|
-
console.print(f"Operation: {item.operation}")
|
|
1768
|
-
console.print(f"Adapter: {item.adapter}")
|
|
1769
|
-
|
|
1770
|
-
# Status with color
|
|
1771
|
-
if item.status == QueueStatus.COMPLETED:
|
|
1772
|
-
console.print(f"Status: [green]{item.status}[/green]")
|
|
1773
|
-
elif item.status == QueueStatus.FAILED:
|
|
1774
|
-
console.print(f"Status: [red]{item.status}[/red]")
|
|
1775
|
-
elif item.status == QueueStatus.PROCESSING:
|
|
1776
|
-
console.print(f"Status: [yellow]{item.status}[/yellow]")
|
|
1777
|
-
else:
|
|
1778
|
-
console.print(f"Status: {item.status}")
|
|
1779
|
-
|
|
1780
|
-
# Timestamps
|
|
1781
|
-
console.print(f"Created: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
1782
|
-
if item.processed_at:
|
|
1783
|
-
console.print(f"Processed: {item.processed_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
1784
|
-
|
|
1785
|
-
# Error or result
|
|
1786
|
-
if item.error_message:
|
|
1787
|
-
console.print(f"\n[red]Error:[/red] {item.error_message}")
|
|
1788
|
-
elif item.result:
|
|
1789
|
-
console.print("\n[green]Result:[/green]")
|
|
1790
|
-
for key, value in item.result.items():
|
|
1791
|
-
console.print(f" {key}: {value}")
|
|
1792
|
-
|
|
1793
|
-
if item.retry_count > 0:
|
|
1794
|
-
console.print(f"\nRetry Count: {item.retry_count}")
|
|
591
|
+
raise typer.Exit(1) from None
|
|
1795
592
|
|
|
1796
593
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
def serve(
|
|
1802
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1803
|
-
None, "--adapter", "-a", help="Override default adapter type"
|
|
594
|
+
@app.command("diagnose", hidden=True)
|
|
595
|
+
def diagnose_alias(
|
|
596
|
+
output_file: str | None = typer.Option(
|
|
597
|
+
None, "--output", "-o", help="Save full report to file"
|
|
1804
598
|
),
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
),
|
|
1808
|
-
):
|
|
1809
|
-
"""Start MCP server for JSON-RPC communication over stdio.
|
|
1810
|
-
|
|
1811
|
-
This command is used by Claude Code/Desktop when connecting to the MCP server.
|
|
1812
|
-
You typically don't need to run this manually - use 'mcp-ticketer mcp' to configure.
|
|
1813
|
-
|
|
1814
|
-
Configuration Resolution:
|
|
1815
|
-
- When MCP server starts, it uses the current working directory (cwd)
|
|
1816
|
-
- The cwd is set by Claude Code/Desktop from the 'cwd' field in .mcp/config.json
|
|
1817
|
-
- Configuration is loaded with this priority:
|
|
1818
|
-
1. Project-specific: .mcp-ticketer/config.json in cwd
|
|
1819
|
-
2. Global: ~/.mcp-ticketer/config.json
|
|
1820
|
-
3. Default: aitrackdown adapter with .aitrackdown base path
|
|
1821
|
-
"""
|
|
1822
|
-
from ..mcp.server import MCPTicketServer
|
|
1823
|
-
|
|
1824
|
-
# Load configuration (respects project-specific config in cwd)
|
|
1825
|
-
config = load_config()
|
|
1826
|
-
|
|
1827
|
-
# Determine adapter type with priority: CLI arg > .env files > config > default
|
|
1828
|
-
if adapter:
|
|
1829
|
-
# Priority 1: Command line argument
|
|
1830
|
-
adapter_type = adapter.value
|
|
1831
|
-
# Get base config from config file
|
|
1832
|
-
adapters_config = config.get("adapters", {})
|
|
1833
|
-
adapter_config = adapters_config.get(adapter_type, {})
|
|
1834
|
-
else:
|
|
1835
|
-
# Priority 2: .env files
|
|
1836
|
-
from ..mcp.server import _load_env_configuration
|
|
1837
|
-
env_config = _load_env_configuration()
|
|
1838
|
-
if env_config:
|
|
1839
|
-
adapter_type = env_config["adapter_type"]
|
|
1840
|
-
adapter_config = env_config["adapter_config"]
|
|
1841
|
-
else:
|
|
1842
|
-
# Priority 3: Configuration file
|
|
1843
|
-
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
1844
|
-
adapters_config = config.get("adapters", {})
|
|
1845
|
-
adapter_config = adapters_config.get(adapter_type, {})
|
|
1846
|
-
|
|
1847
|
-
# Override with command line options if provided (highest priority)
|
|
1848
|
-
if base_path and adapter_type == "aitrackdown":
|
|
1849
|
-
adapter_config["base_path"] = base_path
|
|
1850
|
-
|
|
1851
|
-
# Fallback to legacy config format
|
|
1852
|
-
if not adapter_config and "config" in config:
|
|
1853
|
-
adapter_config = config["config"]
|
|
1854
|
-
|
|
1855
|
-
# MCP server uses stdio for JSON-RPC, so we can't print to stdout
|
|
1856
|
-
# Only print to stderr to avoid interfering with the protocol
|
|
1857
|
-
import sys
|
|
1858
|
-
|
|
1859
|
-
if sys.stderr.isatty():
|
|
1860
|
-
# Only print if stderr is a terminal (not redirected)
|
|
1861
|
-
console.file = sys.stderr
|
|
1862
|
-
console.print(f"[green]Starting MCP server[/green] with {adapter_type} adapter")
|
|
1863
|
-
console.print(
|
|
1864
|
-
"[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
|
|
1865
|
-
)
|
|
1866
|
-
|
|
1867
|
-
# Create and run server
|
|
1868
|
-
try:
|
|
1869
|
-
server = MCPTicketServer(adapter_type, adapter_config)
|
|
1870
|
-
asyncio.run(server.run())
|
|
1871
|
-
except KeyboardInterrupt:
|
|
1872
|
-
# Also send this to stderr
|
|
1873
|
-
if sys.stderr.isatty():
|
|
1874
|
-
console.print("\n[yellow]Server stopped by user[/yellow]")
|
|
1875
|
-
if "server" in locals():
|
|
1876
|
-
asyncio.run(server.stop())
|
|
1877
|
-
except Exception as e:
|
|
1878
|
-
# Log error to stderr
|
|
1879
|
-
sys.stderr.write(f"MCP server error: {e}\n")
|
|
1880
|
-
sys.exit(1)
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
@mcp_app.command(name="claude")
|
|
1884
|
-
def mcp_claude(
|
|
1885
|
-
global_config: bool = typer.Option(
|
|
1886
|
-
False,
|
|
1887
|
-
"--global",
|
|
1888
|
-
"-g",
|
|
1889
|
-
help="Configure Claude Desktop instead of project-level",
|
|
599
|
+
json_output: bool = typer.Option(
|
|
600
|
+
False, "--json", help="Output report in JSON format"
|
|
1890
601
|
),
|
|
1891
|
-
|
|
1892
|
-
False, "--
|
|
602
|
+
simple: bool = typer.Option(
|
|
603
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
1893
604
|
),
|
|
1894
|
-
):
|
|
1895
|
-
"""
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
Claude Code's MCP settings accordingly.
|
|
1899
|
-
|
|
1900
|
-
By default, configures project-level (.mcp/config.json).
|
|
1901
|
-
Use --global to configure Claude Desktop instead.
|
|
1902
|
-
|
|
1903
|
-
Examples:
|
|
1904
|
-
# Configure for current project (default)
|
|
1905
|
-
mcp-ticketer mcp claude
|
|
1906
|
-
|
|
1907
|
-
# Configure Claude Desktop globally
|
|
1908
|
-
mcp-ticketer mcp claude --global
|
|
1909
|
-
|
|
1910
|
-
# Force overwrite existing configuration
|
|
1911
|
-
mcp-ticketer mcp claude --force
|
|
1912
|
-
|
|
1913
|
-
"""
|
|
1914
|
-
from ..cli.mcp_configure import configure_claude_mcp
|
|
1915
|
-
|
|
1916
|
-
try:
|
|
1917
|
-
configure_claude_mcp(global_config=global_config, force=force)
|
|
1918
|
-
except Exception as e:
|
|
1919
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
1920
|
-
raise typer.Exit(1)
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
@mcp_app.command(name="gemini")
|
|
1924
|
-
def mcp_gemini(
|
|
1925
|
-
scope: str = typer.Option(
|
|
1926
|
-
"project",
|
|
1927
|
-
"--scope",
|
|
1928
|
-
"-s",
|
|
1929
|
-
help="Configuration scope: 'project' (default) or 'user'",
|
|
1930
|
-
),
|
|
1931
|
-
force: bool = typer.Option(
|
|
1932
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
1933
|
-
),
|
|
1934
|
-
):
|
|
1935
|
-
"""Configure Gemini CLI to use mcp-ticketer MCP server.
|
|
1936
|
-
|
|
1937
|
-
Reads configuration from .mcp-ticketer/config.json and creates
|
|
1938
|
-
Gemini CLI settings file with mcp-ticketer configuration.
|
|
1939
|
-
|
|
1940
|
-
By default, configures project-level (.gemini/settings.json).
|
|
1941
|
-
Use --scope user to configure user-level (~/.gemini/settings.json).
|
|
1942
|
-
|
|
1943
|
-
Examples:
|
|
1944
|
-
# Configure for current project (default)
|
|
1945
|
-
mcp-ticketer mcp gemini
|
|
1946
|
-
|
|
1947
|
-
# Configure at user level
|
|
1948
|
-
mcp-ticketer mcp gemini --scope user
|
|
1949
|
-
|
|
1950
|
-
# Force overwrite existing configuration
|
|
1951
|
-
mcp-ticketer mcp gemini --force
|
|
1952
|
-
|
|
1953
|
-
"""
|
|
1954
|
-
from ..cli.gemini_configure import configure_gemini_mcp
|
|
1955
|
-
|
|
1956
|
-
# Validate scope parameter
|
|
1957
|
-
if scope not in ["project", "user"]:
|
|
1958
|
-
console.print(
|
|
1959
|
-
f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
|
|
1960
|
-
)
|
|
1961
|
-
raise typer.Exit(1)
|
|
1962
|
-
|
|
1963
|
-
try:
|
|
1964
|
-
configure_gemini_mcp(scope=scope, force=force) # type: ignore
|
|
1965
|
-
except Exception as e:
|
|
1966
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
1967
|
-
raise typer.Exit(1)
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
@mcp_app.command(name="codex")
|
|
1971
|
-
def mcp_codex(
|
|
1972
|
-
force: bool = typer.Option(
|
|
1973
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
1974
|
-
),
|
|
1975
|
-
):
|
|
1976
|
-
"""Configure Codex CLI to use mcp-ticketer MCP server.
|
|
1977
|
-
|
|
1978
|
-
Reads configuration from .mcp-ticketer/config.json and creates
|
|
1979
|
-
Codex CLI config.toml with mcp-ticketer configuration.
|
|
1980
|
-
|
|
1981
|
-
IMPORTANT: Codex CLI ONLY supports global configuration at ~/.codex/config.toml.
|
|
1982
|
-
There is no project-level configuration support. After configuration,
|
|
1983
|
-
you must restart Codex CLI for changes to take effect.
|
|
1984
|
-
|
|
1985
|
-
Examples:
|
|
1986
|
-
# Configure Codex CLI globally
|
|
1987
|
-
mcp-ticketer mcp codex
|
|
1988
|
-
|
|
1989
|
-
# Force overwrite existing configuration
|
|
1990
|
-
mcp-ticketer mcp codex --force
|
|
1991
|
-
|
|
1992
|
-
"""
|
|
1993
|
-
from ..cli.codex_configure import configure_codex_mcp
|
|
1994
|
-
|
|
1995
|
-
try:
|
|
1996
|
-
configure_codex_mcp(force=force)
|
|
1997
|
-
except Exception as e:
|
|
1998
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
1999
|
-
raise typer.Exit(1)
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
@mcp_app.command(name="auggie")
|
|
2003
|
-
def mcp_auggie(
|
|
2004
|
-
force: bool = typer.Option(
|
|
2005
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
2006
|
-
),
|
|
2007
|
-
):
|
|
2008
|
-
"""Configure Auggie CLI to use mcp-ticketer MCP server.
|
|
605
|
+
) -> None:
|
|
606
|
+
"""Run comprehensive system diagnostics and health check (alias for doctor)."""
|
|
607
|
+
# Call the doctor_command function with the same parameters
|
|
608
|
+
doctor_command(output_file=output_file, json_output=json_output, simple=simple)
|
|
2009
609
|
|
|
2010
|
-
Reads configuration from .mcp-ticketer/config.json and creates
|
|
2011
|
-
Auggie CLI settings.json with mcp-ticketer configuration.
|
|
2012
610
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
611
|
+
@app.command("status")
|
|
612
|
+
def status_command() -> None:
|
|
613
|
+
"""Quick health check - shows system status summary (alias: health)."""
|
|
614
|
+
from .simple_health import simple_health_check
|
|
2016
615
|
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
616
|
+
result = simple_health_check()
|
|
617
|
+
if result != 0:
|
|
618
|
+
raise typer.Exit(result) from None
|
|
2020
619
|
|
|
2021
|
-
# Force overwrite existing configuration
|
|
2022
|
-
mcp-ticketer mcp auggie --force
|
|
2023
620
|
|
|
2024
|
-
|
|
2025
|
-
|
|
621
|
+
@app.command("health")
|
|
622
|
+
def health_alias() -> None:
|
|
623
|
+
"""Quick health check - shows system status summary (alias for status)."""
|
|
624
|
+
from .simple_health import simple_health_check
|
|
2026
625
|
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
2031
|
-
raise typer.Exit(1)
|
|
626
|
+
result = simple_health_check()
|
|
627
|
+
if result != 0:
|
|
628
|
+
raise typer.Exit(result) from None
|
|
2032
629
|
|
|
2033
630
|
|
|
2034
631
|
# Add command groups to main app (must be after all subcommands are defined)
|
|
2035
|
-
app.add_typer(linear_app, name="linear")
|
|
2036
632
|
app.add_typer(mcp_app, name="mcp")
|
|
2037
633
|
|
|
2038
634
|
|
|
2039
|
-
def main():
|
|
2040
|
-
"""
|
|
635
|
+
def main() -> None:
|
|
636
|
+
"""Execute the main CLI application entry point."""
|
|
2041
637
|
app()
|
|
2042
638
|
|
|
2043
639
|
|