mcp-ticketer 0.12.0__py3-none-any.whl → 2.2.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +507 -6
- mcp_ticketer/adapters/asana/adapter.py +229 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -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 +47 -5
- 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/adapter.py +2730 -139
- mcp_ticketer/adapters/linear/client.py +175 -3
- mcp_ticketer/adapters/linear/mappers.py +203 -8
- mcp_ticketer/adapters/linear/queries.py +280 -3
- mcp_ticketer/adapters/linear/types.py +120 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +1288 -105
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +267 -3175
- mcp_ticketer/cli/mcp_configure.py +821 -119
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +795 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +705 -103
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +56 -6
- mcp_ticketer/core/adapter.py +533 -2
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +480 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +625 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +33 -11
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- 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 +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/queue.py +68 -0
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.13.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 -1574
- mcp_ticketer/adapters/jira.py +0 -1258
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
mcp_ticketer/cli/main.py
CHANGED
|
@@ -10,24 +10,29 @@ from typing import Any
|
|
|
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
13
|
|
|
15
14
|
# Import adapters module to trigger registration
|
|
16
15
|
import mcp_ticketer.adapters # noqa: F401
|
|
17
16
|
|
|
18
17
|
from ..__version__ import __version__
|
|
19
|
-
from ..core import AdapterRegistry
|
|
20
|
-
from ..core.models import Comment, SearchQuery
|
|
21
|
-
from ..queue import Queue, QueueStatus, WorkerManager
|
|
22
|
-
from ..queue.health_monitor import HealthStatus, QueueHealthMonitor
|
|
23
|
-
from ..queue.ticket_registry import TicketRegistry
|
|
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
|
|
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
|
+
)
|
|
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
|
|
29
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
|
|
30
34
|
from .queue_commands import app as queue_app
|
|
35
|
+
from .setup_command import setup
|
|
31
36
|
from .ticket_commands import app as ticket_app
|
|
32
37
|
|
|
33
38
|
# Load environment variables from .env files
|
|
@@ -93,6 +98,7 @@ def load_config(project_dir: Path | None = None) -> dict:
|
|
|
93
98
|
from user home directory or system-wide locations.
|
|
94
99
|
|
|
95
100
|
Args:
|
|
101
|
+
----
|
|
96
102
|
project_dir: Optional project directory to load config from
|
|
97
103
|
|
|
98
104
|
Resolution order:
|
|
@@ -100,6 +106,7 @@ def load_config(project_dir: Path | None = None) -> dict:
|
|
|
100
106
|
2. Default to aitrackdown adapter
|
|
101
107
|
|
|
102
108
|
Returns:
|
|
109
|
+
-------
|
|
103
110
|
Configuration dictionary with adapter and config keys.
|
|
104
111
|
Defaults to aitrackdown if no local config exists.
|
|
105
112
|
|
|
@@ -151,6 +158,7 @@ def _discover_from_env_files() -> str | None:
|
|
|
151
158
|
"""Discover adapter configuration from .env or .env.local files.
|
|
152
159
|
|
|
153
160
|
Returns:
|
|
161
|
+
-------
|
|
154
162
|
Adapter name if discovered, None otherwise
|
|
155
163
|
|
|
156
164
|
"""
|
|
@@ -196,6 +204,7 @@ def _save_adapter_to_config(adapter_name: str) -> None:
|
|
|
196
204
|
"""Save adapter configuration to config file.
|
|
197
205
|
|
|
198
206
|
Args:
|
|
207
|
+
----
|
|
199
208
|
adapter_name: Name of the adapter to save as default
|
|
200
209
|
|
|
201
210
|
"""
|
|
@@ -246,9 +255,11 @@ def merge_config(updates: dict) -> dict:
|
|
|
246
255
|
"""Merge updates into existing config.
|
|
247
256
|
|
|
248
257
|
Args:
|
|
258
|
+
----
|
|
249
259
|
updates: Configuration updates to merge
|
|
250
260
|
|
|
251
261
|
Returns:
|
|
262
|
+
-------
|
|
252
263
|
Updated configuration
|
|
253
264
|
|
|
254
265
|
"""
|
|
@@ -276,6 +287,7 @@ def get_adapter(
|
|
|
276
287
|
"""Get configured adapter instance.
|
|
277
288
|
|
|
278
289
|
Args:
|
|
290
|
+
----
|
|
279
291
|
override_adapter: Override the default adapter type
|
|
280
292
|
override_config: Override configuration for the adapter
|
|
281
293
|
|
|
@@ -303,7 +315,6 @@ def get_adapter(
|
|
|
303
315
|
adapter_config = config["config"]
|
|
304
316
|
|
|
305
317
|
# Add environment variables for authentication
|
|
306
|
-
import os
|
|
307
318
|
|
|
308
319
|
if adapter_type == "linear":
|
|
309
320
|
if not adapter_config.get("api_key"):
|
|
@@ -320,3220 +331,301 @@ def get_adapter(
|
|
|
320
331
|
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
321
332
|
|
|
322
333
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
334
|
+
@app.command("set")
|
|
335
|
+
def set_config(
|
|
336
|
+
adapter: AdapterType | None = typer.Option(
|
|
337
|
+
None, "--adapter", "-a", help="Set default adapter"
|
|
338
|
+
),
|
|
339
|
+
team_key: str | None = typer.Option(
|
|
340
|
+
None, "--team-key", help="Linear team key (e.g., BTA)"
|
|
341
|
+
),
|
|
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(
|
|
348
|
+
None, "--base-path", help="AITrackdown base path"
|
|
349
|
+
),
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Set default adapter and adapter-specific configuration.
|
|
327
352
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
353
|
+
When called without arguments, shows current configuration.
|
|
354
|
+
"""
|
|
355
|
+
if not any([adapter, team_key, team_id, owner, repo, server, project, base_path]):
|
|
356
|
+
# Show current configuration
|
|
357
|
+
config = load_config()
|
|
358
|
+
console.print("[bold]Current Configuration:[/bold]")
|
|
359
|
+
console.print(
|
|
360
|
+
f"Default adapter: [cyan]{config.get('default_adapter', 'aitrackdown')}[/cyan]"
|
|
361
|
+
)
|
|
331
362
|
|
|
332
|
-
|
|
333
|
-
|
|
363
|
+
adapters_config = config.get("adapters", {})
|
|
364
|
+
if adapters_config:
|
|
365
|
+
console.print("\n[bold]Adapter Settings:[/bold]")
|
|
366
|
+
for adapter_name, adapter_config in adapters_config.items():
|
|
367
|
+
console.print(f"\n[cyan]{adapter_name}:[/cyan]")
|
|
368
|
+
for key, value in adapter_config.items():
|
|
369
|
+
# Don't display sensitive values like tokens
|
|
370
|
+
if (
|
|
371
|
+
"token" in key.lower()
|
|
372
|
+
or "key" in key.lower()
|
|
373
|
+
and "team" not in key.lower()
|
|
374
|
+
):
|
|
375
|
+
value = "***" if value else "not set"
|
|
376
|
+
console.print(f" {key}: {value}")
|
|
377
|
+
return
|
|
334
378
|
|
|
335
|
-
|
|
336
|
-
import json
|
|
379
|
+
updates = {}
|
|
337
380
|
|
|
338
|
-
|
|
381
|
+
# Set default adapter
|
|
382
|
+
if adapter:
|
|
383
|
+
updates["default_adapter"] = adapter.value
|
|
384
|
+
console.print(f"[green]✓[/green] Default adapter set to: {adapter.value}")
|
|
339
385
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
with open(config_file_path) as f:
|
|
343
|
-
config = json.load(f)
|
|
386
|
+
# Build adapter-specific configuration
|
|
387
|
+
adapter_configs = {}
|
|
344
388
|
|
|
345
|
-
|
|
389
|
+
# Linear configuration
|
|
390
|
+
if team_key or team_id:
|
|
391
|
+
linear_config = {}
|
|
392
|
+
if team_key:
|
|
393
|
+
linear_config["team_key"] = team_key
|
|
394
|
+
if team_id:
|
|
395
|
+
linear_config["team_id"] = team_id
|
|
396
|
+
adapter_configs["linear"] = linear_config
|
|
397
|
+
console.print("[green]✓[/green] Linear settings updated")
|
|
346
398
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
399
|
+
# GitHub configuration
|
|
400
|
+
if owner or repo:
|
|
401
|
+
github_config = {}
|
|
402
|
+
if owner:
|
|
403
|
+
github_config["owner"] = owner
|
|
404
|
+
if repo:
|
|
405
|
+
github_config["repo"] = repo
|
|
406
|
+
adapter_configs["github"] = github_config
|
|
407
|
+
console.print("[green]✓[/green] GitHub settings updated")
|
|
350
408
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
409
|
+
# JIRA configuration
|
|
410
|
+
if server or project:
|
|
411
|
+
jira_config = {}
|
|
412
|
+
if server:
|
|
413
|
+
jira_config["server"] = server
|
|
414
|
+
if project:
|
|
415
|
+
jira_config["project_key"] = project
|
|
416
|
+
adapter_configs["jira"] = jira_config
|
|
417
|
+
console.print("[green]✓[/green] JIRA settings updated")
|
|
354
418
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
419
|
+
# AITrackdown configuration
|
|
420
|
+
if base_path:
|
|
421
|
+
adapter_configs["aitrackdown"] = {"base_path": base_path}
|
|
422
|
+
console.print("[green]✓[/green] AITrackdown settings updated")
|
|
359
423
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
"Invalid Linear API key format (should start with 'lin_api_')"
|
|
363
|
-
)
|
|
364
|
-
return issues
|
|
424
|
+
if adapter_configs:
|
|
425
|
+
updates["adapters"] = adapter_configs
|
|
365
426
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
427
|
+
# Merge and save configuration
|
|
428
|
+
if updates:
|
|
429
|
+
config = merge_config(updates)
|
|
430
|
+
save_config(config)
|
|
431
|
+
console.print(f"[dim]Configuration saved to {CONFIG_FILE}[/dim]")
|
|
369
432
|
|
|
370
|
-
adapter = LinearAdapter(adapter_config)
|
|
371
|
-
# Try to list one ticket to verify connectivity
|
|
372
|
-
await adapter.list(limit=1)
|
|
373
|
-
except Exception as e:
|
|
374
|
-
error_msg = str(e)
|
|
375
|
-
if "401" in error_msg or "Unauthorized" in error_msg:
|
|
376
|
-
issues.append(
|
|
377
|
-
"Failed to authenticate with Linear API - invalid API key"
|
|
378
|
-
)
|
|
379
|
-
elif "403" in error_msg or "Forbidden" in error_msg:
|
|
380
|
-
issues.append("Linear API key lacks required permissions")
|
|
381
|
-
elif "team" in error_msg.lower():
|
|
382
|
-
issues.append(f"Linear team configuration error: {error_msg}")
|
|
383
|
-
else:
|
|
384
|
-
issues.append(f"Failed to connect to Linear API: {error_msg}")
|
|
385
|
-
|
|
386
|
-
elif adapter_type == "jira":
|
|
387
|
-
server = adapter_config.get("server")
|
|
388
|
-
email = adapter_config.get("email")
|
|
389
|
-
api_token = adapter_config.get("api_token")
|
|
390
|
-
|
|
391
|
-
# Check required fields
|
|
392
|
-
if not server:
|
|
393
|
-
issues.append("JIRA server URL is missing")
|
|
394
|
-
if not email:
|
|
395
|
-
issues.append("JIRA email is missing")
|
|
396
|
-
if not api_token:
|
|
397
|
-
issues.append("JIRA API token is missing")
|
|
398
|
-
|
|
399
|
-
if issues:
|
|
400
|
-
return issues
|
|
401
|
-
|
|
402
|
-
# Test actual connectivity
|
|
403
|
-
try:
|
|
404
|
-
from ..adapters.jira import JiraAdapter
|
|
405
433
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
# Check required fields
|
|
425
|
-
if not token:
|
|
426
|
-
issues.append("GitHub token is missing")
|
|
427
|
-
if not owner:
|
|
428
|
-
issues.append("GitHub owner is missing")
|
|
429
|
-
if not repo:
|
|
430
|
-
issues.append("GitHub repo is missing")
|
|
431
|
-
|
|
432
|
-
if issues:
|
|
433
|
-
return issues
|
|
434
|
-
|
|
435
|
-
# Test actual connectivity
|
|
436
|
-
try:
|
|
437
|
-
from ..adapters.github import GitHubAdapter
|
|
434
|
+
@app.command("configure")
|
|
435
|
+
def configure_command(
|
|
436
|
+
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
437
|
+
adapter: str | None = typer.Option(
|
|
438
|
+
None, "--adapter", help="Set default adapter type"
|
|
439
|
+
),
|
|
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)"),
|
|
443
|
+
global_scope: bool = typer.Option(
|
|
444
|
+
False,
|
|
445
|
+
"--global",
|
|
446
|
+
"-g",
|
|
447
|
+
help="Save to global config instead of project-specific",
|
|
448
|
+
),
|
|
449
|
+
) -> None:
|
|
450
|
+
"""Configure MCP Ticketer integration.
|
|
438
451
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
):
|
|
448
|
-
issues.append("Failed to authenticate with GitHub - invalid token")
|
|
449
|
-
elif "404" in error_msg or "Not Found" in error_msg:
|
|
450
|
-
issues.append(f"GitHub repository not found: {owner}/{repo}")
|
|
451
|
-
elif "403" in error_msg or "Forbidden" in error_msg:
|
|
452
|
-
issues.append("GitHub token lacks required permissions")
|
|
453
|
-
else:
|
|
454
|
-
issues.append(f"Failed to connect to GitHub: {error_msg}")
|
|
455
|
-
|
|
456
|
-
elif adapter_type == "aitrackdown":
|
|
457
|
-
# AITrackdown doesn't require credentials, just check base_path is set
|
|
458
|
-
base_path = adapter_config.get("base_path")
|
|
459
|
-
if not base_path:
|
|
460
|
-
issues.append("AITrackdown base_path is missing")
|
|
452
|
+
Run without arguments to launch interactive wizard.
|
|
453
|
+
Use --show to display current configuration.
|
|
454
|
+
Use options to set specific values directly.
|
|
455
|
+
"""
|
|
456
|
+
# Show configuration
|
|
457
|
+
if show:
|
|
458
|
+
show_current_config()
|
|
459
|
+
return
|
|
461
460
|
|
|
462
|
-
|
|
463
|
-
|
|
461
|
+
# Direct configuration
|
|
462
|
+
if any([adapter, api_key, project_id, team_id]):
|
|
463
|
+
set_adapter_config(
|
|
464
|
+
adapter=adapter,
|
|
465
|
+
api_key=api_key,
|
|
466
|
+
project_id=project_id,
|
|
467
|
+
team_id=team_id,
|
|
468
|
+
global_scope=global_scope,
|
|
469
|
+
)
|
|
470
|
+
return
|
|
464
471
|
|
|
465
|
-
|
|
472
|
+
# Run interactive wizard
|
|
473
|
+
configure_wizard()
|
|
466
474
|
|
|
467
475
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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)
|
|
472
494
|
|
|
473
|
-
Args:
|
|
474
|
-
console: Rich console for output
|
|
475
|
-
adapter_type: Type of adapter configured
|
|
476
|
-
config_file_path: Path to config file
|
|
477
|
-
proj_path: Project path
|
|
478
495
|
|
|
479
|
-
|
|
480
|
-
|
|
496
|
+
@app.command("migrate-config")
|
|
497
|
+
def migrate_config(
|
|
498
|
+
dry_run: bool = typer.Option(
|
|
499
|
+
False, "--dry-run", help="Show what would be done without making changes"
|
|
500
|
+
),
|
|
501
|
+
) -> None:
|
|
502
|
+
"""Migrate configuration from old format to new format.
|
|
481
503
|
|
|
504
|
+
This command will:
|
|
505
|
+
1. Detect old configuration format
|
|
506
|
+
2. Convert to new schema
|
|
507
|
+
3. Backup old config
|
|
508
|
+
4. Apply new config
|
|
482
509
|
"""
|
|
483
|
-
|
|
484
|
-
retry_count = 0
|
|
510
|
+
migrate_config_command(dry_run=dry_run)
|
|
485
511
|
|
|
486
|
-
while retry_count < max_retries:
|
|
487
|
-
console.print("\n[cyan]🔍 Validating configuration...[/cyan]")
|
|
488
512
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
import sys
|
|
513
|
+
# Add ticket command group to main app
|
|
514
|
+
app.add_typer(ticket_app, name="ticket")
|
|
492
515
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
old_stderr = sys.stderr
|
|
496
|
-
sys.stdout = io.StringIO()
|
|
497
|
-
sys.stderr = io.StringIO()
|
|
516
|
+
# Add platform command group to main app
|
|
517
|
+
app.add_typer(platform_app, name="platform")
|
|
498
518
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
validation_issues = await _validate_adapter_credentials(
|
|
502
|
-
adapter_type, config_file_path
|
|
503
|
-
)
|
|
504
|
-
finally:
|
|
505
|
-
# Restore stdout/stderr
|
|
506
|
-
sys.stdout = old_stdout
|
|
507
|
-
sys.stderr = old_stderr
|
|
508
|
-
|
|
509
|
-
# Check if there are issues
|
|
510
|
-
if not validation_issues:
|
|
511
|
-
console.print("[green]✓ Configuration validated successfully![/green]")
|
|
512
|
-
return True
|
|
513
|
-
|
|
514
|
-
# Display issues found
|
|
515
|
-
console.print("[yellow]⚠️ Configuration validation found issues:[/yellow]")
|
|
516
|
-
for issue in validation_issues:
|
|
517
|
-
console.print(f" [red]❌[/red] {issue}")
|
|
518
|
-
|
|
519
|
-
# Offer user options
|
|
520
|
-
console.print("\n[bold]What would you like to do?[/bold]")
|
|
521
|
-
console.print("1. [cyan]Re-enter configuration values[/cyan] (fix issues)")
|
|
522
|
-
console.print("2. [yellow]Continue anyway[/yellow] (skip validation)")
|
|
523
|
-
console.print("3. [red]Exit[/red] (fix manually later)")
|
|
519
|
+
# Add queue command to main app
|
|
520
|
+
app.add_typer(queue_app, name="queue")
|
|
524
521
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
except typer.Abort:
|
|
528
|
-
console.print("[yellow]Cancelled.[/yellow]")
|
|
529
|
-
return False
|
|
530
|
-
|
|
531
|
-
if choice == 1:
|
|
532
|
-
# Re-enter configuration
|
|
533
|
-
# Check BEFORE increment to fix off-by-one error
|
|
534
|
-
if retry_count >= max_retries:
|
|
535
|
-
console.print(
|
|
536
|
-
f"[red]Maximum retry attempts ({max_retries}) reached.[/red]"
|
|
537
|
-
)
|
|
538
|
-
console.print(
|
|
539
|
-
"[yellow]Please fix configuration manually and run 'mcp-ticketer doctor'[/yellow]"
|
|
540
|
-
)
|
|
541
|
-
return False
|
|
542
|
-
retry_count += 1
|
|
522
|
+
# Add discover command to main app
|
|
523
|
+
app.add_typer(discover_app, name="discover")
|
|
543
524
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
)
|
|
525
|
+
# Add instructions command to main app
|
|
526
|
+
app.add_typer(instruction_app, name="instructions")
|
|
547
527
|
|
|
548
|
-
|
|
549
|
-
|
|
528
|
+
# Add project-update command group to main app
|
|
529
|
+
app.add_typer(project_update_app, name="project-update")
|
|
550
530
|
|
|
551
|
-
|
|
552
|
-
|
|
531
|
+
# Add setup and init commands to main app
|
|
532
|
+
app.command()(setup)
|
|
533
|
+
app.command()(init)
|
|
553
534
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
"[dim]Get your API key at: https://linear.app/settings/api[/dim]\n"
|
|
559
|
-
)
|
|
535
|
+
# Add platform installer commands to main app
|
|
536
|
+
app.command()(install)
|
|
537
|
+
app.command()(remove)
|
|
538
|
+
app.command()(uninstall)
|
|
560
539
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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)
|
|
564
544
|
|
|
565
|
-
console.print("\n[bold]Linear Team Configuration[/bold]")
|
|
566
|
-
console.print("You can provide either:")
|
|
567
|
-
console.print(
|
|
568
|
-
" 1. Team URL (e.g., https://linear.app/workspace/team/TEAMKEY/active)"
|
|
569
|
-
)
|
|
570
|
-
console.print(" 2. Team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
|
|
571
|
-
console.print(" 3. Team ID (UUID)")
|
|
572
|
-
console.print(
|
|
573
|
-
"[dim]Find team URL or key in: Linear → Your Team → Team Issues Page[/dim]\n"
|
|
574
|
-
)
|
|
575
545
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
console.print(
|
|
593
|
-
"[green]✓[/green] Successfully derived team ID from URL"
|
|
594
|
-
)
|
|
595
|
-
else:
|
|
596
|
-
console.print(f"[red]Error:[/red] {error}")
|
|
597
|
-
console.print("Please provide team key or ID manually instead.")
|
|
598
|
-
team_input = typer.prompt("Team key or ID")
|
|
599
|
-
|
|
600
|
-
if len(team_input) > 20: # Likely a UUID
|
|
601
|
-
linear_team_id = team_input
|
|
602
|
-
else:
|
|
603
|
-
linear_team_key = team_input
|
|
604
|
-
else:
|
|
605
|
-
# Input is team key or ID
|
|
606
|
-
if len(team_input) > 20: # Likely a UUID
|
|
607
|
-
linear_team_id = team_input
|
|
608
|
-
else:
|
|
609
|
-
linear_team_key = team_input
|
|
610
|
-
|
|
611
|
-
# Update config
|
|
612
|
-
linear_config = {
|
|
613
|
-
"api_key": linear_api_key,
|
|
614
|
-
"type": "linear",
|
|
615
|
-
}
|
|
616
|
-
if linear_team_key:
|
|
617
|
-
linear_config["team_key"] = linear_team_key
|
|
618
|
-
if linear_team_id:
|
|
619
|
-
linear_config["team_id"] = linear_team_id
|
|
620
|
-
|
|
621
|
-
current_config["adapters"]["linear"] = linear_config
|
|
622
|
-
|
|
623
|
-
elif adapter_type == "jira":
|
|
624
|
-
console.print("\n[bold]JIRA Configuration[/bold]")
|
|
625
|
-
console.print("Enter your JIRA server details.\n")
|
|
626
|
-
|
|
627
|
-
server = typer.prompt(
|
|
628
|
-
"JIRA server URL (e.g., https://company.atlassian.net)"
|
|
629
|
-
)
|
|
630
|
-
email = typer.prompt("Your JIRA email address")
|
|
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"
|
|
551
|
+
),
|
|
552
|
+
json_output: bool = typer.Option(
|
|
553
|
+
False, "--json", help="Output report in JSON format"
|
|
554
|
+
),
|
|
555
|
+
simple: bool = typer.Option(
|
|
556
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
557
|
+
),
|
|
558
|
+
) -> None:
|
|
559
|
+
"""Run comprehensive system diagnostics and health check (alias: diagnose)."""
|
|
560
|
+
if simple:
|
|
561
|
+
from .simple_health import simple_diagnose
|
|
631
562
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
)
|
|
563
|
+
report = simple_diagnose()
|
|
564
|
+
if output_file:
|
|
565
|
+
import json
|
|
636
566
|
|
|
637
|
-
|
|
567
|
+
with open(output_file, "w") as f:
|
|
568
|
+
json.dump(report, f, indent=2)
|
|
569
|
+
console.print(f"\n📄 Report saved to: {output_file}")
|
|
570
|
+
if json_output:
|
|
571
|
+
import json
|
|
638
572
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
573
|
+
console.print("\n" + json.dumps(report, indent=2))
|
|
574
|
+
if report["issues"]:
|
|
575
|
+
raise typer.Exit(1) from None
|
|
576
|
+
else:
|
|
577
|
+
try:
|
|
578
|
+
asyncio.run(
|
|
579
|
+
run_diagnostics(output_file=output_file, json_output=json_output)
|
|
580
|
+
)
|
|
581
|
+
except typer.Exit:
|
|
582
|
+
# typer.Exit is expected - don't fall back to simple diagnostics
|
|
583
|
+
raise
|
|
584
|
+
except Exception as e:
|
|
585
|
+
console.print(f"⚠️ Full diagnostics failed: {e}")
|
|
586
|
+
console.print("🔄 Falling back to simple diagnostics...")
|
|
587
|
+
from .simple_health import simple_diagnose
|
|
644
588
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
"email": email,
|
|
649
|
-
"api_token": token,
|
|
650
|
-
"type": "jira",
|
|
651
|
-
}
|
|
652
|
-
if project:
|
|
653
|
-
jira_config["project_key"] = project
|
|
589
|
+
report = simple_diagnose()
|
|
590
|
+
if report["issues"]:
|
|
591
|
+
raise typer.Exit(1) from None
|
|
654
592
|
|
|
655
|
-
current_config["adapters"]["jira"] = jira_config
|
|
656
593
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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"
|
|
598
|
+
),
|
|
599
|
+
json_output: bool = typer.Option(
|
|
600
|
+
False, "--json", help="Output report in JSON format"
|
|
601
|
+
),
|
|
602
|
+
simple: bool = typer.Option(
|
|
603
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
604
|
+
),
|
|
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)
|
|
660
609
|
|
|
661
|
-
owner = typer.prompt(
|
|
662
|
-
"GitHub repository owner (username or organization)"
|
|
663
|
-
)
|
|
664
|
-
repo = typer.prompt("GitHub repository name")
|
|
665
610
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
console.print(
|
|
671
|
-
"[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n"
|
|
672
|
-
)
|
|
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
|
|
673
615
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
# Update config
|
|
679
|
-
current_config["adapters"]["github"] = {
|
|
680
|
-
"owner": owner,
|
|
681
|
-
"repo": repo,
|
|
682
|
-
"token": token,
|
|
683
|
-
"type": "github",
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
elif adapter_type == "aitrackdown":
|
|
687
|
-
# AITrackdown doesn't need credentials, but save config before returning
|
|
688
|
-
# Save updated configuration
|
|
689
|
-
with open(config_file_path, "w") as f:
|
|
690
|
-
json.dump(current_config, f, indent=2)
|
|
691
|
-
|
|
692
|
-
console.print(
|
|
693
|
-
"[yellow]AITrackdown doesn't require credentials. Continuing...[/yellow]"
|
|
694
|
-
)
|
|
695
|
-
console.print("[dim]✓ Configuration updated[/dim]")
|
|
696
|
-
return True
|
|
697
|
-
|
|
698
|
-
else:
|
|
699
|
-
console.print(f"[red]Unknown adapter type: {adapter_type}[/red]")
|
|
700
|
-
return False
|
|
701
|
-
|
|
702
|
-
# Save updated configuration
|
|
703
|
-
with open(config_file_path, "w") as f:
|
|
704
|
-
json.dump(current_config, f, indent=2)
|
|
705
|
-
|
|
706
|
-
console.print("[dim]✓ Configuration updated[/dim]")
|
|
707
|
-
# Loop will retry validation
|
|
708
|
-
|
|
709
|
-
elif choice == 2:
|
|
710
|
-
# Continue anyway
|
|
711
|
-
console.print(
|
|
712
|
-
"[yellow]⚠️ Continuing with potentially invalid configuration.[/yellow]"
|
|
713
|
-
)
|
|
714
|
-
console.print("[dim]You can validate later with: mcp-ticketer doctor[/dim]")
|
|
715
|
-
return True
|
|
716
|
-
|
|
717
|
-
elif choice == 3:
|
|
718
|
-
# Exit
|
|
719
|
-
console.print(
|
|
720
|
-
"[yellow]Configuration saved but not validated. Run 'mcp-ticketer doctor' to test.[/yellow]"
|
|
721
|
-
)
|
|
722
|
-
return False
|
|
723
|
-
|
|
724
|
-
else:
|
|
725
|
-
console.print(
|
|
726
|
-
f"[red]Invalid choice: {choice}. Please enter 1, 2, or 3.[/red]"
|
|
727
|
-
)
|
|
728
|
-
# Continue loop to ask again
|
|
729
|
-
|
|
730
|
-
return True
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
def _prompt_for_adapter_selection(console: Console) -> str:
|
|
734
|
-
"""Interactive prompt for adapter selection.
|
|
735
|
-
|
|
736
|
-
Args:
|
|
737
|
-
console: Rich console for output
|
|
738
|
-
|
|
739
|
-
Returns:
|
|
740
|
-
Selected adapter type
|
|
741
|
-
|
|
742
|
-
"""
|
|
743
|
-
console.print("\n[bold blue]🚀 MCP Ticketer Setup[/bold blue]")
|
|
744
|
-
console.print("Choose which ticket system you want to connect to:\n")
|
|
745
|
-
|
|
746
|
-
# Define adapter options with descriptions
|
|
747
|
-
adapters = [
|
|
748
|
-
{
|
|
749
|
-
"name": "linear",
|
|
750
|
-
"title": "Linear",
|
|
751
|
-
"description": "Modern project management (linear.app)",
|
|
752
|
-
"requirements": "API key and team ID",
|
|
753
|
-
},
|
|
754
|
-
{
|
|
755
|
-
"name": "github",
|
|
756
|
-
"title": "GitHub Issues",
|
|
757
|
-
"description": "GitHub repository issues",
|
|
758
|
-
"requirements": "Personal access token, owner, and repo",
|
|
759
|
-
},
|
|
760
|
-
{
|
|
761
|
-
"name": "jira",
|
|
762
|
-
"title": "JIRA",
|
|
763
|
-
"description": "Atlassian JIRA project management",
|
|
764
|
-
"requirements": "Server URL, email, and API token",
|
|
765
|
-
},
|
|
766
|
-
{
|
|
767
|
-
"name": "aitrackdown",
|
|
768
|
-
"title": "Local Files (AITrackdown)",
|
|
769
|
-
"description": "Store tickets in local files (no external service)",
|
|
770
|
-
"requirements": "None - works offline",
|
|
771
|
-
},
|
|
772
|
-
]
|
|
773
|
-
|
|
774
|
-
# Display options
|
|
775
|
-
for i, adapter in enumerate(adapters, 1):
|
|
776
|
-
console.print(f"[cyan]{i}.[/cyan] [bold]{adapter['title']}[/bold]")
|
|
777
|
-
console.print(f" {adapter['description']}")
|
|
778
|
-
console.print(f" [dim]Requirements: {adapter['requirements']}[/dim]\n")
|
|
779
|
-
|
|
780
|
-
# Get user selection
|
|
781
|
-
while True:
|
|
782
|
-
try:
|
|
783
|
-
choice = typer.prompt("Select adapter (1-4)", type=int, default=1)
|
|
784
|
-
if 1 <= choice <= len(adapters):
|
|
785
|
-
selected_adapter = adapters[choice - 1]
|
|
786
|
-
console.print(
|
|
787
|
-
f"\n[green]✓ Selected: {selected_adapter['title']}[/green]"
|
|
788
|
-
)
|
|
789
|
-
return selected_adapter["name"]
|
|
790
|
-
else:
|
|
791
|
-
console.print(
|
|
792
|
-
f"[red]Please enter a number between 1 and {len(adapters)}[/red]"
|
|
793
|
-
)
|
|
794
|
-
except (ValueError, typer.Abort):
|
|
795
|
-
console.print("[yellow]Setup cancelled.[/yellow]")
|
|
796
|
-
raise typer.Exit(0) from None
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
@app.command()
|
|
800
|
-
def setup(
|
|
801
|
-
project_path: str | None = typer.Option(
|
|
802
|
-
None, "--path", help="Project path (default: current directory)"
|
|
803
|
-
),
|
|
804
|
-
skip_platforms: bool = typer.Option(
|
|
805
|
-
False,
|
|
806
|
-
"--skip-platforms",
|
|
807
|
-
help="Skip platform installation (only initialize adapter)",
|
|
808
|
-
),
|
|
809
|
-
force_reinit: bool = typer.Option(
|
|
810
|
-
False,
|
|
811
|
-
"--force-reinit",
|
|
812
|
-
help="Force re-initialization even if config exists",
|
|
813
|
-
),
|
|
814
|
-
) -> None:
|
|
815
|
-
"""Smart setup command - combines init + platform installation.
|
|
816
|
-
|
|
817
|
-
This command intelligently detects your current setup state and only
|
|
818
|
-
performs necessary configuration. It's the recommended way to get started.
|
|
819
|
-
|
|
820
|
-
Detection & Smart Actions:
|
|
821
|
-
- First run: Full setup (init + platform installation)
|
|
822
|
-
- Existing config: Skip init, offer platform installation
|
|
823
|
-
- Detects changes: Offers to update configurations
|
|
824
|
-
- Respects existing: Won't overwrite without confirmation
|
|
825
|
-
|
|
826
|
-
Examples:
|
|
827
|
-
# Smart setup (recommended for first-time setup)
|
|
828
|
-
mcp-ticketer setup
|
|
829
|
-
|
|
830
|
-
# Setup for different project
|
|
831
|
-
mcp-ticketer setup --path /path/to/project
|
|
832
|
-
|
|
833
|
-
# Re-initialize configuration
|
|
834
|
-
mcp-ticketer setup --force-reinit
|
|
835
|
-
|
|
836
|
-
# Only init adapter, skip platform installation
|
|
837
|
-
mcp-ticketer setup --skip-platforms
|
|
838
|
-
|
|
839
|
-
Note: For advanced configuration, use 'init' and 'install' separately.
|
|
840
|
-
|
|
841
|
-
"""
|
|
842
|
-
from .platform_detection import PlatformDetector
|
|
843
|
-
|
|
844
|
-
proj_path = Path(project_path) if project_path else Path.cwd()
|
|
845
|
-
config_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
846
|
-
|
|
847
|
-
console.print("[bold cyan]🚀 MCP Ticketer Smart Setup[/bold cyan]\n")
|
|
848
|
-
|
|
849
|
-
# Step 1: Detect existing configuration
|
|
850
|
-
config_exists = config_path.exists()
|
|
851
|
-
config_valid = False
|
|
852
|
-
current_adapter = None
|
|
853
|
-
|
|
854
|
-
if config_exists and not force_reinit:
|
|
855
|
-
try:
|
|
856
|
-
with open(config_path) as f:
|
|
857
|
-
config = json.load(f)
|
|
858
|
-
current_adapter = config.get("default_adapter")
|
|
859
|
-
config_valid = bool(current_adapter and config.get("adapters"))
|
|
860
|
-
except (json.JSONDecodeError, OSError):
|
|
861
|
-
config_valid = False
|
|
862
|
-
|
|
863
|
-
if config_valid:
|
|
864
|
-
console.print("[green]✓[/green] Configuration detected")
|
|
865
|
-
console.print(f"[dim] Adapter: {current_adapter}[/dim]")
|
|
866
|
-
console.print(f"[dim] Location: {config_path}[/dim]\n")
|
|
867
|
-
|
|
868
|
-
# Offer to reconfigure
|
|
869
|
-
if not typer.confirm(
|
|
870
|
-
"Configuration already exists. Keep existing settings?", default=True
|
|
871
|
-
):
|
|
872
|
-
console.print("[cyan]Re-initializing configuration...[/cyan]\n")
|
|
873
|
-
force_reinit = True
|
|
874
|
-
config_valid = False
|
|
875
|
-
else:
|
|
876
|
-
if config_exists:
|
|
877
|
-
console.print(
|
|
878
|
-
"[yellow]⚠[/yellow] Configuration file exists but is invalid\n"
|
|
879
|
-
)
|
|
880
|
-
else:
|
|
881
|
-
console.print("[yellow]⚠[/yellow] No configuration found\n")
|
|
882
|
-
|
|
883
|
-
# Step 2: Initialize adapter configuration if needed
|
|
884
|
-
if not config_valid or force_reinit:
|
|
885
|
-
console.print("[bold]Step 1/2: Adapter Configuration[/bold]\n")
|
|
886
|
-
|
|
887
|
-
# Run init command non-interactively through function call
|
|
888
|
-
# We'll use the discover and prompt flow from init
|
|
889
|
-
from ..core.env_discovery import discover_config
|
|
890
|
-
|
|
891
|
-
discovered = discover_config(proj_path)
|
|
892
|
-
adapter_type = None
|
|
893
|
-
|
|
894
|
-
# Try auto-discovery
|
|
895
|
-
if discovered and discovered.adapters:
|
|
896
|
-
primary = discovered.get_primary_adapter()
|
|
897
|
-
if primary:
|
|
898
|
-
adapter_type = primary.adapter_type
|
|
899
|
-
console.print(f"[green]✓ Auto-detected {adapter_type} adapter[/green]")
|
|
900
|
-
console.print(f"[dim] Source: {primary.found_in}[/dim]")
|
|
901
|
-
console.print(f"[dim] Confidence: {primary.confidence:.0%}[/dim]\n")
|
|
902
|
-
|
|
903
|
-
if not typer.confirm(
|
|
904
|
-
f"Use detected {adapter_type} adapter?", default=True
|
|
905
|
-
):
|
|
906
|
-
adapter_type = None
|
|
907
|
-
|
|
908
|
-
# If no adapter detected, prompt for selection
|
|
909
|
-
if not adapter_type:
|
|
910
|
-
adapter_type = _prompt_for_adapter_selection(console)
|
|
911
|
-
|
|
912
|
-
# Now run the full init with the selected adapter
|
|
913
|
-
console.print(f"\n[cyan]Initializing {adapter_type} adapter...[/cyan]\n")
|
|
914
|
-
|
|
915
|
-
# Call init programmatically
|
|
916
|
-
init(
|
|
917
|
-
adapter=adapter_type,
|
|
918
|
-
project_path=str(proj_path),
|
|
919
|
-
global_config=False,
|
|
920
|
-
)
|
|
921
|
-
|
|
922
|
-
console.print("\n[green]✓ Adapter configuration complete[/green]\n")
|
|
923
|
-
else:
|
|
924
|
-
console.print("[green]✓ Step 1/2: Adapter already configured[/green]\n")
|
|
925
|
-
|
|
926
|
-
# Step 3: Platform installation
|
|
927
|
-
if skip_platforms:
|
|
928
|
-
console.print(
|
|
929
|
-
"[yellow]⚠[/yellow] Skipping platform installation (--skip-platforms)\n"
|
|
930
|
-
)
|
|
931
|
-
_show_setup_complete_message(console, proj_path)
|
|
932
|
-
return
|
|
933
|
-
|
|
934
|
-
console.print("[bold]Step 2/2: Platform Installation[/bold]\n")
|
|
935
|
-
|
|
936
|
-
# Detect available platforms
|
|
937
|
-
detector = PlatformDetector()
|
|
938
|
-
detected = detector.detect_all(project_path=proj_path)
|
|
939
|
-
|
|
940
|
-
if not detected:
|
|
941
|
-
console.print("[yellow]No AI platforms detected on this system.[/yellow]")
|
|
942
|
-
console.print(
|
|
943
|
-
"\n[dim]Supported platforms: Claude Code, Claude Desktop, Gemini, Codex, Auggie[/dim]"
|
|
944
|
-
)
|
|
945
|
-
console.print(
|
|
946
|
-
"[dim]Install these platforms to use them with mcp-ticketer.[/dim]\n"
|
|
947
|
-
)
|
|
948
|
-
_show_setup_complete_message(console, proj_path)
|
|
949
|
-
return
|
|
950
|
-
|
|
951
|
-
# Filter to only installed platforms
|
|
952
|
-
installed = [p for p in detected if p.is_installed]
|
|
953
|
-
|
|
954
|
-
if not installed:
|
|
955
|
-
console.print(
|
|
956
|
-
"[yellow]AI platforms detected but have configuration issues.[/yellow]"
|
|
957
|
-
)
|
|
958
|
-
console.print(
|
|
959
|
-
"\n[dim]Run 'mcp-ticketer install --auto-detect' for details.[/dim]\n"
|
|
960
|
-
)
|
|
961
|
-
_show_setup_complete_message(console, proj_path)
|
|
962
|
-
return
|
|
963
|
-
|
|
964
|
-
# Show detected platforms
|
|
965
|
-
console.print(f"[green]✓[/green] Detected {len(installed)} platform(s):\n")
|
|
966
|
-
for plat in installed:
|
|
967
|
-
console.print(f" • {plat.display_name} ({plat.scope})")
|
|
968
|
-
|
|
969
|
-
console.print()
|
|
970
|
-
|
|
971
|
-
# Check if mcp-ticketer is already configured for these platforms
|
|
972
|
-
already_configured = _check_existing_platform_configs(installed, proj_path)
|
|
973
|
-
|
|
974
|
-
if already_configured:
|
|
975
|
-
console.print(
|
|
976
|
-
f"[green]✓[/green] mcp-ticketer already configured for {len(already_configured)} platform(s)\n"
|
|
977
|
-
)
|
|
978
|
-
for plat_name in already_configured:
|
|
979
|
-
console.print(f" • {plat_name}")
|
|
980
|
-
console.print()
|
|
981
|
-
|
|
982
|
-
if not typer.confirm("Update platform configurations anyway?", default=False):
|
|
983
|
-
console.print("[yellow]Skipping platform installation[/yellow]\n")
|
|
984
|
-
_show_setup_complete_message(console, proj_path)
|
|
985
|
-
return
|
|
986
|
-
|
|
987
|
-
# Offer to install for all or select specific
|
|
988
|
-
console.print("[bold]Platform Installation Options:[/bold]")
|
|
989
|
-
console.print("1. Install for all detected platforms")
|
|
990
|
-
console.print("2. Select specific platform")
|
|
991
|
-
console.print("3. Skip platform installation")
|
|
992
|
-
|
|
993
|
-
try:
|
|
994
|
-
choice = typer.prompt("\nSelect option (1-3)", type=int, default=1)
|
|
995
|
-
except typer.Abort:
|
|
996
|
-
console.print("[yellow]Setup cancelled[/yellow]")
|
|
997
|
-
raise typer.Exit(0) from None
|
|
998
|
-
|
|
999
|
-
if choice == 3:
|
|
1000
|
-
console.print("[yellow]Skipping platform installation[/yellow]\n")
|
|
1001
|
-
_show_setup_complete_message(console, proj_path)
|
|
1002
|
-
return
|
|
1003
|
-
|
|
1004
|
-
# Import configuration functions
|
|
1005
|
-
from .auggie_configure import configure_auggie_mcp
|
|
1006
|
-
from .codex_configure import configure_codex_mcp
|
|
1007
|
-
from .gemini_configure import configure_gemini_mcp
|
|
1008
|
-
from .mcp_configure import configure_claude_mcp
|
|
1009
|
-
|
|
1010
|
-
platform_mapping = {
|
|
1011
|
-
"claude-code": lambda: configure_claude_mcp(global_config=False, force=True),
|
|
1012
|
-
"claude-desktop": lambda: configure_claude_mcp(global_config=True, force=True),
|
|
1013
|
-
"auggie": lambda: configure_auggie_mcp(force=True),
|
|
1014
|
-
"gemini": lambda: configure_gemini_mcp(scope="project", force=True),
|
|
1015
|
-
"codex": lambda: configure_codex_mcp(force=True),
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
platforms_to_install = []
|
|
1019
|
-
|
|
1020
|
-
if choice == 1:
|
|
1021
|
-
# Install for all
|
|
1022
|
-
platforms_to_install = installed
|
|
1023
|
-
elif choice == 2:
|
|
1024
|
-
# Select specific platform
|
|
1025
|
-
console.print("\n[bold]Select platform:[/bold]")
|
|
1026
|
-
for idx, plat in enumerate(installed, 1):
|
|
1027
|
-
console.print(f" {idx}. {plat.display_name} ({plat.scope})")
|
|
1028
|
-
|
|
1029
|
-
try:
|
|
1030
|
-
plat_choice = typer.prompt("\nSelect platform number", type=int)
|
|
1031
|
-
if 1 <= plat_choice <= len(installed):
|
|
1032
|
-
platforms_to_install = [installed[plat_choice - 1]]
|
|
1033
|
-
else:
|
|
1034
|
-
console.print("[red]Invalid selection[/red]")
|
|
1035
|
-
raise typer.Exit(1) from None
|
|
1036
|
-
except typer.Abort:
|
|
1037
|
-
console.print("[yellow]Setup cancelled[/yellow]")
|
|
1038
|
-
raise typer.Exit(0) from None
|
|
1039
|
-
|
|
1040
|
-
# Install for selected platforms
|
|
1041
|
-
console.print()
|
|
1042
|
-
success_count = 0
|
|
1043
|
-
failed = []
|
|
1044
|
-
|
|
1045
|
-
for plat in platforms_to_install:
|
|
1046
|
-
config_func = platform_mapping.get(plat.name)
|
|
1047
|
-
if not config_func:
|
|
1048
|
-
console.print(f"[yellow]⚠[/yellow] No installer for {plat.display_name}")
|
|
1049
|
-
continue
|
|
1050
|
-
|
|
1051
|
-
try:
|
|
1052
|
-
console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
|
|
1053
|
-
config_func()
|
|
1054
|
-
console.print(f"[green]✓[/green] {plat.display_name} configured\n")
|
|
1055
|
-
success_count += 1
|
|
1056
|
-
except Exception as e:
|
|
1057
|
-
console.print(
|
|
1058
|
-
f"[red]✗[/red] Failed to configure {plat.display_name}: {e}\n"
|
|
1059
|
-
)
|
|
1060
|
-
failed.append(plat.display_name)
|
|
1061
|
-
|
|
1062
|
-
# Summary
|
|
1063
|
-
console.print(
|
|
1064
|
-
f"[bold]Platform Installation:[/bold] {success_count}/{len(platforms_to_install)} succeeded"
|
|
1065
|
-
)
|
|
1066
|
-
if failed:
|
|
1067
|
-
console.print(f"[red]Failed:[/red] {', '.join(failed)}")
|
|
1068
|
-
|
|
1069
|
-
console.print()
|
|
1070
|
-
_show_setup_complete_message(console, proj_path)
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
def _check_existing_platform_configs(platforms: list, proj_path: Path) -> list[str]:
|
|
1074
|
-
"""Check if mcp-ticketer is already configured for given platforms.
|
|
1075
|
-
|
|
1076
|
-
Args:
|
|
1077
|
-
platforms: List of DetectedPlatform objects
|
|
1078
|
-
proj_path: Project path
|
|
1079
|
-
|
|
1080
|
-
Returns:
|
|
1081
|
-
List of platform display names that are already configured
|
|
1082
|
-
|
|
1083
|
-
"""
|
|
1084
|
-
configured = []
|
|
1085
|
-
|
|
1086
|
-
for plat in platforms:
|
|
1087
|
-
try:
|
|
1088
|
-
if plat.name == "claude-code":
|
|
1089
|
-
config_path = Path.home() / ".claude.json"
|
|
1090
|
-
if config_path.exists():
|
|
1091
|
-
with open(config_path) as f:
|
|
1092
|
-
config = json.load(f)
|
|
1093
|
-
projects = config.get("projects", {})
|
|
1094
|
-
proj_key = str(proj_path)
|
|
1095
|
-
if proj_key in projects:
|
|
1096
|
-
mcp_servers = projects[proj_key].get("mcpServers", {})
|
|
1097
|
-
if "mcp-ticketer" in mcp_servers:
|
|
1098
|
-
configured.append(plat.display_name)
|
|
1099
|
-
|
|
1100
|
-
elif plat.name == "claude-desktop":
|
|
1101
|
-
if plat.config_path.exists():
|
|
1102
|
-
with open(plat.config_path) as f:
|
|
1103
|
-
config = json.load(f)
|
|
1104
|
-
if "mcp-ticketer" in config.get("mcpServers", {}):
|
|
1105
|
-
configured.append(plat.display_name)
|
|
1106
|
-
|
|
1107
|
-
elif plat.name in ["auggie", "codex", "gemini"]:
|
|
1108
|
-
if plat.config_path.exists():
|
|
1109
|
-
# Check if mcp-ticketer is configured
|
|
1110
|
-
# Implementation depends on each platform's config format
|
|
1111
|
-
# For now, just check if config exists (simplified)
|
|
1112
|
-
pass
|
|
1113
|
-
|
|
1114
|
-
except (json.JSONDecodeError, OSError):
|
|
1115
|
-
pass
|
|
1116
|
-
|
|
1117
|
-
return configured
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
def _show_setup_complete_message(console: Console, proj_path: Path) -> None:
|
|
1121
|
-
"""Show setup complete message with next steps.
|
|
1122
|
-
|
|
1123
|
-
Args:
|
|
1124
|
-
console: Rich console for output
|
|
1125
|
-
proj_path: Project path
|
|
1126
|
-
|
|
1127
|
-
"""
|
|
1128
|
-
console.print("[bold green]🎉 Setup Complete![/bold green]\n")
|
|
1129
|
-
|
|
1130
|
-
console.print("[bold]Quick Start:[/bold]")
|
|
1131
|
-
console.print("1. Create a test ticket:")
|
|
1132
|
-
console.print(" [cyan]mcp-ticketer create 'My first ticket'[/cyan]\n")
|
|
1133
|
-
|
|
1134
|
-
console.print("2. List tickets:")
|
|
1135
|
-
console.print(" [cyan]mcp-ticketer list[/cyan]\n")
|
|
1136
|
-
|
|
1137
|
-
console.print("[bold]Useful Commands:[/bold]")
|
|
1138
|
-
console.print(" [cyan]mcp-ticketer doctor[/cyan] - Validate configuration")
|
|
1139
|
-
console.print(" [cyan]mcp-ticketer install <platform>[/cyan] - Add more platforms")
|
|
1140
|
-
console.print(" [cyan]mcp-ticketer --help[/cyan] - See all commands\n")
|
|
1141
|
-
|
|
1142
|
-
console.print(
|
|
1143
|
-
f"[dim]Configuration: {proj_path / '.mcp-ticketer' / 'config.json'}[/dim]"
|
|
1144
|
-
)
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
@app.command()
|
|
1148
|
-
def init(
|
|
1149
|
-
adapter: str | None = typer.Option(
|
|
1150
|
-
None,
|
|
1151
|
-
"--adapter",
|
|
1152
|
-
"-a",
|
|
1153
|
-
help="Adapter type to use (interactive prompt if not specified)",
|
|
1154
|
-
),
|
|
1155
|
-
project_path: str | None = typer.Option(
|
|
1156
|
-
None, "--path", help="Project path (default: current directory)"
|
|
1157
|
-
),
|
|
1158
|
-
global_config: bool = typer.Option(
|
|
1159
|
-
False,
|
|
1160
|
-
"--global",
|
|
1161
|
-
"-g",
|
|
1162
|
-
help="Save to global config instead of project-specific",
|
|
1163
|
-
),
|
|
1164
|
-
base_path: str | None = typer.Option(
|
|
1165
|
-
None,
|
|
1166
|
-
"--base-path",
|
|
1167
|
-
"-p",
|
|
1168
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
1169
|
-
),
|
|
1170
|
-
api_key: str | None = typer.Option(
|
|
1171
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
1172
|
-
),
|
|
1173
|
-
team_id: str | None = typer.Option(
|
|
1174
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
1175
|
-
),
|
|
1176
|
-
jira_server: str | None = typer.Option(
|
|
1177
|
-
None,
|
|
1178
|
-
"--jira-server",
|
|
1179
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
1180
|
-
),
|
|
1181
|
-
jira_email: str | None = typer.Option(
|
|
1182
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
1183
|
-
),
|
|
1184
|
-
jira_project: str | None = typer.Option(
|
|
1185
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
1186
|
-
),
|
|
1187
|
-
github_owner: str | None = typer.Option(
|
|
1188
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
1189
|
-
),
|
|
1190
|
-
github_repo: str | None = typer.Option(
|
|
1191
|
-
None, "--github-repo", help="GitHub repository name"
|
|
1192
|
-
),
|
|
1193
|
-
github_token: str | None = typer.Option(
|
|
1194
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
1195
|
-
),
|
|
1196
|
-
) -> None:
|
|
1197
|
-
"""Initialize adapter configuration only (without platform installation).
|
|
1198
|
-
|
|
1199
|
-
This command sets up adapter configuration with interactive prompts.
|
|
1200
|
-
It auto-detects adapter configuration from .env files or prompts for
|
|
1201
|
-
interactive setup if no configuration is found.
|
|
1202
|
-
|
|
1203
|
-
Creates .mcp-ticketer/config.json in the current directory.
|
|
1204
|
-
|
|
1205
|
-
RECOMMENDED: Use 'mcp-ticketer setup' instead for a complete setup
|
|
1206
|
-
experience that includes both adapter configuration and platform
|
|
1207
|
-
installation in one command.
|
|
1208
|
-
|
|
1209
|
-
The init command automatically validates your configuration after setup:
|
|
1210
|
-
- If validation passes, setup completes
|
|
1211
|
-
- If issues are detected, you can re-enter credentials, continue anyway, or exit
|
|
1212
|
-
- You get up to 3 retry attempts to fix configuration issues
|
|
1213
|
-
- You can always re-validate later with 'mcp-ticketer doctor'
|
|
1214
|
-
|
|
1215
|
-
Examples:
|
|
1216
|
-
# For first-time setup, use 'setup' instead (recommended)
|
|
1217
|
-
mcp-ticketer setup
|
|
1218
|
-
|
|
1219
|
-
# Initialize adapter only (advanced usage)
|
|
1220
|
-
mcp-ticketer init
|
|
1221
|
-
|
|
1222
|
-
# Force specific adapter
|
|
1223
|
-
mcp-ticketer init --adapter linear
|
|
1224
|
-
|
|
1225
|
-
# Initialize for different project
|
|
1226
|
-
mcp-ticketer init --path /path/to/project
|
|
1227
|
-
|
|
1228
|
-
"""
|
|
1229
|
-
from pathlib import Path
|
|
1230
|
-
|
|
1231
|
-
from ..core.env_discovery import discover_config
|
|
1232
|
-
|
|
1233
|
-
# Determine project path
|
|
1234
|
-
proj_path = Path(project_path) if project_path else Path.cwd()
|
|
1235
|
-
|
|
1236
|
-
# Check if already initialized (unless using --global)
|
|
1237
|
-
if not global_config:
|
|
1238
|
-
config_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
1239
|
-
|
|
1240
|
-
if config_path.exists():
|
|
1241
|
-
if not typer.confirm(
|
|
1242
|
-
f"Configuration already exists at {config_path}. Overwrite?",
|
|
1243
|
-
default=False,
|
|
1244
|
-
):
|
|
1245
|
-
console.print("[yellow]Initialization cancelled.[/yellow]")
|
|
1246
|
-
raise typer.Exit(0) from None
|
|
1247
|
-
|
|
1248
|
-
# 1. Try auto-discovery if no adapter specified
|
|
1249
|
-
discovered = None
|
|
1250
|
-
adapter_type = adapter
|
|
1251
|
-
|
|
1252
|
-
if not adapter_type:
|
|
1253
|
-
console.print(
|
|
1254
|
-
"[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
|
|
1255
|
-
)
|
|
1256
|
-
|
|
1257
|
-
# First try our improved .env configuration loader
|
|
1258
|
-
from ..mcp.server.main import _load_env_configuration
|
|
1259
|
-
|
|
1260
|
-
env_config = _load_env_configuration()
|
|
1261
|
-
|
|
1262
|
-
if env_config:
|
|
1263
|
-
adapter_type = env_config["adapter_type"]
|
|
1264
|
-
console.print(
|
|
1265
|
-
f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
|
|
1266
|
-
)
|
|
1267
|
-
|
|
1268
|
-
# Show what was discovered
|
|
1269
|
-
console.print("\n[dim]Configuration found in: .env files[/dim]")
|
|
1270
|
-
console.print("[dim]Confidence: 100%[/dim]")
|
|
1271
|
-
|
|
1272
|
-
# Ask user to confirm auto-detected adapter
|
|
1273
|
-
if not typer.confirm(
|
|
1274
|
-
f"Use detected {adapter_type} adapter?",
|
|
1275
|
-
default=True,
|
|
1276
|
-
):
|
|
1277
|
-
adapter_type = None # Will trigger interactive selection
|
|
1278
|
-
else:
|
|
1279
|
-
# Fallback to old discovery system for backward compatibility
|
|
1280
|
-
discovered = discover_config(proj_path)
|
|
1281
|
-
|
|
1282
|
-
if discovered and discovered.adapters:
|
|
1283
|
-
primary = discovered.get_primary_adapter()
|
|
1284
|
-
if primary:
|
|
1285
|
-
adapter_type = primary.adapter_type
|
|
1286
|
-
console.print(
|
|
1287
|
-
f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
|
|
1288
|
-
)
|
|
1289
|
-
|
|
1290
|
-
# Show what was discovered
|
|
1291
|
-
console.print(
|
|
1292
|
-
f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
|
|
1293
|
-
)
|
|
1294
|
-
console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
|
|
1295
|
-
|
|
1296
|
-
# Ask user to confirm auto-detected adapter
|
|
1297
|
-
if not typer.confirm(
|
|
1298
|
-
f"Use detected {adapter_type} adapter?",
|
|
1299
|
-
default=True,
|
|
1300
|
-
):
|
|
1301
|
-
adapter_type = None # Will trigger interactive selection
|
|
1302
|
-
else:
|
|
1303
|
-
adapter_type = None # Will trigger interactive selection
|
|
1304
|
-
else:
|
|
1305
|
-
adapter_type = None # Will trigger interactive selection
|
|
1306
|
-
|
|
1307
|
-
# If no adapter determined, show interactive selection
|
|
1308
|
-
if not adapter_type:
|
|
1309
|
-
adapter_type = _prompt_for_adapter_selection(console)
|
|
1310
|
-
|
|
1311
|
-
# 2. Create configuration based on adapter type
|
|
1312
|
-
config = {"default_adapter": adapter_type, "adapters": {}}
|
|
1313
|
-
|
|
1314
|
-
# 3. If discovered and matches adapter_type, use discovered config
|
|
1315
|
-
if discovered and adapter_type != "aitrackdown":
|
|
1316
|
-
discovered_adapter = discovered.get_adapter_by_type(adapter_type)
|
|
1317
|
-
if discovered_adapter:
|
|
1318
|
-
adapter_config = discovered_adapter.config.copy()
|
|
1319
|
-
# Ensure the config has the correct 'type' field
|
|
1320
|
-
adapter_config["type"] = adapter_type
|
|
1321
|
-
# Remove 'adapter' field if present (legacy)
|
|
1322
|
-
adapter_config.pop("adapter", None)
|
|
1323
|
-
config["adapters"][adapter_type] = adapter_config
|
|
1324
|
-
|
|
1325
|
-
# 4. Handle manual configuration for specific adapters
|
|
1326
|
-
if adapter_type == "aitrackdown":
|
|
1327
|
-
config["adapters"]["aitrackdown"] = {
|
|
1328
|
-
"type": "aitrackdown",
|
|
1329
|
-
"base_path": base_path or ".aitrackdown",
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
elif adapter_type == "linear":
|
|
1333
|
-
# If not auto-discovered, build from CLI params or prompt
|
|
1334
|
-
if adapter_type not in config["adapters"]:
|
|
1335
|
-
# API Key
|
|
1336
|
-
linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
|
|
1337
|
-
if not linear_api_key:
|
|
1338
|
-
console.print("\n[bold]Linear Configuration[/bold]")
|
|
1339
|
-
console.print("You need a Linear API key to connect to Linear.")
|
|
1340
|
-
console.print(
|
|
1341
|
-
"[dim]Get your API key at: https://linear.app/settings/api[/dim]\n"
|
|
1342
|
-
)
|
|
1343
|
-
|
|
1344
|
-
linear_api_key = typer.prompt(
|
|
1345
|
-
"Enter your Linear API key", hide_input=True
|
|
1346
|
-
)
|
|
1347
|
-
|
|
1348
|
-
# Team ID or Team Key or Team URL
|
|
1349
|
-
# Try environment variables first
|
|
1350
|
-
linear_team_key = os.getenv("LINEAR_TEAM_KEY")
|
|
1351
|
-
linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
|
|
1352
|
-
|
|
1353
|
-
if not linear_team_key and not linear_team_id:
|
|
1354
|
-
console.print("\n[bold]Linear Team Configuration[/bold]")
|
|
1355
|
-
console.print("You can provide either:")
|
|
1356
|
-
console.print(
|
|
1357
|
-
" 1. Team URL (e.g., https://linear.app/workspace/team/TEAMKEY/active)"
|
|
1358
|
-
)
|
|
1359
|
-
console.print(" 2. Team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
|
|
1360
|
-
console.print(" 3. Team ID (UUID)")
|
|
1361
|
-
console.print(
|
|
1362
|
-
"[dim]Find team URL or key in: Linear → Your Team → Team Issues Page[/dim]\n"
|
|
1363
|
-
)
|
|
1364
|
-
|
|
1365
|
-
team_input = typer.prompt("Team URL, key, or ID")
|
|
1366
|
-
|
|
1367
|
-
# Check if input is a URL
|
|
1368
|
-
if team_input.startswith("https://linear.app/"):
|
|
1369
|
-
console.print("[cyan]Detected team URL, deriving team ID...[/cyan]")
|
|
1370
|
-
import asyncio
|
|
1371
|
-
|
|
1372
|
-
from .linear_commands import derive_team_from_url
|
|
1373
|
-
|
|
1374
|
-
derived_team_id, error = asyncio.run(
|
|
1375
|
-
derive_team_from_url(linear_api_key, team_input)
|
|
1376
|
-
)
|
|
1377
|
-
|
|
1378
|
-
if derived_team_id:
|
|
1379
|
-
linear_team_id = derived_team_id
|
|
1380
|
-
console.print(
|
|
1381
|
-
"[green]✓[/green] Successfully derived team ID from URL"
|
|
1382
|
-
)
|
|
1383
|
-
else:
|
|
1384
|
-
console.print(f"[red]Error:[/red] {error}")
|
|
1385
|
-
console.print("Please provide team key or ID manually instead.")
|
|
1386
|
-
team_input = typer.prompt("Team key or ID")
|
|
1387
|
-
|
|
1388
|
-
# Store as either team_key or team_id based on format
|
|
1389
|
-
if len(team_input) > 20: # Likely a UUID
|
|
1390
|
-
linear_team_id = team_input
|
|
1391
|
-
else:
|
|
1392
|
-
linear_team_key = team_input
|
|
1393
|
-
else:
|
|
1394
|
-
# Input is team key or ID
|
|
1395
|
-
if len(team_input) > 20: # Likely a UUID
|
|
1396
|
-
linear_team_id = team_input
|
|
1397
|
-
else:
|
|
1398
|
-
linear_team_key = team_input
|
|
1399
|
-
|
|
1400
|
-
# Validate required fields (following JIRA pattern)
|
|
1401
|
-
if not linear_api_key:
|
|
1402
|
-
console.print("[red]Error:[/red] Linear API key is required")
|
|
1403
|
-
raise typer.Exit(1) from None
|
|
1404
|
-
|
|
1405
|
-
if not linear_team_id and not linear_team_key:
|
|
1406
|
-
console.print(
|
|
1407
|
-
"[red]Error:[/red] Linear requires either team ID or team key"
|
|
1408
|
-
)
|
|
1409
|
-
raise typer.Exit(1) from None
|
|
1410
|
-
|
|
1411
|
-
# Build configuration
|
|
1412
|
-
linear_config = {
|
|
1413
|
-
"api_key": linear_api_key,
|
|
1414
|
-
"type": "linear",
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
# Save whichever was provided
|
|
1418
|
-
if linear_team_key:
|
|
1419
|
-
linear_config["team_key"] = linear_team_key
|
|
1420
|
-
if linear_team_id:
|
|
1421
|
-
linear_config["team_id"] = linear_team_id
|
|
1422
|
-
|
|
1423
|
-
config["adapters"]["linear"] = linear_config
|
|
1424
|
-
|
|
1425
|
-
elif adapter_type == "jira":
|
|
1426
|
-
# If not auto-discovered, build from CLI params or prompt
|
|
1427
|
-
if adapter_type not in config["adapters"]:
|
|
1428
|
-
server = jira_server or os.getenv("JIRA_SERVER")
|
|
1429
|
-
email = jira_email or os.getenv("JIRA_EMAIL")
|
|
1430
|
-
token = api_key or os.getenv("JIRA_API_TOKEN")
|
|
1431
|
-
project = jira_project or os.getenv("JIRA_PROJECT_KEY")
|
|
1432
|
-
|
|
1433
|
-
# Interactive prompts for missing values
|
|
1434
|
-
if not server:
|
|
1435
|
-
console.print("\n[bold]JIRA Configuration[/bold]")
|
|
1436
|
-
console.print("Enter your JIRA server details.\n")
|
|
1437
|
-
|
|
1438
|
-
server = typer.prompt(
|
|
1439
|
-
"JIRA server URL (e.g., https://company.atlassian.net)"
|
|
1440
|
-
)
|
|
1441
|
-
|
|
1442
|
-
if not email:
|
|
1443
|
-
email = typer.prompt("Your JIRA email address")
|
|
1444
|
-
|
|
1445
|
-
if not token:
|
|
1446
|
-
console.print("\nYou need a JIRA API token.")
|
|
1447
|
-
console.print(
|
|
1448
|
-
"[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
|
|
1449
|
-
)
|
|
1450
|
-
|
|
1451
|
-
token = typer.prompt("Enter your JIRA API token", hide_input=True)
|
|
1452
|
-
|
|
1453
|
-
if not project:
|
|
1454
|
-
project = typer.prompt(
|
|
1455
|
-
"Default JIRA project key (optional, press Enter to skip)",
|
|
1456
|
-
default="",
|
|
1457
|
-
show_default=False,
|
|
1458
|
-
)
|
|
1459
|
-
|
|
1460
|
-
# Validate required fields
|
|
1461
|
-
if not server:
|
|
1462
|
-
console.print("[red]Error:[/red] JIRA server URL is required")
|
|
1463
|
-
raise typer.Exit(1) from None
|
|
1464
|
-
|
|
1465
|
-
if not email:
|
|
1466
|
-
console.print("[red]Error:[/red] JIRA email is required")
|
|
1467
|
-
raise typer.Exit(1) from None
|
|
1468
|
-
|
|
1469
|
-
if not token:
|
|
1470
|
-
console.print("[red]Error:[/red] JIRA API token is required")
|
|
1471
|
-
raise typer.Exit(1) from None
|
|
1472
|
-
|
|
1473
|
-
jira_config = {
|
|
1474
|
-
"server": server,
|
|
1475
|
-
"email": email,
|
|
1476
|
-
"api_token": token,
|
|
1477
|
-
"type": "jira",
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
if project:
|
|
1481
|
-
jira_config["project_key"] = project
|
|
1482
|
-
|
|
1483
|
-
config["adapters"]["jira"] = jira_config
|
|
1484
|
-
|
|
1485
|
-
elif adapter_type == "github":
|
|
1486
|
-
# If not auto-discovered, build from CLI params or prompt
|
|
1487
|
-
if adapter_type not in config["adapters"]:
|
|
1488
|
-
owner = github_owner or os.getenv("GITHUB_OWNER")
|
|
1489
|
-
repo = github_repo or os.getenv("GITHUB_REPO")
|
|
1490
|
-
token = github_token or os.getenv("GITHUB_TOKEN")
|
|
1491
|
-
|
|
1492
|
-
# Interactive prompts for missing values
|
|
1493
|
-
if not owner:
|
|
1494
|
-
console.print("\n[bold]GitHub Configuration[/bold]")
|
|
1495
|
-
console.print("Enter your GitHub repository details.\n")
|
|
1496
|
-
|
|
1497
|
-
owner = typer.prompt(
|
|
1498
|
-
"GitHub repository owner (username or organization)"
|
|
1499
|
-
)
|
|
1500
|
-
|
|
1501
|
-
if not repo:
|
|
1502
|
-
repo = typer.prompt("GitHub repository name")
|
|
1503
|
-
|
|
1504
|
-
if not token:
|
|
1505
|
-
console.print("\nYou need a GitHub Personal Access Token.")
|
|
1506
|
-
console.print(
|
|
1507
|
-
"[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
|
|
1508
|
-
)
|
|
1509
|
-
console.print(
|
|
1510
|
-
"[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n"
|
|
1511
|
-
)
|
|
1512
|
-
|
|
1513
|
-
token = typer.prompt(
|
|
1514
|
-
"Enter your GitHub Personal Access Token", hide_input=True
|
|
1515
|
-
)
|
|
1516
|
-
|
|
1517
|
-
# Validate required fields
|
|
1518
|
-
if not owner:
|
|
1519
|
-
console.print("[red]Error:[/red] GitHub repository owner is required")
|
|
1520
|
-
raise typer.Exit(1) from None
|
|
1521
|
-
|
|
1522
|
-
if not repo:
|
|
1523
|
-
console.print("[red]Error:[/red] GitHub repository name is required")
|
|
1524
|
-
raise typer.Exit(1) from None
|
|
1525
|
-
|
|
1526
|
-
if not token:
|
|
1527
|
-
console.print(
|
|
1528
|
-
"[red]Error:[/red] GitHub Personal Access Token is required"
|
|
1529
|
-
)
|
|
1530
|
-
raise typer.Exit(1) from None
|
|
1531
|
-
|
|
1532
|
-
config["adapters"]["github"] = {
|
|
1533
|
-
"owner": owner,
|
|
1534
|
-
"repo": repo,
|
|
1535
|
-
"token": token,
|
|
1536
|
-
"type": "github",
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
# 5. Save to project-local config (global config deprecated for security)
|
|
1540
|
-
# Always save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
|
|
1541
|
-
config_file_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
1542
|
-
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1543
|
-
|
|
1544
|
-
with open(config_file_path, "w") as f:
|
|
1545
|
-
json.dump(config, f, indent=2)
|
|
1546
|
-
|
|
1547
|
-
if global_config:
|
|
1548
|
-
console.print(
|
|
1549
|
-
"[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
|
|
1550
|
-
)
|
|
1551
|
-
|
|
1552
|
-
console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
|
|
1553
|
-
console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
|
|
1554
|
-
|
|
1555
|
-
# Add .mcp-ticketer to .gitignore if not already there
|
|
1556
|
-
gitignore_path = proj_path / ".gitignore"
|
|
1557
|
-
if gitignore_path.exists():
|
|
1558
|
-
gitignore_content = gitignore_path.read_text()
|
|
1559
|
-
if ".mcp-ticketer" not in gitignore_content:
|
|
1560
|
-
with open(gitignore_path, "a") as f:
|
|
1561
|
-
f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
|
|
1562
|
-
console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
|
|
1563
|
-
else:
|
|
1564
|
-
# Create .gitignore if it doesn't exist
|
|
1565
|
-
with open(gitignore_path, "w") as f:
|
|
1566
|
-
f.write("# MCP Ticketer\n.mcp-ticketer/\n")
|
|
1567
|
-
console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
|
|
1568
|
-
|
|
1569
|
-
# Validate configuration with loop for corrections
|
|
1570
|
-
import asyncio
|
|
1571
|
-
|
|
1572
|
-
if not asyncio.run(
|
|
1573
|
-
_validate_configuration_with_retry(
|
|
1574
|
-
console, adapter_type, config_file_path, proj_path
|
|
1575
|
-
)
|
|
1576
|
-
):
|
|
1577
|
-
# User chose to exit without valid configuration
|
|
1578
|
-
raise typer.Exit(1) from None
|
|
1579
|
-
|
|
1580
|
-
# Show next steps
|
|
1581
|
-
_show_next_steps(console, adapter_type, config_file_path)
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
def _show_next_steps(
|
|
1585
|
-
console: Console, adapter_type: str, config_file_path: Path
|
|
1586
|
-
) -> None:
|
|
1587
|
-
"""Show helpful next steps after initialization.
|
|
1588
|
-
|
|
1589
|
-
Args:
|
|
1590
|
-
console: Rich console for output
|
|
1591
|
-
adapter_type: Type of adapter that was configured
|
|
1592
|
-
config_file_path: Path to the configuration file
|
|
1593
|
-
|
|
1594
|
-
"""
|
|
1595
|
-
console.print("\n[bold green]🎉 Setup Complete![/bold green]")
|
|
1596
|
-
console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
|
|
1597
|
-
|
|
1598
|
-
console.print("[bold]Next Steps:[/bold]")
|
|
1599
|
-
console.print("1. [cyan]Create a test ticket:[/cyan]")
|
|
1600
|
-
console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
|
|
1601
|
-
|
|
1602
|
-
if adapter_type != "aitrackdown":
|
|
1603
|
-
console.print(
|
|
1604
|
-
f"\n2. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]"
|
|
1605
|
-
)
|
|
1606
|
-
if adapter_type == "linear":
|
|
1607
|
-
console.print(" Check your Linear workspace for the new ticket")
|
|
1608
|
-
elif adapter_type == "github":
|
|
1609
|
-
console.print(" Check your GitHub repository's Issues tab")
|
|
1610
|
-
elif adapter_type == "jira":
|
|
1611
|
-
console.print(" Check your JIRA project for the new ticket")
|
|
1612
|
-
else:
|
|
1613
|
-
console.print("\n2. [cyan]Check local ticket storage:[/cyan]")
|
|
1614
|
-
console.print(" ls .aitrackdown/")
|
|
1615
|
-
|
|
1616
|
-
console.print("\n3. [cyan]Install MCP for AI clients (optional):[/cyan]")
|
|
1617
|
-
console.print(" mcp-ticketer install claude-code # For Claude Code")
|
|
1618
|
-
console.print(" mcp-ticketer install claude-desktop # For Claude Desktop")
|
|
1619
|
-
console.print(" mcp-ticketer install auggie # For Auggie")
|
|
1620
|
-
console.print(" mcp-ticketer install gemini # For Gemini CLI")
|
|
1621
|
-
|
|
1622
|
-
console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
|
|
1623
|
-
console.print(
|
|
1624
|
-
"[dim]Run 'mcp-ticketer doctor' to re-validate configuration anytime[/dim]"
|
|
1625
|
-
)
|
|
1626
|
-
console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
@app.command("set")
|
|
1630
|
-
def set_config(
|
|
1631
|
-
adapter: AdapterType | None = typer.Option(
|
|
1632
|
-
None, "--adapter", "-a", help="Set default adapter"
|
|
1633
|
-
),
|
|
1634
|
-
team_key: str | None = typer.Option(
|
|
1635
|
-
None, "--team-key", help="Linear team key (e.g., BTA)"
|
|
1636
|
-
),
|
|
1637
|
-
team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
|
|
1638
|
-
owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
|
|
1639
|
-
repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
|
|
1640
|
-
server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
|
|
1641
|
-
project: str | None = typer.Option(None, "--project", help="JIRA project key"),
|
|
1642
|
-
base_path: str | None = typer.Option(
|
|
1643
|
-
None, "--base-path", help="AITrackdown base path"
|
|
1644
|
-
),
|
|
1645
|
-
) -> None:
|
|
1646
|
-
"""Set default adapter and adapter-specific configuration.
|
|
1647
|
-
|
|
1648
|
-
When called without arguments, shows current configuration.
|
|
1649
|
-
"""
|
|
1650
|
-
if not any([adapter, team_key, team_id, owner, repo, server, project, base_path]):
|
|
1651
|
-
# Show current configuration
|
|
1652
|
-
config = load_config()
|
|
1653
|
-
console.print("[bold]Current Configuration:[/bold]")
|
|
1654
|
-
console.print(
|
|
1655
|
-
f"Default adapter: [cyan]{config.get('default_adapter', 'aitrackdown')}[/cyan]"
|
|
1656
|
-
)
|
|
1657
|
-
|
|
1658
|
-
adapters_config = config.get("adapters", {})
|
|
1659
|
-
if adapters_config:
|
|
1660
|
-
console.print("\n[bold]Adapter Settings:[/bold]")
|
|
1661
|
-
for adapter_name, adapter_config in adapters_config.items():
|
|
1662
|
-
console.print(f"\n[cyan]{adapter_name}:[/cyan]")
|
|
1663
|
-
for key, value in adapter_config.items():
|
|
1664
|
-
# Don't display sensitive values like tokens
|
|
1665
|
-
if (
|
|
1666
|
-
"token" in key.lower()
|
|
1667
|
-
or "key" in key.lower()
|
|
1668
|
-
and "team" not in key.lower()
|
|
1669
|
-
):
|
|
1670
|
-
value = "***" if value else "not set"
|
|
1671
|
-
console.print(f" {key}: {value}")
|
|
1672
|
-
return
|
|
1673
|
-
|
|
1674
|
-
updates = {}
|
|
1675
|
-
|
|
1676
|
-
# Set default adapter
|
|
1677
|
-
if adapter:
|
|
1678
|
-
updates["default_adapter"] = adapter.value
|
|
1679
|
-
console.print(f"[green]✓[/green] Default adapter set to: {adapter.value}")
|
|
1680
|
-
|
|
1681
|
-
# Build adapter-specific configuration
|
|
1682
|
-
adapter_configs = {}
|
|
1683
|
-
|
|
1684
|
-
# Linear configuration
|
|
1685
|
-
if team_key or team_id:
|
|
1686
|
-
linear_config = {}
|
|
1687
|
-
if team_key:
|
|
1688
|
-
linear_config["team_key"] = team_key
|
|
1689
|
-
if team_id:
|
|
1690
|
-
linear_config["team_id"] = team_id
|
|
1691
|
-
adapter_configs["linear"] = linear_config
|
|
1692
|
-
console.print("[green]✓[/green] Linear settings updated")
|
|
1693
|
-
|
|
1694
|
-
# GitHub configuration
|
|
1695
|
-
if owner or repo:
|
|
1696
|
-
github_config = {}
|
|
1697
|
-
if owner:
|
|
1698
|
-
github_config["owner"] = owner
|
|
1699
|
-
if repo:
|
|
1700
|
-
github_config["repo"] = repo
|
|
1701
|
-
adapter_configs["github"] = github_config
|
|
1702
|
-
console.print("[green]✓[/green] GitHub settings updated")
|
|
1703
|
-
|
|
1704
|
-
# JIRA configuration
|
|
1705
|
-
if server or project:
|
|
1706
|
-
jira_config = {}
|
|
1707
|
-
if server:
|
|
1708
|
-
jira_config["server"] = server
|
|
1709
|
-
if project:
|
|
1710
|
-
jira_config["project_key"] = project
|
|
1711
|
-
adapter_configs["jira"] = jira_config
|
|
1712
|
-
console.print("[green]✓[/green] JIRA settings updated")
|
|
1713
|
-
|
|
1714
|
-
# AITrackdown configuration
|
|
1715
|
-
if base_path:
|
|
1716
|
-
adapter_configs["aitrackdown"] = {"base_path": base_path}
|
|
1717
|
-
console.print("[green]✓[/green] AITrackdown settings updated")
|
|
1718
|
-
|
|
1719
|
-
if adapter_configs:
|
|
1720
|
-
updates["adapters"] = adapter_configs
|
|
1721
|
-
|
|
1722
|
-
# Merge and save configuration
|
|
1723
|
-
if updates:
|
|
1724
|
-
config = merge_config(updates)
|
|
1725
|
-
save_config(config)
|
|
1726
|
-
console.print(f"[dim]Configuration saved to {CONFIG_FILE}[/dim]")
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
@app.command("configure")
|
|
1730
|
-
def configure_command(
|
|
1731
|
-
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
1732
|
-
adapter: str | None = typer.Option(
|
|
1733
|
-
None, "--adapter", help="Set default adapter type"
|
|
1734
|
-
),
|
|
1735
|
-
api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
|
|
1736
|
-
project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
|
|
1737
|
-
team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
|
|
1738
|
-
global_scope: bool = typer.Option(
|
|
1739
|
-
False,
|
|
1740
|
-
"--global",
|
|
1741
|
-
"-g",
|
|
1742
|
-
help="Save to global config instead of project-specific",
|
|
1743
|
-
),
|
|
1744
|
-
) -> None:
|
|
1745
|
-
"""Configure MCP Ticketer integration.
|
|
1746
|
-
|
|
1747
|
-
Run without arguments to launch interactive wizard.
|
|
1748
|
-
Use --show to display current configuration.
|
|
1749
|
-
Use options to set specific values directly.
|
|
1750
|
-
"""
|
|
1751
|
-
# Show configuration
|
|
1752
|
-
if show:
|
|
1753
|
-
show_current_config()
|
|
1754
|
-
return
|
|
1755
|
-
|
|
1756
|
-
# Direct configuration
|
|
1757
|
-
if any([adapter, api_key, project_id, team_id]):
|
|
1758
|
-
set_adapter_config(
|
|
1759
|
-
adapter=adapter,
|
|
1760
|
-
api_key=api_key,
|
|
1761
|
-
project_id=project_id,
|
|
1762
|
-
team_id=team_id,
|
|
1763
|
-
global_scope=global_scope,
|
|
1764
|
-
)
|
|
1765
|
-
return
|
|
1766
|
-
|
|
1767
|
-
# Run interactive wizard
|
|
1768
|
-
configure_wizard()
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
@app.command("migrate-config")
|
|
1772
|
-
def migrate_config(
|
|
1773
|
-
dry_run: bool = typer.Option(
|
|
1774
|
-
False, "--dry-run", help="Show what would be done without making changes"
|
|
1775
|
-
),
|
|
1776
|
-
) -> None:
|
|
1777
|
-
"""Migrate configuration from old format to new format.
|
|
1778
|
-
|
|
1779
|
-
This command will:
|
|
1780
|
-
1. Detect old configuration format
|
|
1781
|
-
2. Convert to new schema
|
|
1782
|
-
3. Backup old config
|
|
1783
|
-
4. Apply new config
|
|
1784
|
-
"""
|
|
1785
|
-
migrate_config_command(dry_run=dry_run)
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
@app.command("queue-status", deprecated=True, hidden=True)
|
|
1789
|
-
def old_queue_status_command() -> None:
|
|
1790
|
-
"""Show queue and worker status.
|
|
1791
|
-
|
|
1792
|
-
DEPRECATED: Use 'mcp-ticketer queue status' instead.
|
|
1793
|
-
"""
|
|
1794
|
-
console.print(
|
|
1795
|
-
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer queue status' instead.[/yellow]\n"
|
|
1796
|
-
)
|
|
1797
|
-
|
|
1798
|
-
queue = Queue()
|
|
1799
|
-
manager = WorkerManager()
|
|
1800
|
-
|
|
1801
|
-
# Get queue stats
|
|
1802
|
-
stats = queue.get_stats()
|
|
1803
|
-
pending = stats.get(QueueStatus.PENDING.value, 0)
|
|
1804
|
-
|
|
1805
|
-
# Show queue status
|
|
1806
|
-
console.print("[bold]Queue Status:[/bold]")
|
|
1807
|
-
console.print(f" Pending: {pending}")
|
|
1808
|
-
console.print(f" Processing: {stats.get(QueueStatus.PROCESSING.value, 0)}")
|
|
1809
|
-
console.print(f" Completed: {stats.get(QueueStatus.COMPLETED.value, 0)}")
|
|
1810
|
-
console.print(f" Failed: {stats.get(QueueStatus.FAILED.value, 0)}")
|
|
1811
|
-
|
|
1812
|
-
# Show worker status
|
|
1813
|
-
worker_status = manager.get_status()
|
|
1814
|
-
if worker_status["running"]:
|
|
1815
|
-
console.print(
|
|
1816
|
-
f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})"
|
|
1817
|
-
)
|
|
1818
|
-
else:
|
|
1819
|
-
console.print("\n[red]○ Worker is not running[/red]")
|
|
1820
|
-
if pending > 0:
|
|
1821
|
-
console.print(
|
|
1822
|
-
"[yellow]Note: There are pending items. Start worker with 'mcp-ticketer queue worker start'[/yellow]"
|
|
1823
|
-
)
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
@app.command("queue-health", deprecated=True, hidden=True)
|
|
1827
|
-
def old_queue_health_command(
|
|
1828
|
-
auto_repair: bool = typer.Option(
|
|
1829
|
-
False, "--auto-repair", help="Attempt automatic repair of issues"
|
|
1830
|
-
),
|
|
1831
|
-
verbose: bool = typer.Option(
|
|
1832
|
-
False, "--verbose", "-v", help="Show detailed health information"
|
|
1833
|
-
),
|
|
1834
|
-
) -> None:
|
|
1835
|
-
"""Check queue system health and detect issues immediately.
|
|
1836
|
-
|
|
1837
|
-
DEPRECATED: Use 'mcp-ticketer queue health' instead.
|
|
1838
|
-
"""
|
|
1839
|
-
console.print(
|
|
1840
|
-
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer queue health' instead.[/yellow]\n"
|
|
1841
|
-
)
|
|
1842
|
-
health_monitor = QueueHealthMonitor()
|
|
1843
|
-
health = health_monitor.check_health()
|
|
1844
|
-
|
|
1845
|
-
# Display overall status
|
|
1846
|
-
status_color = {
|
|
1847
|
-
HealthStatus.HEALTHY: "green",
|
|
1848
|
-
HealthStatus.WARNING: "yellow",
|
|
1849
|
-
HealthStatus.CRITICAL: "red",
|
|
1850
|
-
HealthStatus.FAILED: "red",
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
status_icon = {
|
|
1854
|
-
HealthStatus.HEALTHY: "✓",
|
|
1855
|
-
HealthStatus.WARNING: "⚠️",
|
|
1856
|
-
HealthStatus.CRITICAL: "🚨",
|
|
1857
|
-
HealthStatus.FAILED: "❌",
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
color = status_color.get(health["status"], "white")
|
|
1861
|
-
icon = status_icon.get(health["status"], "?")
|
|
1862
|
-
|
|
1863
|
-
console.print(f"[{color}]{icon} Queue Health: {health['status'].upper()}[/{color}]")
|
|
1864
|
-
console.print(f"Last checked: {health['timestamp']}")
|
|
1865
|
-
|
|
1866
|
-
# Display alerts
|
|
1867
|
-
if health["alerts"]:
|
|
1868
|
-
console.print("\n[bold]Issues Found:[/bold]")
|
|
1869
|
-
for alert in health["alerts"]:
|
|
1870
|
-
alert_color = status_color.get(alert["level"], "white")
|
|
1871
|
-
console.print(f"[{alert_color}] • {alert['message']}[/{alert_color}]")
|
|
1872
|
-
|
|
1873
|
-
if verbose and alert.get("details"):
|
|
1874
|
-
for key, value in alert["details"].items():
|
|
1875
|
-
console.print(f" {key}: {value}")
|
|
1876
|
-
else:
|
|
1877
|
-
console.print("\n[green]✓ No issues detected[/green]")
|
|
1878
|
-
|
|
1879
|
-
# Auto-repair if requested
|
|
1880
|
-
if auto_repair and health["status"] in [
|
|
1881
|
-
HealthStatus.CRITICAL,
|
|
1882
|
-
HealthStatus.WARNING,
|
|
1883
|
-
]:
|
|
1884
|
-
console.print("\n[yellow]Attempting automatic repair...[/yellow]")
|
|
1885
|
-
repair_result = health_monitor.auto_repair()
|
|
1886
|
-
|
|
1887
|
-
if repair_result["actions_taken"]:
|
|
1888
|
-
console.print("[green]Repair actions taken:[/green]")
|
|
1889
|
-
for action in repair_result["actions_taken"]:
|
|
1890
|
-
console.print(f"[green] ✓ {action}[/green]")
|
|
1891
|
-
|
|
1892
|
-
# Re-check health
|
|
1893
|
-
console.print("\n[yellow]Re-checking health after repair...[/yellow]")
|
|
1894
|
-
new_health = health_monitor.check_health()
|
|
1895
|
-
new_color = status_color.get(new_health["status"], "white")
|
|
1896
|
-
new_icon = status_icon.get(new_health["status"], "?")
|
|
1897
|
-
console.print(
|
|
1898
|
-
f"[{new_color}]{new_icon} Updated Health: {new_health['status'].upper()}[/{new_color}]"
|
|
1899
|
-
)
|
|
1900
|
-
else:
|
|
1901
|
-
console.print("[yellow]No repair actions available[/yellow]")
|
|
1902
|
-
|
|
1903
|
-
# Exit with appropriate code
|
|
1904
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
1905
|
-
raise typer.Exit(1) from None
|
|
1906
|
-
elif health["status"] == HealthStatus.WARNING:
|
|
1907
|
-
raise typer.Exit(2) from None
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
@app.command(deprecated=True, hidden=True)
|
|
1911
|
-
def create(
|
|
1912
|
-
title: str = typer.Argument(..., help="Ticket title"),
|
|
1913
|
-
description: str | None = typer.Option(
|
|
1914
|
-
None, "--description", "-d", help="Ticket description"
|
|
1915
|
-
),
|
|
1916
|
-
priority: Priority = typer.Option(
|
|
1917
|
-
Priority.MEDIUM, "--priority", "-p", help="Priority level"
|
|
1918
|
-
),
|
|
1919
|
-
tags: list[str] | None = typer.Option(
|
|
1920
|
-
None, "--tag", "-t", help="Tags (can be specified multiple times)"
|
|
1921
|
-
),
|
|
1922
|
-
assignee: str | None = typer.Option(
|
|
1923
|
-
None, "--assignee", "-a", help="Assignee username"
|
|
1924
|
-
),
|
|
1925
|
-
project: str | None = typer.Option(
|
|
1926
|
-
None,
|
|
1927
|
-
"--project",
|
|
1928
|
-
help="Parent project/epic ID (synonym for --epic)",
|
|
1929
|
-
),
|
|
1930
|
-
epic: str | None = typer.Option(
|
|
1931
|
-
None,
|
|
1932
|
-
"--epic",
|
|
1933
|
-
help="Parent epic/project ID (synonym for --project)",
|
|
1934
|
-
),
|
|
1935
|
-
adapter: AdapterType | None = typer.Option(
|
|
1936
|
-
None, "--adapter", help="Override default adapter"
|
|
1937
|
-
),
|
|
1938
|
-
) -> None:
|
|
1939
|
-
"""Create a new ticket with comprehensive health checks.
|
|
1940
|
-
|
|
1941
|
-
DEPRECATED: Use 'mcp-ticketer ticket create' instead.
|
|
1942
|
-
"""
|
|
1943
|
-
console.print(
|
|
1944
|
-
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket create' instead.[/yellow]\n"
|
|
1945
|
-
)
|
|
1946
|
-
|
|
1947
|
-
# IMMEDIATE HEALTH CHECK - Critical for reliability
|
|
1948
|
-
health_monitor = QueueHealthMonitor()
|
|
1949
|
-
health = health_monitor.check_health()
|
|
1950
|
-
|
|
1951
|
-
# Display health status
|
|
1952
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
1953
|
-
console.print("[red]🚨 CRITICAL: Queue system has serious issues![/red]")
|
|
1954
|
-
for alert in health["alerts"]:
|
|
1955
|
-
if alert["level"] == "critical":
|
|
1956
|
-
console.print(f"[red] • {alert['message']}[/red]")
|
|
1957
|
-
|
|
1958
|
-
# Attempt auto-repair
|
|
1959
|
-
console.print("[yellow]Attempting automatic repair...[/yellow]")
|
|
1960
|
-
repair_result = health_monitor.auto_repair()
|
|
1961
|
-
|
|
1962
|
-
if repair_result["actions_taken"]:
|
|
1963
|
-
for action in repair_result["actions_taken"]:
|
|
1964
|
-
console.print(f"[yellow] ✓ {action}[/yellow]")
|
|
1965
|
-
|
|
1966
|
-
# Re-check health after repair
|
|
1967
|
-
health = health_monitor.check_health()
|
|
1968
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
1969
|
-
console.print(
|
|
1970
|
-
"[red]❌ Auto-repair failed. Manual intervention required.[/red]"
|
|
1971
|
-
)
|
|
1972
|
-
console.print(
|
|
1973
|
-
"[red]Cannot safely create ticket. Please check system status.[/red]"
|
|
1974
|
-
)
|
|
1975
|
-
raise typer.Exit(1) from None
|
|
1976
|
-
else:
|
|
1977
|
-
console.print(
|
|
1978
|
-
"[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]"
|
|
1979
|
-
)
|
|
1980
|
-
else:
|
|
1981
|
-
console.print(
|
|
1982
|
-
"[red]❌ No repair actions available. Manual intervention required.[/red]"
|
|
1983
|
-
)
|
|
1984
|
-
raise typer.Exit(1) from None
|
|
1985
|
-
|
|
1986
|
-
elif health["status"] == HealthStatus.WARNING:
|
|
1987
|
-
console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
|
|
1988
|
-
for alert in health["alerts"]:
|
|
1989
|
-
if alert["level"] == "warning":
|
|
1990
|
-
console.print(f"[yellow] • {alert['message']}[/yellow]")
|
|
1991
|
-
console.print("[yellow]Proceeding with ticket creation...[/yellow]")
|
|
1992
|
-
|
|
1993
|
-
# Get the adapter name with priority: 1) argument, 2) config, 3) .env files, 4) default
|
|
1994
|
-
if adapter:
|
|
1995
|
-
# Priority 1: Command-line argument - save to config for future use
|
|
1996
|
-
adapter_name = adapter.value
|
|
1997
|
-
_save_adapter_to_config(adapter_name)
|
|
1998
|
-
else:
|
|
1999
|
-
# Priority 2: Check existing config
|
|
2000
|
-
config = load_config()
|
|
2001
|
-
adapter_name = config.get("default_adapter")
|
|
2002
|
-
|
|
2003
|
-
if not adapter_name or adapter_name == "aitrackdown":
|
|
2004
|
-
# Priority 3: Check .env files and save if found
|
|
2005
|
-
env_adapter = _discover_from_env_files()
|
|
2006
|
-
if env_adapter:
|
|
2007
|
-
adapter_name = env_adapter
|
|
2008
|
-
_save_adapter_to_config(adapter_name)
|
|
2009
|
-
else:
|
|
2010
|
-
# Priority 4: Default
|
|
2011
|
-
adapter_name = "aitrackdown"
|
|
2012
|
-
|
|
2013
|
-
# Resolve project/epic synonym - prefer whichever is provided
|
|
2014
|
-
parent_epic_id = project or epic
|
|
2015
|
-
|
|
2016
|
-
# Create task data
|
|
2017
|
-
# Import Priority for type checking
|
|
2018
|
-
from ..core.models import Priority as PriorityEnum
|
|
2019
|
-
|
|
2020
|
-
task_data = {
|
|
2021
|
-
"title": title,
|
|
2022
|
-
"description": description,
|
|
2023
|
-
"priority": priority.value if isinstance(priority, PriorityEnum) else priority,
|
|
2024
|
-
"tags": tags or [],
|
|
2025
|
-
"assignee": assignee,
|
|
2026
|
-
"parent_epic": parent_epic_id,
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
# WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
|
|
2030
|
-
if adapter_name == "linear":
|
|
2031
|
-
console.print(
|
|
2032
|
-
"[yellow]⚠️[/yellow] Using direct operation for Linear adapter (bypassing queue)"
|
|
2033
|
-
)
|
|
2034
|
-
try:
|
|
2035
|
-
# Load configuration and create adapter directly
|
|
2036
|
-
config = load_config()
|
|
2037
|
-
adapter_config = config.get("adapters", {}).get(adapter_name, {})
|
|
2038
|
-
|
|
2039
|
-
# Import and create adapter
|
|
2040
|
-
from ..core.registry import AdapterRegistry
|
|
2041
|
-
|
|
2042
|
-
adapter = AdapterRegistry.get_adapter(adapter_name, adapter_config)
|
|
2043
|
-
|
|
2044
|
-
# Create task directly
|
|
2045
|
-
from ..core.models import Priority, Task
|
|
2046
|
-
|
|
2047
|
-
task = Task(
|
|
2048
|
-
title=task_data["title"],
|
|
2049
|
-
description=task_data.get("description"),
|
|
2050
|
-
priority=(
|
|
2051
|
-
Priority(task_data["priority"])
|
|
2052
|
-
if task_data.get("priority")
|
|
2053
|
-
else Priority.MEDIUM
|
|
2054
|
-
),
|
|
2055
|
-
tags=task_data.get("tags", []),
|
|
2056
|
-
assignee=task_data.get("assignee"),
|
|
2057
|
-
parent_epic=task_data.get("parent_epic"),
|
|
2058
|
-
)
|
|
2059
|
-
|
|
2060
|
-
# Create ticket synchronously
|
|
2061
|
-
import asyncio
|
|
2062
|
-
|
|
2063
|
-
result = asyncio.run(adapter.create(task))
|
|
2064
|
-
|
|
2065
|
-
console.print(f"[green]✓[/green] Ticket created successfully: {result.id}")
|
|
2066
|
-
console.print(f" Title: {result.title}")
|
|
2067
|
-
console.print(f" Priority: {result.priority}")
|
|
2068
|
-
console.print(f" State: {result.state}")
|
|
2069
|
-
# Get URL from metadata if available
|
|
2070
|
-
if (
|
|
2071
|
-
result.metadata
|
|
2072
|
-
and "linear" in result.metadata
|
|
2073
|
-
and "url" in result.metadata["linear"]
|
|
2074
|
-
):
|
|
2075
|
-
console.print(f" URL: {result.metadata['linear']['url']}")
|
|
2076
|
-
|
|
2077
|
-
return result.id
|
|
2078
|
-
|
|
2079
|
-
except Exception as e:
|
|
2080
|
-
console.print(f"[red]❌[/red] Failed to create ticket: {e}")
|
|
2081
|
-
raise
|
|
2082
|
-
|
|
2083
|
-
# Use queue for other adapters
|
|
2084
|
-
queue = Queue()
|
|
2085
|
-
queue_id = queue.add(
|
|
2086
|
-
ticket_data=task_data,
|
|
2087
|
-
adapter=adapter_name,
|
|
2088
|
-
operation="create",
|
|
2089
|
-
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
2090
|
-
)
|
|
2091
|
-
|
|
2092
|
-
# Register in ticket registry for tracking
|
|
2093
|
-
registry = TicketRegistry()
|
|
2094
|
-
registry.register_ticket_operation(
|
|
2095
|
-
queue_id, adapter_name, "create", title, task_data
|
|
2096
|
-
)
|
|
2097
|
-
|
|
2098
|
-
console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
|
|
2099
|
-
console.print(f" Title: {title}")
|
|
2100
|
-
console.print(f" Priority: {priority}")
|
|
2101
|
-
console.print(f" Adapter: {adapter_name}")
|
|
2102
|
-
console.print("[dim]Use 'mcp-ticketer check {queue_id}' to check progress[/dim]")
|
|
2103
|
-
|
|
2104
|
-
# Start worker if needed with immediate feedback
|
|
2105
|
-
manager = WorkerManager()
|
|
2106
|
-
worker_started = manager.start_if_needed()
|
|
2107
|
-
|
|
2108
|
-
if worker_started:
|
|
2109
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
2110
|
-
|
|
2111
|
-
# Give immediate feedback on processing
|
|
2112
|
-
import time
|
|
2113
|
-
|
|
2114
|
-
time.sleep(1) # Brief pause to let worker start
|
|
2115
|
-
|
|
2116
|
-
# Check if item is being processed
|
|
2117
|
-
item = queue.get_item(queue_id)
|
|
2118
|
-
if item and item.status == QueueStatus.PROCESSING:
|
|
2119
|
-
console.print("[green]✓ Item is being processed by worker[/green]")
|
|
2120
|
-
elif item and item.status == QueueStatus.PENDING:
|
|
2121
|
-
console.print("[yellow]⏳ Item is queued for processing[/yellow]")
|
|
2122
|
-
else:
|
|
2123
|
-
console.print(
|
|
2124
|
-
"[red]⚠️ Item status unclear - check with 'mcp-ticketer check {queue_id}'[/red]"
|
|
2125
|
-
)
|
|
2126
|
-
else:
|
|
2127
|
-
# Worker didn't start - this is a problem
|
|
2128
|
-
pending_count = queue.get_pending_count()
|
|
2129
|
-
if pending_count > 1: # More than just this item
|
|
2130
|
-
console.print(
|
|
2131
|
-
f"[red]❌ Worker failed to start with {pending_count} pending items![/red]"
|
|
2132
|
-
)
|
|
2133
|
-
console.print(
|
|
2134
|
-
"[red]This is a critical issue. Try 'mcp-ticketer queue worker start' manually.[/red]"
|
|
2135
|
-
)
|
|
2136
|
-
else:
|
|
2137
|
-
console.print(
|
|
2138
|
-
"[yellow]Worker not started (no other pending items)[/yellow]"
|
|
2139
|
-
)
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
@app.command("list", deprecated=True, hidden=True)
|
|
2143
|
-
def list_tickets(
|
|
2144
|
-
state: TicketState | None = typer.Option(
|
|
2145
|
-
None, "--state", "-s", help="Filter by state"
|
|
2146
|
-
),
|
|
2147
|
-
priority: Priority | None = typer.Option(
|
|
2148
|
-
None, "--priority", "-p", help="Filter by priority"
|
|
2149
|
-
),
|
|
2150
|
-
limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
|
|
2151
|
-
adapter: AdapterType | None = typer.Option(
|
|
2152
|
-
None, "--adapter", help="Override default adapter"
|
|
2153
|
-
),
|
|
2154
|
-
) -> None:
|
|
2155
|
-
"""List tickets with optional filters.
|
|
2156
|
-
|
|
2157
|
-
DEPRECATED: Use 'mcp-ticketer ticket list' instead.
|
|
2158
|
-
"""
|
|
2159
|
-
console.print(
|
|
2160
|
-
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket list' instead.[/yellow]\n"
|
|
2161
|
-
)
|
|
2162
|
-
|
|
2163
|
-
async def _list() -> None:
|
|
2164
|
-
adapter_instance = get_adapter(
|
|
2165
|
-
override_adapter=adapter.value if adapter else None
|
|
2166
|
-
)
|
|
2167
|
-
filters = {}
|
|
2168
|
-
if state:
|
|
2169
|
-
filters["state"] = state
|
|
2170
|
-
if priority:
|
|
2171
|
-
filters["priority"] = priority
|
|
2172
|
-
return await adapter_instance.list(limit=limit, filters=filters)
|
|
2173
|
-
|
|
2174
|
-
tickets = asyncio.run(_list())
|
|
2175
|
-
|
|
2176
|
-
if not tickets:
|
|
2177
|
-
console.print("[yellow]No tickets found[/yellow]")
|
|
2178
|
-
return
|
|
2179
|
-
|
|
2180
|
-
# Create table
|
|
2181
|
-
table = Table(title="Tickets")
|
|
2182
|
-
table.add_column("ID", style="cyan", no_wrap=True)
|
|
2183
|
-
table.add_column("Title", style="white")
|
|
2184
|
-
table.add_column("State", style="green")
|
|
2185
|
-
table.add_column("Priority", style="yellow")
|
|
2186
|
-
table.add_column("Assignee", style="blue")
|
|
2187
|
-
|
|
2188
|
-
for ticket in tickets:
|
|
2189
|
-
# Handle assignee field - Epic doesn't have assignee, Task does
|
|
2190
|
-
assignee = getattr(ticket, "assignee", None) or "-"
|
|
2191
|
-
|
|
2192
|
-
table.add_row(
|
|
2193
|
-
ticket.id or "N/A",
|
|
2194
|
-
ticket.title,
|
|
2195
|
-
ticket.state,
|
|
2196
|
-
ticket.priority,
|
|
2197
|
-
assignee,
|
|
2198
|
-
)
|
|
2199
|
-
|
|
2200
|
-
console.print(table)
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
@app.command(deprecated=True, hidden=True)
|
|
2204
|
-
def show(
|
|
2205
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
2206
|
-
comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
|
|
2207
|
-
adapter: AdapterType | None = typer.Option(
|
|
2208
|
-
None, "--adapter", help="Override default adapter"
|
|
2209
|
-
),
|
|
2210
|
-
) -> None:
|
|
2211
|
-
"""Show detailed ticket information.
|
|
2212
|
-
|
|
2213
|
-
DEPRECATED: Use 'mcp-ticketer ticket show' instead.
|
|
2214
|
-
"""
|
|
2215
|
-
console.print(
|
|
2216
|
-
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket show' instead.[/yellow]\n"
|
|
2217
|
-
)
|
|
2218
|
-
|
|
2219
|
-
async def _show() -> None:
|
|
2220
|
-
adapter_instance = get_adapter(
|
|
2221
|
-
override_adapter=adapter.value if adapter else None
|
|
2222
|
-
)
|
|
2223
|
-
ticket = await adapter_instance.read(ticket_id)
|
|
2224
|
-
ticket_comments = None
|
|
2225
|
-
if comments and ticket:
|
|
2226
|
-
ticket_comments = await adapter_instance.get_comments(ticket_id)
|
|
2227
|
-
return ticket, ticket_comments
|
|
2228
|
-
|
|
2229
|
-
ticket, ticket_comments = asyncio.run(_show())
|
|
2230
|
-
|
|
2231
|
-
if not ticket:
|
|
2232
|
-
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
2233
|
-
raise typer.Exit(1) from None
|
|
2234
|
-
|
|
2235
|
-
# Display ticket details
|
|
2236
|
-
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
2237
|
-
console.print(f"Title: {ticket.title}")
|
|
2238
|
-
console.print(f"State: [green]{ticket.state}[/green]")
|
|
2239
|
-
console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
|
|
2240
|
-
|
|
2241
|
-
if ticket.description:
|
|
2242
|
-
console.print("\n[dim]Description:[/dim]")
|
|
2243
|
-
console.print(ticket.description)
|
|
2244
|
-
|
|
2245
|
-
if ticket.tags:
|
|
2246
|
-
console.print(f"\nTags: {', '.join(ticket.tags)}")
|
|
2247
|
-
|
|
2248
|
-
if ticket.assignee:
|
|
2249
|
-
console.print(f"Assignee: {ticket.assignee}")
|
|
2250
|
-
|
|
2251
|
-
# Display comments if requested
|
|
2252
|
-
if ticket_comments:
|
|
2253
|
-
console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
|
|
2254
|
-
for comment in ticket_comments:
|
|
2255
|
-
console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
|
|
2256
|
-
console.print(comment.content)
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
@app.command(deprecated=True, hidden=True)
|
|
2260
|
-
def comment(
|
|
2261
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
2262
|
-
content: str = typer.Argument(..., help="Comment content"),
|
|
2263
|
-
adapter: AdapterType | None = typer.Option(
|
|
2264
|
-
None, "--adapter", help="Override default adapter"
|
|
2265
|
-
),
|
|
2266
|
-
) -> None:
|
|
2267
|
-
"""Add a comment to a ticket.
|
|
2268
|
-
|
|
2269
|
-
DEPRECATED: Use 'mcp-ticketer ticket comment' instead.
|
|
2270
|
-
"""
|
|
2271
|
-
console.print(
|
|
2272
|
-
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket comment' instead.[/yellow]\n"
|
|
2273
|
-
)
|
|
2274
|
-
|
|
2275
|
-
async def _comment() -> None:
|
|
2276
|
-
adapter_instance = get_adapter(
|
|
2277
|
-
override_adapter=adapter.value if adapter else None
|
|
2278
|
-
)
|
|
2279
|
-
|
|
2280
|
-
# Create comment
|
|
2281
|
-
comment = Comment(
|
|
2282
|
-
ticket_id=ticket_id,
|
|
2283
|
-
content=content,
|
|
2284
|
-
author="cli-user", # Could be made configurable
|
|
2285
|
-
)
|
|
2286
|
-
|
|
2287
|
-
result = await adapter_instance.add_comment(comment)
|
|
2288
|
-
return result
|
|
2289
|
-
|
|
2290
|
-
try:
|
|
2291
|
-
result = asyncio.run(_comment())
|
|
2292
|
-
console.print("[green]✓[/green] Comment added successfully")
|
|
2293
|
-
if result.id:
|
|
2294
|
-
console.print(f"Comment ID: {result.id}")
|
|
2295
|
-
console.print(f"Content: {content}")
|
|
2296
|
-
except Exception as e:
|
|
2297
|
-
console.print(f"[red]✗[/red] Failed to add comment: {e}")
|
|
2298
|
-
raise typer.Exit(1) from e
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
@app.command(deprecated=True, hidden=True)
|
|
2302
|
-
def update(
|
|
2303
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
2304
|
-
title: str | None = typer.Option(None, "--title", help="New title"),
|
|
2305
|
-
description: str | None = typer.Option(
|
|
2306
|
-
None, "--description", "-d", help="New description"
|
|
2307
|
-
),
|
|
2308
|
-
priority: Priority | None = typer.Option(
|
|
2309
|
-
None, "--priority", "-p", help="New priority"
|
|
2310
|
-
),
|
|
2311
|
-
assignee: str | None = typer.Option(None, "--assignee", "-a", help="New assignee"),
|
|
2312
|
-
adapter: AdapterType | None = typer.Option(
|
|
2313
|
-
None, "--adapter", help="Override default adapter"
|
|
2314
|
-
),
|
|
2315
|
-
) -> None:
|
|
2316
|
-
"""Update ticket fields.
|
|
2317
|
-
|
|
2318
|
-
DEPRECATED: Use 'mcp-ticketer ticket update' instead.
|
|
2319
|
-
"""
|
|
2320
|
-
console.print(
|
|
2321
|
-
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket update' instead.[/yellow]\n"
|
|
2322
|
-
)
|
|
2323
|
-
updates = {}
|
|
2324
|
-
if title:
|
|
2325
|
-
updates["title"] = title
|
|
2326
|
-
if description:
|
|
2327
|
-
updates["description"] = description
|
|
2328
|
-
if priority:
|
|
2329
|
-
updates["priority"] = (
|
|
2330
|
-
priority.value if isinstance(priority, Priority) else priority
|
|
2331
|
-
)
|
|
2332
|
-
if assignee:
|
|
2333
|
-
updates["assignee"] = assignee
|
|
2334
|
-
|
|
2335
|
-
if not updates:
|
|
2336
|
-
console.print("[yellow]No updates specified[/yellow]")
|
|
2337
|
-
raise typer.Exit(1) from None
|
|
2338
|
-
|
|
2339
|
-
# Get the adapter name
|
|
2340
|
-
config = load_config()
|
|
2341
|
-
adapter_name = (
|
|
2342
|
-
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
2343
|
-
)
|
|
2344
|
-
|
|
2345
|
-
# Add ticket_id to updates
|
|
2346
|
-
updates["ticket_id"] = ticket_id
|
|
2347
|
-
|
|
2348
|
-
# Add to queue with explicit project directory
|
|
2349
|
-
queue = Queue()
|
|
2350
|
-
queue_id = queue.add(
|
|
2351
|
-
ticket_data=updates,
|
|
2352
|
-
adapter=adapter_name,
|
|
2353
|
-
operation="update",
|
|
2354
|
-
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
2355
|
-
)
|
|
2356
|
-
|
|
2357
|
-
console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
|
|
2358
|
-
for key, value in updates.items():
|
|
2359
|
-
if key != "ticket_id":
|
|
2360
|
-
console.print(f" {key}: {value}")
|
|
2361
|
-
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
2362
|
-
|
|
2363
|
-
# Start worker if needed
|
|
2364
|
-
manager = WorkerManager()
|
|
2365
|
-
if manager.start_if_needed():
|
|
2366
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
@app.command(deprecated=True, hidden=True)
|
|
2370
|
-
def transition(
|
|
2371
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
2372
|
-
state_positional: TicketState | None = typer.Argument(
|
|
2373
|
-
None, help="Target state (positional - deprecated, use --state instead)"
|
|
2374
|
-
),
|
|
2375
|
-
state: TicketState | None = typer.Option(
|
|
2376
|
-
None, "--state", "-s", help="Target state (recommended)"
|
|
2377
|
-
),
|
|
2378
|
-
adapter: AdapterType | None = typer.Option(
|
|
2379
|
-
None, "--adapter", help="Override default adapter"
|
|
2380
|
-
),
|
|
2381
|
-
) -> None:
|
|
2382
|
-
"""Change ticket state with validation.
|
|
2383
|
-
|
|
2384
|
-
DEPRECATED: Use 'mcp-ticketer ticket transition' instead.
|
|
2385
|
-
|
|
2386
|
-
Examples:
|
|
2387
|
-
# Recommended syntax with flag:
|
|
2388
|
-
mcp-ticketer ticket transition BTA-215 --state done
|
|
2389
|
-
mcp-ticketer ticket transition BTA-215 -s in_progress
|
|
2390
|
-
|
|
2391
|
-
# Legacy positional syntax (still supported):
|
|
2392
|
-
mcp-ticketer ticket transition BTA-215 done
|
|
2393
|
-
|
|
2394
|
-
"""
|
|
2395
|
-
console.print(
|
|
2396
|
-
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket transition' instead.[/yellow]\n"
|
|
2397
|
-
)
|
|
2398
|
-
|
|
2399
|
-
# Determine which state to use (prefer flag over positional)
|
|
2400
|
-
target_state = state if state is not None else state_positional
|
|
2401
|
-
|
|
2402
|
-
if target_state is None:
|
|
2403
|
-
console.print("[red]Error: State is required[/red]")
|
|
2404
|
-
console.print(
|
|
2405
|
-
"Use either:\n"
|
|
2406
|
-
" - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
|
|
2407
|
-
" - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
|
|
2408
|
-
)
|
|
2409
|
-
raise typer.Exit(1) from None
|
|
2410
|
-
|
|
2411
|
-
# Get the adapter name
|
|
2412
|
-
config = load_config()
|
|
2413
|
-
adapter_name = (
|
|
2414
|
-
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
2415
|
-
)
|
|
2416
|
-
|
|
2417
|
-
# Add to queue with explicit project directory
|
|
2418
|
-
queue = Queue()
|
|
2419
|
-
queue_id = queue.add(
|
|
2420
|
-
ticket_data={
|
|
2421
|
-
"ticket_id": ticket_id,
|
|
2422
|
-
"state": (
|
|
2423
|
-
target_state.value if hasattr(target_state, "value") else target_state
|
|
2424
|
-
),
|
|
2425
|
-
},
|
|
2426
|
-
adapter=adapter_name,
|
|
2427
|
-
operation="transition",
|
|
2428
|
-
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
2429
|
-
)
|
|
2430
|
-
|
|
2431
|
-
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
2432
|
-
console.print(f" Ticket: {ticket_id} → {target_state}")
|
|
2433
|
-
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
2434
|
-
|
|
2435
|
-
# Start worker if needed
|
|
2436
|
-
manager = WorkerManager()
|
|
2437
|
-
if manager.start_if_needed():
|
|
2438
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
@app.command(deprecated=True, hidden=True)
|
|
2442
|
-
def search(
|
|
2443
|
-
query: str | None = typer.Argument(None, help="Search query"),
|
|
2444
|
-
state: TicketState | None = typer.Option(None, "--state", "-s"),
|
|
2445
|
-
priority: Priority | None = typer.Option(None, "--priority", "-p"),
|
|
2446
|
-
assignee: str | None = typer.Option(None, "--assignee", "-a"),
|
|
2447
|
-
limit: int = typer.Option(10, "--limit", "-l"),
|
|
2448
|
-
adapter: AdapterType | None = typer.Option(
|
|
2449
|
-
None, "--adapter", help="Override default adapter"
|
|
2450
|
-
),
|
|
2451
|
-
) -> None:
|
|
2452
|
-
"""Search tickets with advanced query.
|
|
2453
|
-
|
|
2454
|
-
DEPRECATED: Use 'mcp-ticketer ticket search' instead.
|
|
2455
|
-
"""
|
|
2456
|
-
console.print(
|
|
2457
|
-
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket search' instead.[/yellow]\n"
|
|
2458
|
-
)
|
|
2459
|
-
|
|
2460
|
-
async def _search() -> None:
|
|
2461
|
-
adapter_instance = get_adapter(
|
|
2462
|
-
override_adapter=adapter.value if adapter else None
|
|
2463
|
-
)
|
|
2464
|
-
search_query = SearchQuery(
|
|
2465
|
-
query=query,
|
|
2466
|
-
state=state,
|
|
2467
|
-
priority=priority,
|
|
2468
|
-
assignee=assignee,
|
|
2469
|
-
limit=limit,
|
|
2470
|
-
)
|
|
2471
|
-
return await adapter_instance.search(search_query)
|
|
2472
|
-
|
|
2473
|
-
tickets = asyncio.run(_search())
|
|
2474
|
-
|
|
2475
|
-
if not tickets:
|
|
2476
|
-
console.print("[yellow]No tickets found matching query[/yellow]")
|
|
2477
|
-
return
|
|
2478
|
-
|
|
2479
|
-
# Display results
|
|
2480
|
-
console.print(f"\n[bold]Found {len(tickets)} ticket(s)[/bold]\n")
|
|
2481
|
-
|
|
2482
|
-
for ticket in tickets:
|
|
2483
|
-
console.print(f"[cyan]{ticket.id}[/cyan]: {ticket.title}")
|
|
2484
|
-
console.print(f" State: {ticket.state} | Priority: {ticket.priority}")
|
|
2485
|
-
if ticket.assignee:
|
|
2486
|
-
console.print(f" Assignee: {ticket.assignee}")
|
|
2487
|
-
console.print()
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
# Add ticket command group to main app
|
|
2491
|
-
app.add_typer(ticket_app, name="ticket")
|
|
2492
|
-
|
|
2493
|
-
# Add platform command group to main app
|
|
2494
|
-
app.add_typer(platform_app, name="platform")
|
|
2495
|
-
|
|
2496
|
-
# Add queue command to main app
|
|
2497
|
-
app.add_typer(queue_app, name="queue")
|
|
2498
|
-
|
|
2499
|
-
# Add discover command to main app
|
|
2500
|
-
app.add_typer(discover_app, name="discover")
|
|
2501
|
-
|
|
2502
|
-
# Add instructions command to main app
|
|
2503
|
-
app.add_typer(instruction_app, name="instructions")
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
# Add diagnostics command
|
|
2507
|
-
@app.command("doctor")
|
|
2508
|
-
def doctor_command(
|
|
2509
|
-
output_file: str | None = typer.Option(
|
|
2510
|
-
None, "--output", "-o", help="Save full report to file"
|
|
2511
|
-
),
|
|
2512
|
-
json_output: bool = typer.Option(
|
|
2513
|
-
False, "--json", help="Output report in JSON format"
|
|
2514
|
-
),
|
|
2515
|
-
simple: bool = typer.Option(
|
|
2516
|
-
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
2517
|
-
),
|
|
2518
|
-
) -> None:
|
|
2519
|
-
"""Run comprehensive system diagnostics and health check (alias: diagnose)."""
|
|
2520
|
-
if simple:
|
|
2521
|
-
from .simple_health import simple_diagnose
|
|
2522
|
-
|
|
2523
|
-
report = simple_diagnose()
|
|
2524
|
-
if output_file:
|
|
2525
|
-
import json
|
|
2526
|
-
|
|
2527
|
-
with open(output_file, "w") as f:
|
|
2528
|
-
json.dump(report, f, indent=2)
|
|
2529
|
-
console.print(f"\n📄 Report saved to: {output_file}")
|
|
2530
|
-
if json_output:
|
|
2531
|
-
import json
|
|
2532
|
-
|
|
2533
|
-
console.print("\n" + json.dumps(report, indent=2))
|
|
2534
|
-
if report["issues"]:
|
|
2535
|
-
raise typer.Exit(1) from None
|
|
2536
|
-
else:
|
|
2537
|
-
try:
|
|
2538
|
-
asyncio.run(
|
|
2539
|
-
run_diagnostics(output_file=output_file, json_output=json_output)
|
|
2540
|
-
)
|
|
2541
|
-
except typer.Exit:
|
|
2542
|
-
# typer.Exit is expected - don't fall back to simple diagnostics
|
|
2543
|
-
raise
|
|
2544
|
-
except Exception as e:
|
|
2545
|
-
console.print(f"⚠️ Full diagnostics failed: {e}")
|
|
2546
|
-
console.print("🔄 Falling back to simple diagnostics...")
|
|
2547
|
-
from .simple_health import simple_diagnose
|
|
2548
|
-
|
|
2549
|
-
report = simple_diagnose()
|
|
2550
|
-
if report["issues"]:
|
|
2551
|
-
raise typer.Exit(1) from None
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
@app.command("diagnose", hidden=True)
|
|
2555
|
-
def diagnose_alias(
|
|
2556
|
-
output_file: str | None = typer.Option(
|
|
2557
|
-
None, "--output", "-o", help="Save full report to file"
|
|
2558
|
-
),
|
|
2559
|
-
json_output: bool = typer.Option(
|
|
2560
|
-
False, "--json", help="Output report in JSON format"
|
|
2561
|
-
),
|
|
2562
|
-
simple: bool = typer.Option(
|
|
2563
|
-
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
2564
|
-
),
|
|
2565
|
-
) -> None:
|
|
2566
|
-
"""Run comprehensive system diagnostics and health check (alias for doctor)."""
|
|
2567
|
-
# Call the doctor_command function with the same parameters
|
|
2568
|
-
doctor_command(output_file=output_file, json_output=json_output, simple=simple)
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
@app.command("status")
|
|
2572
|
-
def status_command() -> None:
|
|
2573
|
-
"""Quick health check - shows system status summary (alias: health)."""
|
|
2574
|
-
from .simple_health import simple_health_check
|
|
2575
|
-
|
|
2576
|
-
result = simple_health_check()
|
|
2577
|
-
if result != 0:
|
|
2578
|
-
raise typer.Exit(result) from None
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
@app.command("health")
|
|
2582
|
-
def health_alias() -> None:
|
|
2583
|
-
"""Quick health check - shows system status summary (alias for status)."""
|
|
2584
|
-
from .simple_health import simple_health_check
|
|
2585
|
-
|
|
2586
|
-
result = simple_health_check()
|
|
2587
|
-
if result != 0:
|
|
2588
|
-
raise typer.Exit(result) from None
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
# Create MCP configuration command group
|
|
2592
|
-
mcp_app = typer.Typer(
|
|
2593
|
-
name="mcp",
|
|
2594
|
-
help="Configure MCP integration for AI clients (Claude, Gemini, Codex, Auggie)",
|
|
2595
|
-
add_completion=False,
|
|
2596
|
-
invoke_without_command=True,
|
|
2597
|
-
)
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
@mcp_app.callback()
|
|
2601
|
-
def mcp_callback(
|
|
2602
|
-
ctx: typer.Context,
|
|
2603
|
-
project_path: str | None = typer.Option(
|
|
2604
|
-
None, "--path", "-p", help="Project directory path (default: current directory)"
|
|
2605
|
-
),
|
|
2606
|
-
) -> None:
|
|
2607
|
-
"""MCP command group - runs MCP server if no subcommand provided.
|
|
2608
|
-
|
|
2609
|
-
Examples:
|
|
2610
|
-
mcp-ticketer mcp # Start server in current directory
|
|
2611
|
-
mcp-ticketer mcp --path /dir # Start server in specific directory
|
|
2612
|
-
mcp-ticketer mcp -p /dir # Start server (short form)
|
|
2613
|
-
mcp-ticketer mcp status # Check MCP status
|
|
2614
|
-
mcp-ticketer mcp serve # Explicitly start server
|
|
2615
|
-
|
|
2616
|
-
"""
|
|
2617
|
-
if ctx.invoked_subcommand is None:
|
|
2618
|
-
# No subcommand provided, run the serve command
|
|
2619
|
-
# Change to project directory if provided
|
|
2620
|
-
if project_path:
|
|
2621
|
-
import os
|
|
2622
|
-
|
|
2623
|
-
os.chdir(project_path)
|
|
2624
|
-
# Invoke the serve command through context
|
|
2625
|
-
ctx.invoke(mcp_serve, adapter=None, base_path=None)
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
@app.command()
|
|
2629
|
-
def install(
|
|
2630
|
-
platform: str | None = typer.Argument(
|
|
2631
|
-
None,
|
|
2632
|
-
help="Platform to install (claude-code, claude-desktop, gemini, codex, auggie)",
|
|
2633
|
-
),
|
|
2634
|
-
auto_detect: bool = typer.Option(
|
|
2635
|
-
False,
|
|
2636
|
-
"--auto-detect",
|
|
2637
|
-
"-d",
|
|
2638
|
-
help="Auto-detect and show all installed AI platforms",
|
|
2639
|
-
),
|
|
2640
|
-
install_all: bool = typer.Option(
|
|
2641
|
-
False,
|
|
2642
|
-
"--all",
|
|
2643
|
-
help="Install for all detected platforms",
|
|
2644
|
-
),
|
|
2645
|
-
adapter: str | None = typer.Option(
|
|
2646
|
-
None,
|
|
2647
|
-
"--adapter",
|
|
2648
|
-
"-a",
|
|
2649
|
-
help="Adapter type to use (interactive prompt if not specified)",
|
|
2650
|
-
),
|
|
2651
|
-
project_path: str | None = typer.Option(
|
|
2652
|
-
None, "--path", help="Project path (default: current directory)"
|
|
2653
|
-
),
|
|
2654
|
-
global_config: bool = typer.Option(
|
|
2655
|
-
False,
|
|
2656
|
-
"--global",
|
|
2657
|
-
"-g",
|
|
2658
|
-
help="Save to global config instead of project-specific",
|
|
2659
|
-
),
|
|
2660
|
-
base_path: str | None = typer.Option(
|
|
2661
|
-
None,
|
|
2662
|
-
"--base-path",
|
|
2663
|
-
"-p",
|
|
2664
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
2665
|
-
),
|
|
2666
|
-
api_key: str | None = typer.Option(
|
|
2667
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
2668
|
-
),
|
|
2669
|
-
team_id: str | None = typer.Option(
|
|
2670
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
2671
|
-
),
|
|
2672
|
-
jira_server: str | None = typer.Option(
|
|
2673
|
-
None,
|
|
2674
|
-
"--jira-server",
|
|
2675
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
2676
|
-
),
|
|
2677
|
-
jira_email: str | None = typer.Option(
|
|
2678
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
2679
|
-
),
|
|
2680
|
-
jira_project: str | None = typer.Option(
|
|
2681
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
2682
|
-
),
|
|
2683
|
-
github_owner: str | None = typer.Option(
|
|
2684
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
2685
|
-
),
|
|
2686
|
-
github_repo: str | None = typer.Option(
|
|
2687
|
-
None, "--github-repo", help="GitHub repository name"
|
|
2688
|
-
),
|
|
2689
|
-
github_token: str | None = typer.Option(
|
|
2690
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
2691
|
-
),
|
|
2692
|
-
dry_run: bool = typer.Option(
|
|
2693
|
-
False,
|
|
2694
|
-
"--dry-run",
|
|
2695
|
-
help="Show what would be done without making changes (for platform installation)",
|
|
2696
|
-
),
|
|
2697
|
-
) -> None:
|
|
2698
|
-
"""Install MCP server configuration for AI platforms.
|
|
2699
|
-
|
|
2700
|
-
This command configures mcp-ticketer as an MCP server for various AI
|
|
2701
|
-
platforms. It updates platform-specific configuration files to enable
|
|
2702
|
-
mcp-ticketer integration.
|
|
2703
|
-
|
|
2704
|
-
RECOMMENDED: Use 'mcp-ticketer setup' for first-time setup, which
|
|
2705
|
-
handles both adapter configuration and platform installation together.
|
|
2706
|
-
|
|
2707
|
-
Platform Installation:
|
|
2708
|
-
# Auto-detect and prompt for platform selection
|
|
2709
|
-
mcp-ticketer install
|
|
2710
|
-
|
|
2711
|
-
# Show all detected platforms
|
|
2712
|
-
mcp-ticketer install --auto-detect
|
|
2713
|
-
|
|
2714
|
-
# Install for all detected platforms
|
|
2715
|
-
mcp-ticketer install --all
|
|
2716
|
-
|
|
2717
|
-
# Install for specific platform
|
|
2718
|
-
mcp-ticketer install claude-code # Claude Code (project-level)
|
|
2719
|
-
mcp-ticketer install claude-desktop # Claude Desktop (global)
|
|
2720
|
-
mcp-ticketer install gemini # Gemini CLI
|
|
2721
|
-
mcp-ticketer install codex # Codex
|
|
2722
|
-
mcp-ticketer install auggie # Auggie
|
|
2723
|
-
|
|
2724
|
-
Legacy Usage (adapter setup, deprecated - use 'init' or 'setup' instead):
|
|
2725
|
-
mcp-ticketer install --adapter linear # Use 'init' or 'setup' instead
|
|
2726
|
-
|
|
2727
|
-
"""
|
|
2728
|
-
from .platform_detection import PlatformDetector, get_platform_by_name
|
|
2729
|
-
|
|
2730
|
-
detector = PlatformDetector()
|
|
2731
|
-
|
|
2732
|
-
# Handle auto-detect flag (just show detected platforms and exit)
|
|
2733
|
-
if auto_detect:
|
|
2734
|
-
detected = detector.detect_all(
|
|
2735
|
-
project_path=Path(project_path) if project_path else Path.cwd()
|
|
2736
|
-
)
|
|
2737
|
-
|
|
2738
|
-
if not detected:
|
|
2739
|
-
console.print("[yellow]No AI platforms detected.[/yellow]")
|
|
2740
|
-
console.print("\n[bold]Supported platforms:[/bold]")
|
|
2741
|
-
console.print(" • Claude Code - Project-level configuration")
|
|
2742
|
-
console.print(" • Claude Desktop - Global GUI application")
|
|
2743
|
-
console.print(" • Auggie - CLI tool with global config")
|
|
2744
|
-
console.print(" • Codex - CLI tool with global config")
|
|
2745
|
-
console.print(" • Gemini - CLI tool with project/global config")
|
|
2746
|
-
console.print(
|
|
2747
|
-
"\n[dim]Install these platforms to use them with mcp-ticketer.[/dim]"
|
|
2748
|
-
)
|
|
2749
|
-
return
|
|
2750
|
-
|
|
2751
|
-
console.print("[bold]Detected AI platforms:[/bold]\n")
|
|
2752
|
-
table = Table(show_header=True, header_style="bold cyan")
|
|
2753
|
-
table.add_column("Platform", style="green")
|
|
2754
|
-
table.add_column("Status", style="yellow")
|
|
2755
|
-
table.add_column("Scope", style="blue")
|
|
2756
|
-
table.add_column("Config Path", style="dim")
|
|
2757
|
-
|
|
2758
|
-
for plat in detected:
|
|
2759
|
-
status = "✓ Installed" if plat.is_installed else "⚠ Config Issue"
|
|
2760
|
-
table.add_row(plat.display_name, status, plat.scope, str(plat.config_path))
|
|
2761
|
-
|
|
2762
|
-
console.print(table)
|
|
2763
|
-
console.print(
|
|
2764
|
-
"\n[dim]Run 'mcp-ticketer install <platform>' to configure a specific platform[/dim]"
|
|
2765
|
-
)
|
|
2766
|
-
console.print(
|
|
2767
|
-
"[dim]Run 'mcp-ticketer install --all' to configure all detected platforms[/dim]"
|
|
2768
|
-
)
|
|
2769
|
-
return
|
|
2770
|
-
|
|
2771
|
-
# Handle --all flag (install for all detected platforms)
|
|
2772
|
-
if install_all:
|
|
2773
|
-
detected = detector.detect_all(
|
|
2774
|
-
project_path=Path(project_path) if project_path else Path.cwd()
|
|
2775
|
-
)
|
|
2776
|
-
|
|
2777
|
-
if not detected:
|
|
2778
|
-
console.print("[yellow]No AI platforms detected.[/yellow]")
|
|
2779
|
-
console.print(
|
|
2780
|
-
"Run 'mcp-ticketer install --auto-detect' to see supported platforms."
|
|
2781
|
-
)
|
|
2782
|
-
return
|
|
2783
|
-
|
|
2784
|
-
# Handle dry-run mode - show what would be installed without actually installing
|
|
2785
|
-
if dry_run:
|
|
2786
|
-
console.print(
|
|
2787
|
-
"\n[yellow]DRY RUN - The following platforms would be configured:[/yellow]\n"
|
|
2788
|
-
)
|
|
2789
|
-
|
|
2790
|
-
installable_count = 0
|
|
2791
|
-
for plat in detected:
|
|
2792
|
-
if plat.is_installed:
|
|
2793
|
-
console.print(f" ✓ {plat.display_name} ({plat.scope})")
|
|
2794
|
-
installable_count += 1
|
|
2795
|
-
else:
|
|
2796
|
-
console.print(
|
|
2797
|
-
f" ⚠ {plat.display_name} ({plat.scope}) - would be skipped (configuration issue)"
|
|
2798
|
-
)
|
|
2799
|
-
|
|
2800
|
-
console.print(
|
|
2801
|
-
f"\n[dim]Would configure {installable_count} platform(s)[/dim]"
|
|
2802
|
-
)
|
|
2803
|
-
return
|
|
2804
|
-
|
|
2805
|
-
console.print(
|
|
2806
|
-
f"[bold]Installing for {len(detected)} detected platform(s)...[/bold]\n"
|
|
2807
|
-
)
|
|
2808
|
-
|
|
2809
|
-
# Import configuration functions
|
|
2810
|
-
from .auggie_configure import configure_auggie_mcp
|
|
2811
|
-
from .codex_configure import configure_codex_mcp
|
|
2812
|
-
from .gemini_configure import configure_gemini_mcp
|
|
2813
|
-
from .mcp_configure import configure_claude_mcp
|
|
2814
|
-
|
|
2815
|
-
# Map platform names to configuration functions
|
|
2816
|
-
platform_mapping = {
|
|
2817
|
-
"claude-code": lambda: configure_claude_mcp(
|
|
2818
|
-
global_config=False, force=True
|
|
2819
|
-
),
|
|
2820
|
-
"claude-desktop": lambda: configure_claude_mcp(
|
|
2821
|
-
global_config=True, force=True
|
|
2822
|
-
),
|
|
2823
|
-
"auggie": lambda: configure_auggie_mcp(force=True),
|
|
2824
|
-
"gemini": lambda: configure_gemini_mcp(scope="project", force=True),
|
|
2825
|
-
"codex": lambda: configure_codex_mcp(force=True),
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
success_count = 0
|
|
2829
|
-
failed = []
|
|
2830
|
-
|
|
2831
|
-
for plat in detected:
|
|
2832
|
-
if not plat.is_installed:
|
|
2833
|
-
console.print(
|
|
2834
|
-
f"[yellow]⚠[/yellow] Skipping {plat.display_name} (configuration issue)"
|
|
2835
|
-
)
|
|
2836
|
-
continue
|
|
2837
|
-
|
|
2838
|
-
config_func = platform_mapping.get(plat.name)
|
|
2839
|
-
if not config_func:
|
|
2840
|
-
console.print(
|
|
2841
|
-
f"[yellow]⚠[/yellow] No installer for {plat.display_name}"
|
|
2842
|
-
)
|
|
2843
|
-
continue
|
|
2844
|
-
|
|
2845
|
-
try:
|
|
2846
|
-
console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
|
|
2847
|
-
config_func()
|
|
2848
|
-
success_count += 1
|
|
2849
|
-
except Exception as e:
|
|
2850
|
-
console.print(
|
|
2851
|
-
f"[red]✗[/red] Failed to install for {plat.display_name}: {e}"
|
|
2852
|
-
)
|
|
2853
|
-
failed.append(plat.display_name)
|
|
2854
|
-
|
|
2855
|
-
console.print(
|
|
2856
|
-
f"\n[bold]Installation complete:[/bold] {success_count} succeeded"
|
|
2857
|
-
)
|
|
2858
|
-
if failed:
|
|
2859
|
-
console.print(f"[red]Failed:[/red] {', '.join(failed)}")
|
|
2860
|
-
return
|
|
2861
|
-
|
|
2862
|
-
# If no platform argument and no adapter flag, auto-detect and prompt
|
|
2863
|
-
if platform is None and adapter is None:
|
|
2864
|
-
detected = detector.detect_all(
|
|
2865
|
-
project_path=Path(project_path) if project_path else Path.cwd()
|
|
2866
|
-
)
|
|
2867
|
-
|
|
2868
|
-
# Filter to only installed platforms
|
|
2869
|
-
installed = [p for p in detected if p.is_installed]
|
|
2870
|
-
|
|
2871
|
-
if not installed:
|
|
2872
|
-
console.print("[yellow]No AI platforms detected.[/yellow]")
|
|
2873
|
-
console.print("\n[bold]To see supported platforms:[/bold]")
|
|
2874
|
-
console.print(" mcp-ticketer install --auto-detect")
|
|
2875
|
-
console.print("\n[bold]Or run legacy adapter setup:[/bold]")
|
|
2876
|
-
console.print(" mcp-ticketer install --adapter <adapter-type>")
|
|
2877
|
-
return
|
|
2878
|
-
|
|
2879
|
-
# Show detected platforms and prompt for selection
|
|
2880
|
-
console.print("[bold]Detected AI platforms:[/bold]\n")
|
|
2881
|
-
for idx, plat in enumerate(installed, 1):
|
|
2882
|
-
console.print(f" {idx}. {plat.display_name} ({plat.scope})")
|
|
2883
|
-
|
|
2884
|
-
console.print(
|
|
2885
|
-
"\n[dim]Enter the number of the platform to configure, or 'q' to quit:[/dim]"
|
|
2886
|
-
)
|
|
2887
|
-
choice = typer.prompt("Select platform")
|
|
2888
|
-
|
|
2889
|
-
if choice.lower() == "q":
|
|
2890
|
-
console.print("Installation cancelled.")
|
|
2891
|
-
return
|
|
2892
|
-
|
|
2893
|
-
try:
|
|
2894
|
-
idx = int(choice) - 1
|
|
2895
|
-
if idx < 0 or idx >= len(installed):
|
|
2896
|
-
console.print("[red]Invalid selection.[/red]")
|
|
2897
|
-
raise typer.Exit(1) from None
|
|
2898
|
-
platform = installed[idx].name
|
|
2899
|
-
except ValueError as e:
|
|
2900
|
-
console.print("[red]Invalid input. Please enter a number.[/red]")
|
|
2901
|
-
raise typer.Exit(1) from e
|
|
2902
|
-
|
|
2903
|
-
# If platform argument is provided, handle MCP platform installation (NEW SYNTAX)
|
|
2904
|
-
if platform is not None:
|
|
2905
|
-
# Validate that the platform is actually installed
|
|
2906
|
-
platform_info = get_platform_by_name(
|
|
2907
|
-
platform, project_path=Path(project_path) if project_path else Path.cwd()
|
|
2908
|
-
)
|
|
2909
|
-
|
|
2910
|
-
if platform_info and not platform_info.is_installed:
|
|
2911
|
-
console.print(
|
|
2912
|
-
f"[yellow]⚠[/yellow] {platform_info.display_name} was detected but has a configuration issue."
|
|
2913
|
-
)
|
|
2914
|
-
console.print(f"[dim]Config path: {platform_info.config_path}[/dim]\n")
|
|
2915
|
-
|
|
2916
|
-
proceed = typer.confirm(
|
|
2917
|
-
"Do you want to proceed with installation anyway?", default=False
|
|
2918
|
-
)
|
|
2919
|
-
if not proceed:
|
|
2920
|
-
console.print("Installation cancelled.")
|
|
2921
|
-
return
|
|
2922
|
-
|
|
2923
|
-
elif not platform_info:
|
|
2924
|
-
# Platform not detected at all - warn but allow proceeding
|
|
2925
|
-
console.print(
|
|
2926
|
-
f"[yellow]⚠[/yellow] Platform '{platform}' not detected on this system."
|
|
2927
|
-
)
|
|
2928
|
-
console.print(
|
|
2929
|
-
"[dim]Run 'mcp-ticketer install --auto-detect' to see detected platforms.[/dim]\n"
|
|
2930
|
-
)
|
|
2931
|
-
|
|
2932
|
-
proceed = typer.confirm(
|
|
2933
|
-
"Do you want to proceed with installation anyway?", default=False
|
|
2934
|
-
)
|
|
2935
|
-
if not proceed:
|
|
2936
|
-
console.print("Installation cancelled.")
|
|
2937
|
-
return
|
|
2938
|
-
|
|
2939
|
-
# Import configuration functions
|
|
2940
|
-
from .auggie_configure import configure_auggie_mcp
|
|
2941
|
-
from .codex_configure import configure_codex_mcp
|
|
2942
|
-
from .gemini_configure import configure_gemini_mcp
|
|
2943
|
-
from .mcp_configure import configure_claude_mcp
|
|
2944
|
-
|
|
2945
|
-
# Map platform names to configuration functions
|
|
2946
|
-
platform_mapping = {
|
|
2947
|
-
"claude-code": {
|
|
2948
|
-
"func": lambda: configure_claude_mcp(global_config=False, force=True),
|
|
2949
|
-
"name": "Claude Code",
|
|
2950
|
-
},
|
|
2951
|
-
"claude-desktop": {
|
|
2952
|
-
"func": lambda: configure_claude_mcp(global_config=True, force=True),
|
|
2953
|
-
"name": "Claude Desktop",
|
|
2954
|
-
},
|
|
2955
|
-
"auggie": {
|
|
2956
|
-
"func": lambda: configure_auggie_mcp(force=True),
|
|
2957
|
-
"name": "Auggie",
|
|
2958
|
-
},
|
|
2959
|
-
"gemini": {
|
|
2960
|
-
"func": lambda: configure_gemini_mcp(scope="project", force=True),
|
|
2961
|
-
"name": "Gemini CLI",
|
|
2962
|
-
},
|
|
2963
|
-
"codex": {
|
|
2964
|
-
"func": lambda: configure_codex_mcp(force=True),
|
|
2965
|
-
"name": "Codex",
|
|
2966
|
-
},
|
|
2967
|
-
}
|
|
2968
|
-
|
|
2969
|
-
if platform not in platform_mapping:
|
|
2970
|
-
console.print(f"[red]Unknown platform: {platform}[/red]")
|
|
2971
|
-
console.print("\n[bold]Available platforms:[/bold]")
|
|
2972
|
-
for p in platform_mapping.keys():
|
|
2973
|
-
console.print(f" • {p}")
|
|
2974
|
-
raise typer.Exit(1) from None
|
|
2975
|
-
|
|
2976
|
-
config = platform_mapping[platform]
|
|
2977
|
-
|
|
2978
|
-
if dry_run:
|
|
2979
|
-
console.print(f"[cyan]DRY RUN - Would install for {config['name']}[/cyan]")
|
|
2980
|
-
return
|
|
2981
|
-
|
|
2982
|
-
try:
|
|
2983
|
-
config["func"]()
|
|
2984
|
-
except Exception as e:
|
|
2985
|
-
console.print(f"[red]Installation failed: {e}[/red]")
|
|
2986
|
-
raise typer.Exit(1) from e
|
|
2987
|
-
return
|
|
2988
|
-
|
|
2989
|
-
# Otherwise, delegate to init for adapter initialization (LEGACY BEHAVIOR)
|
|
2990
|
-
# This makes 'install' and 'init' synonymous when called without platform argument
|
|
2991
|
-
init(
|
|
2992
|
-
adapter=adapter,
|
|
2993
|
-
project_path=project_path,
|
|
2994
|
-
global_config=global_config,
|
|
2995
|
-
base_path=base_path,
|
|
2996
|
-
api_key=api_key,
|
|
2997
|
-
team_id=team_id,
|
|
2998
|
-
jira_server=jira_server,
|
|
2999
|
-
jira_email=jira_email,
|
|
3000
|
-
jira_project=jira_project,
|
|
3001
|
-
github_owner=github_owner,
|
|
3002
|
-
github_repo=github_repo,
|
|
3003
|
-
github_token=github_token,
|
|
3004
|
-
)
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
@app.command()
|
|
3008
|
-
def remove(
|
|
3009
|
-
platform: str | None = typer.Argument(
|
|
3010
|
-
None,
|
|
3011
|
-
help="Platform to remove (claude-code, claude-desktop, auggie, gemini, codex)",
|
|
3012
|
-
),
|
|
3013
|
-
dry_run: bool = typer.Option(
|
|
3014
|
-
False, "--dry-run", help="Show what would be done without making changes"
|
|
3015
|
-
),
|
|
3016
|
-
) -> None:
|
|
3017
|
-
"""Remove mcp-ticketer from AI platforms.
|
|
3018
|
-
|
|
3019
|
-
Without arguments, shows help and available platforms.
|
|
3020
|
-
With a platform argument, removes MCP configuration for that platform.
|
|
3021
|
-
|
|
3022
|
-
Examples:
|
|
3023
|
-
# Remove from Claude Code (project-level)
|
|
3024
|
-
mcp-ticketer remove claude-code
|
|
3025
|
-
|
|
3026
|
-
# Remove from Claude Desktop (global)
|
|
3027
|
-
mcp-ticketer remove claude-desktop
|
|
3028
|
-
|
|
3029
|
-
# Remove from Auggie
|
|
3030
|
-
mcp-ticketer remove auggie
|
|
3031
|
-
|
|
3032
|
-
# Dry run to preview changes
|
|
3033
|
-
mcp-ticketer remove claude-code --dry-run
|
|
3034
|
-
|
|
3035
|
-
"""
|
|
3036
|
-
# If no platform specified, show help message
|
|
3037
|
-
if platform is None:
|
|
3038
|
-
console.print("[bold]Remove mcp-ticketer from AI platforms[/bold]\n")
|
|
3039
|
-
console.print("Usage: mcp-ticketer remove <platform>\n")
|
|
3040
|
-
console.print("[bold]Available platforms:[/bold]")
|
|
3041
|
-
console.print(" • claude-code - Claude Code (project-level)")
|
|
3042
|
-
console.print(" • claude-desktop - Claude Desktop (global)")
|
|
3043
|
-
console.print(" • auggie - Auggie (global)")
|
|
3044
|
-
console.print(" • gemini - Gemini CLI (project-level by default)")
|
|
3045
|
-
console.print(" • codex - Codex (global)")
|
|
3046
|
-
return
|
|
3047
|
-
|
|
3048
|
-
# Import removal functions
|
|
3049
|
-
from .auggie_configure import remove_auggie_mcp
|
|
3050
|
-
from .codex_configure import remove_codex_mcp
|
|
3051
|
-
from .gemini_configure import remove_gemini_mcp
|
|
3052
|
-
from .mcp_configure import remove_claude_mcp
|
|
3053
|
-
|
|
3054
|
-
# Map platform names to removal functions
|
|
3055
|
-
platform_mapping = {
|
|
3056
|
-
"claude-code": {
|
|
3057
|
-
"func": lambda: remove_claude_mcp(global_config=False, dry_run=dry_run),
|
|
3058
|
-
"name": "Claude Code",
|
|
3059
|
-
},
|
|
3060
|
-
"claude-desktop": {
|
|
3061
|
-
"func": lambda: remove_claude_mcp(global_config=True, dry_run=dry_run),
|
|
3062
|
-
"name": "Claude Desktop",
|
|
3063
|
-
},
|
|
3064
|
-
"auggie": {
|
|
3065
|
-
"func": lambda: remove_auggie_mcp(dry_run=dry_run),
|
|
3066
|
-
"name": "Auggie",
|
|
3067
|
-
},
|
|
3068
|
-
"gemini": {
|
|
3069
|
-
"func": lambda: remove_gemini_mcp(scope="project", dry_run=dry_run),
|
|
3070
|
-
"name": "Gemini CLI",
|
|
3071
|
-
},
|
|
3072
|
-
"codex": {
|
|
3073
|
-
"func": lambda: remove_codex_mcp(dry_run=dry_run),
|
|
3074
|
-
"name": "Codex",
|
|
3075
|
-
},
|
|
3076
|
-
}
|
|
3077
|
-
|
|
3078
|
-
if platform not in platform_mapping:
|
|
3079
|
-
console.print(f"[red]Unknown platform: {platform}[/red]")
|
|
3080
|
-
console.print("\n[bold]Available platforms:[/bold]")
|
|
3081
|
-
for p in platform_mapping.keys():
|
|
3082
|
-
console.print(f" • {p}")
|
|
3083
|
-
raise typer.Exit(1) from None
|
|
3084
|
-
|
|
3085
|
-
config = platform_mapping[platform]
|
|
3086
|
-
|
|
3087
|
-
try:
|
|
3088
|
-
config["func"]()
|
|
3089
|
-
except Exception as e:
|
|
3090
|
-
console.print(f"[red]Removal failed: {e}[/red]")
|
|
3091
|
-
raise typer.Exit(1) from e
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
@app.command()
|
|
3095
|
-
def uninstall(
|
|
3096
|
-
platform: str | None = typer.Argument(
|
|
3097
|
-
None,
|
|
3098
|
-
help="Platform to uninstall (claude-code, claude-desktop, auggie, gemini, codex)",
|
|
3099
|
-
),
|
|
3100
|
-
dry_run: bool = typer.Option(
|
|
3101
|
-
False, "--dry-run", help="Show what would be done without making changes"
|
|
3102
|
-
),
|
|
3103
|
-
) -> None:
|
|
3104
|
-
"""Uninstall mcp-ticketer from AI platforms (alias for remove).
|
|
3105
|
-
|
|
3106
|
-
This is an alias for the 'remove' command.
|
|
3107
|
-
|
|
3108
|
-
Without arguments, shows help and available platforms.
|
|
3109
|
-
With a platform argument, removes MCP configuration for that platform.
|
|
3110
|
-
|
|
3111
|
-
Examples:
|
|
3112
|
-
# Uninstall from Claude Code (project-level)
|
|
3113
|
-
mcp-ticketer uninstall claude-code
|
|
3114
|
-
|
|
3115
|
-
# Uninstall from Claude Desktop (global)
|
|
3116
|
-
mcp-ticketer uninstall claude-desktop
|
|
3117
|
-
|
|
3118
|
-
# Uninstall from Auggie
|
|
3119
|
-
mcp-ticketer uninstall auggie
|
|
3120
|
-
|
|
3121
|
-
# Dry run to preview changes
|
|
3122
|
-
mcp-ticketer uninstall claude-code --dry-run
|
|
3123
|
-
|
|
3124
|
-
"""
|
|
3125
|
-
# Call the remove command with the same parameters
|
|
3126
|
-
remove(platform=platform, dry_run=dry_run)
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
@app.command(deprecated=True, hidden=True)
|
|
3130
|
-
def check(queue_id: str = typer.Argument(..., help="Queue ID to check")) -> None:
|
|
3131
|
-
"""Check status of a queued operation.
|
|
3132
|
-
|
|
3133
|
-
DEPRECATED: Use 'mcp-ticketer ticket check' instead.
|
|
3134
|
-
"""
|
|
3135
|
-
console.print(
|
|
3136
|
-
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket check' instead.[/yellow]\n"
|
|
3137
|
-
)
|
|
3138
|
-
queue = Queue()
|
|
3139
|
-
item = queue.get_item(queue_id)
|
|
3140
|
-
|
|
3141
|
-
if not item:
|
|
3142
|
-
console.print(f"[red]Queue item not found: {queue_id}[/red]")
|
|
3143
|
-
raise typer.Exit(1) from None
|
|
3144
|
-
|
|
3145
|
-
# Display status
|
|
3146
|
-
console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
|
|
3147
|
-
console.print(f"Operation: {item.operation}")
|
|
3148
|
-
console.print(f"Adapter: {item.adapter}")
|
|
3149
|
-
|
|
3150
|
-
# Status with color
|
|
3151
|
-
if item.status == QueueStatus.COMPLETED:
|
|
3152
|
-
console.print(f"Status: [green]{item.status}[/green]")
|
|
3153
|
-
elif item.status == QueueStatus.FAILED:
|
|
3154
|
-
console.print(f"Status: [red]{item.status}[/red]")
|
|
3155
|
-
elif item.status == QueueStatus.PROCESSING:
|
|
3156
|
-
console.print(f"Status: [yellow]{item.status}[/yellow]")
|
|
3157
|
-
else:
|
|
3158
|
-
console.print(f"Status: {item.status}")
|
|
3159
|
-
|
|
3160
|
-
# Timestamps
|
|
3161
|
-
console.print(f"Created: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
3162
|
-
if item.processed_at:
|
|
3163
|
-
console.print(f"Processed: {item.processed_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
3164
|
-
|
|
3165
|
-
# Error or result
|
|
3166
|
-
if item.error_message:
|
|
3167
|
-
console.print(f"\n[red]Error:[/red] {item.error_message}")
|
|
3168
|
-
elif item.result:
|
|
3169
|
-
console.print("\n[green]Result:[/green]")
|
|
3170
|
-
for key, value in item.result.items():
|
|
3171
|
-
console.print(f" {key}: {value}")
|
|
3172
|
-
|
|
3173
|
-
if item.retry_count > 0:
|
|
3174
|
-
console.print(f"\nRetry Count: {item.retry_count}")
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
@mcp_app.command(name="serve")
|
|
3178
|
-
def mcp_serve(
|
|
3179
|
-
adapter: AdapterType | None = typer.Option(
|
|
3180
|
-
None, "--adapter", "-a", help="Override default adapter type"
|
|
3181
|
-
),
|
|
3182
|
-
base_path: str | None = typer.Option(
|
|
3183
|
-
None, "--base-path", help="Base path for AITrackdown adapter"
|
|
3184
|
-
),
|
|
3185
|
-
) -> None:
|
|
3186
|
-
"""Start MCP server for JSON-RPC communication over stdio.
|
|
3187
|
-
|
|
3188
|
-
This command is used by Claude Code/Desktop when connecting to the MCP server.
|
|
3189
|
-
You typically don't need to run this manually - use 'mcp-ticketer install add' to configure.
|
|
3190
|
-
|
|
3191
|
-
Configuration Resolution:
|
|
3192
|
-
- When MCP server starts, it uses the current working directory (cwd)
|
|
3193
|
-
- The cwd is set by Claude Code/Desktop from the 'cwd' field in .mcp/config.json
|
|
3194
|
-
- Configuration is loaded with this priority:
|
|
3195
|
-
1. Project-specific: .mcp-ticketer/config.json in cwd
|
|
3196
|
-
2. Global: ~/.mcp-ticketer/config.json
|
|
3197
|
-
3. Default: aitrackdown adapter with .aitrackdown base path
|
|
3198
|
-
"""
|
|
3199
|
-
from ..mcp.server.server_sdk import configure_adapter
|
|
3200
|
-
from ..mcp.server.server_sdk import main as sdk_main
|
|
3201
|
-
|
|
3202
|
-
# Load configuration (respects project-specific config in cwd)
|
|
3203
|
-
config = load_config()
|
|
3204
|
-
|
|
3205
|
-
# Determine adapter type with priority: CLI arg > config > .env files > default
|
|
3206
|
-
if adapter:
|
|
3207
|
-
# Priority 1: Command line argument
|
|
3208
|
-
adapter_type = adapter.value
|
|
3209
|
-
# Get base config from config file
|
|
3210
|
-
adapters_config = config.get("adapters", {})
|
|
3211
|
-
adapter_config = adapters_config.get(adapter_type, {})
|
|
3212
|
-
else:
|
|
3213
|
-
# Priority 2: Configuration file (project-specific)
|
|
3214
|
-
adapter_type = config.get("default_adapter")
|
|
3215
|
-
if adapter_type:
|
|
3216
|
-
adapters_config = config.get("adapters", {})
|
|
3217
|
-
adapter_config = adapters_config.get(adapter_type, {})
|
|
3218
|
-
else:
|
|
3219
|
-
# Priority 3: .env files (auto-detection fallback)
|
|
3220
|
-
from ..mcp.server.main import _load_env_configuration
|
|
3221
|
-
|
|
3222
|
-
env_config = _load_env_configuration()
|
|
3223
|
-
if env_config:
|
|
3224
|
-
adapter_type = env_config["adapter_type"]
|
|
3225
|
-
adapter_config = env_config["adapter_config"]
|
|
3226
|
-
else:
|
|
3227
|
-
# Priority 4: Default fallback
|
|
3228
|
-
adapter_type = "aitrackdown"
|
|
3229
|
-
adapters_config = config.get("adapters", {})
|
|
3230
|
-
adapter_config = adapters_config.get(adapter_type, {})
|
|
3231
|
-
|
|
3232
|
-
# Override with command line options if provided (highest priority)
|
|
3233
|
-
if base_path and adapter_type == "aitrackdown":
|
|
3234
|
-
adapter_config["base_path"] = base_path
|
|
3235
|
-
|
|
3236
|
-
# Fallback to legacy config format
|
|
3237
|
-
if not adapter_config and "config" in config:
|
|
3238
|
-
adapter_config = config["config"]
|
|
3239
|
-
|
|
3240
|
-
# MCP server uses stdio for JSON-RPC, so we can't print to stdout
|
|
3241
|
-
# Only print to stderr to avoid interfering with the protocol
|
|
3242
|
-
import sys
|
|
3243
|
-
|
|
3244
|
-
if sys.stderr.isatty():
|
|
3245
|
-
# Only print if stderr is a terminal (not redirected)
|
|
3246
|
-
console.file = sys.stderr
|
|
3247
|
-
console.print(
|
|
3248
|
-
f"[green]Starting MCP SDK server[/green] with {adapter_type} adapter"
|
|
3249
|
-
)
|
|
3250
|
-
console.print(
|
|
3251
|
-
"[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
|
|
3252
|
-
)
|
|
3253
|
-
|
|
3254
|
-
# Configure adapter and run SDK server
|
|
3255
|
-
try:
|
|
3256
|
-
configure_adapter(adapter_type, adapter_config)
|
|
3257
|
-
sdk_main()
|
|
3258
|
-
except KeyboardInterrupt:
|
|
3259
|
-
# Send this to stderr
|
|
3260
|
-
if sys.stderr.isatty():
|
|
3261
|
-
console.print("\n[yellow]Server stopped by user[/yellow]")
|
|
3262
|
-
sys.exit(0)
|
|
3263
|
-
except Exception as e:
|
|
3264
|
-
# Log error to stderr
|
|
3265
|
-
sys.stderr.write(f"MCP server error: {e}\n")
|
|
3266
|
-
sys.exit(1)
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
@mcp_app.command(name="claude")
|
|
3270
|
-
def mcp_claude(
|
|
3271
|
-
global_config: bool = typer.Option(
|
|
3272
|
-
False,
|
|
3273
|
-
"--global",
|
|
3274
|
-
"-g",
|
|
3275
|
-
help="Configure Claude Desktop instead of project-level",
|
|
3276
|
-
),
|
|
3277
|
-
force: bool = typer.Option(
|
|
3278
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
3279
|
-
),
|
|
3280
|
-
) -> None:
|
|
3281
|
-
"""Configure Claude Code to use mcp-ticketer MCP server.
|
|
3282
|
-
|
|
3283
|
-
Reads configuration from .mcp-ticketer/config.json and updates
|
|
3284
|
-
Claude Code's MCP settings accordingly.
|
|
3285
|
-
|
|
3286
|
-
By default, configures project-level (.mcp/config.json).
|
|
3287
|
-
Use --global to configure Claude Desktop instead.
|
|
3288
|
-
|
|
3289
|
-
Examples:
|
|
3290
|
-
# Configure for current project (default)
|
|
3291
|
-
mcp-ticketer mcp claude
|
|
3292
|
-
|
|
3293
|
-
# Configure Claude Desktop globally
|
|
3294
|
-
mcp-ticketer mcp claude --global
|
|
3295
|
-
|
|
3296
|
-
# Force overwrite existing configuration
|
|
3297
|
-
mcp-ticketer mcp claude --force
|
|
3298
|
-
|
|
3299
|
-
"""
|
|
3300
|
-
from ..cli.mcp_configure import configure_claude_mcp
|
|
3301
|
-
|
|
3302
|
-
try:
|
|
3303
|
-
configure_claude_mcp(global_config=global_config, force=force)
|
|
3304
|
-
except Exception as e:
|
|
3305
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
3306
|
-
raise typer.Exit(1) from e
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
@mcp_app.command(name="gemini")
|
|
3310
|
-
def mcp_gemini(
|
|
3311
|
-
scope: str = typer.Option(
|
|
3312
|
-
"project",
|
|
3313
|
-
"--scope",
|
|
3314
|
-
"-s",
|
|
3315
|
-
help="Configuration scope: 'project' (default) or 'user'",
|
|
3316
|
-
),
|
|
3317
|
-
force: bool = typer.Option(
|
|
3318
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
3319
|
-
),
|
|
3320
|
-
) -> None:
|
|
3321
|
-
"""Configure Gemini CLI to use mcp-ticketer MCP server.
|
|
3322
|
-
|
|
3323
|
-
Reads configuration from .mcp-ticketer/config.json and creates
|
|
3324
|
-
Gemini CLI settings file with mcp-ticketer configuration.
|
|
3325
|
-
|
|
3326
|
-
By default, configures project-level (.gemini/settings.json).
|
|
3327
|
-
Use --scope user to configure user-level (~/.gemini/settings.json).
|
|
3328
|
-
|
|
3329
|
-
Examples:
|
|
3330
|
-
# Configure for current project (default)
|
|
3331
|
-
mcp-ticketer mcp gemini
|
|
3332
|
-
|
|
3333
|
-
# Configure at user level
|
|
3334
|
-
mcp-ticketer mcp gemini --scope user
|
|
3335
|
-
|
|
3336
|
-
# Force overwrite existing configuration
|
|
3337
|
-
mcp-ticketer mcp gemini --force
|
|
3338
|
-
|
|
3339
|
-
"""
|
|
3340
|
-
from ..cli.gemini_configure import configure_gemini_mcp
|
|
3341
|
-
|
|
3342
|
-
# Validate scope parameter
|
|
3343
|
-
if scope not in ["project", "user"]:
|
|
3344
|
-
console.print(
|
|
3345
|
-
f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
|
|
3346
|
-
)
|
|
3347
|
-
raise typer.Exit(1) from None
|
|
3348
|
-
|
|
3349
|
-
try:
|
|
3350
|
-
configure_gemini_mcp(scope=scope, force=force) # type: ignore
|
|
3351
|
-
except Exception as e:
|
|
3352
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
3353
|
-
raise typer.Exit(1) from e
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
@mcp_app.command(name="codex")
|
|
3357
|
-
def mcp_codex(
|
|
3358
|
-
force: bool = typer.Option(
|
|
3359
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
3360
|
-
),
|
|
3361
|
-
) -> None:
|
|
3362
|
-
"""Configure Codex CLI to use mcp-ticketer MCP server.
|
|
3363
|
-
|
|
3364
|
-
Reads configuration from .mcp-ticketer/config.json and creates
|
|
3365
|
-
Codex CLI config.toml with mcp-ticketer configuration.
|
|
3366
|
-
|
|
3367
|
-
IMPORTANT: Codex CLI ONLY supports global configuration at ~/.codex/config.toml.
|
|
3368
|
-
There is no project-level configuration support. After configuration,
|
|
3369
|
-
you must restart Codex CLI for changes to take effect.
|
|
3370
|
-
|
|
3371
|
-
Examples:
|
|
3372
|
-
# Configure Codex CLI globally
|
|
3373
|
-
mcp-ticketer mcp codex
|
|
3374
|
-
|
|
3375
|
-
# Force overwrite existing configuration
|
|
3376
|
-
mcp-ticketer mcp codex --force
|
|
3377
|
-
|
|
3378
|
-
"""
|
|
3379
|
-
from ..cli.codex_configure import configure_codex_mcp
|
|
3380
|
-
|
|
3381
|
-
try:
|
|
3382
|
-
configure_codex_mcp(force=force)
|
|
3383
|
-
except Exception as e:
|
|
3384
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
3385
|
-
raise typer.Exit(1) from e
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
@mcp_app.command(name="auggie")
|
|
3389
|
-
def mcp_auggie(
|
|
3390
|
-
force: bool = typer.Option(
|
|
3391
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
3392
|
-
),
|
|
3393
|
-
) -> None:
|
|
3394
|
-
"""Configure Auggie CLI to use mcp-ticketer MCP server.
|
|
3395
|
-
|
|
3396
|
-
Reads configuration from .mcp-ticketer/config.json and creates
|
|
3397
|
-
Auggie CLI settings.json with mcp-ticketer configuration.
|
|
3398
|
-
|
|
3399
|
-
IMPORTANT: Auggie CLI ONLY supports global configuration at ~/.augment/settings.json.
|
|
3400
|
-
There is no project-level configuration support. After configuration,
|
|
3401
|
-
you must restart Auggie CLI for changes to take effect.
|
|
3402
|
-
|
|
3403
|
-
Examples:
|
|
3404
|
-
# Configure Auggie CLI globally
|
|
3405
|
-
mcp-ticketer mcp auggie
|
|
3406
|
-
|
|
3407
|
-
# Force overwrite existing configuration
|
|
3408
|
-
mcp-ticketer mcp auggie --force
|
|
3409
|
-
|
|
3410
|
-
"""
|
|
3411
|
-
from ..cli.auggie_configure import configure_auggie_mcp
|
|
3412
|
-
|
|
3413
|
-
try:
|
|
3414
|
-
configure_auggie_mcp(force=force)
|
|
3415
|
-
except Exception as e:
|
|
3416
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
3417
|
-
raise typer.Exit(1) from e
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
@mcp_app.command(name="status")
|
|
3421
|
-
def mcp_status() -> None:
|
|
3422
|
-
"""Check MCP server status.
|
|
3423
|
-
|
|
3424
|
-
Shows whether the MCP server is configured and running for various platforms.
|
|
3425
|
-
|
|
3426
|
-
Examples:
|
|
3427
|
-
mcp-ticketer mcp status
|
|
3428
|
-
|
|
3429
|
-
"""
|
|
3430
|
-
import json
|
|
3431
|
-
from pathlib import Path
|
|
3432
|
-
|
|
3433
|
-
console.print("[bold]MCP Server Status[/bold]\n")
|
|
3434
|
-
|
|
3435
|
-
# Check project-level configuration
|
|
3436
|
-
project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
3437
|
-
if project_config.exists():
|
|
3438
|
-
console.print(f"[green]✓[/green] Project config found: {project_config}")
|
|
3439
|
-
try:
|
|
3440
|
-
with open(project_config) as f:
|
|
3441
|
-
config = json.load(f)
|
|
3442
|
-
adapter = config.get("default_adapter", "aitrackdown")
|
|
3443
|
-
console.print(f" Default adapter: [cyan]{adapter}[/cyan]")
|
|
3444
|
-
except Exception as e:
|
|
3445
|
-
console.print(f" [yellow]Warning: Could not read config: {e}[/yellow]")
|
|
3446
|
-
else:
|
|
3447
|
-
console.print("[yellow]○[/yellow] No project config found")
|
|
3448
|
-
|
|
3449
|
-
# Check Claude Code configuration
|
|
3450
|
-
claude_code_config = Path.cwd() / ".mcp" / "config.json"
|
|
3451
|
-
if claude_code_config.exists():
|
|
3452
|
-
console.print(
|
|
3453
|
-
f"\n[green]✓[/green] Claude Code configured: {claude_code_config}"
|
|
3454
|
-
)
|
|
3455
|
-
else:
|
|
3456
|
-
console.print("\n[yellow]○[/yellow] Claude Code not configured")
|
|
3457
|
-
|
|
3458
|
-
# Check Claude Desktop configuration
|
|
3459
|
-
claude_desktop_config = (
|
|
3460
|
-
Path.home()
|
|
3461
|
-
/ "Library"
|
|
3462
|
-
/ "Application Support"
|
|
3463
|
-
/ "Claude"
|
|
3464
|
-
/ "claude_desktop_config.json"
|
|
3465
|
-
)
|
|
3466
|
-
if claude_desktop_config.exists():
|
|
3467
|
-
try:
|
|
3468
|
-
with open(claude_desktop_config) as f:
|
|
3469
|
-
config = json.load(f)
|
|
3470
|
-
if "mcpServers" in config and "mcp-ticketer" in config["mcpServers"]:
|
|
3471
|
-
console.print(
|
|
3472
|
-
f"[green]✓[/green] Claude Desktop configured: {claude_desktop_config}"
|
|
3473
|
-
)
|
|
3474
|
-
else:
|
|
3475
|
-
console.print(
|
|
3476
|
-
"[yellow]○[/yellow] Claude Desktop config exists but mcp-ticketer not found"
|
|
3477
|
-
)
|
|
3478
|
-
except Exception:
|
|
3479
|
-
console.print(
|
|
3480
|
-
"[yellow]○[/yellow] Claude Desktop config exists but could not be read"
|
|
3481
|
-
)
|
|
3482
|
-
else:
|
|
3483
|
-
console.print("[yellow]○[/yellow] Claude Desktop not configured")
|
|
3484
|
-
|
|
3485
|
-
# Check Gemini configuration
|
|
3486
|
-
gemini_project_config = Path.cwd() / ".gemini" / "settings.json"
|
|
3487
|
-
gemini_user_config = Path.home() / ".gemini" / "settings.json"
|
|
3488
|
-
if gemini_project_config.exists():
|
|
3489
|
-
console.print(
|
|
3490
|
-
f"\n[green]✓[/green] Gemini (project) configured: {gemini_project_config}"
|
|
3491
|
-
)
|
|
3492
|
-
elif gemini_user_config.exists():
|
|
3493
|
-
console.print(
|
|
3494
|
-
f"\n[green]✓[/green] Gemini (user) configured: {gemini_user_config}"
|
|
3495
|
-
)
|
|
3496
|
-
else:
|
|
3497
|
-
console.print("\n[yellow]○[/yellow] Gemini not configured")
|
|
3498
|
-
|
|
3499
|
-
# Check Codex configuration
|
|
3500
|
-
codex_config = Path.home() / ".codex" / "config.toml"
|
|
3501
|
-
if codex_config.exists():
|
|
3502
|
-
console.print(f"[green]✓[/green] Codex configured: {codex_config}")
|
|
3503
|
-
else:
|
|
3504
|
-
console.print("[yellow]○[/yellow] Codex not configured")
|
|
3505
|
-
|
|
3506
|
-
# Check Auggie configuration
|
|
3507
|
-
auggie_config = Path.home() / ".augment" / "settings.json"
|
|
3508
|
-
if auggie_config.exists():
|
|
3509
|
-
console.print(f"[green]✓[/green] Auggie configured: {auggie_config}")
|
|
3510
|
-
else:
|
|
3511
|
-
console.print("[yellow]○[/yellow] Auggie not configured")
|
|
3512
|
-
|
|
3513
|
-
console.print(
|
|
3514
|
-
"\n[dim]Run 'mcp-ticketer install <platform>' to configure a platform[/dim]"
|
|
3515
|
-
)
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
@mcp_app.command(name="stop")
|
|
3519
|
-
def mcp_stop() -> None:
|
|
3520
|
-
"""Stop MCP server (placeholder - MCP runs on-demand via stdio).
|
|
616
|
+
result = simple_health_check()
|
|
617
|
+
if result != 0:
|
|
618
|
+
raise typer.Exit(result) from None
|
|
3521
619
|
|
|
3522
|
-
Note: The MCP server runs on-demand when AI clients connect via stdio.
|
|
3523
|
-
It doesn't run as a persistent background service, so there's nothing to stop.
|
|
3524
|
-
This command is provided for consistency but has no effect.
|
|
3525
620
|
|
|
3526
|
-
|
|
3527
|
-
|
|
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
|
|
3528
625
|
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
)
|
|
3533
|
-
console.print("There is no persistent server process to stop.")
|
|
3534
|
-
console.print(
|
|
3535
|
-
"\n[dim]The server starts automatically when AI clients connect and stops when they disconnect.[/dim]"
|
|
3536
|
-
)
|
|
626
|
+
result = simple_health_check()
|
|
627
|
+
if result != 0:
|
|
628
|
+
raise typer.Exit(result) from None
|
|
3537
629
|
|
|
3538
630
|
|
|
3539
631
|
# Add command groups to main app (must be after all subcommands are defined)
|