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.

Files changed (129) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/aitrackdown.py +507 -6
  5. mcp_ticketer/adapters/asana/adapter.py +229 -0
  6. mcp_ticketer/adapters/asana/mappers.py +14 -0
  7. mcp_ticketer/adapters/github/__init__.py +26 -0
  8. mcp_ticketer/adapters/github/adapter.py +3229 -0
  9. mcp_ticketer/adapters/github/client.py +335 -0
  10. mcp_ticketer/adapters/github/mappers.py +797 -0
  11. mcp_ticketer/adapters/github/queries.py +692 -0
  12. mcp_ticketer/adapters/github/types.py +460 -0
  13. mcp_ticketer/adapters/hybrid.py +47 -5
  14. mcp_ticketer/adapters/jira/__init__.py +35 -0
  15. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  16. mcp_ticketer/adapters/jira/client.py +271 -0
  17. mcp_ticketer/adapters/jira/mappers.py +246 -0
  18. mcp_ticketer/adapters/jira/queries.py +216 -0
  19. mcp_ticketer/adapters/jira/types.py +304 -0
  20. mcp_ticketer/adapters/linear/adapter.py +2730 -139
  21. mcp_ticketer/adapters/linear/client.py +175 -3
  22. mcp_ticketer/adapters/linear/mappers.py +203 -8
  23. mcp_ticketer/adapters/linear/queries.py +280 -3
  24. mcp_ticketer/adapters/linear/types.py +120 -4
  25. mcp_ticketer/analysis/__init__.py +56 -0
  26. mcp_ticketer/analysis/dependency_graph.py +255 -0
  27. mcp_ticketer/analysis/health_assessment.py +304 -0
  28. mcp_ticketer/analysis/orphaned.py +218 -0
  29. mcp_ticketer/analysis/project_status.py +594 -0
  30. mcp_ticketer/analysis/similarity.py +224 -0
  31. mcp_ticketer/analysis/staleness.py +266 -0
  32. mcp_ticketer/automation/__init__.py +11 -0
  33. mcp_ticketer/automation/project_updates.py +378 -0
  34. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  35. mcp_ticketer/cli/auggie_configure.py +17 -5
  36. mcp_ticketer/cli/codex_configure.py +97 -61
  37. mcp_ticketer/cli/configure.py +1288 -105
  38. mcp_ticketer/cli/cursor_configure.py +314 -0
  39. mcp_ticketer/cli/diagnostics.py +13 -12
  40. mcp_ticketer/cli/discover.py +5 -0
  41. mcp_ticketer/cli/gemini_configure.py +17 -5
  42. mcp_ticketer/cli/init_command.py +880 -0
  43. mcp_ticketer/cli/install_mcp_server.py +418 -0
  44. mcp_ticketer/cli/instruction_commands.py +6 -0
  45. mcp_ticketer/cli/main.py +267 -3175
  46. mcp_ticketer/cli/mcp_configure.py +821 -119
  47. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  48. mcp_ticketer/cli/platform_detection.py +77 -12
  49. mcp_ticketer/cli/platform_installer.py +545 -0
  50. mcp_ticketer/cli/project_update_commands.py +350 -0
  51. mcp_ticketer/cli/setup_command.py +795 -0
  52. mcp_ticketer/cli/simple_health.py +12 -10
  53. mcp_ticketer/cli/ticket_commands.py +705 -103
  54. mcp_ticketer/cli/utils.py +113 -0
  55. mcp_ticketer/core/__init__.py +56 -6
  56. mcp_ticketer/core/adapter.py +533 -2
  57. mcp_ticketer/core/config.py +21 -21
  58. mcp_ticketer/core/exceptions.py +7 -1
  59. mcp_ticketer/core/label_manager.py +732 -0
  60. mcp_ticketer/core/mappers.py +31 -19
  61. mcp_ticketer/core/milestone_manager.py +252 -0
  62. mcp_ticketer/core/models.py +480 -0
  63. mcp_ticketer/core/onepassword_secrets.py +1 -1
  64. mcp_ticketer/core/priority_matcher.py +463 -0
  65. mcp_ticketer/core/project_config.py +132 -14
  66. mcp_ticketer/core/project_utils.py +281 -0
  67. mcp_ticketer/core/project_validator.py +376 -0
  68. mcp_ticketer/core/session_state.py +176 -0
  69. mcp_ticketer/core/state_matcher.py +625 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/mcp/server/__main__.py +2 -1
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/main.py +106 -25
  75. mcp_ticketer/mcp/server/routing.py +723 -0
  76. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  77. mcp_ticketer/mcp/server/tools/__init__.py +33 -11
  78. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  79. mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
  80. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  81. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  82. mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
  83. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  84. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  85. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  86. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  87. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  88. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  89. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  90. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  91. mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
  92. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  93. mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
  94. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  95. mcp_ticketer/queue/queue.py +68 -0
  96. mcp_ticketer/queue/worker.py +1 -1
  97. mcp_ticketer/utils/__init__.py +5 -0
  98. mcp_ticketer/utils/token_utils.py +246 -0
  99. mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
  100. mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
  101. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  102. py_mcp_installer/examples/phase3_demo.py +178 -0
  103. py_mcp_installer/scripts/manage_version.py +54 -0
  104. py_mcp_installer/setup.py +6 -0
  105. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  106. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  107. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  108. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  109. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  110. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  111. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  112. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  113. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  114. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  115. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  116. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  117. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  118. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  119. py_mcp_installer/tests/__init__.py +0 -0
  120. py_mcp_installer/tests/platforms/__init__.py +0 -0
  121. py_mcp_installer/tests/test_platform_detector.py +17 -0
  122. mcp_ticketer/adapters/github.py +0 -1574
  123. mcp_ticketer/adapters/jira.py +0 -1258
  124. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  125. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  126. mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
  127. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  128. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  129. {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, Priority, TicketState
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
- async def _validate_adapter_credentials(
324
- adapter_type: str, config_file_path: Path
325
- ) -> list[str]:
326
- """Validate adapter credentials by performing real connectivity tests.
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
- Args:
329
- adapter_type: Type of adapter to validate
330
- config_file_path: Path to config file
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
- Returns:
333
- List of validation issues (empty if valid)
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
- issues = []
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
- try:
341
- # Load config
342
- with open(config_file_path) as f:
343
- config = json.load(f)
386
+ # Build adapter-specific configuration
387
+ adapter_configs = {}
344
388
 
345
- adapter_config = config.get("adapters", {}).get(adapter_type, {})
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
- if not adapter_config:
348
- issues.append(f"No configuration found for {adapter_type}")
349
- return issues
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
- # Validate based on adapter type
352
- if adapter_type == "linear":
353
- api_key = adapter_config.get("api_key")
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
- # Check API key format
356
- if not api_key:
357
- issues.append("Linear API key is missing")
358
- return issues
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
- if not api_key.startswith("lin_api_"):
361
- issues.append(
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
- # Test actual connectivity
367
- try:
368
- from ..adapters.linear import LinearAdapter
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
- adapter = JiraAdapter(adapter_config)
407
- await adapter.list(limit=1)
408
- except Exception as e:
409
- error_msg = str(e)
410
- if "401" in error_msg or "Unauthorized" in error_msg:
411
- issues.append(
412
- "Failed to authenticate with JIRA - invalid credentials"
413
- )
414
- elif "403" in error_msg or "Forbidden" in error_msg:
415
- issues.append("JIRA credentials lack required permissions")
416
- else:
417
- issues.append(f"Failed to connect to JIRA: {error_msg}")
418
-
419
- elif adapter_type == "github":
420
- token = adapter_config.get("token") or adapter_config.get("api_key")
421
- owner = adapter_config.get("owner")
422
- repo = adapter_config.get("repo")
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
- adapter = GitHubAdapter(adapter_config)
440
- await adapter.list(limit=1)
441
- except Exception as e:
442
- error_msg = str(e)
443
- if (
444
- "401" in error_msg
445
- or "Unauthorized" in error_msg
446
- or "Bad credentials" in error_msg
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
- except Exception as e:
463
- issues.append(f"Validation error: {str(e)}")
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
- return issues
472
+ # Run interactive wizard
473
+ configure_wizard()
466
474
 
467
475
 
468
- async def _validate_configuration_with_retry(
469
- console: Console, adapter_type: str, config_file_path: Path, proj_path: Path
470
- ) -> bool:
471
- """Validate configuration with retry loop for corrections.
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
- Returns:
480
- True if validation passed or user chose to continue, False if user chose to exit
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
- max_retries = 3
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
- # Run real adapter validation (suppress verbose output)
490
- import io
491
- import sys
513
+ # Add ticket command group to main app
514
+ app.add_typer(ticket_app, name="ticket")
492
515
 
493
- # Capture output to suppress verbose diagnostics output
494
- old_stdout = sys.stdout
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
- try:
500
- # Perform real adapter validation using diagnostics
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
- try:
526
- choice = typer.prompt("\nSelect option (1-3)", type=int, default=1)
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
- console.print(
545
- f"\n[cyan]Retry {retry_count}/{max_retries} - Re-entering configuration...[/cyan]"
546
- )
525
+ # Add instructions command to main app
526
+ app.add_typer(instruction_app, name="instructions")
547
527
 
548
- # Reload current config to get values
549
- import json
528
+ # Add project-update command group to main app
529
+ app.add_typer(project_update_app, name="project-update")
550
530
 
551
- with open(config_file_path) as f:
552
- current_config = json.load(f)
531
+ # Add setup and init commands to main app
532
+ app.command()(setup)
533
+ app.command()(init)
553
534
 
554
- # Re-prompt for adapter-specific configuration
555
- if adapter_type == "linear":
556
- console.print("\n[bold]Linear Configuration[/bold]")
557
- console.print(
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
- linear_api_key = typer.prompt(
562
- "Enter your Linear API key", hide_input=True
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
- team_input = typer.prompt("Team URL, key, or ID")
577
-
578
- # Check if input is a URL
579
- linear_team_id = None
580
- linear_team_key = None
581
-
582
- if team_input.startswith("https://linear.app/"):
583
- console.print("[cyan]Detected team URL, deriving team ID...[/cyan]")
584
- from .linear_commands import derive_team_from_url
585
-
586
- derived_team_id, error = await derive_team_from_url(
587
- linear_api_key, team_input
588
- )
589
-
590
- if derived_team_id:
591
- linear_team_id = derived_team_id
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
- console.print("\nYou need a JIRA API token.")
633
- console.print(
634
- "[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
635
- )
563
+ report = simple_diagnose()
564
+ if output_file:
565
+ import json
636
566
 
637
- token = typer.prompt("Enter your JIRA API token", hide_input=True)
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
- project = typer.prompt(
640
- "Default JIRA project key (optional, press Enter to skip)",
641
- default="",
642
- show_default=False,
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
- # Update config
646
- jira_config = {
647
- "server": server,
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
- elif adapter_type == "github":
658
- console.print("\n[bold]GitHub Configuration[/bold]")
659
- console.print("Enter your GitHub repository details.\n")
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
- console.print("\nYou need a GitHub Personal Access Token.")
667
- console.print(
668
- "[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
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
- token = typer.prompt(
675
- "Enter your GitHub Personal Access Token", hide_input=True
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
- Examples:
3527
- mcp-ticketer mcp stop
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
- console.print(
3531
- "[yellow]ℹ[/yellow] MCP server runs on-demand via stdio (not as a background service)"
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)