mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__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 (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py CHANGED
@@ -10,24 +10,24 @@ 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
27
23
  from .instruction_commands import app as instruction_app
24
+ from .mcp_server_commands import mcp_app
28
25
  from .migrate_config import migrate_config_command
29
26
  from .platform_commands import app as platform_app
27
+ from .platform_installer import install, remove, uninstall
28
+ from .project_update_commands import app as project_update_app
30
29
  from .queue_commands import app as queue_app
30
+ from .setup_command import setup
31
31
  from .ticket_commands import app as ticket_app
32
32
 
33
33
  # Load environment variables from .env files
@@ -93,6 +93,7 @@ def load_config(project_dir: Path | None = None) -> dict:
93
93
  from user home directory or system-wide locations.
94
94
 
95
95
  Args:
96
+ ----
96
97
  project_dir: Optional project directory to load config from
97
98
 
98
99
  Resolution order:
@@ -100,6 +101,7 @@ def load_config(project_dir: Path | None = None) -> dict:
100
101
  2. Default to aitrackdown adapter
101
102
 
102
103
  Returns:
104
+ -------
103
105
  Configuration dictionary with adapter and config keys.
104
106
  Defaults to aitrackdown if no local config exists.
105
107
 
@@ -151,6 +153,7 @@ def _discover_from_env_files() -> str | None:
151
153
  """Discover adapter configuration from .env or .env.local files.
152
154
 
153
155
  Returns:
156
+ -------
154
157
  Adapter name if discovered, None otherwise
155
158
 
156
159
  """
@@ -196,6 +199,7 @@ def _save_adapter_to_config(adapter_name: str) -> None:
196
199
  """Save adapter configuration to config file.
197
200
 
198
201
  Args:
202
+ ----
199
203
  adapter_name: Name of the adapter to save as default
200
204
 
201
205
  """
@@ -246,9 +250,11 @@ def merge_config(updates: dict) -> dict:
246
250
  """Merge updates into existing config.
247
251
 
248
252
  Args:
253
+ ----
249
254
  updates: Configuration updates to merge
250
255
 
251
256
  Returns:
257
+ -------
252
258
  Updated configuration
253
259
 
254
260
  """
@@ -276,6 +282,7 @@ def get_adapter(
276
282
  """Get configured adapter instance.
277
283
 
278
284
  Args:
285
+ ----
279
286
  override_adapter: Override the default adapter type
280
287
  override_config: Override configuration for the adapter
281
288
 
@@ -303,7 +310,6 @@ def get_adapter(
303
310
  adapter_config = config["config"]
304
311
 
305
312
  # Add environment variables for authentication
306
- import os
307
313
 
308
314
  if adapter_type == "linear":
309
315
  if not adapter_config.get("api_key"):
@@ -320,3220 +326,296 @@ def get_adapter(
320
326
  return AdapterRegistry.get_adapter(adapter_type, adapter_config)
321
327
 
322
328
 
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.
327
-
328
- Args:
329
- adapter_type: Type of adapter to validate
330
- config_file_path: Path to config file
331
-
332
- Returns:
333
- List of validation issues (empty if valid)
334
-
335
- """
336
- import json
337
-
338
- issues = []
339
-
340
- try:
341
- # Load config
342
- with open(config_file_path) as f:
343
- config = json.load(f)
344
-
345
- adapter_config = config.get("adapters", {}).get(adapter_type, {})
346
-
347
- if not adapter_config:
348
- issues.append(f"No configuration found for {adapter_type}")
349
- return issues
350
-
351
- # Validate based on adapter type
352
- if adapter_type == "linear":
353
- api_key = adapter_config.get("api_key")
354
-
355
- # Check API key format
356
- if not api_key:
357
- issues.append("Linear API key is missing")
358
- return issues
359
-
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
365
-
366
- # Test actual connectivity
367
- try:
368
- from ..adapters.linear import LinearAdapter
369
-
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
-
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
438
-
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")
461
-
462
- except Exception as e:
463
- issues.append(f"Validation error: {str(e)}")
464
-
465
- return issues
466
-
467
-
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.
472
-
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
-
479
- Returns:
480
- True if validation passed or user chose to continue, False if user chose to exit
481
-
482
- """
483
- max_retries = 3
484
- retry_count = 0
485
-
486
- while retry_count < max_retries:
487
- console.print("\n[cyan]šŸ” Validating configuration...[/cyan]")
488
-
489
- # Run real adapter validation (suppress verbose output)
490
- import io
491
- import sys
492
-
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()
498
-
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)")
524
-
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
543
-
544
- console.print(
545
- f"\n[cyan]Retry {retry_count}/{max_retries} - Re-entering configuration...[/cyan]"
546
- )
547
-
548
- # Reload current config to get values
549
- import json
550
-
551
- with open(config_file_path) as f:
552
- current_config = json.load(f)
553
-
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
- )
560
-
561
- linear_api_key = typer.prompt(
562
- "Enter your Linear API key", hide_input=True
563
- )
564
-
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
-
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")
631
-
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
- )
636
-
637
- token = typer.prompt("Enter your JIRA API token", hide_input=True)
638
-
639
- project = typer.prompt(
640
- "Default JIRA project key (optional, press Enter to skip)",
641
- default="",
642
- show_default=False,
643
- )
644
-
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
654
-
655
- current_config["adapters"]["jira"] = jira_config
656
-
657
- elif adapter_type == "github":
658
- console.print("\n[bold]GitHub Configuration[/bold]")
659
- console.print("Enter your GitHub repository details.\n")
660
-
661
- owner = typer.prompt(
662
- "GitHub repository owner (username or organization)"
663
- )
664
- repo = typer.prompt("GitHub repository name")
665
-
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
- )
673
-
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)"
329
+ @app.command("set")
330
+ def set_config(
331
+ adapter: AdapterType | None = typer.Option(
332
+ None, "--adapter", "-a", help="Set default adapter"
2653
333
  ),
2654
- global_config: bool = typer.Option(
2655
- False,
2656
- "--global",
2657
- "-g",
2658
- help="Save to global config instead of project-specific",
334
+ team_key: str | None = typer.Option(
335
+ None, "--team-key", help="Linear team key (e.g., BTA)"
2659
336
  ),
337
+ team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
338
+ owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
339
+ repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
340
+ server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
341
+ project: str | None = typer.Option(None, "--project", help="JIRA project key"),
2660
342
  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)",
343
+ None, "--base-path", help="AITrackdown base path"
2696
344
  ),
2697
345
  ) -> 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
346
+ """Set default adapter and adapter-specific configuration.
2726
347
 
348
+ When called without arguments, shows current configuration.
2727
349
  """
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
-
350
+ if not any([adapter, team_key, team_id, owner, repo, server, project, base_path]):
351
+ # Show current configuration
352
+ config = load_config()
353
+ console.print("[bold]Current Configuration:[/bold]")
2884
354
  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()
355
+ f"Default adapter: [cyan]{config.get('default_adapter', 'aitrackdown')}[/cyan]"
2908
356
  )
2909
357
 
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)")
358
+ adapters_config = config.get("adapters", {})
359
+ if adapters_config:
360
+ console.print("\n[bold]Adapter Settings:[/bold]")
361
+ for adapter_name, adapter_config in adapters_config.items():
362
+ console.print(f"\n[cyan]{adapter_name}:[/cyan]")
363
+ for key, value in adapter_config.items():
364
+ # Don't display sensitive values like tokens
365
+ if (
366
+ "token" in key.lower()
367
+ or "key" in key.lower()
368
+ and "team" not in key.lower()
369
+ ):
370
+ value = "***" if value else "not set"
371
+ console.print(f" {key}: {value}")
3046
372
  return
3047
373
 
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
374
+ updates = {}
3120
375
 
3121
- # Dry run to preview changes
3122
- mcp-ticketer uninstall claude-code --dry-run
376
+ # Set default adapter
377
+ if adapter:
378
+ updates["default_adapter"] = adapter.value
379
+ console.print(f"[green]āœ“[/green] Default adapter set to: {adapter.value}")
3123
380
 
3124
- """
3125
- # Call the remove command with the same parameters
3126
- remove(platform=platform, dry_run=dry_run)
381
+ # Build adapter-specific configuration
382
+ adapter_configs = {}
3127
383
 
384
+ # Linear configuration
385
+ if team_key or team_id:
386
+ linear_config = {}
387
+ if team_key:
388
+ linear_config["team_key"] = team_key
389
+ if team_id:
390
+ linear_config["team_id"] = team_id
391
+ adapter_configs["linear"] = linear_config
392
+ console.print("[green]āœ“[/green] Linear settings updated")
3128
393
 
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.
394
+ # GitHub configuration
395
+ if owner or repo:
396
+ github_config = {}
397
+ if owner:
398
+ github_config["owner"] = owner
399
+ if repo:
400
+ github_config["repo"] = repo
401
+ adapter_configs["github"] = github_config
402
+ console.print("[green]āœ“[/green] GitHub settings updated")
3132
403
 
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}")
404
+ # JIRA configuration
405
+ if server or project:
406
+ jira_config = {}
407
+ if server:
408
+ jira_config["server"] = server
409
+ if project:
410
+ jira_config["project_key"] = project
411
+ adapter_configs["jira"] = jira_config
412
+ console.print("[green]āœ“[/green] JIRA settings updated")
3159
413
 
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')}")
414
+ # AITrackdown configuration
415
+ if base_path:
416
+ adapter_configs["aitrackdown"] = {"base_path": base_path}
417
+ console.print("[green]āœ“[/green] AITrackdown settings updated")
3164
418
 
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}")
419
+ if adapter_configs:
420
+ updates["adapters"] = adapter_configs
3172
421
 
3173
- if item.retry_count > 0:
3174
- console.print(f"\nRetry Count: {item.retry_count}")
422
+ # Merge and save configuration
423
+ if updates:
424
+ config = merge_config(updates)
425
+ save_config(config)
426
+ console.print(f"[dim]Configuration saved to {CONFIG_FILE}[/dim]")
3175
427
 
3176
428
 
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"
429
+ @app.command("configure")
430
+ def configure_command(
431
+ show: bool = typer.Option(False, "--show", help="Show current configuration"),
432
+ adapter: str | None = typer.Option(
433
+ None, "--adapter", help="Set default adapter type"
3181
434
  ),
3182
- base_path: str | None = typer.Option(
3183
- None, "--base-path", help="Base path for AITrackdown adapter"
435
+ api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
436
+ project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
437
+ team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
438
+ global_scope: bool = typer.Option(
439
+ False,
440
+ "--global",
441
+ "-g",
442
+ help="Save to global config instead of project-specific",
3184
443
  ),
3185
444
  ) -> 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"]
445
+ """Configure MCP Ticketer integration.
3239
446
 
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
447
+ Run without arguments to launch interactive wizard.
448
+ Use --show to display current configuration.
449
+ Use options to set specific values directly.
450
+ """
451
+ # Show configuration
452
+ if show:
453
+ show_current_config()
454
+ return
3243
455
 
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]"
456
+ # Direct configuration
457
+ if any([adapter, api_key, project_id, team_id]):
458
+ set_adapter_config(
459
+ adapter=adapter,
460
+ api_key=api_key,
461
+ project_id=project_id,
462
+ team_id=team_id,
463
+ global_scope=global_scope,
3252
464
  )
465
+ return
3253
466
 
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)
467
+ # Run interactive wizard
468
+ configure_wizard()
3267
469
 
3268
470
 
3269
- @mcp_app.command(name="claude")
3270
- def mcp_claude(
3271
- global_config: bool = typer.Option(
471
+ @app.command("config")
472
+ def config_alias(
473
+ show: bool = typer.Option(False, "--show", help="Show current configuration"),
474
+ adapter: str | None = typer.Option(
475
+ None, "--adapter", help="Set default adapter type"
476
+ ),
477
+ api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
478
+ project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
479
+ team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
480
+ global_scope: bool = typer.Option(
3272
481
  False,
3273
482
  "--global",
3274
483
  "-g",
3275
- help="Configure Claude Desktop instead of project-level",
3276
- ),
3277
- force: bool = typer.Option(
3278
- False, "--force", "-f", help="Overwrite existing configuration"
484
+ help="Save to global config instead of project-specific",
3279
485
  ),
3280
486
  ) -> 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
487
+ """Alias for configure command - shorter syntax."""
488
+ configure_command(show, adapter, api_key, project_id, team_id, global_scope)
3298
489
 
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
490
 
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"
491
+ @app.command("migrate-config")
492
+ def migrate_config(
493
+ dry_run: bool = typer.Option(
494
+ False, "--dry-run", help="Show what would be done without making changes"
3319
495
  ),
3320
496
  ) -> 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
497
+ """Migrate configuration from old format to new format.
3338
498
 
499
+ This command will:
500
+ 1. Detect old configuration format
501
+ 2. Convert to new schema
502
+ 3. Backup old config
503
+ 4. Apply new config
3339
504
  """
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
505
+ migrate_config_command(dry_run=dry_run)
3348
506
 
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
507
 
508
+ # Add ticket command group to main app
509
+ app.add_typer(ticket_app, name="ticket")
3355
510
 
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.
511
+ # Add platform command group to main app
512
+ app.add_typer(platform_app, name="platform")
3363
513
 
3364
- Reads configuration from .mcp-ticketer/config.json and creates
3365
- Codex CLI config.toml with mcp-ticketer configuration.
514
+ # Add queue command to main app
515
+ app.add_typer(queue_app, name="queue")
3366
516
 
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.
517
+ # Add discover command to main app
518
+ app.add_typer(discover_app, name="discover")
3370
519
 
3371
- Examples:
3372
- # Configure Codex CLI globally
3373
- mcp-ticketer mcp codex
520
+ # Add instructions command to main app
521
+ app.add_typer(instruction_app, name="instructions")
3374
522
 
3375
- # Force overwrite existing configuration
3376
- mcp-ticketer mcp codex --force
523
+ # Add project-update command group to main app
524
+ app.add_typer(project_update_app, name="project-update")
3377
525
 
3378
- """
3379
- from ..cli.codex_configure import configure_codex_mcp
526
+ # Add setup and init commands to main app
527
+ app.command()(setup)
528
+ app.command()(init)
3380
529
 
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
530
+ # Add platform installer commands to main app
531
+ app.command()(install)
532
+ app.command()(remove)
533
+ app.command()(uninstall)
3386
534
 
3387
535
 
3388
- @mcp_app.command(name="auggie")
3389
- def mcp_auggie(
3390
- force: bool = typer.Option(
3391
- False, "--force", "-f", help="Overwrite existing configuration"
536
+ # Add diagnostics command
537
+ @app.command("doctor")
538
+ def doctor_command(
539
+ output_file: str | None = typer.Option(
540
+ None, "--output", "-o", help="Save full report to file"
541
+ ),
542
+ json_output: bool = typer.Option(
543
+ False, "--json", help="Output report in JSON format"
544
+ ),
545
+ simple: bool = typer.Option(
546
+ False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
3392
547
  ),
3393
548
  ) -> 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
549
+ """Run comprehensive system diagnostics and health check (alias: diagnose)."""
550
+ if simple:
551
+ from .simple_health import simple_diagnose
3432
552
 
3433
- console.print("[bold]MCP Server Status[/bold]\n")
553
+ report = simple_diagnose()
554
+ if output_file:
555
+ import json
3434
556
 
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")
557
+ with open(output_file, "w") as f:
558
+ json.dump(report, f, indent=2)
559
+ console.print(f"\nšŸ“„ Report saved to: {output_file}")
560
+ if json_output:
561
+ import json
3448
562
 
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
- )
563
+ console.print("\n" + json.dumps(report, indent=2))
564
+ if report["issues"]:
565
+ raise typer.Exit(1) from None
3455
566
  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
567
  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"
568
+ asyncio.run(
569
+ run_diagnostics(output_file=output_file, json_output=json_output)
3481
570
  )
3482
- else:
3483
- console.print("[yellow]ā—‹[/yellow] Claude Desktop not configured")
571
+ except typer.Exit:
572
+ # typer.Exit is expected - don't fall back to simple diagnostics
573
+ raise
574
+ except Exception as e:
575
+ console.print(f"āš ļø Full diagnostics failed: {e}")
576
+ console.print("šŸ”„ Falling back to simple diagnostics...")
577
+ from .simple_health import simple_diagnose
3484
578
 
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")
579
+ report = simple_diagnose()
580
+ if report["issues"]:
581
+ raise typer.Exit(1) from None
3498
582
 
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
583
 
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")
584
+ @app.command("diagnose", hidden=True)
585
+ def diagnose_alias(
586
+ output_file: str | None = typer.Option(
587
+ None, "--output", "-o", help="Save full report to file"
588
+ ),
589
+ json_output: bool = typer.Option(
590
+ False, "--json", help="Output report in JSON format"
591
+ ),
592
+ simple: bool = typer.Option(
593
+ False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
594
+ ),
595
+ ) -> None:
596
+ """Run comprehensive system diagnostics and health check (alias for doctor)."""
597
+ # Call the doctor_command function with the same parameters
598
+ doctor_command(output_file=output_file, json_output=json_output, simple=simple)
3512
599
 
3513
- console.print(
3514
- "\n[dim]Run 'mcp-ticketer install <platform>' to configure a platform[/dim]"
3515
- )
3516
600
 
601
+ @app.command("status")
602
+ def status_command() -> None:
603
+ """Quick health check - shows system status summary (alias: health)."""
604
+ from .simple_health import simple_health_check
3517
605
 
3518
- @mcp_app.command(name="stop")
3519
- def mcp_stop() -> None:
3520
- """Stop MCP server (placeholder - MCP runs on-demand via stdio).
606
+ result = simple_health_check()
607
+ if result != 0:
608
+ raise typer.Exit(result) from None
3521
609
 
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
610
 
3526
- Examples:
3527
- mcp-ticketer mcp stop
611
+ @app.command("health")
612
+ def health_alias() -> None:
613
+ """Quick health check - shows system status summary (alias for status)."""
614
+ from .simple_health import simple_health_check
3528
615
 
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
- )
616
+ result = simple_health_check()
617
+ if result != 0:
618
+ raise typer.Exit(result) from None
3537
619
 
3538
620
 
3539
621
  # Add command groups to main app (must be after all subcommands are defined)