mcp-ticketer 0.4.11__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 (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py CHANGED
@@ -5,27 +5,29 @@ import json
5
5
  import os
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
+ from typing import Any
8
9
 
9
10
  import typer
10
11
  from dotenv import load_dotenv
11
12
  from rich.console import Console
12
- from rich.table import Table
13
13
 
14
14
  # Import adapters module to trigger registration
15
15
  import mcp_ticketer.adapters # noqa: F401
16
16
 
17
17
  from ..__version__ import __version__
18
- from ..core import AdapterRegistry, Priority, TicketState
19
- from ..core.models import Comment, SearchQuery
20
- from ..queue import Queue, QueueStatus, WorkerManager
21
- from ..queue.health_monitor import HealthStatus, QueueHealthMonitor
22
- from ..queue.ticket_registry import TicketRegistry
18
+ from ..core import AdapterRegistry
23
19
  from .configure import configure_wizard, set_adapter_config, show_current_config
24
20
  from .diagnostics import run_diagnostics
25
21
  from .discover import app as discover_app
22
+ from .init_command import init
23
+ from .instruction_commands import app as instruction_app
24
+ from .mcp_server_commands import mcp_app
26
25
  from .migrate_config import migrate_config_command
27
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
28
29
  from .queue_commands import app as queue_app
30
+ from .setup_command import setup
29
31
  from .ticket_commands import app as ticket_app
30
32
 
31
33
  # Load environment variables from .env files
@@ -48,11 +50,11 @@ app = typer.Typer(
48
50
  console = Console()
49
51
 
50
52
 
51
- def version_callback(value: bool):
53
+ def version_callback(value: bool) -> None:
52
54
  """Print version and exit."""
53
55
  if value:
54
56
  console.print(f"mcp-ticketer version {__version__}")
55
- raise typer.Exit()
57
+ raise typer.Exit() from None
56
58
 
57
59
 
58
60
  @app.callback()
@@ -65,7 +67,7 @@ def main_callback(
65
67
  is_eager=True,
66
68
  help="Show version and exit",
67
69
  ),
68
- ):
70
+ ) -> None:
69
71
  """MCP Ticketer - Universal ticket management interface."""
70
72
  pass
71
73
 
@@ -91,6 +93,7 @@ def load_config(project_dir: Path | None = None) -> dict:
91
93
  from user home directory or system-wide locations.
92
94
 
93
95
  Args:
96
+ ----
94
97
  project_dir: Optional project directory to load config from
95
98
 
96
99
  Resolution order:
@@ -98,6 +101,7 @@ def load_config(project_dir: Path | None = None) -> dict:
98
101
  2. Default to aitrackdown adapter
99
102
 
100
103
  Returns:
104
+ -------
101
105
  Configuration dictionary with adapter and config keys.
102
106
  Defaults to aitrackdown if no local config exists.
103
107
 
@@ -149,6 +153,7 @@ def _discover_from_env_files() -> str | None:
149
153
  """Discover adapter configuration from .env or .env.local files.
150
154
 
151
155
  Returns:
156
+ -------
152
157
  Adapter name if discovered, None otherwise
153
158
 
154
159
  """
@@ -194,6 +199,7 @@ def _save_adapter_to_config(adapter_name: str) -> None:
194
199
  """Save adapter configuration to config file.
195
200
 
196
201
  Args:
202
+ ----
197
203
  adapter_name: Name of the adapter to save as default
198
204
 
199
205
  """
@@ -244,9 +250,11 @@ def merge_config(updates: dict) -> dict:
244
250
  """Merge updates into existing config.
245
251
 
246
252
  Args:
253
+ ----
247
254
  updates: Configuration updates to merge
248
255
 
249
256
  Returns:
257
+ -------
250
258
  Updated configuration
251
259
 
252
260
  """
@@ -270,10 +278,11 @@ def merge_config(updates: dict) -> dict:
270
278
 
271
279
  def get_adapter(
272
280
  override_adapter: str | None = None, override_config: dict | None = None
273
- ):
281
+ ) -> Any:
274
282
  """Get configured adapter instance.
275
283
 
276
284
  Args:
285
+ ----
277
286
  override_adapter: Override the default adapter type
278
287
  override_config: Override configuration for the adapter
279
288
 
@@ -301,7 +310,6 @@ def get_adapter(
301
310
  adapter_config = config["config"]
302
311
 
303
312
  # Add environment variables for authentication
304
- import os
305
313
 
306
314
  if adapter_type == "linear":
307
315
  if not adapter_config.get("api_key"):
@@ -318,592 +326,6 @@ def get_adapter(
318
326
  return AdapterRegistry.get_adapter(adapter_type, adapter_config)
319
327
 
320
328
 
321
- def _prompt_for_adapter_selection(console: Console) -> str:
322
- """Interactive prompt for adapter selection.
323
-
324
- Args:
325
- console: Rich console for output
326
-
327
- Returns:
328
- Selected adapter type
329
-
330
- """
331
- console.print("\n[bold blue]🚀 MCP Ticketer Setup[/bold blue]")
332
- console.print("Choose which ticket system you want to connect to:\n")
333
-
334
- # Define adapter options with descriptions
335
- adapters = [
336
- {
337
- "name": "linear",
338
- "title": "Linear",
339
- "description": "Modern project management (linear.app)",
340
- "requirements": "API key and team ID",
341
- },
342
- {
343
- "name": "github",
344
- "title": "GitHub Issues",
345
- "description": "GitHub repository issues",
346
- "requirements": "Personal access token, owner, and repo",
347
- },
348
- {
349
- "name": "jira",
350
- "title": "JIRA",
351
- "description": "Atlassian JIRA project management",
352
- "requirements": "Server URL, email, and API token",
353
- },
354
- {
355
- "name": "aitrackdown",
356
- "title": "Local Files (AITrackdown)",
357
- "description": "Store tickets in local files (no external service)",
358
- "requirements": "None - works offline",
359
- },
360
- ]
361
-
362
- # Display options
363
- for i, adapter in enumerate(adapters, 1):
364
- console.print(f"[cyan]{i}.[/cyan] [bold]{adapter['title']}[/bold]")
365
- console.print(f" {adapter['description']}")
366
- console.print(f" [dim]Requirements: {adapter['requirements']}[/dim]\n")
367
-
368
- # Get user selection
369
- while True:
370
- try:
371
- choice = typer.prompt("Select adapter (1-4)", type=int, default=1)
372
- if 1 <= choice <= len(adapters):
373
- selected_adapter = adapters[choice - 1]
374
- console.print(
375
- f"\n[green]✓ Selected: {selected_adapter['title']}[/green]"
376
- )
377
- return selected_adapter["name"]
378
- else:
379
- console.print(
380
- f"[red]Please enter a number between 1 and {len(adapters)}[/red]"
381
- )
382
- except (ValueError, typer.Abort):
383
- console.print("[yellow]Setup cancelled.[/yellow]")
384
- raise typer.Exit(0)
385
-
386
-
387
- @app.command()
388
- def setup(
389
- adapter: str | None = typer.Option(
390
- None,
391
- "--adapter",
392
- "-a",
393
- help="Adapter type to use (interactive prompt if not specified)",
394
- ),
395
- project_path: str | None = typer.Option(
396
- None, "--path", help="Project path (default: current directory)"
397
- ),
398
- global_config: bool = typer.Option(
399
- False,
400
- "--global",
401
- "-g",
402
- help="Save to global config instead of project-specific",
403
- ),
404
- base_path: str | None = typer.Option(
405
- None,
406
- "--base-path",
407
- "-p",
408
- help="Base path for ticket storage (AITrackdown only)",
409
- ),
410
- api_key: str | None = typer.Option(
411
- None, "--api-key", help="API key for Linear or API token for JIRA"
412
- ),
413
- team_id: str | None = typer.Option(
414
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
415
- ),
416
- jira_server: str | None = typer.Option(
417
- None,
418
- "--jira-server",
419
- help="JIRA server URL (e.g., https://company.atlassian.net)",
420
- ),
421
- jira_email: str | None = typer.Option(
422
- None, "--jira-email", help="JIRA user email for authentication"
423
- ),
424
- jira_project: str | None = typer.Option(
425
- None, "--jira-project", help="Default JIRA project key"
426
- ),
427
- github_owner: str | None = typer.Option(
428
- None, "--github-owner", help="GitHub repository owner"
429
- ),
430
- github_repo: str | None = typer.Option(
431
- None, "--github-repo", help="GitHub repository name"
432
- ),
433
- github_token: str | None = typer.Option(
434
- None, "--github-token", help="GitHub Personal Access Token"
435
- ),
436
- ) -> None:
437
- """Interactive setup wizard for MCP Ticketer (alias for init).
438
-
439
- This command provides a user-friendly setup experience with prompts
440
- to guide you through configuring MCP Ticketer for your preferred
441
- ticket management system. It's identical to 'init' and 'install'.
442
-
443
- Examples:
444
- # Run interactive setup
445
- mcp-ticketer setup
446
-
447
- # Setup with specific adapter
448
- mcp-ticketer setup --adapter linear
449
-
450
- # Setup for different project
451
- mcp-ticketer setup --path /path/to/project
452
-
453
- """
454
- # Call init with all parameters
455
- init(
456
- adapter=adapter,
457
- project_path=project_path,
458
- global_config=global_config,
459
- base_path=base_path,
460
- api_key=api_key,
461
- team_id=team_id,
462
- jira_server=jira_server,
463
- jira_email=jira_email,
464
- jira_project=jira_project,
465
- github_owner=github_owner,
466
- github_repo=github_repo,
467
- github_token=github_token,
468
- )
469
-
470
-
471
- @app.command()
472
- def init(
473
- adapter: str | None = typer.Option(
474
- None,
475
- "--adapter",
476
- "-a",
477
- help="Adapter type to use (interactive prompt if not specified)",
478
- ),
479
- project_path: str | None = typer.Option(
480
- None, "--path", help="Project path (default: current directory)"
481
- ),
482
- global_config: bool = typer.Option(
483
- False,
484
- "--global",
485
- "-g",
486
- help="Save to global config instead of project-specific",
487
- ),
488
- base_path: str | None = typer.Option(
489
- None,
490
- "--base-path",
491
- "-p",
492
- help="Base path for ticket storage (AITrackdown only)",
493
- ),
494
- api_key: str | None = typer.Option(
495
- None, "--api-key", help="API key for Linear or API token for JIRA"
496
- ),
497
- team_id: str | None = typer.Option(
498
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
499
- ),
500
- jira_server: str | None = typer.Option(
501
- None,
502
- "--jira-server",
503
- help="JIRA server URL (e.g., https://company.atlassian.net)",
504
- ),
505
- jira_email: str | None = typer.Option(
506
- None, "--jira-email", help="JIRA user email for authentication"
507
- ),
508
- jira_project: str | None = typer.Option(
509
- None, "--jira-project", help="Default JIRA project key"
510
- ),
511
- github_owner: str | None = typer.Option(
512
- None, "--github-owner", help="GitHub repository owner"
513
- ),
514
- github_repo: str | None = typer.Option(
515
- None, "--github-repo", help="GitHub repository name"
516
- ),
517
- github_token: str | None = typer.Option(
518
- None, "--github-token", help="GitHub Personal Access Token"
519
- ),
520
- ) -> None:
521
- """Initialize mcp-ticketer for the current project (synonymous with 'install' and 'setup').
522
-
523
- This command sets up MCP Ticketer configuration with interactive prompts
524
- to guide you through the process. It auto-detects adapter configuration
525
- from .env files or prompts for interactive setup if no configuration is found.
526
-
527
- Creates .mcp-ticketer/config.json in the current directory with
528
- auto-detected or specified adapter configuration.
529
-
530
- Note: 'init', 'install', and 'setup' are all synonyms - use whichever feels natural.
531
-
532
- Examples:
533
- # Interactive setup (all three commands are identical)
534
- mcp-ticketer init
535
- mcp-ticketer install
536
- mcp-ticketer setup
537
-
538
- # Force specific adapter
539
- mcp-ticketer init --adapter linear
540
-
541
- # Initialize for different project
542
- mcp-ticketer init --path /path/to/project
543
-
544
- # Save globally (not recommended)
545
- mcp-ticketer init --global
546
-
547
- """
548
- from pathlib import Path
549
-
550
- from ..core.env_discovery import discover_config
551
- from ..core.project_config import ConfigResolver
552
-
553
- # Determine project path
554
- proj_path = Path(project_path) if project_path else Path.cwd()
555
-
556
- # Check if already initialized (unless using --global)
557
- if not global_config:
558
- config_path = proj_path / ".mcp-ticketer" / "config.json"
559
-
560
- if config_path.exists():
561
- if not typer.confirm(
562
- f"Configuration already exists at {config_path}. Overwrite?",
563
- default=False,
564
- ):
565
- console.print("[yellow]Initialization cancelled.[/yellow]")
566
- raise typer.Exit(0)
567
-
568
- # 1. Try auto-discovery if no adapter specified
569
- discovered = None
570
- adapter_type = adapter
571
-
572
- if not adapter_type:
573
- console.print(
574
- "[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
575
- )
576
-
577
- # First try our improved .env configuration loader
578
- from ..mcp.server.main import _load_env_configuration
579
-
580
- env_config = _load_env_configuration()
581
-
582
- if env_config:
583
- adapter_type = env_config["adapter_type"]
584
- console.print(
585
- f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
586
- )
587
-
588
- # Show what was discovered
589
- console.print("\n[dim]Configuration found in: .env files[/dim]")
590
- console.print("[dim]Confidence: 100%[/dim]")
591
-
592
- # Ask user to confirm auto-detected adapter
593
- if not typer.confirm(
594
- f"Use detected {adapter_type} adapter?",
595
- default=True,
596
- ):
597
- adapter_type = None # Will trigger interactive selection
598
- else:
599
- # Fallback to old discovery system for backward compatibility
600
- discovered = discover_config(proj_path)
601
-
602
- if discovered and discovered.adapters:
603
- primary = discovered.get_primary_adapter()
604
- if primary:
605
- adapter_type = primary.adapter_type
606
- console.print(
607
- f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
608
- )
609
-
610
- # Show what was discovered
611
- console.print(
612
- f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
613
- )
614
- console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
615
-
616
- # Ask user to confirm auto-detected adapter
617
- if not typer.confirm(
618
- f"Use detected {adapter_type} adapter?",
619
- default=True,
620
- ):
621
- adapter_type = None # Will trigger interactive selection
622
- else:
623
- adapter_type = None # Will trigger interactive selection
624
- else:
625
- adapter_type = None # Will trigger interactive selection
626
-
627
- # If no adapter determined, show interactive selection
628
- if not adapter_type:
629
- adapter_type = _prompt_for_adapter_selection(console)
630
-
631
- # 2. Create configuration based on adapter type
632
- config = {"default_adapter": adapter_type, "adapters": {}}
633
-
634
- # 3. If discovered and matches adapter_type, use discovered config
635
- if discovered and adapter_type != "aitrackdown":
636
- discovered_adapter = discovered.get_adapter_by_type(adapter_type)
637
- if discovered_adapter:
638
- adapter_config = discovered_adapter.config.copy()
639
- # Ensure the config has the correct 'type' field
640
- adapter_config["type"] = adapter_type
641
- # Remove 'adapter' field if present (legacy)
642
- adapter_config.pop("adapter", None)
643
- config["adapters"][adapter_type] = adapter_config
644
-
645
- # 4. Handle manual configuration for specific adapters
646
- if adapter_type == "aitrackdown":
647
- config["adapters"]["aitrackdown"] = {
648
- "type": "aitrackdown",
649
- "base_path": base_path or ".aitrackdown",
650
- }
651
-
652
- elif adapter_type == "linear":
653
- # If not auto-discovered, build from CLI params or prompt
654
- if adapter_type not in config["adapters"]:
655
- linear_config = {}
656
-
657
- # API Key
658
- linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
659
- if not linear_api_key and not discovered:
660
- console.print("\n[bold]Linear Configuration[/bold]")
661
- console.print("You need a Linear API key to connect to Linear.")
662
- console.print(
663
- "[dim]Get your API key at: https://linear.app/settings/api[/dim]\n"
664
- )
665
-
666
- linear_api_key = typer.prompt(
667
- "Enter your Linear API key", hide_input=True
668
- )
669
-
670
- if linear_api_key:
671
- linear_config["api_key"] = linear_api_key
672
-
673
- # Team ID or Team Key
674
- # Try environment variables first
675
- linear_team_key = os.getenv("LINEAR_TEAM_KEY")
676
- linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
677
-
678
- if not linear_team_key and not linear_team_id and not discovered:
679
- console.print("\n[bold]Linear Team Configuration[/bold]")
680
- console.print("Enter your team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
681
- console.print(
682
- "[dim]Find it in: Linear Settings → Teams → Your Team → Key field[/dim]\n"
683
- )
684
-
685
- linear_team_key = typer.prompt("Team key")
686
-
687
- # Save whichever was provided
688
- if linear_team_key:
689
- linear_config["team_key"] = linear_team_key
690
- if linear_team_id:
691
- linear_config["team_id"] = linear_team_id
692
-
693
- if not linear_config.get("api_key") or (
694
- not linear_config.get("team_id") and not linear_config.get("team_key")
695
- ):
696
- console.print(
697
- "[red]Error:[/red] Linear requires both API key and team ID/key"
698
- )
699
- console.print(
700
- "Run 'mcp-ticketer init --adapter linear' with proper credentials"
701
- )
702
- raise typer.Exit(1)
703
-
704
- linear_config["type"] = "linear"
705
- config["adapters"]["linear"] = linear_config
706
-
707
- elif adapter_type == "jira":
708
- # If not auto-discovered, build from CLI params or prompt
709
- if adapter_type not in config["adapters"]:
710
- server = jira_server or os.getenv("JIRA_SERVER")
711
- email = jira_email or os.getenv("JIRA_EMAIL")
712
- token = api_key or os.getenv("JIRA_API_TOKEN")
713
- project = jira_project or os.getenv("JIRA_PROJECT_KEY")
714
-
715
- # Interactive prompts for missing values
716
- if not server and not discovered:
717
- console.print("\n[bold]JIRA Configuration[/bold]")
718
- console.print("Enter your JIRA server details.\n")
719
-
720
- server = typer.prompt(
721
- "JIRA server URL (e.g., https://company.atlassian.net)"
722
- )
723
-
724
- if not email and not discovered:
725
- email = typer.prompt("Your JIRA email address")
726
-
727
- if not token and not discovered:
728
- console.print("\nYou need a JIRA API token.")
729
- console.print(
730
- "[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
731
- )
732
-
733
- token = typer.prompt("Enter your JIRA API token", hide_input=True)
734
-
735
- if not project and not discovered:
736
- project = typer.prompt(
737
- "Default JIRA project key (optional, press Enter to skip)",
738
- default="",
739
- show_default=False,
740
- )
741
-
742
- # Validate required fields
743
- if not server:
744
- console.print("[red]Error:[/red] JIRA server URL is required")
745
- raise typer.Exit(1)
746
-
747
- if not email:
748
- console.print("[red]Error:[/red] JIRA email is required")
749
- raise typer.Exit(1)
750
-
751
- if not token:
752
- console.print("[red]Error:[/red] JIRA API token is required")
753
- raise typer.Exit(1)
754
-
755
- jira_config = {
756
- "server": server,
757
- "email": email,
758
- "api_token": token,
759
- "type": "jira",
760
- }
761
-
762
- if project:
763
- jira_config["project_key"] = project
764
-
765
- config["adapters"]["jira"] = jira_config
766
-
767
- elif adapter_type == "github":
768
- # If not auto-discovered, build from CLI params or prompt
769
- if adapter_type not in config["adapters"]:
770
- owner = github_owner or os.getenv("GITHUB_OWNER")
771
- repo = github_repo or os.getenv("GITHUB_REPO")
772
- token = github_token or os.getenv("GITHUB_TOKEN")
773
-
774
- # Interactive prompts for missing values
775
- if not owner and not discovered:
776
- console.print("\n[bold]GitHub Configuration[/bold]")
777
- console.print("Enter your GitHub repository details.\n")
778
-
779
- owner = typer.prompt(
780
- "GitHub repository owner (username or organization)"
781
- )
782
-
783
- if not repo and not discovered:
784
- repo = typer.prompt("GitHub repository name")
785
-
786
- if not token and not discovered:
787
- console.print("\nYou need a GitHub Personal Access Token.")
788
- console.print(
789
- "[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
790
- )
791
- console.print(
792
- "[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n"
793
- )
794
-
795
- token = typer.prompt(
796
- "Enter your GitHub Personal Access Token", hide_input=True
797
- )
798
-
799
- # Validate required fields
800
- if not owner:
801
- console.print("[red]Error:[/red] GitHub repository owner is required")
802
- raise typer.Exit(1)
803
-
804
- if not repo:
805
- console.print("[red]Error:[/red] GitHub repository name is required")
806
- raise typer.Exit(1)
807
-
808
- if not token:
809
- console.print(
810
- "[red]Error:[/red] GitHub Personal Access Token is required"
811
- )
812
- raise typer.Exit(1)
813
-
814
- config["adapters"]["github"] = {
815
- "owner": owner,
816
- "repo": repo,
817
- "token": token,
818
- "type": "github",
819
- }
820
-
821
- # 5. Save to appropriate location
822
- if global_config:
823
- # Save to ~/.mcp-ticketer/config.json
824
- resolver = ConfigResolver(project_path=proj_path)
825
- config_file_path = resolver.GLOBAL_CONFIG_PATH
826
- config_file_path.parent.mkdir(parents=True, exist_ok=True)
827
-
828
- with open(config_file_path, "w") as f:
829
- json.dump(config, f, indent=2)
830
-
831
- console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
832
- console.print(f"[dim]Global configuration saved to {config_file_path}[/dim]")
833
- else:
834
- # Save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
835
- config_file_path = proj_path / ".mcp-ticketer" / "config.json"
836
- config_file_path.parent.mkdir(parents=True, exist_ok=True)
837
-
838
- with open(config_file_path, "w") as f:
839
- json.dump(config, f, indent=2)
840
-
841
- console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
842
- console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
843
-
844
- # Add .mcp-ticketer to .gitignore if not already there
845
- gitignore_path = proj_path / ".gitignore"
846
- if gitignore_path.exists():
847
- gitignore_content = gitignore_path.read_text()
848
- if ".mcp-ticketer" not in gitignore_content:
849
- with open(gitignore_path, "a") as f:
850
- f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
851
- console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
852
- else:
853
- # Create .gitignore if it doesn't exist
854
- with open(gitignore_path, "w") as f:
855
- f.write("# MCP Ticketer\n.mcp-ticketer/\n")
856
- console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
857
-
858
- # Show next steps
859
- _show_next_steps(console, adapter_type, config_file_path)
860
-
861
-
862
- def _show_next_steps(
863
- console: Console, adapter_type: str, config_file_path: Path
864
- ) -> None:
865
- """Show helpful next steps after initialization.
866
-
867
- Args:
868
- console: Rich console for output
869
- adapter_type: Type of adapter that was configured
870
- config_file_path: Path to the configuration file
871
-
872
- """
873
- console.print("\n[bold green]🎉 Setup Complete![/bold green]")
874
- console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
875
-
876
- console.print("[bold]Next Steps:[/bold]")
877
- console.print("1. [cyan]Test your configuration:[/cyan]")
878
- console.print(" mcp-ticketer diagnose")
879
- console.print("\n2. [cyan]Create a test ticket:[/cyan]")
880
- console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
881
-
882
- if adapter_type != "aitrackdown":
883
- console.print(
884
- f"\n3. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]"
885
- )
886
-
887
- if adapter_type == "linear":
888
- console.print(" Check your Linear workspace for the new ticket")
889
- elif adapter_type == "github":
890
- console.print(" Check your GitHub repository's Issues tab")
891
- elif adapter_type == "jira":
892
- console.print(" Check your JIRA project for the new ticket")
893
- else:
894
- console.print("\n3. [cyan]Check local ticket storage:[/cyan]")
895
- console.print(" ls .aitrackdown/")
896
-
897
- console.print("\n4. [cyan]Install MCP for AI clients (optional):[/cyan]")
898
- console.print(" mcp-ticketer install claude-code # For Claude Code")
899
- console.print(" mcp-ticketer install claude-desktop # For Claude Desktop")
900
- console.print(" mcp-ticketer install auggie # For Auggie")
901
- console.print(" mcp-ticketer install gemini # For Gemini CLI")
902
-
903
- console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
904
- console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
905
-
906
-
907
329
  @app.command("set")
908
330
  def set_config(
909
331
  adapter: AdapterType | None = typer.Option(
@@ -1046,6 +468,26 @@ def configure_command(
1046
468
  configure_wizard()
1047
469
 
1048
470
 
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(
481
+ False,
482
+ "--global",
483
+ "-g",
484
+ help="Save to global config instead of project-specific",
485
+ ),
486
+ ) -> None:
487
+ """Alias for configure command - shorter syntax."""
488
+ configure_command(show, adapter, api_key, project_id, team_id, global_scope)
489
+
490
+
1049
491
  @app.command("migrate-config")
1050
492
  def migrate_config(
1051
493
  dry_run: bool = typer.Option(
@@ -1063,784 +505,97 @@ def migrate_config(
1063
505
  migrate_config_command(dry_run=dry_run)
1064
506
 
1065
507
 
1066
- @app.command("queue-status", deprecated=True, hidden=True)
1067
- def old_queue_status_command():
1068
- """Show queue and worker status.
508
+ # Add ticket command group to main app
509
+ app.add_typer(ticket_app, name="ticket")
1069
510
 
1070
- DEPRECATED: Use 'mcp-ticketer queue status' instead.
1071
- """
1072
- console.print(
1073
- "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer queue status' instead.[/yellow]\n"
1074
- )
1075
-
1076
- queue = Queue()
1077
- manager = WorkerManager()
1078
-
1079
- # Get queue stats
1080
- stats = queue.get_stats()
1081
- pending = stats.get(QueueStatus.PENDING.value, 0)
1082
-
1083
- # Show queue status
1084
- console.print("[bold]Queue Status:[/bold]")
1085
- console.print(f" Pending: {pending}")
1086
- console.print(f" Processing: {stats.get(QueueStatus.PROCESSING.value, 0)}")
1087
- console.print(f" Completed: {stats.get(QueueStatus.COMPLETED.value, 0)}")
1088
- console.print(f" Failed: {stats.get(QueueStatus.FAILED.value, 0)}")
1089
-
1090
- # Show worker status
1091
- worker_status = manager.get_status()
1092
- if worker_status["running"]:
1093
- console.print(
1094
- f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})"
1095
- )
1096
- else:
1097
- console.print("\n[red]○ Worker is not running[/red]")
1098
- if pending > 0:
1099
- console.print(
1100
- "[yellow]Note: There are pending items. Start worker with 'mcp-ticketer queue worker start'[/yellow]"
1101
- )
511
+ # Add platform command group to main app
512
+ app.add_typer(platform_app, name="platform")
1102
513
 
514
+ # Add queue command to main app
515
+ app.add_typer(queue_app, name="queue")
1103
516
 
1104
- @app.command("queue-health", deprecated=True, hidden=True)
1105
- def old_queue_health_command(
1106
- auto_repair: bool = typer.Option(
1107
- False, "--auto-repair", help="Attempt automatic repair of issues"
1108
- ),
1109
- verbose: bool = typer.Option(
1110
- False, "--verbose", "-v", help="Show detailed health information"
1111
- ),
1112
- ) -> None:
1113
- """Check queue system health and detect issues immediately.
517
+ # Add discover command to main app
518
+ app.add_typer(discover_app, name="discover")
1114
519
 
1115
- DEPRECATED: Use 'mcp-ticketer queue health' instead.
1116
- """
1117
- console.print(
1118
- "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer queue health' instead.[/yellow]\n"
1119
- )
1120
- health_monitor = QueueHealthMonitor()
1121
- health = health_monitor.check_health()
1122
-
1123
- # Display overall status
1124
- status_color = {
1125
- HealthStatus.HEALTHY: "green",
1126
- HealthStatus.WARNING: "yellow",
1127
- HealthStatus.CRITICAL: "red",
1128
- HealthStatus.FAILED: "red",
1129
- }
1130
-
1131
- status_icon = {
1132
- HealthStatus.HEALTHY: "✓",
1133
- HealthStatus.WARNING: "⚠️",
1134
- HealthStatus.CRITICAL: "🚨",
1135
- HealthStatus.FAILED: "❌",
1136
- }
1137
-
1138
- color = status_color.get(health["status"], "white")
1139
- icon = status_icon.get(health["status"], "?")
1140
-
1141
- console.print(f"[{color}]{icon} Queue Health: {health['status'].upper()}[/{color}]")
1142
- console.print(f"Last checked: {health['timestamp']}")
1143
-
1144
- # Display alerts
1145
- if health["alerts"]:
1146
- console.print("\n[bold]Issues Found:[/bold]")
1147
- for alert in health["alerts"]:
1148
- alert_color = status_color.get(alert["level"], "white")
1149
- console.print(f"[{alert_color}] • {alert['message']}[/{alert_color}]")
1150
-
1151
- if verbose and alert.get("details"):
1152
- for key, value in alert["details"].items():
1153
- console.print(f" {key}: {value}")
1154
- else:
1155
- console.print("\n[green]✓ No issues detected[/green]")
1156
-
1157
- # Auto-repair if requested
1158
- if auto_repair and health["status"] in [
1159
- HealthStatus.CRITICAL,
1160
- HealthStatus.WARNING,
1161
- ]:
1162
- console.print("\n[yellow]Attempting automatic repair...[/yellow]")
1163
- repair_result = health_monitor.auto_repair()
1164
-
1165
- if repair_result["actions_taken"]:
1166
- console.print("[green]Repair actions taken:[/green]")
1167
- for action in repair_result["actions_taken"]:
1168
- console.print(f"[green] ✓ {action}[/green]")
1169
-
1170
- # Re-check health
1171
- console.print("\n[yellow]Re-checking health after repair...[/yellow]")
1172
- new_health = health_monitor.check_health()
1173
- new_color = status_color.get(new_health["status"], "white")
1174
- new_icon = status_icon.get(new_health["status"], "?")
1175
- console.print(
1176
- f"[{new_color}]{new_icon} Updated Health: {new_health['status'].upper()}[/{new_color}]"
1177
- )
1178
- else:
1179
- console.print("[yellow]No repair actions available[/yellow]")
520
+ # Add instructions command to main app
521
+ app.add_typer(instruction_app, name="instructions")
1180
522
 
1181
- # Exit with appropriate code
1182
- if health["status"] == HealthStatus.CRITICAL:
1183
- raise typer.Exit(1)
1184
- elif health["status"] == HealthStatus.WARNING:
1185
- raise typer.Exit(2)
523
+ # Add project-update command group to main app
524
+ app.add_typer(project_update_app, name="project-update")
1186
525
 
526
+ # Add setup and init commands to main app
527
+ app.command()(setup)
528
+ app.command()(init)
1187
529
 
1188
- @app.command(deprecated=True, hidden=True)
1189
- def create(
1190
- title: str = typer.Argument(..., help="Ticket title"),
1191
- description: str | None = typer.Option(
1192
- None, "--description", "-d", help="Ticket description"
1193
- ),
1194
- priority: Priority = typer.Option(
1195
- Priority.MEDIUM, "--priority", "-p", help="Priority level"
1196
- ),
1197
- tags: list[str] | None = typer.Option(
1198
- None, "--tag", "-t", help="Tags (can be specified multiple times)"
1199
- ),
1200
- assignee: str | None = typer.Option(
1201
- None, "--assignee", "-a", help="Assignee username"
1202
- ),
1203
- project: str | None = typer.Option(
1204
- None,
1205
- "--project",
1206
- help="Parent project/epic ID (synonym for --epic)",
530
+ # Add platform installer commands to main app
531
+ app.command()(install)
532
+ app.command()(remove)
533
+ app.command()(uninstall)
534
+
535
+
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"
1207
541
  ),
1208
- epic: str | None = typer.Option(
1209
- None,
1210
- "--epic",
1211
- help="Parent epic/project ID (synonym for --project)",
542
+ json_output: bool = typer.Option(
543
+ False, "--json", help="Output report in JSON format"
1212
544
  ),
1213
- adapter: AdapterType | None = typer.Option(
1214
- None, "--adapter", help="Override default adapter"
545
+ simple: bool = typer.Option(
546
+ False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1215
547
  ),
1216
548
  ) -> None:
1217
- """Create a new ticket with comprehensive health checks.
549
+ """Run comprehensive system diagnostics and health check (alias: diagnose)."""
550
+ if simple:
551
+ from .simple_health import simple_diagnose
1218
552
 
1219
- DEPRECATED: Use 'mcp-ticketer ticket create' instead.
1220
- """
1221
- console.print(
1222
- "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket create' instead.[/yellow]\n"
1223
- )
1224
-
1225
- # IMMEDIATE HEALTH CHECK - Critical for reliability
1226
- health_monitor = QueueHealthMonitor()
1227
- health = health_monitor.check_health()
1228
-
1229
- # Display health status
1230
- if health["status"] == HealthStatus.CRITICAL:
1231
- console.print("[red]🚨 CRITICAL: Queue system has serious issues![/red]")
1232
- for alert in health["alerts"]:
1233
- if alert["level"] == "critical":
1234
- console.print(f"[red] • {alert['message']}[/red]")
1235
-
1236
- # Attempt auto-repair
1237
- console.print("[yellow]Attempting automatic repair...[/yellow]")
1238
- repair_result = health_monitor.auto_repair()
1239
-
1240
- if repair_result["actions_taken"]:
1241
- for action in repair_result["actions_taken"]:
1242
- console.print(f"[yellow] ✓ {action}[/yellow]")
1243
-
1244
- # Re-check health after repair
1245
- health = health_monitor.check_health()
1246
- if health["status"] == HealthStatus.CRITICAL:
1247
- console.print(
1248
- "[red]❌ Auto-repair failed. Manual intervention required.[/red]"
1249
- )
1250
- console.print(
1251
- "[red]Cannot safely create ticket. Please check system status.[/red]"
1252
- )
1253
- raise typer.Exit(1)
1254
- else:
1255
- console.print(
1256
- "[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]"
1257
- )
1258
- else:
1259
- console.print(
1260
- "[red]❌ No repair actions available. Manual intervention required.[/red]"
1261
- )
1262
- raise typer.Exit(1)
553
+ report = simple_diagnose()
554
+ if output_file:
555
+ import json
1263
556
 
1264
- elif health["status"] == HealthStatus.WARNING:
1265
- console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
1266
- for alert in health["alerts"]:
1267
- if alert["level"] == "warning":
1268
- console.print(f"[yellow] • {alert['message']}[/yellow]")
1269
- console.print("[yellow]Proceeding with ticket creation...[/yellow]")
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
1270
562
 
1271
- # Get the adapter name with priority: 1) argument, 2) config, 3) .env files, 4) default
1272
- if adapter:
1273
- # Priority 1: Command-line argument - save to config for future use
1274
- adapter_name = adapter.value
1275
- _save_adapter_to_config(adapter_name)
563
+ console.print("\n" + json.dumps(report, indent=2))
564
+ if report["issues"]:
565
+ raise typer.Exit(1) from None
1276
566
  else:
1277
- # Priority 2: Check existing config
1278
- config = load_config()
1279
- adapter_name = config.get("default_adapter")
1280
-
1281
- if not adapter_name or adapter_name == "aitrackdown":
1282
- # Priority 3: Check .env files and save if found
1283
- env_adapter = _discover_from_env_files()
1284
- if env_adapter:
1285
- adapter_name = env_adapter
1286
- _save_adapter_to_config(adapter_name)
1287
- else:
1288
- # Priority 4: Default
1289
- adapter_name = "aitrackdown"
1290
-
1291
- # Resolve project/epic synonym - prefer whichever is provided
1292
- parent_epic_id = project or epic
1293
-
1294
- # Create task data
1295
- # Import Priority for type checking
1296
- from ..core.models import Priority as PriorityEnum
1297
-
1298
- task_data = {
1299
- "title": title,
1300
- "description": description,
1301
- "priority": priority.value if isinstance(priority, PriorityEnum) else priority,
1302
- "tags": tags or [],
1303
- "assignee": assignee,
1304
- "parent_epic": parent_epic_id,
1305
- }
1306
-
1307
- # WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
1308
- if adapter_name == "linear":
1309
- console.print(
1310
- "[yellow]⚠️[/yellow] Using direct operation for Linear adapter (bypassing queue)"
1311
- )
1312
567
  try:
1313
- # Load configuration and create adapter directly
1314
- config = load_config()
1315
- adapter_config = config.get("adapters", {}).get(adapter_name, {})
1316
-
1317
- # Import and create adapter
1318
- from ..core.registry import AdapterRegistry
1319
-
1320
- adapter = AdapterRegistry.get_adapter(adapter_name, adapter_config)
1321
-
1322
- # Create task directly
1323
- from ..core.models import Priority, Task
1324
-
1325
- task = Task(
1326
- title=task_data["title"],
1327
- description=task_data.get("description"),
1328
- priority=(
1329
- Priority(task_data["priority"])
1330
- if task_data.get("priority")
1331
- else Priority.MEDIUM
1332
- ),
1333
- tags=task_data.get("tags", []),
1334
- assignee=task_data.get("assignee"),
1335
- parent_epic=task_data.get("parent_epic"),
568
+ asyncio.run(
569
+ run_diagnostics(output_file=output_file, json_output=json_output)
1336
570
  )
1337
-
1338
- # Create ticket synchronously
1339
- import asyncio
1340
-
1341
- result = asyncio.run(adapter.create(task))
1342
-
1343
- console.print(f"[green]✓[/green] Ticket created successfully: {result.id}")
1344
- console.print(f" Title: {result.title}")
1345
- console.print(f" Priority: {result.priority}")
1346
- console.print(f" State: {result.state}")
1347
- # Get URL from metadata if available
1348
- if (
1349
- result.metadata
1350
- and "linear" in result.metadata
1351
- and "url" in result.metadata["linear"]
1352
- ):
1353
- console.print(f" URL: {result.metadata['linear']['url']}")
1354
-
1355
- return result.id
1356
-
1357
- except Exception as e:
1358
- console.print(f"[red]❌[/red] Failed to create ticket: {e}")
571
+ except typer.Exit:
572
+ # typer.Exit is expected - don't fall back to simple diagnostics
1359
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
1360
578
 
1361
- # Use queue for other adapters
1362
- queue = Queue()
1363
- queue_id = queue.add(
1364
- ticket_data=task_data,
1365
- adapter=adapter_name,
1366
- operation="create",
1367
- project_dir=str(Path.cwd()), # Explicitly pass current project directory
1368
- )
1369
-
1370
- # Register in ticket registry for tracking
1371
- registry = TicketRegistry()
1372
- registry.register_ticket_operation(
1373
- queue_id, adapter_name, "create", title, task_data
1374
- )
1375
-
1376
- console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
1377
- console.print(f" Title: {title}")
1378
- console.print(f" Priority: {priority}")
1379
- console.print(f" Adapter: {adapter_name}")
1380
- console.print("[dim]Use 'mcp-ticketer check {queue_id}' to check progress[/dim]")
1381
-
1382
- # Start worker if needed with immediate feedback
1383
- manager = WorkerManager()
1384
- worker_started = manager.start_if_needed()
1385
-
1386
- if worker_started:
1387
- console.print("[dim]Worker started to process request[/dim]")
1388
-
1389
- # Give immediate feedback on processing
1390
- import time
1391
-
1392
- time.sleep(1) # Brief pause to let worker start
1393
-
1394
- # Check if item is being processed
1395
- item = queue.get_item(queue_id)
1396
- if item and item.status == QueueStatus.PROCESSING:
1397
- console.print("[green]✓ Item is being processed by worker[/green]")
1398
- elif item and item.status == QueueStatus.PENDING:
1399
- console.print("[yellow]⏳ Item is queued for processing[/yellow]")
1400
- else:
1401
- console.print(
1402
- "[red]⚠️ Item status unclear - check with 'mcp-ticketer check {queue_id}'[/red]"
1403
- )
1404
- else:
1405
- # Worker didn't start - this is a problem
1406
- pending_count = queue.get_pending_count()
1407
- if pending_count > 1: # More than just this item
1408
- console.print(
1409
- f"[red]❌ Worker failed to start with {pending_count} pending items![/red]"
1410
- )
1411
- console.print(
1412
- "[red]This is a critical issue. Try 'mcp-ticketer queue worker start' manually.[/red]"
1413
- )
1414
- else:
1415
- console.print(
1416
- "[yellow]Worker not started (no other pending items)[/yellow]"
1417
- )
579
+ report = simple_diagnose()
580
+ if report["issues"]:
581
+ raise typer.Exit(1) from None
1418
582
 
1419
583
 
1420
- @app.command("list", deprecated=True, hidden=True)
1421
- def list_tickets(
1422
- state: TicketState | None = typer.Option(
1423
- None, "--state", "-s", help="Filter by state"
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"
1424
588
  ),
1425
- priority: Priority | None = typer.Option(
1426
- None, "--priority", "-p", help="Filter by priority"
589
+ json_output: bool = typer.Option(
590
+ False, "--json", help="Output report in JSON format"
1427
591
  ),
1428
- limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
1429
- adapter: AdapterType | None = typer.Option(
1430
- None, "--adapter", help="Override default adapter"
592
+ simple: bool = typer.Option(
593
+ False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1431
594
  ),
1432
595
  ) -> None:
1433
- """List tickets with optional filters.
1434
-
1435
- DEPRECATED: Use 'mcp-ticketer ticket list' instead.
1436
- """
1437
- console.print(
1438
- "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket list' instead.[/yellow]\n"
1439
- )
1440
-
1441
- async def _list():
1442
- adapter_instance = get_adapter(
1443
- override_adapter=adapter.value if adapter else None
1444
- )
1445
- filters = {}
1446
- if state:
1447
- filters["state"] = state
1448
- if priority:
1449
- filters["priority"] = priority
1450
- return await adapter_instance.list(limit=limit, filters=filters)
1451
-
1452
- tickets = asyncio.run(_list())
1453
-
1454
- if not tickets:
1455
- console.print("[yellow]No tickets found[/yellow]")
1456
- return
1457
-
1458
- # Create table
1459
- table = Table(title="Tickets")
1460
- table.add_column("ID", style="cyan", no_wrap=True)
1461
- table.add_column("Title", style="white")
1462
- table.add_column("State", style="green")
1463
- table.add_column("Priority", style="yellow")
1464
- table.add_column("Assignee", style="blue")
1465
-
1466
- for ticket in tickets:
1467
- # Handle assignee field - Epic doesn't have assignee, Task does
1468
- assignee = getattr(ticket, "assignee", None) or "-"
1469
-
1470
- table.add_row(
1471
- ticket.id or "N/A",
1472
- ticket.title,
1473
- ticket.state,
1474
- ticket.priority,
1475
- assignee,
1476
- )
1477
-
1478
- console.print(table)
1479
-
1480
-
1481
- @app.command(deprecated=True, hidden=True)
1482
- def show(
1483
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1484
- comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
1485
- adapter: AdapterType | None = typer.Option(
1486
- None, "--adapter", help="Override default adapter"
1487
- ),
1488
- ) -> None:
1489
- """Show detailed ticket information.
1490
-
1491
- DEPRECATED: Use 'mcp-ticketer ticket show' instead.
1492
- """
1493
- console.print(
1494
- "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket show' instead.[/yellow]\n"
1495
- )
1496
-
1497
- async def _show():
1498
- adapter_instance = get_adapter(
1499
- override_adapter=adapter.value if adapter else None
1500
- )
1501
- ticket = await adapter_instance.read(ticket_id)
1502
- ticket_comments = None
1503
- if comments and ticket:
1504
- ticket_comments = await adapter_instance.get_comments(ticket_id)
1505
- return ticket, ticket_comments
1506
-
1507
- ticket, ticket_comments = asyncio.run(_show())
1508
-
1509
- if not ticket:
1510
- console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
1511
- raise typer.Exit(1)
1512
-
1513
- # Display ticket details
1514
- console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
1515
- console.print(f"Title: {ticket.title}")
1516
- console.print(f"State: [green]{ticket.state}[/green]")
1517
- console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
1518
-
1519
- if ticket.description:
1520
- console.print("\n[dim]Description:[/dim]")
1521
- console.print(ticket.description)
1522
-
1523
- if ticket.tags:
1524
- console.print(f"\nTags: {', '.join(ticket.tags)}")
1525
-
1526
- if ticket.assignee:
1527
- console.print(f"Assignee: {ticket.assignee}")
1528
-
1529
- # Display comments if requested
1530
- if ticket_comments:
1531
- console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
1532
- for comment in ticket_comments:
1533
- console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
1534
- console.print(comment.content)
1535
-
1536
-
1537
- @app.command(deprecated=True, hidden=True)
1538
- def comment(
1539
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1540
- content: str = typer.Argument(..., help="Comment content"),
1541
- adapter: AdapterType | None = typer.Option(
1542
- None, "--adapter", help="Override default adapter"
1543
- ),
1544
- ) -> None:
1545
- """Add a comment to a ticket.
1546
-
1547
- DEPRECATED: Use 'mcp-ticketer ticket comment' instead.
1548
- """
1549
- console.print(
1550
- "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket comment' instead.[/yellow]\n"
1551
- )
1552
-
1553
- async def _comment():
1554
- adapter_instance = get_adapter(
1555
- override_adapter=adapter.value if adapter else None
1556
- )
1557
-
1558
- # Create comment
1559
- comment = Comment(
1560
- ticket_id=ticket_id,
1561
- content=content,
1562
- author="cli-user", # Could be made configurable
1563
- )
1564
-
1565
- result = await adapter_instance.add_comment(comment)
1566
- return result
1567
-
1568
- try:
1569
- result = asyncio.run(_comment())
1570
- console.print("[green]✓[/green] Comment added successfully")
1571
- if result.id:
1572
- console.print(f"Comment ID: {result.id}")
1573
- console.print(f"Content: {content}")
1574
- except Exception as e:
1575
- console.print(f"[red]✗[/red] Failed to add comment: {e}")
1576
- raise typer.Exit(1)
1577
-
1578
-
1579
- @app.command(deprecated=True, hidden=True)
1580
- def update(
1581
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1582
- title: str | None = typer.Option(None, "--title", help="New title"),
1583
- description: str | None = typer.Option(
1584
- None, "--description", "-d", help="New description"
1585
- ),
1586
- priority: Priority | None = typer.Option(
1587
- None, "--priority", "-p", help="New priority"
1588
- ),
1589
- assignee: str | None = typer.Option(None, "--assignee", "-a", help="New assignee"),
1590
- adapter: AdapterType | None = typer.Option(
1591
- None, "--adapter", help="Override default adapter"
1592
- ),
1593
- ) -> None:
1594
- """Update ticket fields.
1595
-
1596
- DEPRECATED: Use 'mcp-ticketer ticket update' instead.
1597
- """
1598
- console.print(
1599
- "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket update' instead.[/yellow]\n"
1600
- )
1601
- updates = {}
1602
- if title:
1603
- updates["title"] = title
1604
- if description:
1605
- updates["description"] = description
1606
- if priority:
1607
- updates["priority"] = (
1608
- priority.value if isinstance(priority, Priority) else priority
1609
- )
1610
- if assignee:
1611
- updates["assignee"] = assignee
1612
-
1613
- if not updates:
1614
- console.print("[yellow]No updates specified[/yellow]")
1615
- raise typer.Exit(1)
1616
-
1617
- # Get the adapter name
1618
- config = load_config()
1619
- adapter_name = (
1620
- adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1621
- )
1622
-
1623
- # Add ticket_id to updates
1624
- updates["ticket_id"] = ticket_id
1625
-
1626
- # Add to queue with explicit project directory
1627
- queue = Queue()
1628
- queue_id = queue.add(
1629
- ticket_data=updates,
1630
- adapter=adapter_name,
1631
- operation="update",
1632
- project_dir=str(Path.cwd()), # Explicitly pass current project directory
1633
- )
1634
-
1635
- console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
1636
- for key, value in updates.items():
1637
- if key != "ticket_id":
1638
- console.print(f" {key}: {value}")
1639
- console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
1640
-
1641
- # Start worker if needed
1642
- manager = WorkerManager()
1643
- if manager.start_if_needed():
1644
- console.print("[dim]Worker started to process request[/dim]")
1645
-
1646
-
1647
- @app.command(deprecated=True, hidden=True)
1648
- def transition(
1649
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1650
- state_positional: TicketState | None = typer.Argument(
1651
- None, help="Target state (positional - deprecated, use --state instead)"
1652
- ),
1653
- state: TicketState | None = typer.Option(
1654
- None, "--state", "-s", help="Target state (recommended)"
1655
- ),
1656
- adapter: AdapterType | None = typer.Option(
1657
- None, "--adapter", help="Override default adapter"
1658
- ),
1659
- ) -> None:
1660
- """Change ticket state with validation.
1661
-
1662
- DEPRECATED: Use 'mcp-ticketer ticket transition' instead.
1663
-
1664
- Examples:
1665
- # Recommended syntax with flag:
1666
- mcp-ticketer ticket transition BTA-215 --state done
1667
- mcp-ticketer ticket transition BTA-215 -s in_progress
1668
-
1669
- # Legacy positional syntax (still supported):
1670
- mcp-ticketer ticket transition BTA-215 done
1671
-
1672
- """
1673
- console.print(
1674
- "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket transition' instead.[/yellow]\n"
1675
- )
1676
-
1677
- # Determine which state to use (prefer flag over positional)
1678
- target_state = state if state is not None else state_positional
1679
-
1680
- if target_state is None:
1681
- console.print("[red]Error: State is required[/red]")
1682
- console.print(
1683
- "Use either:\n"
1684
- " - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
1685
- " - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
1686
- )
1687
- raise typer.Exit(1)
1688
-
1689
- # Get the adapter name
1690
- config = load_config()
1691
- adapter_name = (
1692
- adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1693
- )
1694
-
1695
- # Add to queue with explicit project directory
1696
- queue = Queue()
1697
- queue_id = queue.add(
1698
- ticket_data={
1699
- "ticket_id": ticket_id,
1700
- "state": (
1701
- target_state.value if hasattr(target_state, "value") else target_state
1702
- ),
1703
- },
1704
- adapter=adapter_name,
1705
- operation="transition",
1706
- project_dir=str(Path.cwd()), # Explicitly pass current project directory
1707
- )
1708
-
1709
- console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
1710
- console.print(f" Ticket: {ticket_id} → {target_state}")
1711
- console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
1712
-
1713
- # Start worker if needed
1714
- manager = WorkerManager()
1715
- if manager.start_if_needed():
1716
- console.print("[dim]Worker started to process request[/dim]")
1717
-
1718
-
1719
- @app.command(deprecated=True, hidden=True)
1720
- def search(
1721
- query: str | None = typer.Argument(None, help="Search query"),
1722
- state: TicketState | None = typer.Option(None, "--state", "-s"),
1723
- priority: Priority | None = typer.Option(None, "--priority", "-p"),
1724
- assignee: str | None = typer.Option(None, "--assignee", "-a"),
1725
- limit: int = typer.Option(10, "--limit", "-l"),
1726
- adapter: AdapterType | None = typer.Option(
1727
- None, "--adapter", help="Override default adapter"
1728
- ),
1729
- ) -> None:
1730
- """Search tickets with advanced query.
1731
-
1732
- DEPRECATED: Use 'mcp-ticketer ticket search' instead.
1733
- """
1734
- console.print(
1735
- "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket search' instead.[/yellow]\n"
1736
- )
1737
-
1738
- async def _search():
1739
- adapter_instance = get_adapter(
1740
- override_adapter=adapter.value if adapter else None
1741
- )
1742
- search_query = SearchQuery(
1743
- query=query,
1744
- state=state,
1745
- priority=priority,
1746
- assignee=assignee,
1747
- limit=limit,
1748
- )
1749
- return await adapter_instance.search(search_query)
1750
-
1751
- tickets = asyncio.run(_search())
1752
-
1753
- if not tickets:
1754
- console.print("[yellow]No tickets found matching query[/yellow]")
1755
- return
1756
-
1757
- # Display results
1758
- console.print(f"\n[bold]Found {len(tickets)} ticket(s)[/bold]\n")
1759
-
1760
- for ticket in tickets:
1761
- console.print(f"[cyan]{ticket.id}[/cyan]: {ticket.title}")
1762
- console.print(f" State: {ticket.state} | Priority: {ticket.priority}")
1763
- if ticket.assignee:
1764
- console.print(f" Assignee: {ticket.assignee}")
1765
- console.print()
1766
-
1767
-
1768
- # Add ticket command group to main app
1769
- app.add_typer(ticket_app, name="ticket")
1770
-
1771
- # Add platform command group to main app
1772
- app.add_typer(platform_app, name="platform")
1773
-
1774
- # Add queue command to main app
1775
- app.add_typer(queue_app, name="queue")
1776
-
1777
- # Add discover command to main app
1778
- app.add_typer(discover_app, name="discover")
1779
-
1780
-
1781
- # Add diagnostics command
1782
- @app.command("diagnose")
1783
- def diagnose_command(
1784
- output_file: str | None = typer.Option(
1785
- None, "--output", "-o", help="Save full report to file"
1786
- ),
1787
- json_output: bool = typer.Option(
1788
- False, "--json", help="Output report in JSON format"
1789
- ),
1790
- simple: bool = typer.Option(
1791
- False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1792
- ),
1793
- ) -> None:
1794
- """Run comprehensive system diagnostics and health check (alias: doctor)."""
1795
- if simple:
1796
- from .simple_health import simple_diagnose
1797
-
1798
- report = simple_diagnose()
1799
- if output_file:
1800
- import json
1801
-
1802
- with open(output_file, "w") as f:
1803
- json.dump(report, f, indent=2)
1804
- console.print(f"\n📄 Report saved to: {output_file}")
1805
- if json_output:
1806
- import json
1807
-
1808
- console.print("\n" + json.dumps(report, indent=2))
1809
- if report["issues"]:
1810
- raise typer.Exit(1)
1811
- else:
1812
- try:
1813
- asyncio.run(
1814
- run_diagnostics(output_file=output_file, json_output=json_output)
1815
- )
1816
- except typer.Exit:
1817
- # typer.Exit is expected - don't fall back to simple diagnostics
1818
- raise
1819
- except Exception as e:
1820
- console.print(f"⚠️ Full diagnostics failed: {e}")
1821
- console.print("🔄 Falling back to simple diagnostics...")
1822
- from .simple_health import simple_diagnose
1823
-
1824
- report = simple_diagnose()
1825
- if report["issues"]:
1826
- raise typer.Exit(1)
1827
-
1828
-
1829
- @app.command("doctor")
1830
- def doctor_alias(
1831
- output_file: str | None = typer.Option(
1832
- None, "--output", "-o", help="Save full report to file"
1833
- ),
1834
- json_output: bool = typer.Option(
1835
- False, "--json", help="Output report in JSON format"
1836
- ),
1837
- simple: bool = typer.Option(
1838
- False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1839
- ),
1840
- ) -> None:
1841
- """Run comprehensive system diagnostics and health check (alias for diagnose)."""
1842
- # Call the diagnose_command function with the same parameters
1843
- diagnose_command(output_file=output_file, json_output=json_output, simple=simple)
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)
1844
599
 
1845
600
 
1846
601
  @app.command("status")
@@ -1850,7 +605,7 @@ def status_command() -> None:
1850
605
 
1851
606
  result = simple_health_check()
1852
607
  if result != 0:
1853
- raise typer.Exit(result)
608
+ raise typer.Exit(result) from None
1854
609
 
1855
610
 
1856
611
  @app.command("health")
@@ -1860,716 +615,15 @@ def health_alias() -> None:
1860
615
 
1861
616
  result = simple_health_check()
1862
617
  if result != 0:
1863
- raise typer.Exit(result)
1864
-
1865
-
1866
- # Create MCP configuration command group
1867
- mcp_app = typer.Typer(
1868
- name="mcp",
1869
- help="Configure MCP integration for AI clients (Claude, Gemini, Codex, Auggie)",
1870
- add_completion=False,
1871
- invoke_without_command=True,
1872
- )
1873
-
1874
-
1875
- @mcp_app.callback()
1876
- def mcp_callback(
1877
- ctx: typer.Context,
1878
- project_path: str | None = typer.Argument(
1879
- None, help="Project directory path (optional - uses cwd if not provided)"
1880
- ),
1881
- ):
1882
- """MCP command group - runs MCP server if no subcommand provided."""
1883
- if ctx.invoked_subcommand is None:
1884
- # No subcommand provided, run the serve command
1885
- # Change to project directory if provided
1886
- if project_path:
1887
- import os
1888
-
1889
- os.chdir(project_path)
1890
- # Invoke the serve command through context
1891
- ctx.invoke(mcp_serve, adapter=None, base_path=None)
1892
-
1893
-
1894
- @app.command()
1895
- def install(
1896
- platform: str | None = typer.Argument(
1897
- None,
1898
- help="Platform to install (claude-code, claude-desktop, gemini, codex, auggie)",
1899
- ),
1900
- adapter: str | None = typer.Option(
1901
- None,
1902
- "--adapter",
1903
- "-a",
1904
- help="Adapter type to use (interactive prompt if not specified)",
1905
- ),
1906
- project_path: str | None = typer.Option(
1907
- None, "--path", help="Project path (default: current directory)"
1908
- ),
1909
- global_config: bool = typer.Option(
1910
- False,
1911
- "--global",
1912
- "-g",
1913
- help="Save to global config instead of project-specific",
1914
- ),
1915
- base_path: str | None = typer.Option(
1916
- None,
1917
- "--base-path",
1918
- "-p",
1919
- help="Base path for ticket storage (AITrackdown only)",
1920
- ),
1921
- api_key: str | None = typer.Option(
1922
- None, "--api-key", help="API key for Linear or API token for JIRA"
1923
- ),
1924
- team_id: str | None = typer.Option(
1925
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
1926
- ),
1927
- jira_server: str | None = typer.Option(
1928
- None,
1929
- "--jira-server",
1930
- help="JIRA server URL (e.g., https://company.atlassian.net)",
1931
- ),
1932
- jira_email: str | None = typer.Option(
1933
- None, "--jira-email", help="JIRA user email for authentication"
1934
- ),
1935
- jira_project: str | None = typer.Option(
1936
- None, "--jira-project", help="Default JIRA project key"
1937
- ),
1938
- github_owner: str | None = typer.Option(
1939
- None, "--github-owner", help="GitHub repository owner"
1940
- ),
1941
- github_repo: str | None = typer.Option(
1942
- None, "--github-repo", help="GitHub repository name"
1943
- ),
1944
- github_token: str | None = typer.Option(
1945
- None, "--github-token", help="GitHub Personal Access Token"
1946
- ),
1947
- dry_run: bool = typer.Option(
1948
- False,
1949
- "--dry-run",
1950
- help="Show what would be done without making changes (for platform installation)",
1951
- ),
1952
- ) -> None:
1953
- """Install MCP for AI platforms OR initialize adapter setup.
1954
-
1955
- With platform argument (new syntax): Install MCP configuration for AI platforms
1956
- Without platform argument (legacy): Run adapter setup wizard (same as 'init' and 'setup')
1957
-
1958
- New Command Structure:
1959
- # Install MCP for AI platforms
1960
- mcp-ticketer install claude-code # Claude Code (project-level)
1961
- mcp-ticketer install claude-desktop # Claude Desktop (global)
1962
- mcp-ticketer install gemini # Gemini CLI
1963
- mcp-ticketer install codex # Codex
1964
- mcp-ticketer install auggie # Auggie
1965
-
1966
- Legacy Adapter Setup (still supported):
1967
- mcp-ticketer install # Interactive setup wizard
1968
- mcp-ticketer install --adapter linear
1969
-
1970
- """
1971
- # If platform argument is provided, handle MCP platform installation (NEW SYNTAX)
1972
- if platform is not None:
1973
- # Import configuration functions
1974
- from .auggie_configure import configure_auggie_mcp
1975
- from .codex_configure import configure_codex_mcp
1976
- from .gemini_configure import configure_gemini_mcp
1977
- from .mcp_configure import configure_claude_mcp
1978
-
1979
- # Map platform names to configuration functions
1980
- platform_mapping = {
1981
- "claude-code": {
1982
- "func": lambda: configure_claude_mcp(global_config=False, force=True),
1983
- "name": "Claude Code",
1984
- },
1985
- "claude-desktop": {
1986
- "func": lambda: configure_claude_mcp(global_config=True, force=True),
1987
- "name": "Claude Desktop",
1988
- },
1989
- "auggie": {
1990
- "func": lambda: configure_auggie_mcp(force=True),
1991
- "name": "Auggie",
1992
- },
1993
- "gemini": {
1994
- "func": lambda: configure_gemini_mcp(scope="project", force=True),
1995
- "name": "Gemini CLI",
1996
- },
1997
- "codex": {
1998
- "func": lambda: configure_codex_mcp(force=True),
1999
- "name": "Codex",
2000
- },
2001
- }
2002
-
2003
- if platform not in platform_mapping:
2004
- console.print(f"[red]Unknown platform: {platform}[/red]")
2005
- console.print("\n[bold]Available platforms:[/bold]")
2006
- for p in platform_mapping.keys():
2007
- console.print(f" • {p}")
2008
- raise typer.Exit(1)
2009
-
2010
- config = platform_mapping[platform]
2011
-
2012
- if dry_run:
2013
- console.print(f"[cyan]DRY RUN - Would install for {config['name']}[/cyan]")
2014
- return
2015
-
2016
- try:
2017
- config["func"]()
2018
- except Exception as e:
2019
- console.print(f"[red]Installation failed: {e}[/red]")
2020
- raise typer.Exit(1)
2021
- return
2022
-
2023
- # Otherwise, delegate to init for adapter initialization (LEGACY BEHAVIOR)
2024
- # This makes 'install' and 'init' synonymous when called without platform argument
2025
- init(
2026
- adapter=adapter,
2027
- project_path=project_path,
2028
- global_config=global_config,
2029
- base_path=base_path,
2030
- api_key=api_key,
2031
- team_id=team_id,
2032
- jira_server=jira_server,
2033
- jira_email=jira_email,
2034
- jira_project=jira_project,
2035
- github_owner=github_owner,
2036
- github_repo=github_repo,
2037
- github_token=github_token,
2038
- )
2039
-
2040
-
2041
- @app.command()
2042
- def remove(
2043
- platform: str | None = typer.Argument(
2044
- None,
2045
- help="Platform to remove (claude-code, claude-desktop, auggie, gemini, codex)",
2046
- ),
2047
- dry_run: bool = typer.Option(
2048
- False, "--dry-run", help="Show what would be done without making changes"
2049
- ),
2050
- ) -> None:
2051
- """Remove mcp-ticketer from AI platforms.
2052
-
2053
- Without arguments, shows help and available platforms.
2054
- With a platform argument, removes MCP configuration for that platform.
2055
-
2056
- Examples:
2057
- # Remove from Claude Code (project-level)
2058
- mcp-ticketer remove claude-code
2059
-
2060
- # Remove from Claude Desktop (global)
2061
- mcp-ticketer remove claude-desktop
2062
-
2063
- # Remove from Auggie
2064
- mcp-ticketer remove auggie
2065
-
2066
- # Dry run to preview changes
2067
- mcp-ticketer remove claude-code --dry-run
2068
-
2069
- """
2070
- # If no platform specified, show help message
2071
- if platform is None:
2072
- console.print("[bold]Remove mcp-ticketer from AI platforms[/bold]\n")
2073
- console.print("Usage: mcp-ticketer remove <platform>\n")
2074
- console.print("[bold]Available platforms:[/bold]")
2075
- console.print(" • claude-code - Claude Code (project-level)")
2076
- console.print(" • claude-desktop - Claude Desktop (global)")
2077
- console.print(" • auggie - Auggie (global)")
2078
- console.print(" • gemini - Gemini CLI (project-level by default)")
2079
- console.print(" • codex - Codex (global)")
2080
- return
2081
-
2082
- # Import removal functions
2083
- from .auggie_configure import remove_auggie_mcp
2084
- from .codex_configure import remove_codex_mcp
2085
- from .gemini_configure import remove_gemini_mcp
2086
- from .mcp_configure import remove_claude_mcp
2087
-
2088
- # Map platform names to removal functions
2089
- platform_mapping = {
2090
- "claude-code": {
2091
- "func": lambda: remove_claude_mcp(global_config=False, dry_run=dry_run),
2092
- "name": "Claude Code",
2093
- },
2094
- "claude-desktop": {
2095
- "func": lambda: remove_claude_mcp(global_config=True, dry_run=dry_run),
2096
- "name": "Claude Desktop",
2097
- },
2098
- "auggie": {
2099
- "func": lambda: remove_auggie_mcp(dry_run=dry_run),
2100
- "name": "Auggie",
2101
- },
2102
- "gemini": {
2103
- "func": lambda: remove_gemini_mcp(scope="project", dry_run=dry_run),
2104
- "name": "Gemini CLI",
2105
- },
2106
- "codex": {
2107
- "func": lambda: remove_codex_mcp(dry_run=dry_run),
2108
- "name": "Codex",
2109
- },
2110
- }
2111
-
2112
- if platform not in platform_mapping:
2113
- console.print(f"[red]Unknown platform: {platform}[/red]")
2114
- console.print("\n[bold]Available platforms:[/bold]")
2115
- for p in platform_mapping.keys():
2116
- console.print(f" • {p}")
2117
- raise typer.Exit(1)
2118
-
2119
- config = platform_mapping[platform]
2120
-
2121
- try:
2122
- config["func"]()
2123
- except Exception as e:
2124
- console.print(f"[red]Removal failed: {e}[/red]")
2125
- raise typer.Exit(1)
2126
-
2127
-
2128
- @app.command()
2129
- def uninstall(
2130
- platform: str | None = typer.Argument(
2131
- None,
2132
- help="Platform to uninstall (claude-code, claude-desktop, auggie, gemini, codex)",
2133
- ),
2134
- dry_run: bool = typer.Option(
2135
- False, "--dry-run", help="Show what would be done without making changes"
2136
- ),
2137
- ) -> None:
2138
- """Uninstall mcp-ticketer from AI platforms (alias for remove).
2139
-
2140
- This is an alias for the 'remove' command.
2141
-
2142
- Without arguments, shows help and available platforms.
2143
- With a platform argument, removes MCP configuration for that platform.
2144
-
2145
- Examples:
2146
- # Uninstall from Claude Code (project-level)
2147
- mcp-ticketer uninstall claude-code
2148
-
2149
- # Uninstall from Claude Desktop (global)
2150
- mcp-ticketer uninstall claude-desktop
2151
-
2152
- # Uninstall from Auggie
2153
- mcp-ticketer uninstall auggie
2154
-
2155
- # Dry run to preview changes
2156
- mcp-ticketer uninstall claude-code --dry-run
2157
-
2158
- """
2159
- # Call the remove command with the same parameters
2160
- remove(platform=platform, dry_run=dry_run)
2161
-
2162
-
2163
- @app.command(deprecated=True, hidden=True)
2164
- def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
2165
- """Check status of a queued operation.
2166
-
2167
- DEPRECATED: Use 'mcp-ticketer ticket check' instead.
2168
- """
2169
- console.print(
2170
- "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket check' instead.[/yellow]\n"
2171
- )
2172
- queue = Queue()
2173
- item = queue.get_item(queue_id)
2174
-
2175
- if not item:
2176
- console.print(f"[red]Queue item not found: {queue_id}[/red]")
2177
- raise typer.Exit(1)
2178
-
2179
- # Display status
2180
- console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
2181
- console.print(f"Operation: {item.operation}")
2182
- console.print(f"Adapter: {item.adapter}")
2183
-
2184
- # Status with color
2185
- if item.status == QueueStatus.COMPLETED:
2186
- console.print(f"Status: [green]{item.status}[/green]")
2187
- elif item.status == QueueStatus.FAILED:
2188
- console.print(f"Status: [red]{item.status}[/red]")
2189
- elif item.status == QueueStatus.PROCESSING:
2190
- console.print(f"Status: [yellow]{item.status}[/yellow]")
2191
- else:
2192
- console.print(f"Status: {item.status}")
2193
-
2194
- # Timestamps
2195
- console.print(f"Created: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
2196
- if item.processed_at:
2197
- console.print(f"Processed: {item.processed_at.strftime('%Y-%m-%d %H:%M:%S')}")
2198
-
2199
- # Error or result
2200
- if item.error_message:
2201
- console.print(f"\n[red]Error:[/red] {item.error_message}")
2202
- elif item.result:
2203
- console.print("\n[green]Result:[/green]")
2204
- for key, value in item.result.items():
2205
- console.print(f" {key}: {value}")
2206
-
2207
- if item.retry_count > 0:
2208
- console.print(f"\nRetry Count: {item.retry_count}")
2209
-
2210
-
2211
- @mcp_app.command(name="serve")
2212
- def mcp_serve(
2213
- adapter: AdapterType | None = typer.Option(
2214
- None, "--adapter", "-a", help="Override default adapter type"
2215
- ),
2216
- base_path: str | None = typer.Option(
2217
- None, "--base-path", help="Base path for AITrackdown adapter"
2218
- ),
2219
- ):
2220
- """Start MCP server for JSON-RPC communication over stdio.
2221
-
2222
- This command is used by Claude Code/Desktop when connecting to the MCP server.
2223
- You typically don't need to run this manually - use 'mcp-ticketer install add' to configure.
2224
-
2225
- Configuration Resolution:
2226
- - When MCP server starts, it uses the current working directory (cwd)
2227
- - The cwd is set by Claude Code/Desktop from the 'cwd' field in .mcp/config.json
2228
- - Configuration is loaded with this priority:
2229
- 1. Project-specific: .mcp-ticketer/config.json in cwd
2230
- 2. Global: ~/.mcp-ticketer/config.json
2231
- 3. Default: aitrackdown adapter with .aitrackdown base path
2232
- """
2233
- from ..mcp.server.server_sdk import configure_adapter
2234
- from ..mcp.server.server_sdk import main as sdk_main
2235
-
2236
- # Load configuration (respects project-specific config in cwd)
2237
- config = load_config()
2238
-
2239
- # Determine adapter type with priority: CLI arg > .env files > config > default
2240
- if adapter:
2241
- # Priority 1: Command line argument
2242
- adapter_type = adapter.value
2243
- # Get base config from config file
2244
- adapters_config = config.get("adapters", {})
2245
- adapter_config = adapters_config.get(adapter_type, {})
2246
- else:
2247
- # Priority 2: .env files
2248
- from ..mcp.server.main import _load_env_configuration
2249
-
2250
- env_config = _load_env_configuration()
2251
- if env_config:
2252
- adapter_type = env_config["adapter_type"]
2253
- adapter_config = env_config["adapter_config"]
2254
- else:
2255
- # Priority 3: Configuration file
2256
- adapter_type = config.get("default_adapter", "aitrackdown")
2257
- adapters_config = config.get("adapters", {})
2258
- adapter_config = adapters_config.get(adapter_type, {})
2259
-
2260
- # Override with command line options if provided (highest priority)
2261
- if base_path and adapter_type == "aitrackdown":
2262
- adapter_config["base_path"] = base_path
2263
-
2264
- # Fallback to legacy config format
2265
- if not adapter_config and "config" in config:
2266
- adapter_config = config["config"]
2267
-
2268
- # MCP server uses stdio for JSON-RPC, so we can't print to stdout
2269
- # Only print to stderr to avoid interfering with the protocol
2270
- import sys
2271
-
2272
- if sys.stderr.isatty():
2273
- # Only print if stderr is a terminal (not redirected)
2274
- console.file = sys.stderr
2275
- console.print(
2276
- f"[green]Starting MCP SDK server[/green] with {adapter_type} adapter"
2277
- )
2278
- console.print(
2279
- "[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
2280
- )
2281
-
2282
- # Configure adapter and run SDK server
2283
- try:
2284
- configure_adapter(adapter_type, adapter_config)
2285
- sdk_main()
2286
- except KeyboardInterrupt:
2287
- # Send this to stderr
2288
- if sys.stderr.isatty():
2289
- console.print("\n[yellow]Server stopped by user[/yellow]")
2290
- sys.exit(0)
2291
- except Exception as e:
2292
- # Log error to stderr
2293
- sys.stderr.write(f"MCP server error: {e}\n")
2294
- sys.exit(1)
2295
-
2296
-
2297
- @mcp_app.command(name="claude")
2298
- def mcp_claude(
2299
- global_config: bool = typer.Option(
2300
- False,
2301
- "--global",
2302
- "-g",
2303
- help="Configure Claude Desktop instead of project-level",
2304
- ),
2305
- force: bool = typer.Option(
2306
- False, "--force", "-f", help="Overwrite existing configuration"
2307
- ),
2308
- ):
2309
- """Configure Claude Code to use mcp-ticketer MCP server.
2310
-
2311
- Reads configuration from .mcp-ticketer/config.json and updates
2312
- Claude Code's MCP settings accordingly.
2313
-
2314
- By default, configures project-level (.mcp/config.json).
2315
- Use --global to configure Claude Desktop instead.
2316
-
2317
- Examples:
2318
- # Configure for current project (default)
2319
- mcp-ticketer mcp claude
2320
-
2321
- # Configure Claude Desktop globally
2322
- mcp-ticketer mcp claude --global
2323
-
2324
- # Force overwrite existing configuration
2325
- mcp-ticketer mcp claude --force
2326
-
2327
- """
2328
- from ..cli.mcp_configure import configure_claude_mcp
2329
-
2330
- try:
2331
- configure_claude_mcp(global_config=global_config, force=force)
2332
- except Exception as e:
2333
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
2334
- raise typer.Exit(1)
2335
-
2336
-
2337
- @mcp_app.command(name="gemini")
2338
- def mcp_gemini(
2339
- scope: str = typer.Option(
2340
- "project",
2341
- "--scope",
2342
- "-s",
2343
- help="Configuration scope: 'project' (default) or 'user'",
2344
- ),
2345
- force: bool = typer.Option(
2346
- False, "--force", "-f", help="Overwrite existing configuration"
2347
- ),
2348
- ):
2349
- """Configure Gemini CLI to use mcp-ticketer MCP server.
2350
-
2351
- Reads configuration from .mcp-ticketer/config.json and creates
2352
- Gemini CLI settings file with mcp-ticketer configuration.
2353
-
2354
- By default, configures project-level (.gemini/settings.json).
2355
- Use --scope user to configure user-level (~/.gemini/settings.json).
2356
-
2357
- Examples:
2358
- # Configure for current project (default)
2359
- mcp-ticketer mcp gemini
2360
-
2361
- # Configure at user level
2362
- mcp-ticketer mcp gemini --scope user
2363
-
2364
- # Force overwrite existing configuration
2365
- mcp-ticketer mcp gemini --force
2366
-
2367
- """
2368
- from ..cli.gemini_configure import configure_gemini_mcp
2369
-
2370
- # Validate scope parameter
2371
- if scope not in ["project", "user"]:
2372
- console.print(
2373
- f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
2374
- )
2375
- raise typer.Exit(1)
2376
-
2377
- try:
2378
- configure_gemini_mcp(scope=scope, force=force) # type: ignore
2379
- except Exception as e:
2380
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
2381
- raise typer.Exit(1)
2382
-
2383
-
2384
- @mcp_app.command(name="codex")
2385
- def mcp_codex(
2386
- force: bool = typer.Option(
2387
- False, "--force", "-f", help="Overwrite existing configuration"
2388
- ),
2389
- ):
2390
- """Configure Codex CLI to use mcp-ticketer MCP server.
2391
-
2392
- Reads configuration from .mcp-ticketer/config.json and creates
2393
- Codex CLI config.toml with mcp-ticketer configuration.
2394
-
2395
- IMPORTANT: Codex CLI ONLY supports global configuration at ~/.codex/config.toml.
2396
- There is no project-level configuration support. After configuration,
2397
- you must restart Codex CLI for changes to take effect.
2398
-
2399
- Examples:
2400
- # Configure Codex CLI globally
2401
- mcp-ticketer mcp codex
2402
-
2403
- # Force overwrite existing configuration
2404
- mcp-ticketer mcp codex --force
2405
-
2406
- """
2407
- from ..cli.codex_configure import configure_codex_mcp
2408
-
2409
- try:
2410
- configure_codex_mcp(force=force)
2411
- except Exception as e:
2412
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
2413
- raise typer.Exit(1)
2414
-
2415
-
2416
- @mcp_app.command(name="auggie")
2417
- def mcp_auggie(
2418
- force: bool = typer.Option(
2419
- False, "--force", "-f", help="Overwrite existing configuration"
2420
- ),
2421
- ):
2422
- """Configure Auggie CLI to use mcp-ticketer MCP server.
2423
-
2424
- Reads configuration from .mcp-ticketer/config.json and creates
2425
- Auggie CLI settings.json with mcp-ticketer configuration.
2426
-
2427
- IMPORTANT: Auggie CLI ONLY supports global configuration at ~/.augment/settings.json.
2428
- There is no project-level configuration support. After configuration,
2429
- you must restart Auggie CLI for changes to take effect.
2430
-
2431
- Examples:
2432
- # Configure Auggie CLI globally
2433
- mcp-ticketer mcp auggie
2434
-
2435
- # Force overwrite existing configuration
2436
- mcp-ticketer mcp auggie --force
2437
-
2438
- """
2439
- from ..cli.auggie_configure import configure_auggie_mcp
2440
-
2441
- try:
2442
- configure_auggie_mcp(force=force)
2443
- except Exception as e:
2444
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
2445
- raise typer.Exit(1)
2446
-
2447
-
2448
- @mcp_app.command(name="status")
2449
- def mcp_status():
2450
- """Check MCP server status.
2451
-
2452
- Shows whether the MCP server is configured and running for various platforms.
2453
-
2454
- Examples:
2455
- mcp-ticketer mcp status
2456
-
2457
- """
2458
- import json
2459
- from pathlib import Path
2460
-
2461
- console.print("[bold]MCP Server Status[/bold]\n")
2462
-
2463
- # Check project-level configuration
2464
- project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
2465
- if project_config.exists():
2466
- console.print(f"[green]✓[/green] Project config found: {project_config}")
2467
- try:
2468
- with open(project_config) as f:
2469
- config = json.load(f)
2470
- adapter = config.get("default_adapter", "aitrackdown")
2471
- console.print(f" Default adapter: [cyan]{adapter}[/cyan]")
2472
- except Exception as e:
2473
- console.print(f" [yellow]Warning: Could not read config: {e}[/yellow]")
2474
- else:
2475
- console.print("[yellow]○[/yellow] No project config found")
2476
-
2477
- # Check Claude Code configuration
2478
- claude_code_config = Path.cwd() / ".mcp" / "config.json"
2479
- if claude_code_config.exists():
2480
- console.print(
2481
- f"\n[green]✓[/green] Claude Code configured: {claude_code_config}"
2482
- )
2483
- else:
2484
- console.print("\n[yellow]○[/yellow] Claude Code not configured")
2485
-
2486
- # Check Claude Desktop configuration
2487
- claude_desktop_config = (
2488
- Path.home()
2489
- / "Library"
2490
- / "Application Support"
2491
- / "Claude"
2492
- / "claude_desktop_config.json"
2493
- )
2494
- if claude_desktop_config.exists():
2495
- try:
2496
- with open(claude_desktop_config) as f:
2497
- config = json.load(f)
2498
- if "mcpServers" in config and "mcp-ticketer" in config["mcpServers"]:
2499
- console.print(
2500
- f"[green]✓[/green] Claude Desktop configured: {claude_desktop_config}"
2501
- )
2502
- else:
2503
- console.print(
2504
- "[yellow]○[/yellow] Claude Desktop config exists but mcp-ticketer not found"
2505
- )
2506
- except Exception:
2507
- console.print(
2508
- "[yellow]○[/yellow] Claude Desktop config exists but could not be read"
2509
- )
2510
- else:
2511
- console.print("[yellow]○[/yellow] Claude Desktop not configured")
2512
-
2513
- # Check Gemini configuration
2514
- gemini_project_config = Path.cwd() / ".gemini" / "settings.json"
2515
- gemini_user_config = Path.home() / ".gemini" / "settings.json"
2516
- if gemini_project_config.exists():
2517
- console.print(
2518
- f"\n[green]✓[/green] Gemini (project) configured: {gemini_project_config}"
2519
- )
2520
- elif gemini_user_config.exists():
2521
- console.print(
2522
- f"\n[green]✓[/green] Gemini (user) configured: {gemini_user_config}"
2523
- )
2524
- else:
2525
- console.print("\n[yellow]○[/yellow] Gemini not configured")
2526
-
2527
- # Check Codex configuration
2528
- codex_config = Path.home() / ".codex" / "config.toml"
2529
- if codex_config.exists():
2530
- console.print(f"[green]✓[/green] Codex configured: {codex_config}")
2531
- else:
2532
- console.print("[yellow]○[/yellow] Codex not configured")
2533
-
2534
- # Check Auggie configuration
2535
- auggie_config = Path.home() / ".augment" / "settings.json"
2536
- if auggie_config.exists():
2537
- console.print(f"[green]✓[/green] Auggie configured: {auggie_config}")
2538
- else:
2539
- console.print("[yellow]○[/yellow] Auggie not configured")
2540
-
2541
- console.print(
2542
- "\n[dim]Run 'mcp-ticketer install <platform>' to configure a platform[/dim]"
2543
- )
2544
-
2545
-
2546
- @mcp_app.command(name="stop")
2547
- def mcp_stop():
2548
- """Stop MCP server (placeholder - MCP runs on-demand via stdio).
2549
-
2550
- Note: The MCP server runs on-demand when AI clients connect via stdio.
2551
- It doesn't run as a persistent background service, so there's nothing to stop.
2552
- This command is provided for consistency but has no effect.
2553
-
2554
- Examples:
2555
- mcp-ticketer mcp stop
2556
-
2557
- """
2558
- console.print(
2559
- "[yellow]ℹ[/yellow] MCP server runs on-demand via stdio (not as a background service)"
2560
- )
2561
- console.print("There is no persistent server process to stop.")
2562
- console.print(
2563
- "\n[dim]The server starts automatically when AI clients connect and stops when they disconnect.[/dim]"
2564
- )
618
+ raise typer.Exit(result) from None
2565
619
 
2566
620
 
2567
621
  # Add command groups to main app (must be after all subcommands are defined)
2568
622
  app.add_typer(mcp_app, name="mcp")
2569
623
 
2570
624
 
2571
- def main():
2572
- """Main entry point."""
625
+ def main() -> None:
626
+ """Execute the main CLI application entry point."""
2573
627
  app()
2574
628
 
2575
629