mcp-ticketer 0.4.2__py3-none-any.whl → 0.4.4__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 (48) hide show
  1. mcp_ticketer/__version__.py +1 -1
  2. mcp_ticketer/adapters/aitrackdown.py +254 -11
  3. mcp_ticketer/adapters/github.py +13 -13
  4. mcp_ticketer/adapters/hybrid.py +11 -11
  5. mcp_ticketer/adapters/jira.py +20 -24
  6. mcp_ticketer/cache/memory.py +6 -5
  7. mcp_ticketer/cli/codex_configure.py +2 -2
  8. mcp_ticketer/cli/configure.py +4 -5
  9. mcp_ticketer/cli/diagnostics.py +2 -2
  10. mcp_ticketer/cli/discover.py +4 -5
  11. mcp_ticketer/cli/gemini_configure.py +2 -2
  12. mcp_ticketer/cli/linear_commands.py +6 -7
  13. mcp_ticketer/cli/main.py +341 -250
  14. mcp_ticketer/cli/mcp_configure.py +1 -2
  15. mcp_ticketer/cli/ticket_commands.py +27 -30
  16. mcp_ticketer/cli/utils.py +23 -22
  17. mcp_ticketer/core/__init__.py +2 -1
  18. mcp_ticketer/core/adapter.py +82 -13
  19. mcp_ticketer/core/config.py +27 -29
  20. mcp_ticketer/core/env_discovery.py +10 -10
  21. mcp_ticketer/core/env_loader.py +8 -8
  22. mcp_ticketer/core/http_client.py +16 -16
  23. mcp_ticketer/core/mappers.py +10 -10
  24. mcp_ticketer/core/models.py +50 -20
  25. mcp_ticketer/core/project_config.py +40 -34
  26. mcp_ticketer/core/registry.py +2 -2
  27. mcp_ticketer/mcp/dto.py +32 -32
  28. mcp_ticketer/mcp/response_builder.py +2 -2
  29. mcp_ticketer/mcp/server.py +3 -3
  30. mcp_ticketer/mcp/server_sdk.py +2 -2
  31. mcp_ticketer/mcp/tools/attachment_tools.py +3 -4
  32. mcp_ticketer/mcp/tools/comment_tools.py +2 -2
  33. mcp_ticketer/mcp/tools/hierarchy_tools.py +8 -8
  34. mcp_ticketer/mcp/tools/pr_tools.py +2 -2
  35. mcp_ticketer/mcp/tools/search_tools.py +6 -6
  36. mcp_ticketer/mcp/tools/ticket_tools.py +12 -12
  37. mcp_ticketer/queue/health_monitor.py +4 -4
  38. mcp_ticketer/queue/manager.py +2 -2
  39. mcp_ticketer/queue/queue.py +16 -16
  40. mcp_ticketer/queue/ticket_registry.py +7 -7
  41. mcp_ticketer/queue/worker.py +2 -2
  42. {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.4.dist-info}/METADATA +61 -2
  43. mcp_ticketer-0.4.4.dist-info/RECORD +73 -0
  44. mcp_ticketer-0.4.2.dist-info/RECORD +0 -73
  45. {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.4.dist-info}/WHEEL +0 -0
  46. {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.4.dist-info}/entry_points.txt +0 -0
  47. {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.4.dist-info}/licenses/LICENSE +0 -0
  48. {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.4.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py CHANGED
@@ -5,7 +5,6 @@ import json
5
5
  import os
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
- from typing import Optional
9
8
 
10
9
  import typer
11
10
  from dotenv import load_dotenv
@@ -84,7 +83,7 @@ class AdapterType(str, Enum):
84
83
  GITHUB = "github"
85
84
 
86
85
 
87
- def load_config(project_dir: Optional[Path] = None) -> dict:
86
+ def load_config(project_dir: Path | None = None) -> dict:
88
87
  """Load configuration from project-local config file ONLY.
89
88
 
90
89
  SECURITY: This method ONLY reads from the current project directory
@@ -146,7 +145,7 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
146
145
  return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
147
146
 
148
147
 
149
- def _discover_from_env_files() -> Optional[str]:
148
+ def _discover_from_env_files() -> str | None:
150
149
  """Discover adapter configuration from .env or .env.local files.
151
150
 
152
151
  Returns:
@@ -270,7 +269,7 @@ def merge_config(updates: dict) -> dict:
270
269
 
271
270
 
272
271
  def get_adapter(
273
- override_adapter: Optional[str] = None, override_config: Optional[dict] = None
272
+ override_adapter: str | None = None, override_config: dict | None = None
274
273
  ):
275
274
  """Get configured adapter instance.
276
275
 
@@ -387,13 +386,13 @@ def _prompt_for_adapter_selection(console: Console) -> str:
387
386
 
388
387
  @app.command()
389
388
  def setup(
390
- adapter: Optional[str] = typer.Option(
389
+ adapter: str | None = typer.Option(
391
390
  None,
392
391
  "--adapter",
393
392
  "-a",
394
393
  help="Adapter type to use (interactive prompt if not specified)",
395
394
  ),
396
- project_path: Optional[str] = typer.Option(
395
+ project_path: str | None = typer.Option(
397
396
  None, "--path", help="Project path (default: current directory)"
398
397
  ),
399
398
  global_config: bool = typer.Option(
@@ -402,36 +401,36 @@ def setup(
402
401
  "-g",
403
402
  help="Save to global config instead of project-specific",
404
403
  ),
405
- base_path: Optional[str] = typer.Option(
404
+ base_path: str | None = typer.Option(
406
405
  None,
407
406
  "--base-path",
408
407
  "-p",
409
408
  help="Base path for ticket storage (AITrackdown only)",
410
409
  ),
411
- api_key: Optional[str] = typer.Option(
410
+ api_key: str | None = typer.Option(
412
411
  None, "--api-key", help="API key for Linear or API token for JIRA"
413
412
  ),
414
- team_id: Optional[str] = typer.Option(
413
+ team_id: str | None = typer.Option(
415
414
  None, "--team-id", help="Linear team ID (required for Linear adapter)"
416
415
  ),
417
- jira_server: Optional[str] = typer.Option(
416
+ jira_server: str | None = typer.Option(
418
417
  None,
419
418
  "--jira-server",
420
419
  help="JIRA server URL (e.g., https://company.atlassian.net)",
421
420
  ),
422
- jira_email: Optional[str] = typer.Option(
421
+ jira_email: str | None = typer.Option(
423
422
  None, "--jira-email", help="JIRA user email for authentication"
424
423
  ),
425
- jira_project: Optional[str] = typer.Option(
424
+ jira_project: str | None = typer.Option(
426
425
  None, "--jira-project", help="Default JIRA project key"
427
426
  ),
428
- github_owner: Optional[str] = typer.Option(
427
+ github_owner: str | None = typer.Option(
429
428
  None, "--github-owner", help="GitHub repository owner"
430
429
  ),
431
- github_repo: Optional[str] = typer.Option(
430
+ github_repo: str | None = typer.Option(
432
431
  None, "--github-repo", help="GitHub repository name"
433
432
  ),
434
- github_token: Optional[str] = typer.Option(
433
+ github_token: str | None = typer.Option(
435
434
  None, "--github-token", help="GitHub Personal Access Token"
436
435
  ),
437
436
  ) -> None:
@@ -471,13 +470,13 @@ def setup(
471
470
 
472
471
  @app.command()
473
472
  def init(
474
- adapter: Optional[str] = typer.Option(
473
+ adapter: str | None = typer.Option(
475
474
  None,
476
475
  "--adapter",
477
476
  "-a",
478
477
  help="Adapter type to use (interactive prompt if not specified)",
479
478
  ),
480
- project_path: Optional[str] = typer.Option(
479
+ project_path: str | None = typer.Option(
481
480
  None, "--path", help="Project path (default: current directory)"
482
481
  ),
483
482
  global_config: bool = typer.Option(
@@ -486,40 +485,40 @@ def init(
486
485
  "-g",
487
486
  help="Save to global config instead of project-specific",
488
487
  ),
489
- base_path: Optional[str] = typer.Option(
488
+ base_path: str | None = typer.Option(
490
489
  None,
491
490
  "--base-path",
492
491
  "-p",
493
492
  help="Base path for ticket storage (AITrackdown only)",
494
493
  ),
495
- api_key: Optional[str] = typer.Option(
494
+ api_key: str | None = typer.Option(
496
495
  None, "--api-key", help="API key for Linear or API token for JIRA"
497
496
  ),
498
- team_id: Optional[str] = typer.Option(
497
+ team_id: str | None = typer.Option(
499
498
  None, "--team-id", help="Linear team ID (required for Linear adapter)"
500
499
  ),
501
- jira_server: Optional[str] = typer.Option(
500
+ jira_server: str | None = typer.Option(
502
501
  None,
503
502
  "--jira-server",
504
503
  help="JIRA server URL (e.g., https://company.atlassian.net)",
505
504
  ),
506
- jira_email: Optional[str] = typer.Option(
505
+ jira_email: str | None = typer.Option(
507
506
  None, "--jira-email", help="JIRA user email for authentication"
508
507
  ),
509
- jira_project: Optional[str] = typer.Option(
508
+ jira_project: str | None = typer.Option(
510
509
  None, "--jira-project", help="Default JIRA project key"
511
510
  ),
512
- github_owner: Optional[str] = typer.Option(
511
+ github_owner: str | None = typer.Option(
513
512
  None, "--github-owner", help="GitHub repository owner"
514
513
  ),
515
- github_repo: Optional[str] = typer.Option(
514
+ github_repo: str | None = typer.Option(
516
515
  None, "--github-repo", help="GitHub repository name"
517
516
  ),
518
- github_token: Optional[str] = typer.Option(
517
+ github_token: str | None = typer.Option(
519
518
  None, "--github-token", help="GitHub Personal Access Token"
520
519
  ),
521
520
  ) -> None:
522
- """Initialize mcp-ticketer for the current project.
521
+ """Initialize mcp-ticketer for the current project (synonymous with 'install' and 'setup').
523
522
 
524
523
  This command sets up MCP Ticketer configuration with interactive prompts
525
524
  to guide you through the process. It auto-detects adapter configuration
@@ -528,11 +527,13 @@ def init(
528
527
  Creates .mcp-ticketer/config.json in the current directory with
529
528
  auto-detected or specified adapter configuration.
530
529
 
531
- Note: 'setup' and 'install' are synonyms for this command.
530
+ Note: 'init', 'install', and 'setup' are all synonyms - use whichever feels natural.
532
531
 
533
532
  Examples:
534
- # Interactive setup (same as 'setup' and 'install')
533
+ # Interactive setup (all three commands are identical)
535
534
  mcp-ticketer init
535
+ mcp-ticketer install
536
+ mcp-ticketer setup
536
537
 
537
538
  # Force specific adapter
538
539
  mcp-ticketer init --adapter linear
@@ -669,20 +670,31 @@ def init(
669
670
  if linear_api_key:
670
671
  linear_config["api_key"] = linear_api_key
671
672
 
672
- # Team ID
673
+ # Team ID or Team Key
674
+ # Try environment variables first
675
+ linear_team_key = os.getenv("LINEAR_TEAM_KEY")
673
676
  linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
674
- if not linear_team_id and not discovered:
675
- console.print("\nYou need your Linear team ID.")
676
- console.print("[dim]Find it in Linear settings or team URL[/dim]\n")
677
677
 
678
- linear_team_id = typer.prompt("Enter your Linear team ID")
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")
679
686
 
687
+ # Save whichever was provided
688
+ if linear_team_key:
689
+ linear_config["team_key"] = linear_team_key
680
690
  if linear_team_id:
681
691
  linear_config["team_id"] = linear_team_id
682
692
 
683
- if not linear_config.get("api_key") or not linear_config.get("team_id"):
693
+ if not linear_config.get("api_key") or (
694
+ not linear_config.get("team_id") and not linear_config.get("team_key")
695
+ ):
684
696
  console.print(
685
- "[red]Error:[/red] Linear requires both API key and team ID"
697
+ "[red]Error:[/red] Linear requires both API key and team ID/key"
686
698
  )
687
699
  console.print(
688
700
  "Run 'mcp-ticketer init --adapter linear' with proper credentials"
@@ -882,107 +894,30 @@ def _show_next_steps(
882
894
  console.print("\n3. [cyan]Check local ticket storage:[/cyan]")
883
895
  console.print(" ls .aitrackdown/")
884
896
 
885
- console.print("\n4. [cyan]Configure MCP clients (optional):[/cyan]")
886
- console.print(" mcp-ticketer mcp claude # For Claude Code")
887
- console.print(" mcp-ticketer mcp auggie # For Auggie")
888
- console.print(" mcp-ticketer mcp gemini # For Gemini CLI")
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")
889
902
 
890
903
  console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
891
904
  console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
892
905
 
893
906
 
894
- # Keep the old install command as deprecated alias to init
895
- @app.command(deprecated=True, hidden=True)
896
- def install(
897
- adapter: Optional[str] = typer.Option(
898
- None,
899
- "--adapter",
900
- "-a",
901
- help="Adapter type to use (auto-detected from .env if not specified)",
902
- ),
903
- project_path: Optional[str] = typer.Option(
904
- None, "--path", help="Project path (default: current directory)"
905
- ),
906
- global_config: bool = typer.Option(
907
- False,
908
- "--global",
909
- "-g",
910
- help="Save to global config instead of project-specific",
911
- ),
912
- base_path: Optional[str] = typer.Option(
913
- None,
914
- "--base-path",
915
- "-p",
916
- help="Base path for ticket storage (AITrackdown only)",
917
- ),
918
- api_key: Optional[str] = typer.Option(
919
- None, "--api-key", help="API key for Linear or API token for JIRA"
920
- ),
921
- team_id: Optional[str] = typer.Option(
922
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
923
- ),
924
- jira_server: Optional[str] = typer.Option(
925
- None,
926
- "--jira-server",
927
- help="JIRA server URL (e.g., https://company.atlassian.net)",
928
- ),
929
- jira_email: Optional[str] = typer.Option(
930
- None, "--jira-email", help="JIRA user email for authentication"
931
- ),
932
- jira_project: Optional[str] = typer.Option(
933
- None, "--jira-project", help="Default JIRA project key"
934
- ),
935
- github_owner: Optional[str] = typer.Option(
936
- None, "--github-owner", help="GitHub repository owner"
937
- ),
938
- github_repo: Optional[str] = typer.Option(
939
- None, "--github-repo", help="GitHub repository name"
940
- ),
941
- github_token: Optional[str] = typer.Option(
942
- None, "--github-token", help="GitHub Personal Access Token"
943
- ),
944
- ) -> None:
945
- """DEPRECATED: Use 'mcp-ticketer init' instead.
946
-
947
- This command is deprecated. Use 'mcp-ticketer init' for project initialization.
948
-
949
- """
950
- console.print(
951
- "[yellow]⚠️ 'install' is deprecated. Use 'mcp-ticketer init' instead.[/yellow]\n"
952
- )
953
- # Call init with all parameters
954
- init(
955
- adapter=adapter,
956
- project_path=project_path,
957
- global_config=global_config,
958
- base_path=base_path,
959
- api_key=api_key,
960
- team_id=team_id,
961
- jira_server=jira_server,
962
- jira_email=jira_email,
963
- jira_project=jira_project,
964
- github_owner=github_owner,
965
- github_repo=github_repo,
966
- github_token=github_token,
967
- )
968
-
969
-
970
907
  @app.command("set")
971
908
  def set_config(
972
- adapter: Optional[AdapterType] = typer.Option(
909
+ adapter: AdapterType | None = typer.Option(
973
910
  None, "--adapter", "-a", help="Set default adapter"
974
911
  ),
975
- team_key: Optional[str] = typer.Option(
912
+ team_key: str | None = typer.Option(
976
913
  None, "--team-key", help="Linear team key (e.g., BTA)"
977
914
  ),
978
- team_id: Optional[str] = typer.Option(None, "--team-id", help="Linear team ID"),
979
- owner: Optional[str] = typer.Option(
980
- None, "--owner", help="GitHub repository owner"
981
- ),
982
- repo: Optional[str] = typer.Option(None, "--repo", help="GitHub repository name"),
983
- server: Optional[str] = typer.Option(None, "--server", help="JIRA server URL"),
984
- project: Optional[str] = typer.Option(None, "--project", help="JIRA project key"),
985
- base_path: Optional[str] = typer.Option(
915
+ team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
916
+ owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
917
+ repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
918
+ server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
919
+ project: str | None = typer.Option(None, "--project", help="JIRA project key"),
920
+ base_path: str | None = typer.Option(
986
921
  None, "--base-path", help="AITrackdown base path"
987
922
  ),
988
923
  ) -> None:
@@ -1072,16 +1007,12 @@ def set_config(
1072
1007
  @app.command("configure")
1073
1008
  def configure_command(
1074
1009
  show: bool = typer.Option(False, "--show", help="Show current configuration"),
1075
- adapter: Optional[str] = typer.Option(
1010
+ adapter: str | None = typer.Option(
1076
1011
  None, "--adapter", help="Set default adapter type"
1077
1012
  ),
1078
- api_key: Optional[str] = typer.Option(None, "--api-key", help="Set API key/token"),
1079
- project_id: Optional[str] = typer.Option(
1080
- None, "--project-id", help="Set project ID"
1081
- ),
1082
- team_id: Optional[str] = typer.Option(
1083
- None, "--team-id", help="Set team ID (Linear)"
1084
- ),
1013
+ api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
1014
+ project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
1015
+ team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
1085
1016
  global_scope: bool = typer.Option(
1086
1017
  False,
1087
1018
  "--global",
@@ -1257,29 +1188,29 @@ def old_queue_health_command(
1257
1188
  @app.command(deprecated=True, hidden=True)
1258
1189
  def create(
1259
1190
  title: str = typer.Argument(..., help="Ticket title"),
1260
- description: Optional[str] = typer.Option(
1191
+ description: str | None = typer.Option(
1261
1192
  None, "--description", "-d", help="Ticket description"
1262
1193
  ),
1263
1194
  priority: Priority = typer.Option(
1264
1195
  Priority.MEDIUM, "--priority", "-p", help="Priority level"
1265
1196
  ),
1266
- tags: Optional[list[str]] = typer.Option(
1197
+ tags: list[str] | None = typer.Option(
1267
1198
  None, "--tag", "-t", help="Tags (can be specified multiple times)"
1268
1199
  ),
1269
- assignee: Optional[str] = typer.Option(
1200
+ assignee: str | None = typer.Option(
1270
1201
  None, "--assignee", "-a", help="Assignee username"
1271
1202
  ),
1272
- project: Optional[str] = typer.Option(
1203
+ project: str | None = typer.Option(
1273
1204
  None,
1274
1205
  "--project",
1275
1206
  help="Parent project/epic ID (synonym for --epic)",
1276
1207
  ),
1277
- epic: Optional[str] = typer.Option(
1208
+ epic: str | None = typer.Option(
1278
1209
  None,
1279
1210
  "--epic",
1280
1211
  help="Parent epic/project ID (synonym for --project)",
1281
1212
  ),
1282
- adapter: Optional[AdapterType] = typer.Option(
1213
+ adapter: AdapterType | None = typer.Option(
1283
1214
  None, "--adapter", help="Override default adapter"
1284
1215
  ),
1285
1216
  ) -> None:
@@ -1488,14 +1419,14 @@ def create(
1488
1419
 
1489
1420
  @app.command("list", deprecated=True, hidden=True)
1490
1421
  def list_tickets(
1491
- state: Optional[TicketState] = typer.Option(
1422
+ state: TicketState | None = typer.Option(
1492
1423
  None, "--state", "-s", help="Filter by state"
1493
1424
  ),
1494
- priority: Optional[Priority] = typer.Option(
1425
+ priority: Priority | None = typer.Option(
1495
1426
  None, "--priority", "-p", help="Filter by priority"
1496
1427
  ),
1497
1428
  limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
1498
- adapter: Optional[AdapterType] = typer.Option(
1429
+ adapter: AdapterType | None = typer.Option(
1499
1430
  None, "--adapter", help="Override default adapter"
1500
1431
  ),
1501
1432
  ) -> None:
@@ -1551,7 +1482,7 @@ def list_tickets(
1551
1482
  def show(
1552
1483
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1553
1484
  comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
1554
- adapter: Optional[AdapterType] = typer.Option(
1485
+ adapter: AdapterType | None = typer.Option(
1555
1486
  None, "--adapter", help="Override default adapter"
1556
1487
  ),
1557
1488
  ) -> None:
@@ -1607,7 +1538,7 @@ def show(
1607
1538
  def comment(
1608
1539
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1609
1540
  content: str = typer.Argument(..., help="Comment content"),
1610
- adapter: Optional[AdapterType] = typer.Option(
1541
+ adapter: AdapterType | None = typer.Option(
1611
1542
  None, "--adapter", help="Override default adapter"
1612
1543
  ),
1613
1544
  ) -> None:
@@ -1648,17 +1579,15 @@ def comment(
1648
1579
  @app.command(deprecated=True, hidden=True)
1649
1580
  def update(
1650
1581
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1651
- title: Optional[str] = typer.Option(None, "--title", help="New title"),
1652
- description: Optional[str] = typer.Option(
1582
+ title: str | None = typer.Option(None, "--title", help="New title"),
1583
+ description: str | None = typer.Option(
1653
1584
  None, "--description", "-d", help="New description"
1654
1585
  ),
1655
- priority: Optional[Priority] = typer.Option(
1586
+ priority: Priority | None = typer.Option(
1656
1587
  None, "--priority", "-p", help="New priority"
1657
1588
  ),
1658
- assignee: Optional[str] = typer.Option(
1659
- None, "--assignee", "-a", help="New assignee"
1660
- ),
1661
- adapter: Optional[AdapterType] = typer.Option(
1589
+ assignee: str | None = typer.Option(None, "--assignee", "-a", help="New assignee"),
1590
+ adapter: AdapterType | None = typer.Option(
1662
1591
  None, "--adapter", help="Override default adapter"
1663
1592
  ),
1664
1593
  ) -> None:
@@ -1718,13 +1647,13 @@ def update(
1718
1647
  @app.command(deprecated=True, hidden=True)
1719
1648
  def transition(
1720
1649
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1721
- state_positional: Optional[TicketState] = typer.Argument(
1650
+ state_positional: TicketState | None = typer.Argument(
1722
1651
  None, help="Target state (positional - deprecated, use --state instead)"
1723
1652
  ),
1724
- state: Optional[TicketState] = typer.Option(
1653
+ state: TicketState | None = typer.Option(
1725
1654
  None, "--state", "-s", help="Target state (recommended)"
1726
1655
  ),
1727
- adapter: Optional[AdapterType] = typer.Option(
1656
+ adapter: AdapterType | None = typer.Option(
1728
1657
  None, "--adapter", help="Override default adapter"
1729
1658
  ),
1730
1659
  ) -> None:
@@ -1789,12 +1718,12 @@ def transition(
1789
1718
 
1790
1719
  @app.command(deprecated=True, hidden=True)
1791
1720
  def search(
1792
- query: Optional[str] = typer.Argument(None, help="Search query"),
1793
- state: Optional[TicketState] = typer.Option(None, "--state", "-s"),
1794
- priority: Optional[Priority] = typer.Option(None, "--priority", "-p"),
1795
- assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
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"),
1796
1725
  limit: int = typer.Option(10, "--limit", "-l"),
1797
- adapter: Optional[AdapterType] = typer.Option(
1726
+ adapter: AdapterType | None = typer.Option(
1798
1727
  None, "--adapter", help="Override default adapter"
1799
1728
  ),
1800
1729
  ) -> None:
@@ -1852,7 +1781,7 @@ app.add_typer(discover_app, name="discover")
1852
1781
  # Add diagnostics command
1853
1782
  @app.command("diagnose")
1854
1783
  def diagnose_command(
1855
- output_file: Optional[str] = typer.Option(
1784
+ output_file: str | None = typer.Option(
1856
1785
  None, "--output", "-o", help="Save full report to file"
1857
1786
  ),
1858
1787
  json_output: bool = typer.Option(
@@ -1899,7 +1828,7 @@ def diagnose_command(
1899
1828
 
1900
1829
  @app.command("doctor")
1901
1830
  def doctor_alias(
1902
- output_file: Optional[str] = typer.Option(
1831
+ output_file: str | None = typer.Option(
1903
1832
  None, "--output", "-o", help="Save full report to file"
1904
1833
  ),
1905
1834
  json_output: bool = typer.Option(
@@ -1944,111 +1873,154 @@ mcp_app = typer.Typer(
1944
1873
 
1945
1874
  @app.command()
1946
1875
  def install(
1947
- platform: Optional[str] = typer.Argument(
1876
+ platform: str | None = typer.Argument(
1948
1877
  None,
1949
- help="Platform to install (claude-code, claude-desktop, auggie, gemini, codex)",
1878
+ help="Platform to install (claude-code, claude-desktop, gemini, codex, auggie)",
1879
+ ),
1880
+ adapter: str | None = typer.Option(
1881
+ None,
1882
+ "--adapter",
1883
+ "-a",
1884
+ help="Adapter type to use (interactive prompt if not specified)",
1885
+ ),
1886
+ project_path: str | None = typer.Option(
1887
+ None, "--path", help="Project path (default: current directory)"
1888
+ ),
1889
+ global_config: bool = typer.Option(
1890
+ False,
1891
+ "--global",
1892
+ "-g",
1893
+ help="Save to global config instead of project-specific",
1894
+ ),
1895
+ base_path: str | None = typer.Option(
1896
+ None,
1897
+ "--base-path",
1898
+ "-p",
1899
+ help="Base path for ticket storage (AITrackdown only)",
1900
+ ),
1901
+ api_key: str | None = typer.Option(
1902
+ None, "--api-key", help="API key for Linear or API token for JIRA"
1903
+ ),
1904
+ team_id: str | None = typer.Option(
1905
+ None, "--team-id", help="Linear team ID (required for Linear adapter)"
1906
+ ),
1907
+ jira_server: str | None = typer.Option(
1908
+ None,
1909
+ "--jira-server",
1910
+ help="JIRA server URL (e.g., https://company.atlassian.net)",
1911
+ ),
1912
+ jira_email: str | None = typer.Option(
1913
+ None, "--jira-email", help="JIRA user email for authentication"
1914
+ ),
1915
+ jira_project: str | None = typer.Option(
1916
+ None, "--jira-project", help="Default JIRA project key"
1917
+ ),
1918
+ github_owner: str | None = typer.Option(
1919
+ None, "--github-owner", help="GitHub repository owner"
1920
+ ),
1921
+ github_repo: str | None = typer.Option(
1922
+ None, "--github-repo", help="GitHub repository name"
1923
+ ),
1924
+ github_token: str | None = typer.Option(
1925
+ None, "--github-token", help="GitHub Personal Access Token"
1950
1926
  ),
1951
1927
  dry_run: bool = typer.Option(
1952
- False, "--dry-run", help="Show what would be done without making changes"
1928
+ False,
1929
+ "--dry-run",
1930
+ help="Show what would be done without making changes (for platform installation)",
1953
1931
  ),
1954
1932
  ) -> None:
1955
- """Install mcp-ticketer for AI platforms.
1956
-
1957
- Without arguments, shows installation status and available platforms.
1958
- With a platform argument, installs MCP configuration for that platform.
1933
+ """Install MCP for AI platforms OR initialize adapter setup.
1959
1934
 
1960
- Each platform gets the right configuration automatically:
1961
- - claude-code: Project-level MCP server
1962
- - claude-desktop: Global MCP server
1963
- - auggie: Project-level MCP server
1964
- - gemini: Project-level MCP server
1965
- - codex: Project-level MCP server
1935
+ With platform argument (new syntax): Install MCP configuration for AI platforms
1936
+ Without platform argument (legacy): Run adapter setup wizard (same as 'init' and 'setup')
1966
1937
 
1967
- Examples:
1968
- # Show status and available platforms
1969
- mcp-ticketer install
1938
+ New Command Structure:
1939
+ # Install MCP for AI platforms
1940
+ mcp-ticketer install claude-code # Claude Code (project-level)
1941
+ mcp-ticketer install claude-desktop # Claude Desktop (global)
1942
+ mcp-ticketer install gemini # Gemini CLI
1943
+ mcp-ticketer install codex # Codex
1944
+ mcp-ticketer install auggie # Auggie
1970
1945
 
1971
- # Install for Claude Code (project-level)
1972
- mcp-ticketer install claude-code
1973
-
1974
- # Install for Claude Desktop (global)
1975
- mcp-ticketer install claude-desktop
1976
-
1977
- # Install for Auggie
1978
- mcp-ticketer install auggie
1979
-
1980
- # Dry run to preview changes
1981
- mcp-ticketer install claude-code --dry-run
1946
+ Legacy Adapter Setup (still supported):
1947
+ mcp-ticketer install # Interactive setup wizard
1948
+ mcp-ticketer install --adapter linear
1982
1949
 
1983
1950
  """
1984
- # If no platform specified, show help message
1985
- if platform is None:
1986
- console.print("[green]✓[/green] mcp-ticketer CLI is already installed.\n")
1987
- console.print(
1988
- "[bold]To configure MCP for a specific platform, use:[/bold]\n"
1989
- " mcp-ticketer install <platform>\n"
1990
- )
1991
- console.print("[bold]Available platforms:[/bold]")
1992
- console.print(" • claude-code - Claude Code (project-level)")
1993
- console.print(" • claude-desktop - Claude Desktop (global)")
1994
- console.print(" • auggie - Auggie (project-level)")
1995
- console.print(" gemini - Gemini CLI (project-level)")
1996
- console.print(" codex - Codex (project-level)")
1997
- return
1951
+ # If platform argument is provided, handle MCP platform installation (NEW SYNTAX)
1952
+ if platform is not None:
1953
+ # Import configuration functions
1954
+ from .auggie_configure import configure_auggie_mcp
1955
+ from .codex_configure import configure_codex_mcp
1956
+ from .gemini_configure import configure_gemini_mcp
1957
+ from .mcp_configure import configure_claude_mcp
1958
+
1959
+ # Map platform names to configuration functions
1960
+ platform_mapping = {
1961
+ "claude-code": {
1962
+ "func": lambda: configure_claude_mcp(global_config=False, force=True),
1963
+ "name": "Claude Code",
1964
+ },
1965
+ "claude-desktop": {
1966
+ "func": lambda: configure_claude_mcp(global_config=True, force=True),
1967
+ "name": "Claude Desktop",
1968
+ },
1969
+ "auggie": {
1970
+ "func": lambda: configure_auggie_mcp(force=True),
1971
+ "name": "Auggie",
1972
+ },
1973
+ "gemini": {
1974
+ "func": lambda: configure_gemini_mcp(scope="project", force=True),
1975
+ "name": "Gemini CLI",
1976
+ },
1977
+ "codex": {
1978
+ "func": lambda: configure_codex_mcp(force=True),
1979
+ "name": "Codex",
1980
+ },
1981
+ }
1998
1982
 
1999
- # Import configuration functions
2000
- from .auggie_configure import configure_auggie_mcp
2001
- from .codex_configure import configure_codex_mcp
2002
- from .gemini_configure import configure_gemini_mcp
2003
- from .mcp_configure import configure_claude_mcp
1983
+ if platform not in platform_mapping:
1984
+ console.print(f"[red]Unknown platform: {platform}[/red]")
1985
+ console.print("\n[bold]Available platforms:[/bold]")
1986
+ for p in platform_mapping.keys():
1987
+ console.print(f" • {p}")
1988
+ raise typer.Exit(1)
2004
1989
 
2005
- # Map platform names to configuration functions
2006
- platform_mapping = {
2007
- "claude-code": {
2008
- "func": lambda: configure_claude_mcp(global_config=False, force=True),
2009
- "name": "Claude Code",
2010
- },
2011
- "claude-desktop": {
2012
- "func": lambda: configure_claude_mcp(global_config=True, force=True),
2013
- "name": "Claude Desktop",
2014
- },
2015
- "auggie": {
2016
- "func": lambda: configure_auggie_mcp(force=True),
2017
- "name": "Auggie",
2018
- },
2019
- "gemini": {
2020
- "func": lambda: configure_gemini_mcp(scope="project", force=True),
2021
- "name": "Gemini CLI",
2022
- },
2023
- "codex": {
2024
- "func": lambda: configure_codex_mcp(force=True),
2025
- "name": "Codex",
2026
- },
2027
- }
1990
+ config = platform_mapping[platform]
2028
1991
 
2029
- if platform not in platform_mapping:
2030
- console.print(f"[red]Unknown platform: {platform}[/red]")
2031
- console.print("\n[bold]Available platforms:[/bold]")
2032
- for p in platform_mapping.keys():
2033
- console.print(f" • {p}")
2034
- raise typer.Exit(1)
2035
-
2036
- config = platform_mapping[platform]
1992
+ if dry_run:
1993
+ console.print(f"[cyan]DRY RUN - Would install for {config['name']}[/cyan]")
1994
+ return
2037
1995
 
2038
- if dry_run:
2039
- console.print(f"[cyan]DRY RUN - Would install for {config['name']}[/cyan]")
1996
+ try:
1997
+ config["func"]()
1998
+ except Exception as e:
1999
+ console.print(f"[red]Installation failed: {e}[/red]")
2000
+ raise typer.Exit(1)
2040
2001
  return
2041
2002
 
2042
- try:
2043
- config["func"]()
2044
- except Exception as e:
2045
- console.print(f"[red]Installation failed: {e}[/red]")
2046
- raise typer.Exit(1)
2003
+ # Otherwise, delegate to init for adapter initialization (LEGACY BEHAVIOR)
2004
+ # This makes 'install' and 'init' synonymous when called without platform argument
2005
+ init(
2006
+ adapter=adapter,
2007
+ project_path=project_path,
2008
+ global_config=global_config,
2009
+ base_path=base_path,
2010
+ api_key=api_key,
2011
+ team_id=team_id,
2012
+ jira_server=jira_server,
2013
+ jira_email=jira_email,
2014
+ jira_project=jira_project,
2015
+ github_owner=github_owner,
2016
+ github_repo=github_repo,
2017
+ github_token=github_token,
2018
+ )
2047
2019
 
2048
2020
 
2049
2021
  @app.command()
2050
2022
  def remove(
2051
- platform: Optional[str] = typer.Argument(
2023
+ platform: str | None = typer.Argument(
2052
2024
  None,
2053
2025
  help="Platform to remove (claude-code, claude-desktop, auggie, gemini, codex)",
2054
2026
  ),
@@ -2135,7 +2107,7 @@ def remove(
2135
2107
 
2136
2108
  @app.command()
2137
2109
  def uninstall(
2138
- platform: Optional[str] = typer.Argument(
2110
+ platform: str | None = typer.Argument(
2139
2111
  None,
2140
2112
  help="Platform to uninstall (claude-code, claude-desktop, auggie, gemini, codex)",
2141
2113
  ),
@@ -2218,10 +2190,10 @@ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
2218
2190
 
2219
2191
  @mcp_app.command(name="serve")
2220
2192
  def mcp_serve(
2221
- adapter: Optional[AdapterType] = typer.Option(
2193
+ adapter: AdapterType | None = typer.Option(
2222
2194
  None, "--adapter", "-a", help="Override default adapter type"
2223
2195
  ),
2224
- base_path: Optional[str] = typer.Option(
2196
+ base_path: str | None = typer.Option(
2225
2197
  None, "--base-path", help="Base path for AITrackdown adapter"
2226
2198
  ),
2227
2199
  ):
@@ -2453,6 +2425,125 @@ def mcp_auggie(
2453
2425
  raise typer.Exit(1)
2454
2426
 
2455
2427
 
2428
+ @mcp_app.command(name="status")
2429
+ def mcp_status():
2430
+ """Check MCP server status.
2431
+
2432
+ Shows whether the MCP server is configured and running for various platforms.
2433
+
2434
+ Examples:
2435
+ mcp-ticketer mcp status
2436
+
2437
+ """
2438
+ import json
2439
+ from pathlib import Path
2440
+
2441
+ console.print("[bold]MCP Server Status[/bold]\n")
2442
+
2443
+ # Check project-level configuration
2444
+ project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
2445
+ if project_config.exists():
2446
+ console.print(f"[green]✓[/green] Project config found: {project_config}")
2447
+ try:
2448
+ with open(project_config) as f:
2449
+ config = json.load(f)
2450
+ adapter = config.get("default_adapter", "aitrackdown")
2451
+ console.print(f" Default adapter: [cyan]{adapter}[/cyan]")
2452
+ except Exception as e:
2453
+ console.print(f" [yellow]Warning: Could not read config: {e}[/yellow]")
2454
+ else:
2455
+ console.print("[yellow]○[/yellow] No project config found")
2456
+
2457
+ # Check Claude Code configuration
2458
+ claude_code_config = Path.cwd() / ".mcp" / "config.json"
2459
+ if claude_code_config.exists():
2460
+ console.print(
2461
+ f"\n[green]✓[/green] Claude Code configured: {claude_code_config}"
2462
+ )
2463
+ else:
2464
+ console.print("\n[yellow]○[/yellow] Claude Code not configured")
2465
+
2466
+ # Check Claude Desktop configuration
2467
+ claude_desktop_config = (
2468
+ Path.home()
2469
+ / "Library"
2470
+ / "Application Support"
2471
+ / "Claude"
2472
+ / "claude_desktop_config.json"
2473
+ )
2474
+ if claude_desktop_config.exists():
2475
+ try:
2476
+ with open(claude_desktop_config) as f:
2477
+ config = json.load(f)
2478
+ if "mcpServers" in config and "mcp-ticketer" in config["mcpServers"]:
2479
+ console.print(
2480
+ f"[green]✓[/green] Claude Desktop configured: {claude_desktop_config}"
2481
+ )
2482
+ else:
2483
+ console.print(
2484
+ "[yellow]○[/yellow] Claude Desktop config exists but mcp-ticketer not found"
2485
+ )
2486
+ except Exception:
2487
+ console.print(
2488
+ "[yellow]○[/yellow] Claude Desktop config exists but could not be read"
2489
+ )
2490
+ else:
2491
+ console.print("[yellow]○[/yellow] Claude Desktop not configured")
2492
+
2493
+ # Check Gemini configuration
2494
+ gemini_project_config = Path.cwd() / ".gemini" / "settings.json"
2495
+ gemini_user_config = Path.home() / ".gemini" / "settings.json"
2496
+ if gemini_project_config.exists():
2497
+ console.print(
2498
+ f"\n[green]✓[/green] Gemini (project) configured: {gemini_project_config}"
2499
+ )
2500
+ elif gemini_user_config.exists():
2501
+ console.print(
2502
+ f"\n[green]✓[/green] Gemini (user) configured: {gemini_user_config}"
2503
+ )
2504
+ else:
2505
+ console.print("\n[yellow]○[/yellow] Gemini not configured")
2506
+
2507
+ # Check Codex configuration
2508
+ codex_config = Path.home() / ".codex" / "config.toml"
2509
+ if codex_config.exists():
2510
+ console.print(f"[green]✓[/green] Codex configured: {codex_config}")
2511
+ else:
2512
+ console.print("[yellow]○[/yellow] Codex not configured")
2513
+
2514
+ # Check Auggie configuration
2515
+ auggie_config = Path.home() / ".augment" / "settings.json"
2516
+ if auggie_config.exists():
2517
+ console.print(f"[green]✓[/green] Auggie configured: {auggie_config}")
2518
+ else:
2519
+ console.print("[yellow]○[/yellow] Auggie not configured")
2520
+
2521
+ console.print(
2522
+ "\n[dim]Run 'mcp-ticketer install <platform>' to configure a platform[/dim]"
2523
+ )
2524
+
2525
+
2526
+ @mcp_app.command(name="stop")
2527
+ def mcp_stop():
2528
+ """Stop MCP server (placeholder - MCP runs on-demand via stdio).
2529
+
2530
+ Note: The MCP server runs on-demand when AI clients connect via stdio.
2531
+ It doesn't run as a persistent background service, so there's nothing to stop.
2532
+ This command is provided for consistency but has no effect.
2533
+
2534
+ Examples:
2535
+ mcp-ticketer mcp stop
2536
+
2537
+ """
2538
+ console.print(
2539
+ "[yellow]ℹ[/yellow] MCP server runs on-demand via stdio (not as a background service)"
2540
+ )
2541
+ console.print("There is no persistent server process to stop.")
2542
+ console.print(
2543
+ "\n[dim]The server starts automatically when AI clients connect and stops when they disconnect.[/dim]"
2544
+ )
2545
+
2546
+
2456
2547
  # Add command groups to main app (must be after all subcommands are defined)
2457
2548
  app.add_typer(mcp_app, name="mcp")
2458
2549